daemon/walk.bend
cswap-bend-cli@0.1.0.0 · 283 lines · 49 definitions
1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3import cswap-bend-cli@0.1.0.0/act.bend as Act
4import cswap-bend-cli@0.1.0.0/clock.bend as Clock
5import cswap-bend-cli@0.1.0.0/store/base.bend as SBase
6import cswap-bend-cli@0.1.0.0/daemon/events.bend as Ev
7import cswap-bend-cli@0.1.0.0/daemon/cadence.bend as Cad
8import cswap-bend-cli@0.1.0.0/daemon/dec.bend as Dc
9import cswap-bend-cli@0.1.0.0/daemon/apply.bend as Ap
10import cswap-bend-cli@0.1.0.0/daemon/tick.bend as Tk
11import cswap-bend-cli@0.1.0.0/daemon/gather.bend as G
12import cswap-bend-cli@0.1.0.0/daemon/freshen.bend as Fr
13import cswap-bend-cli@0.1.0.0/daemon/quarantine.bend as Q
14
15
16
17
18
19
20
21
22
23type Trail is Data:
24 Trail{skip: List<&2, String>, events: List<&2, Json.Json>, transient: Bool, first: Maybe<&2, Tk.Out>, sys: String}
25
26
27type Move is Data:
28 MDone{out: Tk.Out}
29 MRetry{trail: Trail}
30
31
32type W is Data:
33 W{env: Tk.Env, t0: U32, ts: String, m: Maybe<&2, G.Snap>}
34
35
36type Verdict is Data:
37 VOk{}
38 VDead{}
39 VConflict{}
40 VSkip{}
41 VSys{}
42 VTrans{}
43
44def ActM(-A: Type) -> Type:
45 Act.of(A)
46
47def ActM.pure(-A: Type, x: A) -> Act.of(A):
48 Act.pure(A, x)
49
50def ActM.bind(-A: Type, -B: Type, m: Act.of(A), f: A -> Act.of(B)) -> Act.of(B):
51 Act.bind(A, B, m, f)
52
53def no_trail() -> Trail:
54 Trail{[], [], False{}, None{}, ""}
55
56def trail_skip(t: Trail) -> List<&2, String>:
57 match t:
58 case Trail{s, e, tr, f, sy}:
59 s
60
61def trail_transient(t: Trail) -> Bool:
62 match t:
63 case Trail{s, e, tr, f, sy}:
64 tr
65
66
67def lead_of(t: Trail, +o: Tk.Out) -> Tk.Out:
68 match t:
69 case Trail{s, e, tr, Some{f}, sy}:
70 f
71 case _:
72 o
73
74def with_events(+o: Tk.Out, +extra: List<&2, Json.Json>) -> Tk.Out:
75 match o:
76 case Tk.Out{Dc.TickRes{i, evs}, m, a, es, ac, ck}:
77 Tk.Out{Dc.TickRes{i, List.append(&2, Json.Json, evs, extra)}, m, a, es, ac, ck}
78
79def trail_events(t: Trail) -> List<&2, Json.Json>:
80 match t:
81 case Trail{s, e, tr, f, sy}:
82 e
83
84def with_info(+o: Tk.Out, i: Cad.Info) -> Tk.Out:
85 match o:
86 case Tk.Out{Dc.TickRes{i0, evs}, m, a, es, ac, ck}:
87 Tk.Out{Dc.TickRes{i, evs}, m, a, es, ac, ck}
88
89
90def merged(+o: Tk.Out, res: Dc.TickRes) -> Tk.Out:
91 match o res:
92 case Tk.Out{Dc.TickRes{i0, lead}, m, a, es, ac, ck} Dc.TickRes{info, evs}:
93 Tk.Out{Dc.TickRes{info, List.append(&2, Json.Json, lead, evs)}, m, a, es, ac, ck}
94
95def applied(+o: Tk.Out, c: Dc.Ctx, d: Dc.Dec) -> Act.of(Tk.Out):
96 do ActM<Tk.Out>:
97 res : Dc.TickRes <- Ap.apply(c, d)
98 return merged(o, res)
99
100def info_of(o: Cad.Outcome, +np: Maybe<&2, U32>) -> Cad.Info:
101 Cad.Info{o, None{}, False{}, False{}, np}
102
103def msg2(+a: Bool, +b: Bool, +c: Bool) -> String:
104 match a b c:
105 case True{} _ _:
106 "CLAUDE_SECURESTORAGE_CONFIG_DIR is set — unset it or run cswap from a normal shell"
107 case False{} True{} _:
108 "cswap's OAuth client was rejected — systemic, not this account"
109 case False{} False{} True{}:
110 "a stashed successor is unreadable — unlock the keychain or fix the file, then retry; `cswap unclaimed` inspects it"
111 case False{} False{} False{}:
112 "another cswap surface holds the slot — retries next pass"
113
114def sys_message(+s: String) -> String:
115 msg2(String.eq(s, "store-unmirrored"), String.eq(s, "invalid_client"), String.eq(s, "stash-unreadable"))
116
117def failed_with(+o: Tk.Out, +ts: String, +msg: String) -> Tk.Out:
118 with_info(with_events(o, [Ev.error(ts, msg, True{})]), info_of(Cad.Failed{}, None{}))
119
120def gave_up3(+nothing_systemic: Bool, +transient: Bool, +sy: String, +o: Tk.Out, +ts: String) -> Tk.Out:
121 match nothing_systemic transient:
122 case False{} _:
123 failed_with(o, ts, "could not freshen: " ++ sys_message(sy))
124 case True{} True{}:
125 failed_with(o, ts, "could not freshen any candidate (network?)")
126 case True{} False{}:
127 with_info(with_events(o, [Ev.no_switch(ts, "no-viable-target", "")]), info_of(Cad.Blocked{}, None{}))
128
129def trail_sys(+t: Trail) -> String:
130 match t:
131 case Trail{s, e, tr, f, sy}:
132 sy
133
134
135
136def gave_up(+t: Trail, +o: Tk.Out, +ts: String) -> Tk.Out:
137 gave_up3(String.is_empty(trail_sys(t)), trail_transient(t), trail_sys(t), with_events(lead_of(t, o), trail_events(t)), ts)
138
139def extend(+t: Trail, +slot: String, +more: List<&2, Json.Json>, +transient: Bool, +o: Tk.Out) -> Trail:
140 Trail{slot <> trail_skip(t), List.append(&2, Json.Json, trail_events(t), more), trail_transient(t) || transient, Some{lead_of(t, o)}, trail_sys(t)}
141
142def worse2(+take_new: Bool, +old: String, +new: String) -> String:
143 match take_new:
144 case True{}:
145 new
146 case False{}:
147 old
148
149
150
151def worse(+old: String, +new: String) -> String:
152 worse2(String.is_empty(old) || U32.is_lt(Fr.systemic_rank(new), Fr.systemic_rank(old)), old, new)
153
154def extend_sys(+t: Trail, +slot: String, +status: String, +o: Tk.Out) -> Trail:
155 Trail{slot <> trail_skip(t), trail_events(t), trail_transient(t), Some{lead_of(t, o)}, worse(trail_sys(t), status)}
156
157
158
159def w_env(w: W) -> Tk.Env:
160 match w:
161 case W{e, t, s, m}:
162 e
163
164def w_t0(w: W) -> U32:
165 match w:
166 case W{e, t, s, m}:
167 t
168
169def w_ts(w: W) -> String:
170 match w:
171 case W{e, t, s, m}:
172 s
173
174def w_m(w: W) -> Maybe<&2, G.Snap>:
175 match w:
176 case W{e, t, s, m}:
177 m
178
179def done_with(w: W, +tr: Trail, +o: Tk.Out, c: Dc.Ctx, d: Dc.Dec) -> Act.of(Move):
180 do ActM<Move>:
181 r : Tk.Out <- applied(with_events(lead_of(tr, o), trail_events(tr)), c, d)
182 return MDone{r}
183
184def quarantined(+w: W, tr: Trail, +o: Tk.Out, +slot: String, +email: String, +reason: String) -> Act.of(Move):
185 do ActM<Move>:
186 q : Json.Json <- Q.quarantine(SBase.root_of(Tk.env_ctx(w_env(w))), slot, email, reason, w_ts(w))
187 return MRetry{extend(tr, slot, [q], False{}, o)}
188
189def verdict2(+is_sys: Bool, +st: String) -> Verdict:
190 match is_sys:
191 case True{}:
192 VSys{}
193 case False{}:
194 VTrans{}
195
196def verdict1(+ok: Bool, +dead: Bool, +conflict: Bool, +skip: Bool, +st: String) -> Verdict:
197 match ok dead conflict skip:
198 case True{} _ _ _:
199 VOk{}
200 case False{} True{} _ _:
201 VDead{}
202 case False{} False{} True{} _:
203 VConflict{}
204 case False{} False{} False{} True{}:
205 VSkip{}
206 case _ _ _ _:
207 verdict2(U32.is_lt(0, Fr.systemic_rank(st)), st)
208
209def verdict(+st: String) -> Verdict:
210 verdict1(String.eq(st, "ok"), String.eq(st, "invalid_grant"), String.eq(st, "identity-conflict"), String.eq(st, "skip-live-session"), st)
211
212def route(+v: Verdict, +st: String, w: W, tr: Trail, +o: Tk.Out, c: Dc.Ctx, d: Dc.Dec, +slot: String, +email: String) -> Act.of(Move):
213 match v:
214 case VOk{}:
215 done_with(w, tr, o, c, d)
216 case VDead{}:
217 quarantined(w, tr, o, slot, email, "invalid_grant")
218 case VConflict{}:
219 quarantined(w, tr, o, slot, email, "identity-conflict")
220 case VSkip{}:
221 Act.pure(Move, MRetry{extend(tr, slot, [], False{}, o)})
222 case VSys{}:
223 Act.pure(Move, MRetry{extend_sys(tr, slot, st, o)})
224 case VTrans{}:
225 Act.pure(Move, MRetry{extend(tr, slot, [], True{}, o)})
226
227def try_target(+w: W, tr: Trail, +o: Tk.Out, c: Dc.Ctx, d: Dc.Dec, +slot: String, +email: String) -> Act.of(Move):
228 do ActM<Move>:
229 +st : String <- Fr.status(SBase.root_of(Tk.env_ctx(w_env(w))), slot, email, w_t0(w))
230 route(verdict(st), st, w, tr, o, c, d, slot, email)
231
232def on_go(+w: W, +tr: Trail, +o: Tk.Out, +c: Dc.Ctx, +d: Dc.Dec, +slot: String, +email: String) -> Act.of(Move):
233 Act.cond(Move, Tk.env_dry(w_env(w)), done_with(w, tr, o, c, d), try_target(w, tr, o, c, d, slot, email))
234
235def on_other(+w: W, +tr: Trail, +o: Tk.Out, c: Dc.Ctx, d: Dc.Dec) -> Act.of(Move):
236 Act.cond(Move, List.is_empty(&2, String, trail_skip(tr)), done_with(w, tr, o, c, d), Act.pure(Move, MDone{gave_up(tr, o, w_ts(w))}))
237
238def on_act(w: W, tr: Trail, +o: Tk.Out, c: Dc.Ctx, d: Dc.Dec) -> Act.of(Move):
239 match d:
240 case Dc.DGo{+to, +email, trig, lh, lr}:
241 on_go(w, tr, o, c, d, to, email)
242 case _:
243 on_other(w, tr, o, c, d)
244
245def next(w: W, tr: Trail, +nx: Tk.Next) -> Act.of(Move):
246 match nx:
247 case Tk.NDone{o}:
248 Act.pure(Move, MDone{lead_of(tr, o)})
249 case Tk.NAct{o, c, d}:
250 on_act(w, tr, o, c, d)
251
252
253def attempt(+w: W, +tr: Trail) -> Act.of(Move):
254 next(w, tr, Tk.tick(Tk.with_skip(w_env(w), trail_skip(tr)), w_t0(w), w_m(w)))
255
256def retry(w: W, mv: Move) -> Act.of(Move):
257 match mv:
258 case MDone{o}:
259 Act.pure(Move, MDone{o})
260 case MRetry{tr}:
261 attempt(w, tr)
262
263def settle(+w: W, mv: Move) -> Tk.Out:
264 match mv:
265 case MDone{o}:
266 o
267 case MRetry{tr}:
268 gave_up(tr, Tk.idle_out(w_env(w), w_ts(w), False{}), w_ts(w))
269
270def again(w: W, a: Act.of(Move)) -> Act.of(Move):
271 Act.bind(Move, Move, a, mv => retry(w, mv))
272
273def again4(+w: W, a: Act.of(Move)) -> Act.of(Move):
274 again(w, again(w, again(w, again(w, a))))
275
276def walk(+w: W) -> Act.of(Tk.Out):
277 Act.bind(Move, Tk.Out, again4(w, again4(w, again4(w, again4(w, attempt(w, no_trail()))))), mv => Act.pure(Tk.Out, settle(w, mv)))
278
279
280
281
282def run(+env: Tk.Env, +t0: U32, +ts: String, +m: Maybe<&2, G.Snap>) -> Act.of(Tk.Out):
283 walk(W{env, t0, ts, m})