@cotal-ai/workspace 0.35.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,206 @@
1
+ import { type SecretStore } from "@cotal-ai/core";
2
+ import { type SpaceMaterialComposition } from "./space-segmentation.js";
3
+ /** `<root>/.cotal/auth/creds` — the PARENT every space's agent-secret segment sits in, and the
4
+ * directory the pre-P1 layout wrote its files directly into.
5
+ *
6
+ * Resolves nothing and migrates nothing, so it is what the two callers that must NOT move material
7
+ * ask for: {@link agentSecretKeysUnder}, which is a DELETER (§3.1, the same reason `clean`'s sweep
8
+ * addresses `segmentedKey`), and {@link agentCredsDir} itself, which needs the parent to enumerate
9
+ * before it can migrate. Every other caller wants {@link agentCredsDir}.
10
+ *
11
+ * It keeps the name `creds`, which is load-bearing beyond this file: it is the one sibling of the
12
+ * auth dir that `migrateLegacyUserAuthState` excludes BY NAME and that `userAuthSpacesOnDisk` skips,
13
+ * so a space's segment landing inside it (rather than beside it, under `auth/`) leaves both of those
14
+ * statements true as written. That is §3's reason for this placement. */
15
+ export declare function agentCredsRoot(root: string): string;
16
+ /** THE per-agent file segment — the single guarded encoder under every agent-secret key AND path
17
+ * (the {@link spaceSegment} posture: one encoder, guarded before any key or path exists). The
18
+ * alphabet is the manager's spawn-name discipline (`manager.nameError`); the CLI's `--name`
19
+ * override historically had no such guard, so a path-hostile name is refused HERE, before it can
20
+ * address a key or file outside the creds dir. */
21
+ export declare function agentSecretSegment(name: string): string;
22
+ /**
23
+ * THE CHOKE POINT (§2 rule 1): `<root>/.cotal/auth/creds/space.<hex>` — this space's agent-secret
24
+ * dir, having moved any pre-P1 root-scoped files into it first.
25
+ *
26
+ * Every path and key below resolves through here, which is what makes "migrate on first touch"
27
+ * reach every flow rather than the ones someone remembered to update. A caller that builds
28
+ * `join(agentCredsRoot(root), …)` itself has reintroduced the read-fallback §2 rejects.
29
+ *
30
+ * THE MIGRATION IS PER FILE, and that is rule 2 met rather than dodged. P7 moves a fixed set of five
31
+ * kinds; this set is OPEN — one file per agent per kind, discovered by enumeration — so "the move is
32
+ * one `renameSync`" holds per FILE here instead of per kind. Each file is therefore wholly legacy or
33
+ * wholly canonical across a crash, and a crash midway through the set leaves a mixture that the next
34
+ * first touch simply finishes. That is §2.1's cause one, self-healing, and it needs no repair verb.
35
+ *
36
+ * Rules 3 and 4 come from the shared choke point unchanged, which is the point of calling it: rule 4
37
+ * is re-asked per file rather than once for the set, so the cost is one tenant-count read per file
38
+ * MOVED. That is bounded by the one-time migration of a root — after it, the set is empty and the
39
+ * only cost is the enumeration — and it buys a single implementation of the rule instead of a
40
+ * sweep-shaped copy of it.
41
+ */
42
+ export declare function agentCredsDir(root: string, space: string): string;
43
+ /** Canonical {@link SecretStore} keys of the per-agent standing secrets, mirroring the
44
+ * `.cotal/auth/creds/space.<hex>/<name>.<kind>` layout byte-for-byte under the workspace FS
45
+ * composition. `<name>.creds` is the static-auth scoped cred; the actor token + sentinel cred are
46
+ * the user-mode pair a spawn mints. The transient `<name>.auth-health.json` is runtime state, NOT a
47
+ * secret kind — it stays plain-file (and moves with its family, see {@link MIGRATABLE_SUFFIXES}). */
48
+ export declare const agentCredsKey: (space: string, name: string, composition: SpaceMaterialComposition) => string;
49
+ export declare const agentActorTokenKey: (space: string, name: string, composition: SpaceMaterialComposition) => string;
50
+ export declare const agentSentinelCredsKey: (space: string, name: string, composition: SpaceMaterialComposition) => string;
51
+ /** Lifecycle-keyed {@link SecretStore} keys of one INCARNATION's secret family — the
52
+ * per-incarnation counterparts of the standing name-keyed keys above, for any endpoint that
53
+ * provisions managed lifecycles (the manager is the first client; delivery and future endpoints
54
+ * ride the same seam). See {@link agentIncarnationBase} for why the uid is embedded. */
55
+ export declare const agentLifecycleCredsKey: (space: string, name: string, lifecycleUid: string, composition: SpaceMaterialComposition) => string;
56
+ export declare const agentLifecycleActorTokenKey: (space: string, name: string, lifecycleUid: string, composition: SpaceMaterialComposition) => string;
57
+ export declare const agentLifecycleSentinelCredsKey: (space: string, name: string, lifecycleUid: string, composition: SpaceMaterialComposition) => string;
58
+ /** The FS materialization paths of one agent's secret family (plus its non-secret health file) —
59
+ * built from the SAME filename source as the key builders. These are the paths subprocesses read
60
+ * (the bearer re-exec's `--token-file`, a launch's creds handoff), never an alternate source of
61
+ * truth: under the local FS composition each path IS its key's storage location. */
62
+ export declare function agentSecretFilePaths(root: string, space: string, name: string): {
63
+ creds: string;
64
+ actorToken: string;
65
+ sentinelCreds: string;
66
+ health: string;
67
+ };
68
+ /** The lifecycle-keyed FS materialization paths of one INCARNATION's secret family (plus its
69
+ * non-secret health file) — same projection rule as {@link agentSecretFilePaths}, built from the
70
+ * {@link agentIncarnationBase} so a retired incarnation's teardown can never address a same-alias
71
+ * successor's files. */
72
+ export declare function agentLifecycleSecretFilePaths(root: string, space: string, name: string, lifecycleUid: string): {
73
+ creds: string;
74
+ actorToken: string;
75
+ sentinelCreds: string;
76
+ health: string;
77
+ };
78
+ /**
79
+ * The store key of a materialized agent-secret FILE — the same projection the builders above apply,
80
+ * for callers that hold a RECORDED path (a resume inventory, a manifest ledger) rather than the
81
+ * (name, uid) coordinates: under mixed generations the recorded path is the truth, and its key must
82
+ * be derived from the SAME filename, never re-derived from the name alone (which would silently
83
+ * address a different generation's row).
84
+ *
85
+ * IT TAKES THE SPACE, AND CHECKS IT AGAINST THE PATH — the one signature change in this commit that
86
+ * is not bookkeeping. The segment could be read out of the recorded path instead, and nothing would
87
+ * have to change at a single call site; that is precisely why it must not be. A recorded path is
88
+ * caller-supplied data, and once the segment carries a tenant's identity, deriving it from that data
89
+ * means a record written for tenant A can name tenant B's segment and this function will hand back a
90
+ * key into it — which the manager then reads, overwrites, or DELETES. The space a caller is entitled
91
+ * to comes from its own authority (the manager's `space`, the ledger's), so it is passed in and the
92
+ * path is checked against it. A path outside the space's segment is refused rather than resolved,
93
+ * the same posture as the filename check that was already here.
94
+ */
95
+ export declare function agentSecretKeyForFile(path: string, space: string): string;
96
+ /**
97
+ * Enumerate the store keys of every per-agent standing secret currently materialized under this
98
+ * root — the reset/backstop sweep (`clean all`; despawn owns the primary delete). Deliberately
99
+ * filename-driven over the LOCAL creds dir: this surface is the FS composition (a hosted reset rides
100
+ * its own store), and a file only maps to a key if a valid spawn could have written it — health
101
+ * files and strays are left to the caller's raw cleanup.
102
+ *
103
+ * ROOT-WIDE AND MIGRATION-FREE, both deliberate. It takes no space because `clean all` resets the
104
+ * whole root, so it enumerates EVERY tenant's segment; and it is a DELETER (§3.1), so it addresses
105
+ * what is on disk rather than resolving through {@link agentCredsDir} — moving material into the
106
+ * path a sweep is about to delete is work done to undo itself, and a §2 rule 3 or 4 refusal must not
107
+ * fail a reset over material the reset does not care about.
108
+ *
109
+ * It reports BOTH levels: this space's segment (`auth/creds/space.<hex>/<file>`) and any pre-P1 file
110
+ * still sitting flat in the creds dir. Dropping the flat level would be the same defect this series
111
+ * exists to end, reintroduced in the sweeper — a reset that leaves an unmigrated root's agent creds
112
+ * on disk and reports success.
113
+ */
114
+ export declare function agentSecretKeysUnder(root: string): string[];
115
+ /**
116
+ * {@link agentSecretKeysUnder} narrowed to ONE tenant — what `cotal space rm` reaps, and what it can
117
+ * list before it commits to reaping.
118
+ *
119
+ * IT DOES NOT REPORT THE FLAT LEVEL, and that is the whole difference between the two. `clean all`
120
+ * resets the root, so it must name a pre-P1 file sitting directly in the creds dir or leave it
121
+ * behind. A per-space reap must NOT: a flat file names no tenant, so attributing it to the tenant
122
+ * being removed is a guess, and acting on the guess deletes what may be a survivor's live material.
123
+ * That case is refused up front by {@link assertAgentSecretsReapable} rather than resolved here.
124
+ *
125
+ * Migration-free for the same reason as the root-wide sweep: this is a DELETER (§3.1).
126
+ */
127
+ export declare function agentSecretKeysForSpace(root: string, space: string): string[];
128
+ /** The pre-P1 agent-secret files still sitting flat in this root's creds dir — what
129
+ * {@link assertAgentSecretsReapable} refuses over.
130
+ *
131
+ * It is {@link legacyAgentSecretFiles} at the one parent that matters, exported rather than
132
+ * re-derived so the door and the MIGRATION weigh the same set by construction: the files this
133
+ * refuses over are exactly the files the next {@link agentCredsDir} touch would move, which is the
134
+ * claim the refusal's text makes. Two implementations of "what counts as legacy" would drift at
135
+ * whichever one is not the one someone edits. */
136
+ export declare function unsegmentedAgentSecrets(root: string): string[];
137
+ /**
138
+ * `cotal space rm` STEP 1'S PRECONDITION for the step 7 agent-secret reap
139
+ * (`per-space-lifecycle.md` §2.2) — {@link assertSpaceMaterialReapable}'s P1 counterpart, asked at
140
+ * step 1 for the same reason: step 5 is the point of no return, so a precondition discovered at step
141
+ * 7 would refuse after the tenant's data is already gone, leave the journal entry that gates every
142
+ * other verb standing, and fail identically on every re-run of the removal it is supposed to let a
143
+ * crash finish.
144
+ *
145
+ * WHAT IT REFUSES: a root still holding pre-P1 agent secrets flat in `auth/creds`. P7's counterpart
146
+ * refuses over the same unattributability — the files name no tenant, so reaping around them strands
147
+ * what may be the departing tenant's and reaping them may take a survivor's — and that argument
148
+ * applies here unchanged.
149
+ *
150
+ * BUT P1'S CASE IS SHARPER, and this is the part that is not a translation of P7's. Those flat files
151
+ * are inert TODAY only because §2 rule 4 refuses to migrate on a multi-tenant root. Step 7 deletes
152
+ * the departing tenant's account record; on a two-tenant root that leaves ONE space in the inventory,
153
+ * which is exactly the condition under which rule 4 stops refusing. So the next `cotal spawn`, `mint`
154
+ * or `doctor auth` by the survivor resolves {@link agentCredsDir} and MOVES the departed tenant's
155
+ * secrets into the survivor's segment — where the layout now reads as an assertion that they are the
156
+ * survivor's. Removing a tenant is what converts material that is legibly unowned into a confident
157
+ * attribution nobody made, and it does so silently, later, from an unrelated verb. Refusing here is
158
+ * the only place that can be stopped while the evidence still exists.
159
+ *
160
+ * NOT YET CALLED: `cotal space rm` does not exist as a command today (§2.2 designs it; no `space`
161
+ * verb is implemented). This lands with the material it guards so the verb cannot be written without
162
+ * it, the same reason {@link assertSpaceMaterialReapable} landed with P7's reap.
163
+ */
164
+ export declare function assertAgentSecretsReapable(root: string, space: string, operation: string): void;
165
+ /**
166
+ * `cotal space rm` STEP 7 (`per-space-lifecycle.md` §2.2): reap ONE tenant's per-agent standing
167
+ * secrets — the residue that step used to LIST and leave behind, because before this series the
168
+ * creds dir named no tenant and reaping one space's would have risked a sibling's.
169
+ *
170
+ * IT CANNOT REFUSE, the same contract {@link reapSpaceMaterial} carries and for the same reason: it
171
+ * runs past step 5's point of no return, where a throw strands the journal entry and recurs
172
+ * identically on every re-run. Seam failures are returned, not thrown, and the caller must read
173
+ * `failed` or the material silently survives the tenant. Its precondition is
174
+ * {@link assertAgentSecretsReapable}, asked at step 1.
175
+ *
176
+ * THE ENUMERATION IS ALSO GUARDED, which P7's reap does not need. P7 sweeps a fixed array of four
177
+ * kinds; this set is OPEN and discovered by reading the segment, so the reap has an I/O step BEFORE
178
+ * its first seam call, and an unreadable segment there would throw straight out of a function that
179
+ * has promised not to. It is reported like any other failure instead. (The one input that could
180
+ * still throw is the space name, encoded once up front before anything has happened — and it comes
181
+ * from the verb's own inventory read, not from disk.)
182
+ *
183
+ * The seam deletes come FIRST and are addressed by the on-disk key, never through
184
+ * {@link agentCredsDir}: a reaper is a DELETER (§3.1), so it must not migrate material into the path
185
+ * it is about to remove — and on the multi-tenant root this verb runs on, that migration is the very
186
+ * mis-attribution the precondition exists to prevent. The store is asked because under a
187
+ * non-plain-file composition the key is where the material lives; the directory removal after it is
188
+ * what takes the non-key files in the segment (the `.auth-health.json` runtime state, which is never
189
+ * a store key) and anything a stray left behind.
190
+ *
191
+ * It removes only THIS space's segment, never `auth/creds` itself. That parent is the shared one —
192
+ * every tenant's segment is a child of it — so the glob-shaped reach a reader arrives at after
193
+ * seeing a directory removal would take every survivor's live agent secrets and report success.
194
+ *
195
+ * WHAT IT DOES NOT REACH: an INJECTED composition. The reap enumerates from the filesystem because
196
+ * {@link SecretStore} has no list operation and P1's key set is open — there is no fixed array to
197
+ * sweep the way P7 sweeps its four kinds — so a hosted deployment reaps a tenant's agent secrets
198
+ * through its own store's teardown. That is the same boundary {@link agentSecretKeysUnder} already
199
+ * draws for `clean all`, stated here rather than left to be discovered: a `removed` list from a root
200
+ * with no local segment is empty because there was nothing local, not because a store was checked.
201
+ */
202
+ export declare function reapAgentSecrets(root: string, space: string, secrets: SecretStore): Promise<{
203
+ removed: string[];
204
+ failed: string[];
205
+ }>;
206
+ //# sourceMappingURL=agent-secrets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-secrets.d.ts","sourceRoot":"","sources":["../src/agent-secrets.ts"],"names":[],"mappings":"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"}