daemon/gather.bend

cswap-bend-cli@0.1.0.0 · 194 lines · 34 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/store/base.bend as SBase
7import cswap-bend-cli@0.1.0.0/store/io.bend as SIo
8import cswap-bend-cli@0.1.0.0/store/ops.bend as SOps
9import cswap-bend-cli@0.1.0.0/store/seq.bend as SSeq
10import cswap-bend-cli@0.1.0.0/usage.bend as Usage
11import cswap-bend-cli@0.1.0.0/swlive.bend as L
12import cswap-bend-cli@0.1.0.0/swbase.bend as SwB
13import cswap-bend-cli@0.1.0.0/policy/types.bend as T
14import cswap-bend-cli@0.1.0.0/daemon/rows.bend as Rows
15import cswap-bend-cli@0.1.0.0/daemon/state.bend as St
16
17# What a tick looks at: the roster, the live login, the autoswitch state, the
18# cached usage rows, and per managed account its credential and the policy's
19# view of it (kind, disabled, usage row).
20
21# One managed account: its identity, its stored credential, and the policy's
22# `Account` (a quarantined, disabled or unswitchable one is passed as disabled).
23
24type Ent is Data:
25  Ent{slot: String, email: String, org: String, creds: String, acct: T.Account}
26
27type Snap is Data:
28  Snap{seq: Json.Json, live: L.Live, active: Maybe<&2, String>, state: Json.Json, rows: Json.Json, ents: List<&2, Ent>}
29
30def ActM(-A: Type) -> Type:
31  Act.of(A)
32
33def ActM.pure(-A: Type, x: A) -> Act.of(A):
34  Act.pure(A, x)
35
36def ActM.bind(-A: Type, -B: Type, m: Act.of(A), f: A -> Act.of(B)) -> Act.of(B):
37  Act.bind(A, B, m, f)
38
39def is_true(+m: Maybe<&2, Json.Json>) -> Bool:
40  match m:
41    case Some{Json.JBool{b}}:
42      b
43    case _:
44      False{}
45
46def refresh_of(+creds: String) -> String:
47  SSeq.field_str(SSeq.get_or("claudeAiOauth", Json.unwrap(Json.parse(creds)), Json.JNull{}), "refreshToken")
48
49def kind2(+api: Bool, +refresh: Bool) -> T.AcctKind:
50  match api refresh:
51    case True{} _:
52      T.ApiKey{}
53    case False{} True{}:
54      T.FullLogin{}
55    case False{} False{}:
56      T.SetupToken{}
57
58def kind_of(+api_rec: Bool, +creds: String) -> T.AcctKind:
59  kind2(api_rec || SBase.looks_like_api_key(creds), SBase.nonempty(refresh_of(creds)))
60
61def ent_of(+slot: String, +rec: Json.Json, +creds: String, +cfg_ok: Bool, +quarantined: Bool) -> Ent:
62  Ent{slot, SSeq.field_str(rec, "email"), SSeq.field_str(rec, "organizationUuid"), creds,
63    T.Account{U32.to_nat(SSeq.str_u32(slot)), SSeq.field_str(rec, "email"), kind_of(SSeq.is_api_key_rec(rec), creds),
64      is_true(Json.get("disabled", rec)) || quarantined || Bool.not(SBase.nonempty(creds) && cfg_ok)}}
65
66def mem_str(+x: String, +xs: List<&2, String>) -> Bool:
67  match xs:
68    case Nil{}:
69      False{}
70    case Con{h, t}:
71      String.eq(h, x) || mem_str(x, t)
72
73def has_config(+root: String, +seq: Json.Json, +slot: String) -> Act.of(Bool):
74  do ActM<Bool>:
75    cfg : Maybe<&2, String> <- L.read_text_opt(SBase.config_path(root, slot, SIo.slot_email(seq, slot)))
76    return SBase.nonempty(SwB.text_or(cfg))
77
78def cred_of(+root: String, +seq: Json.Json, +slot: String) -> Act.of(String):
79  SIo.read_cred(root, slot, SIo.slot_email(seq, slot))
80
81def ent_one(+root: String, +seq: Json.Json, +slot: String, +st: Json.Json) -> Act.of(Ent):
82  do ActM<Ent>:
83    creds : String <- cred_of(root, seq, slot)
84    ok : Bool <- has_config(root, seq, slot)
85    return ent_of(slot, SSeq.get_or(slot, SSeq.accounts_of(seq), Json.JNull{}), creds, ok, mem_str(slot, St.quarantined_slots(st)))
86
87def ents_of(+root: String, +seq: Json.Json, +slots: List<&2, String>, +st: Json.Json) -> Act.of(List<&2, Ent>):
88  match slots:
89    case Nil{}:
90      Act.pure(List<&2, Ent>, [])
91    case Con{s, t}:
92      do ActM<List<&2, Ent>>:
93        e : Ent <- ent_one(root, seq, s, st)
94        rest : List<&2, Ent> <- ents_of(root, seq, t, st)
95        return e <> rest
96
97def rows3(+v: String, +j: Json.Json) -> Json.Json:
98  Store.sel.j(String.eq(v, "2"), SSeq.accounts_of(j), Json.JObj{Json.JNil{}})
99
100def rows2(r: Result<&2, &2, String, Json.Json>) -> Json.Json:
101  match r:
102    case Fail{e}:
103      Json.JObj{Json.JNil{}}
104    case Done{+j}:
105      rows3(SOps.field_json(j, "schemaVersion"), j)
106
107# The cached rows (accounts object); {} for a missing, torn or other-schema file.
108def rows_of(r: Fs.Res(String)) -> Json.Json:
109  match r:
110    case Fail{e}:
111      SSeq.accounts_of(Json.JNull{})
112    case Done{t}:
113      rows2(Json.parse(t))
114
115def read_rows(+root: String) -> IO(Json.Json):
116  IO.bind(Fs.Res(String), Json.Json, Fs.read_file(root ++ "/cache/usage.json"), r => IO.pure(Json.Json, rows_of(r)))
117
118def ents_for(+ctx: SBase.Ctx, +seq: Json.Json, +st: Json.Json) -> Act.of(List<&2, Ent>):
119  ents_of(SBase.root_of(ctx), seq, SwB.slots_of(seq), st)
120
121def state_of(+ctx: SBase.Ctx) -> Act.of(Json.Json):
122  Act.lift(Json.Json, St.read_state(SBase.root_of(ctx)))
123
124def load(+ctx: SBase.Ctx, +seq: Json.Json) -> Act.of(Snap):
125  do ActM<Snap>:
126    +live : L.Live <- L.read_live(ctx)
127    +st : Json.Json <- state_of(ctx)
128    rows : Json.Json <- Act.lift(Json.Json, read_rows(SBase.root_of(ctx)))
129    ents : List<&2, Ent> <- ents_for(ctx, seq, st)
130    return Snap{seq, live, L.lv_slot(live, seq), st, rows, ents}
131
132def some_snap(s: Snap) -> Act.of(Maybe<&2, Snap>):
133  Act.pure(Maybe<&2, Snap>, Some{s})
134
135def snapshot2(+ctx: SBase.Ctx, +m: Maybe<&2, Json.Json>) -> Act.of(Maybe<&2, Snap>):
136  match m:
137    case None{}:
138      Act.pure(Maybe<&2, Snap>, None{})
139    case Some{seq}:
140      Act.bind(Snap, Maybe<&2, Snap>, load(ctx, seq), s => some_snap(s))
141
142# Look the whole picture up.
143def snapshot(+ctx: SBase.Ctx) -> Act.of(Maybe<&2, Snap>):
144  do ActM<Maybe<&2, Snap>>:
145    m : Maybe<&2, Json.Json> <- SIo.read_seq(SBase.root_of(ctx))
146    snapshot2(ctx, m)
147
148def snap_ents(+s: Snap) -> List<&2, Ent>:
149  match s:
150    case Snap{seq, live, active, st, rows, ents}:
151      ents
152
153def snap_active(+s: Snap) -> Maybe<&2, String>:
154  match s:
155    case Snap{seq, live, active, st, rows, ents}:
156      active
157
158def snap_state(+s: Snap) -> Json.Json:
159  match s:
160    case Snap{seq, live, active, st, rows, ents}:
161      st
162
163def snap_rows(+s: Snap) -> Json.Json:
164  match s:
165    case Snap{seq, live, active, st, rows, ents}:
166      rows
167
168def in_skip(+slot: String, +skip: List<&2, String>) -> Bool:
169  mem_str(slot, skip)
170
171def disable_ent(e: Ent, +skip: List<&2, String>) -> Ent:
172  match e:
173    case Ent{+slot, email, org, creds, T.Account{n, ae, k, d}}:
174      Ent{slot, email, org, creds, T.Account{n, ae, k, d || in_skip(slot, skip)}}
175
176def disable_ents(+es: List<&2, Ent>, +skip: List<&2, String>) -> List<&2, Ent>:
177  match es:
178    case Nil{}:
179      []
180    case Con{e, t}:
181      disable_ent(e, skip) <> disable_ents(t, skip)
182
183def skip_one(s: Snap, +skip: List<&2, String>) -> Snap:
184  match s:
185    case Snap{seq, live, active, st, rows, ents}:
186      Snap{seq, live, active, st, rows, disable_ents(ents, skip)}
187
188# The snapshot with the given slots held out of the candidates.
189def skip_snap(+m: Maybe<&2, Snap>, +skip: List<&2, String>) -> Maybe<&2, Snap>:
190  match m:
191    case None{}:
192      None{}
193    case Some{s}:
194      Some{skip_one(s, skip)}