1 import Base 2 import ./json.bend as Json 3 import ./fs.bend as Fs 4 import ./act.bend as Act 5 import ./store.bend as Store 6 import ./swbase.bend as B 7 import ./swcreds.bend as C 8 import ./swlive.bend as L 9 import ./fs/write.bend as FsW 10 import ./store/base.bend as SBase 11 import ./store/io.bend as SIo 12 import ./store/seq.bend as SSeq 13 14 # Switch, part 5: the locked switch, rollback and the post-switch check. 15 16 # `do ActM:` (see store.bend). 17 18 def ActM(-A: Type) -> Type: 19 Act.of(A) 20 21 def ActM.pure(-A: Type, x: A) -> Act.of(A): 22 Act.pure(A, x) 23 24 def ActM.bind(-A: Type, -B: Type, m: Act.of(A), f: A -> Act.of(B)) -> Act.of(B): 25 Act.bind(A, B, m, f) 26 27 def need(+ok: Bool, +msg: String) -> Act.of(Unit): 28 Act.guard(ok, 4, msg) 29 30 def reinstruct(+slot: String) -> String: 31 "Re-add with: cswap --add-account --slot " ++ slot 32 33 def oauth_present(+j: Json.Json) -> Bool: 34 match j: 35 case Json.JObj{Json.JField{k, v, t}}: 36 True{} 37 case _: 38 False{} 39 40 # The outgoing account's backup, by what the live credential is. 41 def backup_outgoing(+k: C.CKind, +root: String, +cur: String, +email: String, +creds: String, +cfg_text: String) -> Act.of(Unit): 42 match k: 43 case C.KOwnBytes{}: 44 SIo.write_config(root, cur, email, cfg_text) 45 case C.KWiped{}: 46 SIo.write_config(root, cur, email, cfg_text) 47 case C.KForeignSynced{s}: 48 Act.pure(Unit, Unit{}) 49 case C.KForeign{s}: 50 Act.pure(Unit, Unit{}) 51 case C.KAlien{}: 52 Act.pure(Unit, Unit{}) 53 case _: 54 do ActM: 55 SIo.write_cred(root, cur, email, creds) 56 SIo.write_config(root, cur, email, cfg_text) 57 58 def salvage(+bad: Bool, +ctx: SBase.Ctx, +ts: String) -> Act.of(Unit): 59 Act.cond(Unit, bad, FsW.rename(SBase.global_config_path(ctx), SBase.global_config_path(ctx) ++ ".corrupt-" ++ ts), Act.pure(Unit, Unit{})) 60 61 # What ~/.claude.json becomes: the live one with the target's oauthAccount, or 62 # the target's whole config backup when there is no readable live one. 63 def new_config(+l: L.Live, +oauth: Json.Json, +tcreds: String, +tcfg: Json.Json) -> Json.Json: 64 Store.sel.j(L.lv_cfg_ok(l), B.activate_config(L.lv_cfg(l), oauth, tcreds), tcfg) 65 66 def live_salvage(+ctx: SBase.Ctx, +ts: String, +l: L.Live) -> Act.of(Unit): 67 salvage(Bool.not(L.lv_cfg_ok(l)) && B.is_some_str(L.lv_cfg_text(l)), ctx, ts) 68 69 def activate_files(+ctx: SBase.Ctx, +ts: String, +l: L.Live, +oauth: Json.Json, +tcreds: String, +tcfg: Json.Json) -> Act.of(Unit): 70 do ActM: 71 L.write_live_creds(ctx, C.prepare_activation(tcreds, L.lv_creds(l))) 72 live_salvage(ctx, ts, l) 73 L.write_cfg(ctx, new_config(l, oauth, tcreds, tcfg)) 74 75 def final_steps(+ctx: SBase.Ctx, +root: String, +target: String, +ts: String, +seq: Json.Json, +l: L.Live, +oauth: Json.Json, +tcreds: String, +tcfg: Json.Json) -> Act.of(Unit): 76 do ActM: 77 activate_files(ctx, ts, l, oauth, tcreds, tcfg) 78 SIo.write_seq(root, SSeq.stamp(SSeq.set_active(seq, SSeq.str_u32(target)), ts)) 79 80 def transact(+ctx: SBase.Ctx, +root: String, +ts: String, +seq_text: String, +l: L.Live, steps: Act.of(Unit)) -> Act.of(Unit): 81 IO.bind(Fs.Res(Unit), Fs.Res(Unit), Act.on_fail(Unit, steps, L.restore(ctx, root, L.lv_creds_text(l), L.lv_cfg_text(l), seq_text)), r => IO.pure(Fs.Res(Unit), L.wrap_failure(r))) 82 83 def archive_for_a(+root: String, +cts: String, +l: L.Live, +tcreds: String) -> Act.of(Unit): 84 L.archive_if(SBase.nonempty(L.lv_creds(l)) && Bool.not(String.eq(L.lv_creds(l), tcreds)), root, L.lv_email(l), L.lv_creds(l), cts) 85 86 def finish_a(+root: String, +target: String, +temail: String, +l: L.Live, +oauth: Json.Json, +cur: Maybe<&2, String>) -> Act.of(Json.Json): 87 do ActM: 88 SIo.log(root, "INFO", "Activated account " ++ target ++ " (live login " ++ SBase.sel.s(String.is_empty(L.lv_email(l)), "absent", "was " ++ L.lv_email(l)) ++ ")") 89 return B.op_json(B.from_ref(L.lv_email(l), cur), B.ref(Some{target}, temail), [], SSeq.field_str(oauth, "organizationUuid")) 90 91 def switch_a(+ctx: SBase.Ctx, +root: String, +target: String, +temail: String, +ts: String, +cts: String, +seq_text: String, +seq: Json.Json, +l: L.Live, +oauth: Json.Json, +tcreds: String, +tcfg: Json.Json, +cur: Maybe<&2, String>) -> Act.of(Json.Json): 92 do ActM: 93 archive_for_a(root, cts, l, tcreds) 94 transact(ctx, root, ts, seq_text, l, final_steps(ctx, root, target, ts, seq, l, oauth, tcreds, tcfg)) 95 finish_a(root, target, temail, l, oauth, cur) 96 97 def live_present(+l: L.Live) -> Act.of(Unit): 98 Act.guard(SBase.nonempty(L.lv_creds(l)), 5, "Current account credential is empty (Keychain unreadable?); refusing to overwrite its backup") 99 100 def classify_in(+pre: L.Pre, +seq: Json.Json, +cur: String, +l: L.Live, +backup: String, +others: List<&2, C.Other>) -> C.CKind: 101 C.final_kind(C.classify(L.lv_creds(l), backup, others), seq, cur, L.lv_creds(l), others, L.pre_prof(pre, L.lv_creds(l))) 102 103 def classify_b(+pre: L.Pre, +root: String, +seq: Json.Json, +cur: String, +l: L.Live) -> Act.of(C.CKind): 104 do ActM: 105 live_present(l) 106 backup : String <- SIo.read_cred(root, cur, L.lv_email(l)) 107 +others : List<&2, C.Other> <- L.other_creds(root, seq, B.slots_of(seq), cur) 108 return classify_in(pre, seq, cur, l, backup, others) 109 110 def backup_out(+k: C.CKind, +root: String, +cur: String, +l: L.Live) -> Act.of(Unit): 111 backup_outgoing(k, root, cur, L.lv_email(l), L.lv_creds(l), B.text_or(L.lv_cfg_text(l))) 112 113 def save_b(+k: C.CKind, +root: String, +cur: String, +ts: String, +l: L.Live) -> Act.of(Unit): 114 do ActM: 115 L.archive_if(C.preserve(k), root, L.lv_email(l), L.lv_creds(l), SBase.compact_ts(ts)) 116 backup_out(k, root, cur, l) 117 118 def finish_b(+k: C.CKind, +root: String, +target: String, +temail: String, +l: L.Live, +oauth: Json.Json, +cur: String) -> Act.of(Json.Json): 119 do ActM: 120 SIo.log(root, "INFO", "Switched from account " ++ cur ++ " to " ++ target) 121 return B.op_json(B.from_ref(L.lv_email(l), Some{cur}), B.ref(Some{target}, temail), C.kind_warning(k, cur), SSeq.field_str(oauth, "organizationUuid")) 122 123 def switch_b(+pre: L.Pre, +ctx: SBase.Ctx, +root: String, +target: String, +temail: String, +ts: String, +seq_text: String, +seq: Json.Json, +l: L.Live, +oauth: Json.Json, +tcreds: String, +tcfg: Json.Json, +cur: String) -> Act.of(Json.Json): 124 do ActM: 125 +k : C.CKind <- classify_b(pre, root, seq, cur, l) 126 save_b(k, root, cur, ts, l) 127 transact(ctx, root, ts, seq_text, l, final_steps(ctx, root, target, ts, seq, l, oauth, tcreds, tcfg)) 128 finish_b(k, root, target, temail, l, oauth, cur) 129 130 def perform5(+case_a: Bool, +pre: L.Pre, +ctx: SBase.Ctx, +root: String, +target: String, +ts: String, +seq_text: String, +seq: Json.Json, +temail: String, +l: L.Live, +tcreds: String, +tcfg: Json.Json, +oauth: Json.Json) -> Act.of(Json.Json): 131 Act.cond(Json.Json, case_a, 132 switch_a(ctx, root, target, temail, ts, SBase.compact_ts(ts), seq_text, seq, l, oauth, tcreds, tcfg, L.lv_slot(l, seq)), 133 switch_b(pre, ctx, root, target, temail, ts, seq_text, seq, l, oauth, tcreds, tcfg, SSeq.str_or(L.lv_slot(l, seq), ""))) 134 135 def perform4(+ctx: SBase.Ctx, +root: String, +target: String, +force: Bool, +pre: L.Pre, +ts: String, +seq_text: String, +seq: Json.Json, +temail: String, +l: L.Live, +tcreds: String, +tcfg: Json.Json, +oauth: Json.Json) -> Act.of(Json.Json): 136 perform5(force || String.is_empty(L.lv_email(l)) || Bool.not(B.is_some_str(L.lv_slot(l, seq))), pre, ctx, root, target, ts, seq_text, seq, temail, l, tcreds, tcfg, oauth) 137 138 def perform7(+ok: Bool, +ctx: SBase.Ctx, +root: String, +target: String, +force: Bool, +pre: L.Pre, +ts: String, +seq_text: String, +seq: Json.Json, +temail: String, +l: L.Live, +tcreds: String, +tcfg: Json.Json) -> Act.of(Json.Json): 139 Act.cond(Json.Json, ok, 140 perform4(ctx, root, target, force, pre, ts, seq_text, seq, temail, l, tcreds, tcfg, SSeq.get_or("oauthAccount", tcfg, Json.JNull{})), 141 Act.fail(Json.Json, 4, "Invalid oauthAccount in backup")) 142 143 def perform6(+ctx: SBase.Ctx, +root: String, +target: String, +force: Bool, +pre: L.Pre, +ts: String, +seq_text: String, +seq: Json.Json, +temail: String, +l: L.Live, +tcreds: String, parsed: Result<&2, &2, String, Json.Json>) -> Act.of(Json.Json): 144 match parsed: 145 case Fail{e}: 146 Act.fail(Json.Json, 4, "Invalid backup config: " ++ e) 147 case Done{+tcfg}: 148 perform7(oauth_present(SSeq.get_or("oauthAccount", tcfg, Json.JNull{})), ctx, root, target, force, pre, ts, seq_text, seq, temail, l, tcreds, tcfg) 149 150 def tcfg_need(+target: String, +tcfg_text: Maybe<&2, String>) -> Act.of(Unit): 151 need(SBase.nonempty(B.text_or(tcfg_text)), "Account-" ++ target ++ " has no stored config backup. " ++ reinstruct(target)) 152 153 def perform3b(+ctx: SBase.Ctx, +root: String, +target: String, +force: Bool, +pre: L.Pre, +ts: String, +seq_text: String, +seq: Json.Json, +temail: String, +l: L.Live, +tcreds: String) -> Act.of(Json.Json): 154 do ActM: 155 +tcfg_text : Maybe<&2, String> <- L.read_text_opt(SBase.config_path(root, target, temail)) 156 tcfg_need(target, tcfg_text) 157 perform6(ctx, root, target, force, pre, ts, seq_text, seq, temail, l, tcreds, Json.parse(B.text_or(tcfg_text))) 158 159 def perform3(+ctx: SBase.Ctx, +root: String, +target: String, +force: Bool, +pre: L.Pre, +ts: String, +seq_text: String, +seq: Json.Json, +temail: String) -> Act.of(Json.Json): 160 do ActM: 161 l : L.Live <- L.read_live(ctx) 162 +tcreds : String <- SIo.read_cred(root, target, temail) 163 need(SBase.nonempty(tcreds), "Account-" ++ target ++ " has no stored credentials. " ++ reinstruct(target)) 164 perform3b(ctx, root, target, force, pre, ts, seq_text, seq, temail, l, tcreds) 165 166 def perform2b(+ctx: SBase.Ctx, +root: String, +target: String, +force: Bool, +pre: L.Pre, +ts: String, +seq_text: String, +seq: Json.Json, +rec: Maybe<&2, Json.Json>) -> Act.of(Json.Json): 167 match rec: 168 case None{}: 169 Act.fail(Json.Json, 3, "Account-" ++ target ++ " does not exist") 170 case Some{r}: 171 perform3(ctx, root, target, force, pre, ts, seq_text, seq, SSeq.field_str(r, "email")) 172 173 def perform2(+ctx: SBase.Ctx, +root: String, +target: String, +force: Bool, +pre: L.Pre, +ts: String, +seq_text: String, parsed: Result<&2, &2, String, Json.Json>) -> Act.of(Json.Json): 174 match parsed: 175 case Fail{e}: 176 Act.fail(Json.Json, 1, SBase.seq_path(root) ++ " exists but could not be parsed (" ++ e ++ "). Repair or move it, then retry — refusing to overwrite it unread.") 177 case Done{+seq}: 178 perform2b(ctx, root, target, force, pre, ts, seq_text, seq, SSeq.slot_record(seq, target)) 179 180 # The switch itself, under every lock the reference takes. 181 def perform_locked(+ctx: SBase.Ctx, +target: String, +force: Bool, +pre: L.Pre, +ts: String) -> Act.of(Json.Json): 182 do ActM: 183 +seq_text : String <- Fs.read_file(SBase.seq_path(SBase.root_of(ctx))) 184 perform2(ctx, SBase.root_of(ctx), target, force, pre, ts, seq_text, Json.parse(seq_text)) 185 186 def verify2(+l: L.Live, +target: String, +temail: String, +torg: String) -> Act.of(Unit): 187 Act.guard(String.eq(L.lv_email(l), temail) && String.eq(L.lv_org(l), torg), B.e_overridden(), B.override_msg(target, temail, L.lv_email(l))) 188 189 # Claude Code on macOS prefers a keychain login over ~/.claude/.credentials.json, 190 # so a file-only switch can be silently undone: it rewrites oauthAccount from 191 # the keychain login. Wait `opts`, read ~/.claude.json again, and if it no 192 # longer names the target report `live-login-overridden`. The keychain is never 193 # touched; the remedy is the user's. 194 def verify_live(+ctx: SBase.Ctx, +delay_ms: U32, +target: String, +temail: String, +torg: String) -> Act.of(Unit): 195 do ActM: 196 Act.lift(Unit, IO.sleep(delay_ms)) 197 l : L.Live <- L.read_live(ctx) 198 verify2(l, target, temail, torg) 199 200 def do_switch2(+ctx: SBase.Ctx, +target: String, +force: Bool, +opts: L.Opts, +pre: L.Pre) -> Act.of(Json.Json): 201 do ActM: 202 ts : String <- SIo.now_iso() 203 +op : Json.Json <- L.with_locks(Json.Json, ctx, perform_locked(ctx, target, force, pre, ts)) 204 verify_live(ctx, L.opts_delay(opts), target, SSeq.field_str(SSeq.get_or("to", op, Json.JNull{}), "email"), SSeq.field_str(op, "targetOrg")) 205 return op 206 207 def do_switch(+ctx: SBase.Ctx, +target: String, +force: Bool, +opts: L.Opts) -> Act.of(Json.Json): 208 do ActM: 209 pre : L.Pre <- Act.cond(L.Pre, force, Act.pure(L.Pre, L.no_pre()), L.prefetch(ctx, L.opts_oracle(opts))) 210 do_switch2(ctx, target, force, opts, pre)