swrun.bend

cswap-bend-cli@0.1.0.0 · 210 lines · 37 definitions

1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3import cswap-bend-cli@0.1.0.0/fs.bend as Fs
4import cswap-bend-cli@0.1.0.0/act.bend as Act
5import cswap-bend-cli@0.1.0.0/store.bend as Store
6import cswap-bend-cli@0.1.0.0/swbase.bend as B
7import cswap-bend-cli@0.1.0.0/swcreds.bend as C
8import cswap-bend-cli@0.1.0.0/swlive.bend as L
9import cswap-bend-cli@0.1.0.0/fs/write.bend as FsW
10import cswap-bend-cli@0.1.0.0/store/base.bend as SBase
11import cswap-bend-cli@0.1.0.0/store/io.bend as SIo
12import cswap-bend-cli@0.1.0.0/store/seq.bend as SSeq
13
14# Switch, part 5: the locked switch, rollback and the post-switch check.
15
16# `do ActM<A>:` (see store.bend).
17
18def ActM(-A: Type) -> Type:
19  Act.of(A)
20
21def ActM.pure(-A: Type, x: A) -> Act.of(A):
22  Act.pure(A, x)
23
24def 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
27def need(+ok: Bool, +msg: String) -> Act.of(Unit):
28  Act.guard(ok, 4, msg)
29
30def reinstruct(+slot: String) -> String:
31  "Re-add with: cswap --add-account --slot " ++ slot
32
33def 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.
41def 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<Unit>:
55        SIo.write_cred(root, cur, email, creds)
56        SIo.write_config(root, cur, email, cfg_text)
57
58def 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.
63def 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
66def 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
69def activate_files(+ctx: SBase.Ctx, +ts: String, +l: L.Live, +oauth: Json.Json, +tcreds: String, +tcfg: Json.Json) -> Act.of(Unit):
70  do ActM<Unit>:
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
75def 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<Unit>:
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
80def 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
83def 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
86def 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<Json.Json>:
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
91def 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<Json.Json>:
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
97def 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
100def 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
103def classify_b(+pre: L.Pre, +root: String, +seq: Json.Json, +cur: String, +l: L.Live) -> Act.of(C.CKind):
104  do ActM<C.CKind>:
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
110def 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
113def save_b(+k: C.CKind, +root: String, +cur: String, +ts: String, +l: L.Live) -> Act.of(Unit):
114  do ActM<Unit>:
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
118def 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<Json.Json>:
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
123def 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<Json.Json>:
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
130def 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
135def 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
138def 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
143def 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
150def 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
153def 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<Json.Json>:
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
159def 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<Json.Json>:
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
166def 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
173def 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.
181def perform_locked(+ctx: SBase.Ctx, +target: String, +force: Bool, +pre: L.Pre, +ts: String) -> Act.of(Json.Json):
182  do ActM<Json.Json>:
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
186def 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.
194def verify_live(+ctx: SBase.Ctx, +delay_ms: U32, +target: String, +temail: String, +torg: String) -> Act.of(Unit):
195  do ActM<Unit>:
196    Act.lift(Unit, IO.sleep(delay_ms))
197    l : L.Live <- L.read_live(ctx)
198    verify2(l, target, temail, torg)
199
200def do_switch2(+ctx: SBase.Ctx, +target: String, +force: Bool, +opts: L.Opts, +pre: L.Pre) -> Act.of(Json.Json):
201  do ActM<Json.Json>:
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
207def do_switch(+ctx: SBase.Ctx, +target: String, +force: Bool, +opts: L.Opts) -> Act.of(Json.Json):
208  do ActM<Json.Json>:
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)