1 import Base 2 import ../fs.bend as Fs 3 import ../act.bend as Act 4 import ../clock.bend as Clock 5 import ../store.bend as Store 6 import ../cmd.bend as Cmd 7 import ../store/base.bend as SBase 8 import ../store/seq.bend as SSeq 9 import ../swlive.bend as L 10 import ./config.bend as Cfg 11 import ../fmt/out.bend as O 12 import ./live.bend as Lv 13 import ../profflag.bend as Profflag 14 import ./auto.bend as Auto 15 import ../policy/types.bend as PT 16 import ./pidlock.bend as Pid 17 import ./shadowcli.bend as ShC 18 19 # The entry the CLI dispatches `auto` to: flags over settings.json over 20 # defaults, the single-decider lock, then one tick (`--once`) or the loop. 21 # Exit codes: 0 switched, 1 error, 2 no action (or a usage error), 3 blocked. 22 23 def ActM(-A: Type) -> Type: 24 Act.of(A) 25 26 def ActM.pure(-A: Type, x: A) -> Act.of(A): 27 Act.pure(A, x) 28 29 def ActM.bind(-A: Type, -B: Type, m: Act.of(A), f: A -> Act.of(B)) -> Act.of(B): 30 Act.bind(A, B, m, f) 31 32 def env_or(r: Result<&1, &1, U32 & String, String>, +d: String) -> String: 33 match r: 34 case Fail{e}: 35 d 36 case Done{s}: 37 s 38 39 def oracle_of(r: Result<&1, &1, U32 & String, String>) -> L.Oracle: 40 match r: 41 case Fail{e}: 42 L.OHttp{} 43 case Done{p}: 44 L.OFile{p} 45 46 # How the daemon switches: the delay before the post-switch check 47 # ($CSWAP_SWITCH_VERIFY_DELAY_MS, default 2000) and where the ownership lookup 48 # goes ($CSWAP_SWITCH_ORACLE_FILE answers from a file; else the profile API). 49 def switch_opts(d: Result<&1, &1, U32 & String, String>, o: Result<&1, &1, U32 & String, String>) -> L.Opts: 50 L.Opts{Maybe.default(&2, U32, U32.read(env_or(d, "2000")), 2000), oracle_of(o)} 51 52 def settings_text(r: Fs.Res(String)) -> String: 53 match r: 54 case Fail{e}: 55 "" 56 case Done{t}: 57 t 58 59 def settings_io(+ctx: SBase.Ctx) -> IO(String): 60 IO.bind(Fs.Res(String), String, Fs.read_file(SBase.root_of(ctx) ++ "/settings.json"), r => IO.pure(String, settings_text(r))) 61 62 def tick_body(+r: Auto.Run, +once: Bool) -> Act.of(U32): 63 Act.cond(U32, once, Auto.once(r), Auto.forever(r, PT.fresh_mem(), False{})) 64 65 # The stream's first line, when the format has one (CSV: the header), then the ticks. 66 def body(+r: Auto.Run, +once: Bool) -> Act.of(U32): 67 do ActM: 68 Act.lift(Unit, IO.write(O.stream_head(Auto.run_fmt(r)))) 69 tick_body(r, once) 70 71 def flag_dry(+f: Cfg.Flags) -> Bool: 72 match f: 73 case Cfg.Flags{o, j, d, th, iv, cd, st, md, ia}: 74 d 75 76 def flag_fmt(+f: Cfg.Flags) -> O.Fmt: 77 match f: 78 case Cfg.Flags{o, j, d, th, iv, cd, st, md, ia}: 79 O.of_code(j) 80 81 def flag_once(+f: Cfg.Flags) -> Bool: 82 match f: 83 case Cfg.Flags{o, j, d, th, iv, cd, st, md, ia}: 84 o 85 86 def started(+ctx: SBase.Ctx, +f: Cfg.Flags, +state_dir: String, +opts: L.Opts, +version: String, +cfg: Cfg.Cfg, +pid: U32, +ts: String, +lv: Lv.Live) -> Act.of(U32): 87 Pid.with_pidlock(U32, state_dir, body(Auto.Run{ctx, state_dir, cfg, flag_fmt(f), flag_dry(f), opts, ts, version, pid, lv}, flag_once(f))) 88 89 def go3(+ctx: SBase.Ctx, +f: Cfg.Flags, +state_dir: String, +opts: L.Opts, +version: String, +text: String, +over: Maybe<&2, String>) -> Act.of(U32): 90 do ActM: 91 pid : U32 <- Act.lift(U32, Pid.my_pid()) 92 t : U32 <- Act.lift(U32, Clock.now_s()) 93 started(ctx, f, state_dir, opts, version, Lv.cfg_for(text, f, over), pid, Clock.iso8601_utc(t), Lv.live_for(text, f, over)) 94 95 # A `--profile` that names nothing is a usage error. 96 def go2(+bad: Maybe<&2, String>, +ctx: SBase.Ctx, +f: Cfg.Flags, +state_dir: String, +opts: L.Opts, +version: String, +text: String, +over: Maybe<&2, String>) -> Act.of(U32): 97 match bad: 98 case Some{n}: 99 Act.fail(U32, 2, "cswap auto: error: unknown profile '" ++ n ++ "'; `cswap profile list` shows the profiles") 100 case None{}: 101 go3(ctx, f, state_dir, opts, version, text, over) 102 103 def go(+ctx: SBase.Ctx, +f: Cfg.Flags, +state_dir: String, +opts: L.Opts, +version: String, +over: Maybe<&2, String>) -> Act.of(U32): 104 do ActM: 105 +text : String <- Act.lift(String, settings_io(ctx)) 106 go2(Lv.unknown(text, over), ctx, f, state_dir, opts, version, text, over) 107 108 def with_flags(+ctx: SBase.Ctx, +state_dir: String, +opts: L.Opts, +version: String, +over: Maybe<&2, String>, r: Result<&2, &2, String, Cfg.Flags>) -> Act.of(U32): 109 match r: 110 case Fail{msg}: 111 Act.fail(U32, 2, "cswap auto: error: " ++ msg) 112 case Done{f}: 113 go(ctx, f, state_dir, opts, version, over) 114 115 # `cswap auto ARGS` (ARGS after the verb). `state_dir` is where the pid file, 116 # status file and log live; pass the backup root when there is no better place. 117 def plain3(+ok: Bool, +ctx: SBase.Ctx, +state_dir: String, +opts: L.Opts, +version: String, +over: Maybe<&2, String>, +rest: List<&2, String>, +err: String) -> Act.of(U32): 118 match ok: 119 case True{}: 120 with_flags(ctx, state_dir, opts, version, over, Cfg.parse_flags(rest)) 121 case False{}: 122 Act.fail(U32, 2, "cswap auto: error: " ++ err) 123 124 def plain2(+ctx: SBase.Ctx, +state_dir: String, +opts: L.Opts, +version: String, +sp: Profflag.Split) -> Act.of(U32): 125 match sp: 126 case Profflag.Split{rest, over, err, pending}: 127 plain3(String.is_empty(err), ctx, state_dir, opts, version, over, rest, err) 128 129 def plain(+ctx: SBase.Ctx, +args: List<&2, String>, +state_dir: String, +opts: L.Opts, +version: String) -> Act.of(U32): 130 plain2(ctx, state_dir, opts, version, Profflag.split(args)) 131 132 def routed(+ctx: SBase.Ctx, +sp: ShC.Split, +state_dir: String, +opts: L.Opts, +version: String) -> Act.of(U32): 133 Act.cond(U32, ShC.sp_shadow(sp), ShC.run(ctx, opts, sp), plain(ctx, ShC.sp_rest(sp), state_dir, opts, version)) 134 135 def auto(+ctx: SBase.Ctx, +args: List<&2, String>, +state_dir: String, +opts: L.Opts, +version: String) -> Act.of(U32): 136 routed(ctx, ShC.split(Cfg.normalize(args), False{}), state_dir, opts, version) 137 138 # ---- as a program entry ---------------------------------------------------------------------------------------- 139 140 # 2 for a usage error, 1 for everything else. 141 def code_for(+code: U32) -> U32: 142 SSeq.sel.u(U32.is_eq(code, 2), 2, 1) 143 144 def finish(r: Fs.Res(U32)) -> IO(Unit): 145 match r: 146 case Done{code}: 147 IO.exit(code) 148 case Fail{(code, msg)}: 149 IO.bind(Unit, Unit, IO.print_err(msg), u => IO.exit(code_for(code))) 150 151 # The verb as a command result: events already went to stdout as they happened, 152 # so only the exit code (and a failure's message) remain. 153 def cmd_of(r: Fs.Res(U32)) -> IO(Cmd.Out): 154 match r: 155 case Done{code}: 156 IO.pure(Cmd.Out, Cmd.Out{code, "", ""}) 157 case Fail{(code, msg)}: 158 IO.pure(Cmd.Out, Cmd.Out{code_for(code), "", msg ++ "\n"}) 159 160 def run2(+ctx: SBase.Ctx, +args: List<&2, String>, +version: String, st: Result<&1, &1, U32 & String, String>, d: Result<&1, &1, U32 & String, String>, o: Result<&1, &1, U32 & String, String>) -> IO(Fs.Res(U32)): 161 auto(ctx, args, env_or(st, SBase.root_of(ctx)), switch_opts(d, o), version) 162 163 def run1(+ctx: SBase.Ctx, +args: List<&2, String>, +version: String, st: Result<&1, &1, U32 & String, String>, d: Result<&1, &1, U32 & String, String>) -> IO(Fs.Res(U32)): 164 IO.bind(Result<&1, &1, U32 & String, String>, Fs.Res(U32), IO.get_env("CSWAP_SWITCH_ORACLE_FILE"), o => run2(ctx, args, version, st, d, o)) 165 166 def run0(+ctx: SBase.Ctx, +args: List<&2, String>, +version: String, st: Result<&1, &1, U32 & String, String>) -> IO(Fs.Res(U32)): 167 IO.bind(Result<&1, &1, U32 & String, String>, Fs.Res(U32), IO.get_env("CSWAP_SWITCH_VERIFY_DELAY_MS"), d => run1(ctx, args, version, st, d)) 168 169 # `cswap auto ARGS` reading $HOME, $CLAUDE_CONFIG_DIR and $CSWAP_STATE_DIR itself. 170 def run(+args: List<&2, String>, +version: String) -> IO(Fs.Res(U32)): 171 IO.bind(SBase.Ctx, Fs.Res(U32), Store.ctx_env(), ctx => IO.bind(Result<&1, &1, U32 & String, String>, Fs.Res(U32), IO.get_env("CSWAP_STATE_DIR"), st => run0(ctx, args, version, st))) 172 173 # The whole verb as an IO program that exits with the tick's code. 174 def auto_main(+args: List<&2, String>, +version: String) -> IO(Unit): 175 IO.bind(Fs.Res(U32), Unit, run(args, version), finish) 176 177 # The verb for the CLI dispatch (`cswap auto ARGS`). 178 def auto_cmd(+args: List<&2, String>, +version: String) -> IO(Cmd.Out): 179 IO.bind(Fs.Res(U32), Cmd.Out, run(args, version), cmd_of)