@cotal-ai/workspace 0.34.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,252 @@
1
+ import { type SecretStore } from "@cotal-ai/core";
2
+ /** `<root>/.cotal` — the dir whose children the segment must never collide with. */
3
+ export declare function cotalDir(root: string): string;
4
+ /**
5
+ * The reserved children of `<root>/.cotal/` that the codebase itself writes.
6
+ *
7
+ * {@link spaceSegment}'s collision guarantee was written against the reserved siblings of the AUTH
8
+ * dir. P7 puts a segment directly under `.cotal/`, which is a WIDER namespace, so the guarantee has
9
+ * to hold there too. It does today — no `.cotal` child begins with `space.`, the nearest being
10
+ * `auth-service.<spaceKey>.pid` — but it held by ACCIDENT until something asserted it, which is what
11
+ * `smoke:space-segmentation` now does.
12
+ *
13
+ * Keep in sync with the writers of `.cotal/` children (`cotalPath(...)` in `up.ts`, the raw removal
14
+ * list at `clean.ts:272-279`). A name added here that starts with `space.` is a real collision and
15
+ * the guard suite fails rather than the layout silently aliasing a tenant's segment.
16
+ */
17
+ export declare const RESERVED_COTAL_CHILDREN: readonly string[];
18
+ /** The delivery daemon's scoped cred — the KIND, i.e. the basename its location ends in and the
19
+ * name every operator-facing string spells. Its store key is `space.<hex>/delivery.creds`, built by
20
+ * {@link deliveryCredsKey}; the bare kind is also the LEGACY root-scoped key this series migrates
21
+ * away from, which is why one constant serves both (a second literal is how the two would drift).
22
+ * Lives in workspace because the key↔filename convention is the workspace layout's; implementations
23
+ * never import each other. */
24
+ export declare const DELIVERY_CREDS_KIND = "delivery.creds";
25
+ /** The membership feed's data-account rw cred kind — same discipline as {@link DELIVERY_CREDS_KIND}.
26
+ * Named (not a bare literal) so the renewal owner can map a remint result back to the daemon's
27
+ * `membership` component without a hand-copied string. */
28
+ export declare const MEMBERSHIP_RW_CREDS_KIND = "membership-rw.creds";
29
+ /** The `$SYS` CONNZ observer's kind — the graph feed's read connection and the account-scoped sweep
30
+ * every liveness/eviction verdict is measured against.
31
+ *
32
+ * NOT in `REMINTABLE_DAEMON_CREDS` and never will be: this is `rotation-renewed`, so no persisted
33
+ * seed can re-sign it (the `$SYS` signing seed is discarded at provision). The key exists so a
34
+ * HOSTED composition can inject the cred a system-account rotation minted; it does not make the
35
+ * cred renewable. See `docs/design/u3-membership-sys-injection.md` §2. */
36
+ export declare const MEMBERSHIP_OBSERVER_CREDS_KIND = "membership-observer.creds";
37
+ /** The `$SYS` KICK-only evictor's kind — the write half of live eviction, paired with
38
+ * {@link MEMBERSHIP_OBSERVER_CREDS_KIND} by one rotation and read per call.
39
+ *
40
+ * Same `rotation-renewed` posture and the same non-membership of `REMINTABLE_DAEMON_CREDS`. Its
41
+ * permission (`$SYS.REQ.SERVER.*.KICK`) carries NO account, so unlike the observer it cannot be
42
+ * tenancy-checked from its own JWT; its containment is that every cid it is handed comes from the
43
+ * observer's account-scoped scan. Keep the two spelled together for that reason. */
44
+ export declare const CONNECTION_EVICTOR_CREDS_KIND = "connection-evictor.creds";
45
+ /** The DATA account id the CONNZ/event subjects pin — non-secret, but kept 0600 beside the creds.
46
+ * The only P7 kind with no store reader: it is read raw by the eviction path's workstation
47
+ * cross-check, so its resolver returns a PATH ({@link membershipConfigPath}) and has no hosted arm
48
+ * at all. */
49
+ export declare const MEMBERSHIP_CONFIG_KIND = "membership.json";
50
+ /** The P7 kinds' ROOT-SCOPED locations — the legacy layout this series retires. The two store keys
51
+ * (`membership-rw.creds`, `delivery.creds`) appear as plain names because under the local FS
52
+ * composition a key IS a path under `.cotal/`; see {@link migrateLegacyCotalMaterial} on why the
53
+ * migration is FS-composition-only. `delivery.creds` is here by the §3.2 widening. */
54
+ export declare const P7_LEGACY_MATERIAL: readonly string[];
55
+ /** Why the choke point would refuse to migrate on a root, in two parts a caller composes into its
56
+ * own sentence: the {@link reason} it cannot be shown to hold one tenant, and the {@link remedy} an
57
+ * operator actually has — which is sometimes NONE, and saying so is the point. */
58
+ export interface SpaceMaterialMigrationRefusal {
59
+ /** The root's tenant state, phrased to be quoted after a caller's own "refusing to X:" prefix. */
60
+ reason: string;
61
+ /** What the operator can do. Where nothing works this says so and names no command. */
62
+ remedy: string;
63
+ }
64
+ /**
65
+ * §2 RULE 4'S PRECONDITION, ASKABLE: would {@link migrateLegacyCotalMaterial} refuse on this root,
66
+ * and why? `undefined` means the root can be shown to hold exactly one tenant.
67
+ *
68
+ * THE ONE IMPLEMENTATION of the rule, exported so a guard that needs to know whether `cotal up` can
69
+ * migrate this root ASKS instead of re-deriving a tenant count. That is commit 2's `repairAdvice`
70
+ * lesson (`sys-creds.ts`) applied where it was still owed: a count is a second implementation of the
71
+ * rule, and it reads "one" on the corrupt-inventory root where this fails CLOSED. A guard built on a
72
+ * count would print a remedy on exactly the root where the remedy refuses.
73
+ *
74
+ * The two callers need it for opposite reasons — the choke point to refuse, the doors below to say
75
+ * truthfully what an operator can do next — and a rule with two implementations drifts at whichever
76
+ * one is not the one someone edits.
77
+ */
78
+ export declare function spaceMaterialMigrationRefusal(root: string): SpaceMaterialMigrationRefusal | undefined;
79
+ /**
80
+ * THE CHOKE POINT (§2 rules 1-4): resolve one kind's per-space location, migrating a legacy
81
+ * root-scoped copy into it on first touch, or REFUSING when the move cannot be made honestly.
82
+ *
83
+ * Returns the canonical path. A caller must obtain the location from here and never build it
84
+ * itself — that is rule 1, and it is what makes "migrate on first touch" reach every flow rather
85
+ * than the ones someone remembered to update.
86
+ *
87
+ * FS COMPOSITION ONLY, and the signature says so rather than the comment alone: this takes a root
88
+ * PATH and no `SecretStore`, so a hosted composition cannot call it. Rule 2's atomicity — the move
89
+ * is one `renameSync`, so a crash leaves each kind wholly legacy or wholly canonical — is a property
90
+ * of the filesystem, and the same move against a hosted store would be a get, put and delete with no
91
+ * atomicity across the three. That is not a gap: a hosted composition provisions these keys
92
+ * externally and re-keys by the coordinated change of §3.1, never by migrating in place. Taking no
93
+ * store makes the unsound call impossible to express, the same reason `rotateSystemCreds` takes none
94
+ * (`system-rotation.ts:88-95`).
95
+ *
96
+ * `parent` is the directory the legacy copy sits DIRECTLY in and under which the segment is created.
97
+ * It is a parameter rather than `cotalDir(root)` because §3 settled two different placements for the
98
+ * two series — P7's segment is a child of `.cotal/`, P1's a child of `.cotal/auth/creds/` — and that
99
+ * was decided in the plan, not discovered by P1. Everything the rules turn on is the same at both
100
+ * placements, so parameterizing the parent is what keeps them ONE implementation; a P1-local copy of
101
+ * rules 2-4 is the second idiom §4 forbids. `root` stays alongside it because rule 4's tenant count
102
+ * is a property of the ROOT's account records, not of whichever directory the material sits in.
103
+ */
104
+ export declare function migrateLegacyMaterialIn(parent: string, root: string, space: string, kind: string): string;
105
+ /** {@link migrateLegacyMaterialIn} at P7's placement — a legacy copy sitting directly under
106
+ * `<root>/.cotal/`. The five P7 resolvers' one entry point. */
107
+ export declare function migrateLegacyCotalMaterial(root: string, space: string, kind: string): string;
108
+ /**
109
+ * THE `space add` DOOR (§2.1): refuse to add a second tenant to a root that still holds unmigrated
110
+ * root-scoped material.
111
+ *
112
+ * Adding a tenant to such a root creates the one state segmentation cannot resolve — legacy material
113
+ * whose owner is unrecorded — and it is the only door that creates it, because `up` cannot mint a
114
+ * second tenant on an established root (`ensureRootForSpace` refuses at `up.ts:2233`). Checking here
115
+ * costs one inventory read in a verb that is already taking the lock and reading the inventory
116
+ * (`per-space-lifecycle.md` §2.1 step 1).
117
+ *
118
+ * This keeps that state from being CREATED. It does not keep it from being ENCOUNTERED — roots
119
+ * already multi-tenant when this series lands, and backups of them, bypass the door entirely. Rule 4
120
+ * of {@link migrateLegacyCotalMaterial} is what catches those. The two are one design and neither is
121
+ * sufficient alone.
122
+ *
123
+ * NOT YET CALLED: `cotal space add` does not exist as a command today (the verb is designed in
124
+ * `per-space-lifecycle.md` §2.1 and not implemented). This is the guarantee it must call when it is
125
+ * built, landed with the foundation so the verb cannot be written without it.
126
+ */
127
+ export declare function assertNoUnsegmentedLegacyMaterial(root: string, operation: string): void;
128
+ /** The P7 kinds still sitting at their root-scoped location on this root — the unmigrated set both
129
+ * doors weigh. Named rather than inlined twice because the two doors must agree on what counts. */
130
+ export declare function unsegmentedLegacyMaterial(root: string): string[];
131
+ /**
132
+ * WHICH COMPOSITION IS ASKING — the one input the per-kind resolvers below cannot infer.
133
+ *
134
+ * A UNION, not a `root?: string`, for the reason `SysCredsSource` is one (`sys-creds.ts`): the two
135
+ * arms answer differently and the workstation arm cannot do its job without a root. Rule 2's
136
+ * migration is FILESYSTEM-ONLY, so a hosted caller must get the canonical key and no rename; a
137
+ * workstation caller must get the migration, because skipping it is not a cosmetic miss — the kinds
138
+ * have absent-means-mint writers (`up.ts:2885`, `up.ts:2889`), so a canonical read on an unmigrated
139
+ * root reads ABSENT and mints a SECOND live cred beside the one the daemons are using. An optional
140
+ * root would let a workstation caller silently take the hosted answer and land in exactly that
141
+ * state. Requiring the root on that arm makes the unsound call impossible to express instead.
142
+ *
143
+ * `injected` is always the composition root's own fact — never inferred by probing the store or
144
+ * sniffing `.cotal/`, both of which report "workstation" for a hosted daemon.
145
+ */
146
+ export type SpaceMaterialComposition = {
147
+ injected: true;
148
+ root?: undefined;
149
+ } | {
150
+ injected: false;
151
+ root: string;
152
+ };
153
+ /**
154
+ * The KEY SHAPE alone — `space.<hex>/<kind>` — resolving nothing and moving nothing.
155
+ *
156
+ * THE ONE SPELLING of the segmented key, so {@link spaceMaterialKey} and the two owners below cannot
157
+ * drift into two layouts. It is exported for the owners that must NOT move material, and there are
158
+ * exactly two kinds of those:
159
+ *
160
+ * - DELETERS. `clean`'s store-seam sweep names the keys it is about to remove; migrating material
161
+ * into the path it will then delete is work done to undo itself, and on the refusal paths (§2
162
+ * rules 3 and 4) it would fail the sweep for material the sweep does not care about.
163
+ * - THE RENEWAL OWNER. {@link REMINTABLE_DAEMON_CREDS}'s `(space) => key` builders (§3.1) feed
164
+ * `remintDaemonCreds`, which has NO absent-means-mint path — its absence case is a loud
165
+ * `skipped: "missing-file"`, never a second cred — and which may hold an INJECTED store while
166
+ * still being handed a workstation `root` it does not own (`manager.ts:870` defaults the store,
167
+ * so `store !== undefined` is not the hosted fact there). The hazard rule 1 exists to stop is not
168
+ * reachable from it, and `up`'s provisioners migrate before any daemon exists to renew.
169
+ *
170
+ * Every other caller wants {@link spaceMaterialKey}: reaching for this one to skip a migration is
171
+ * the read-fallback the design forbids.
172
+ */
173
+ export declare function segmentedKey(kind: string, space: string): string;
174
+ /**
175
+ * THE PER-KIND RESOLVER (§2 rule 1), generic over the kind: the canonical store key for `kind` in
176
+ * `space`, having migrated a legacy root-scoped copy into it first on the FS composition.
177
+ *
178
+ * The named per-kind wrappers below are the surface callers use; this is the one body they share, so
179
+ * "migrate on first touch" cannot exist for four kinds and be forgotten for the fifth. Under the
180
+ * local FS composition the returned key IS the path under `.cotal/` that
181
+ * {@link migrateLegacyCotalMaterial} just moved the material to — the two agree by construction
182
+ * rather than by two spellings of the same layout.
183
+ */
184
+ export declare function spaceMaterialKey(kind: string, space: string, composition: SpaceMaterialComposition): string;
185
+ /** {@link DELIVERY_CREDS_KIND}'s key for `space` — see {@link spaceMaterialKey}. */
186
+ export declare function deliveryCredsKey(space: string, composition: SpaceMaterialComposition): string;
187
+ /** {@link MEMBERSHIP_RW_CREDS_KIND}'s key for `space` — see {@link spaceMaterialKey}. */
188
+ export declare function membershipRwCredsKey(space: string, composition: SpaceMaterialComposition): string;
189
+ /** {@link MEMBERSHIP_OBSERVER_CREDS_KIND}'s key for `space` — see {@link spaceMaterialKey}. */
190
+ export declare function membershipObserverCredsKey(space: string, composition: SpaceMaterialComposition): string;
191
+ /** {@link CONNECTION_EVICTOR_CREDS_KIND}'s key for `space` — see {@link spaceMaterialKey}. */
192
+ export declare function connectionEvictorCredsKey(space: string, composition: SpaceMaterialComposition): string;
193
+ /** {@link MEMBERSHIP_CONFIG_KIND}'s PATH for `space`, migrated on first touch.
194
+ *
195
+ * A path and not a key, and it takes a bare `root` rather than a {@link SpaceMaterialComposition},
196
+ * because this kind has no hosted arm to choose between: it is read raw by the eviction path's
197
+ * workstation-only cross-check (`evict-exec.ts:76`) and a hosted composition never has one. */
198
+ export declare function membershipConfigPath(root: string, space: string): string;
199
+ /** The per-space area itself, `<root>/.cotal/space.<hex>/` — for the enumerating callers (the
200
+ * `clean` sweep) that remove a tenant's whole segment rather than one kind of it. Resolves NOTHING
201
+ * and migrates NOTHING: a sweeper must not move material it is about to delete. */
202
+ export declare function spaceMaterialDir(root: string, space: string): string;
203
+ /**
204
+ * `cotal space rm` STEP 1'S PRECONDITION for the step 7 reap (`per-space-lifecycle.md` §2.2), which
205
+ * is deliberately not checked at step 7.
206
+ *
207
+ * THE ORDERING IS THE DESIGN. Step 5 is the point of no return: it deletes the tenant's streams and
208
+ * buckets. Step 7 is the local reap. A precondition discovered at step 7 would refuse AFTER the data
209
+ * is gone and the config re-rendered, leaving the journal entry standing — and since the check would
210
+ * fail identically on every re-run, the removal a crash is supposed to be able to finish could never
211
+ * finish at all. So the question is asked at step 1, beside the inventory read that is already
212
+ * happening, where a refusal costs the operator nothing.
213
+ *
214
+ * WHAT IT REFUSES: a root still holding root-scoped material for any P7 kind. `space rm` runs only on
215
+ * a multi-tenant root (§2.2 step 2 refuses the last tenant), and on such a root that material is
216
+ * unattributable in the §2.1 sense — it belongs to whichever tenant booted first, unrecorded. Reaping
217
+ * around it strands what may be the departing tenant's live `$SYS` pair for a survivor to inherit;
218
+ * reaping it may take a survivor's. There is no third answer, and the honest move is to refuse before
219
+ * anything is destroyed rather than to pick one silently.
220
+ *
221
+ * NOT YET CALLED: `cotal space rm` does not exist as a command today (§2.2 designs it; no `space`
222
+ * verb is implemented). This lands with the material it guards so the verb cannot be written without
223
+ * it, the same reason {@link assertNoUnsegmentedLegacyMaterial} landed with commit 1.
224
+ */
225
+ export declare function assertSpaceMaterialReapable(root: string, space: string, operation: string): void;
226
+ /**
227
+ * `cotal space rm` STEP 7 (`per-space-lifecycle.md` §2.2): reap ONE tenant's segmented material — the
228
+ * `$SYS` pair that step names, plus the rest of that tenant's segment, which P7 keyed alongside it.
229
+ *
230
+ * IT CANNOT REFUSE, and that is a contract, not an omission. It runs past step 5's point of no
231
+ * return, where a throw would strand the journal entry that gates every other verb on the root; §2.2
232
+ * relies on steps 5 to 7 being individually idempotent so a re-run after a crash FINISHES the
233
+ * removal. So seam failures are returned, not thrown — the same posture, for the same reason, as
234
+ * `remintDaemonCreds` (`renewal.ts`), and the caller must read `failed` or the material silently
235
+ * survives the tenant. Its precondition is {@link assertSpaceMaterialReapable}, asked at step 1.
236
+ *
237
+ * The seam deletes come FIRST and are addressed by {@link segmentedKey}, never by a resolver: a
238
+ * reaper is a DELETER, so it must not move material into the path it is about to remove, and a §2
239
+ * rule 3/4 refusal must not fail a reap over material the reap does not care about. It sweeps every
240
+ * store-backed kind rather than only the two `clean` does, because `clean` is a whole-root reset with
241
+ * a raw sweep of `.cotal/` to fall back on and this is not: for an INJECTED store the segment removal
242
+ * below reaches nothing, so a kind missing from the seam loop would outlive its tenant.
243
+ *
244
+ * It removes only THIS space's segment. A reap spelled `.cotal/space.*` is the shape a reader reaches
245
+ * for after seeing a directory removal, and on the multi-tenant root that is the only root this verb
246
+ * runs on it would take every surviving tenant's live material and report success.
247
+ */
248
+ export declare function reapSpaceMaterial(root: string, space: string, secrets: SecretStore): Promise<{
249
+ removed: string[];
250
+ failed: string[];
251
+ }>;
252
+ //# sourceMappingURL=space-segmentation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"space-segmentation.d.ts","sourceRoot":"","sources":["../src/space-segmentation.ts"],"names":[],"mappings":"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"}