@intentic/sandbox-contract 1.217.0 → 1.218.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.
- package/dist/publish-drafts.js +1 -1
- package/dist/publish-drafts.js.map +1 -1
- package/dist/schemas.d.ts +6 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +2 -1
- package/dist/schemas.js.map +1 -1
- package/dist/workspace-state.d.ts +64 -54
- package/dist/workspace-state.d.ts.map +1 -1
- package/dist/workspace-state.js +93 -64
- package/dist/workspace-state.js.map +1 -1
- package/package.json +4 -4
- package/src/contracts/extensions.contract.ts +1 -1
- package/src/contracts/settings.contract.ts +1 -1
- package/src/events.ts +2 -2
- package/src/publish-drafts.ts +1 -1
- package/src/schemas.ts +45 -39
- package/src/workspace-state.test.ts +226 -79
- package/src/workspace-state.ts +240 -68
package/src/workspace-state.ts
CHANGED
|
@@ -37,9 +37,9 @@ import type { StateFile } from "./state-portability.js";
|
|
|
37
37
|
*
|
|
38
38
|
* `path` is workspace-root-relative, forward-slash — the space `workspaceChanged` paths arrive in. Matching is
|
|
39
39
|
* by PREFIX, which lets one entry cover three shapes without a second matching rule:
|
|
40
|
-
* - an exact file `.intentic/settings.json`
|
|
41
|
-
* - a directory `.intentic/drafts/` (one file per draft)
|
|
42
|
-
* - a name family `.intentic/environment.custom.` (…Dockerfile and anything later named beside it)
|
|
40
|
+
* - an exact file `.intentic/config/settings.json`
|
|
41
|
+
* - a directory `.intentic/config/drafts/` (one file per draft)
|
|
42
|
+
* - a name family `.intentic/config/environment.custom.` (…Dockerfile and anything later named beside it)
|
|
43
43
|
* A directory entry keeps its trailing slash so it can never prefix-match a sibling file. Entries may NEST —
|
|
44
44
|
* see stateFileFor, which resolves the longest match rather than the first. */
|
|
45
45
|
export interface WorkspaceStateFile extends StateFile {
|
|
@@ -94,6 +94,26 @@ export interface WorkspaceStateFile extends StateFile {
|
|
|
94
94
|
* hand-keeping a deny list that goes stale the day a store is added (which is how a 98 kB loop ledger and
|
|
95
95
|
* whole third-party extension checkouts ended up ranking in code search). */
|
|
96
96
|
readonly authored?: true;
|
|
97
|
+
/* WHETHER DESKTOP-SYNC COPIES THIS DOWN — the fifth question, and the one `portability` cannot answer even
|
|
98
|
+
* though it looks like it should.
|
|
99
|
+
*
|
|
100
|
+
* Portability asks whether a piece of state may RESTORE into a different sandbox. Backup asks whether it may
|
|
101
|
+
* be COPIED to the owner's own machine so the loss of this sandbox is not the loss of the work. Those read as
|
|
102
|
+
* the same question and are not: `members.json` is the clearest case, and its own entry argues the half that
|
|
103
|
+
* was already written down — an access list that TRAVELLED would let a source sandbox hand itself the
|
|
104
|
+
* target's ownership, which is why it may never be `carry`. None of that reasoning says the owner may not
|
|
105
|
+
* hold a copy of who could drive their own sandbox. Conflating the two is what made the sync ignore the state
|
|
106
|
+
* dir wholesale, so a sandbox that went away took every persona, skill, draft and transcript with it.
|
|
107
|
+
*
|
|
108
|
+
* SO IT DERIVES, and this flag exists only to carve out the entry where the derivation is wrong. The default
|
|
109
|
+
* (backedUp below) is `carry` plus `identity`: ordinary state and the small records that bind this sandbox to
|
|
110
|
+
* its owner. `derived` is excluded for size — it is the caches, the checkouts and the browser profiles, all
|
|
111
|
+
* rebuildable and all bulk — and `secret` is excluded because a credential's blast radius is the number of
|
|
112
|
+
* places it exists, and a laptop is one more place.
|
|
113
|
+
*
|
|
114
|
+
* `false` is the only value: an entry either accepts the derived answer or opts out of the copy, and there is
|
|
115
|
+
* no entry that needs opting IN against its class. Anything opting out says why on the entry. */
|
|
116
|
+
readonly backup?: false;
|
|
97
117
|
/* WHO BUILDS THIS TREE, when it is not the daemon — an extension, pnpm, another process entirely. Declared
|
|
98
118
|
* on the entry because the coverage guard's second direction ("every declared entry is built somewhere in
|
|
99
119
|
* the daemon") is only meaningful for entries the daemon owns: one it can never build must say who does, or
|
|
@@ -134,7 +154,7 @@ const STATE_FILES = [
|
|
|
134
154
|
* credential — Komodo's api key beside its api secret, which its own connector card calls "like a database
|
|
135
155
|
* user" — is echoed, and therefore lands in the diff exactly as a database username would. */
|
|
136
156
|
{
|
|
137
|
-
path: ".intentic/capabilities.json",
|
|
157
|
+
path: ".intentic/config/capabilities.json",
|
|
138
158
|
invalidates: ["capabilities", "environment", "panels", "manifests"],
|
|
139
159
|
portability: "carry",
|
|
140
160
|
versioned: true,
|
|
@@ -145,14 +165,14 @@ const STATE_FILES = [
|
|
|
145
165
|
* it travels because a decision about what this workspace does NOT need is as much the owner's as the
|
|
146
166
|
* connections themselves — an export that dropped it would greet them on the target with the same
|
|
147
167
|
* suggestions they had already dismissed. Holds no credential: it is a card name and a file path. */
|
|
148
|
-
{ path: ".intentic/capability-dismissals.json", invalidates: ["capabilities"], portability: "carry", versioned: true },
|
|
168
|
+
{ path: ".intentic/config/capability-dismissals.json", invalidates: ["capabilities"], portability: "carry", versioned: true },
|
|
149
169
|
|
|
150
170
|
/* The secret use ledger — one row per moment the agent's exits spent a stored secret (a `{{secret:name}}`
|
|
151
171
|
* reference resolved into a shell command, a value typed into a browser field), joined onto the secrets
|
|
152
172
|
* inventory as each entry's "last used" (sandbox's secrets/secret-uses.ts). Holds names and destinations,
|
|
153
173
|
* never values — which is why it may `carry`: like the automations' run ledger, a use history is about the
|
|
154
174
|
* secrets, and an export that dropped it would arrive claiming none had ever been touched. */
|
|
155
|
-
{ path: ".intentic/secret-uses.json", invalidates: ["secrets"], portability: "carry" },
|
|
175
|
+
{ path: ".intentic/records/secret-uses.json", invalidates: ["secrets"], portability: "carry" },
|
|
156
176
|
|
|
157
177
|
/* The named personas this sandbox shows the outside world — which connected accounts each one speaks for,
|
|
158
178
|
* what a session wearing it may do, where it works (schemas.ts PersonaSchema). It invalidates `capabilities` as well as
|
|
@@ -166,7 +186,7 @@ const STATE_FILES = [
|
|
|
166
186
|
* argument it was carved out on — a card is configuration, holds no secret, and belongs in review — is the
|
|
167
187
|
* one `versioned` now generalises to the rest of the config slice (personas/personas-store.ts argues it at
|
|
168
188
|
* length, and its reasoning is why the flag exists rather than a second hand-kept list). */
|
|
169
|
-
{ path: ".intentic/personas.json", invalidates: ["personas", "capabilities", "manifests"], portability: "carry", versioned: true },
|
|
189
|
+
{ path: ".intentic/config/personas.json", invalidates: ["personas", "capabilities", "manifests"], portability: "carry", versioned: true },
|
|
170
190
|
|
|
171
191
|
/* The overlay Dockerfile, four files that a single `.intentic/environment.` prefix used to cover. They are
|
|
172
192
|
* split here because they answer PORTABILITY differently while answering invalidation identically, and the
|
|
@@ -178,22 +198,22 @@ const STATE_FILES = [
|
|
|
178
198
|
* not be on (see composeEnvironment's baseImageOf);
|
|
179
199
|
* - the proposal and the per-tool drafts under environment.d/ are the agent's pending requests, which the
|
|
180
200
|
* owner has not answered yet; they travel so the question survives the move. */
|
|
181
|
-
{ path: ".intentic/environment.custom.Dockerfile", invalidates: ["environment"], portability: "carry", versioned: true },
|
|
182
|
-
{ path: ".intentic/environment.Dockerfile", invalidates: ["environment"], portability: "carry", versioned: true },
|
|
183
|
-
{ path: ".intentic/environment.d/", invalidates: ["environment"], portability: "carry", versioned: true },
|
|
201
|
+
{ path: ".intentic/config/environment.custom.Dockerfile", invalidates: ["environment"], portability: "carry", versioned: true },
|
|
202
|
+
{ path: ".intentic/config/environment.Dockerfile", invalidates: ["environment"], portability: "carry", versioned: true },
|
|
203
|
+
{ path: ".intentic/config/environment.d/", invalidates: ["environment"], portability: "carry", versioned: true },
|
|
184
204
|
{
|
|
185
|
-
path: ".intentic/environment.approved.Dockerfile",
|
|
205
|
+
path: ".intentic/local/environment.approved.Dockerfile",
|
|
186
206
|
invalidates: ["environment"],
|
|
187
207
|
portability: "derived",
|
|
188
208
|
note: "The target composes its own overlay on first boot; rebuild it there to install the tools it names.",
|
|
189
209
|
},
|
|
190
210
|
|
|
191
|
-
{ path: ".intentic/settings.json", invalidates: ["settings", "manifests"], portability: "carry", versioned: true },
|
|
211
|
+
{ path: ".intentic/config/settings.json", invalidates: ["settings", "manifests"], portability: "carry", versioned: true },
|
|
192
212
|
// The rule table's last-fired stamps, beside the rules themselves. `derived` rather than `carry`: it is a
|
|
193
213
|
// record of what happened in THIS sandbox, and carrying it to a fresh one would date every rule to work
|
|
194
214
|
// that machine never did.
|
|
195
215
|
{
|
|
196
|
-
path: ".intentic/rule-firings.json",
|
|
216
|
+
path: ".intentic/local/rule-firings.json",
|
|
197
217
|
invalidates: ["rule-firings"],
|
|
198
218
|
portability: "derived",
|
|
199
219
|
note: "Stamps of when each rule last did something; the new sandbox starts its own record.",
|
|
@@ -213,13 +233,13 @@ const STATE_FILES = [
|
|
|
213
233
|
* `postedAt`/`postedUrl` stamped at the end), and it is KEPT afterwards rather than consumed — so tracking
|
|
214
234
|
* yields a durable record instead of the add/delete churn a queue would produce. Nothing in a draft is a
|
|
215
235
|
* credential: it is a platform, a target URL and the body itself, all of it bound for publication anyway. */
|
|
216
|
-
{ path: ".intentic/drafts/", invalidates: ["drafts"], portability: "carry", versioned: true, authored: true },
|
|
236
|
+
{ path: ".intentic/config/drafts/", invalidates: ["drafts"], portability: "carry", versioned: true, authored: true },
|
|
217
237
|
// ---- declared by the extension that renders them (contributes.files), not here ----
|
|
218
238
|
// The path is the DAEMON's (automations-store writes both), the query keys are the intentic.automations
|
|
219
239
|
// extension's. It declares them in its own manifest and the browser unions the two lists, so uninstalling
|
|
220
240
|
// the extension takes its invalidations with it instead of leaving a rule for a view that no longer exists.
|
|
221
241
|
{
|
|
222
|
-
path: ".intentic/automations.json",
|
|
242
|
+
path: ".intentic/config/automations.json",
|
|
223
243
|
invalidates: [],
|
|
224
244
|
why: "Declared by the intentic.automations extension's contributes.files — `automations` is its query key, not core's.",
|
|
225
245
|
portability: "carry",
|
|
@@ -239,13 +259,13 @@ const STATE_FILES = [
|
|
|
239
259
|
* rather than inherited, because the row renders its run history from this file now: without its own entry
|
|
240
260
|
* a completed run would stop refreshing the view the moment it stopped living in automations.json. */
|
|
241
261
|
{
|
|
242
|
-
path: ".intentic/automation-runs.json",
|
|
262
|
+
path: ".intentic/records/automation-runs.json",
|
|
243
263
|
invalidates: [],
|
|
244
264
|
why: "Declared by the intentic.automations extension's contributes.files — `automations` is its query key, not core's.",
|
|
245
265
|
portability: "carry",
|
|
246
266
|
},
|
|
247
267
|
{
|
|
248
|
-
path: ".intentic/approvals/",
|
|
268
|
+
path: ".intentic/records/approvals/",
|
|
249
269
|
invalidates: [],
|
|
250
270
|
why: "Declared by the intentic.automations extension's contributes.files — `automation-approvals` is its query key, not core's.",
|
|
251
271
|
portability: "carry",
|
|
@@ -256,7 +276,7 @@ const STATE_FILES = [
|
|
|
256
276
|
* evidence about this workspace, so `carry` like the run ledgers: an export that dropped it would arrive
|
|
257
277
|
* claiming no chore had ever been checked. */
|
|
258
278
|
{
|
|
259
|
-
path: ".intentic/chores/",
|
|
279
|
+
path: ".intentic/records/chores/",
|
|
260
280
|
invalidates: [],
|
|
261
281
|
why: "Declared by the intentic.maintenance extension's contributes.files — `maintenance-report`/`maintenance-runs` are its query keys, not core's.",
|
|
262
282
|
portability: "carry",
|
|
@@ -266,7 +286,7 @@ const STATE_FILES = [
|
|
|
266
286
|
* these are draft READMEs, drafts-shaped in every way that matters, and "find the staged page about X" is
|
|
267
287
|
* as ordinary a search as finding a post draft. */
|
|
268
288
|
{
|
|
269
|
-
path: ".intentic/docs/",
|
|
289
|
+
path: ".intentic/config/docs/",
|
|
270
290
|
invalidates: [],
|
|
271
291
|
why: "Declared by the intentic.documentation extension's contributes.files — `documentation`/`documentation-runs` are its query keys, not core's.",
|
|
272
292
|
portability: "carry",
|
|
@@ -280,17 +300,17 @@ const STATE_FILES = [
|
|
|
280
300
|
* run surfaces have: the scheduler writes the ledger several times per step and nothing polls for it.
|
|
281
301
|
* The runs file invalidates `workflows` too, because GET /workflows embeds each design's runs
|
|
282
302
|
* (WorkflowSummary) — a settled step changes that answer as surely as an edited design does. */
|
|
283
|
-
{ path: ".intentic/workflows.json", invalidates: ["workflows"], portability: "carry", versioned: true },
|
|
284
|
-
{ path: ".intentic/workflow-runs.json", invalidates: ["workflows", "workflow-runs"], portability: "carry" },
|
|
303
|
+
{ path: ".intentic/config/workflows.json", invalidates: ["workflows"], portability: "carry", versioned: true },
|
|
304
|
+
{ path: ".intentic/records/workflow-runs.json", invalidates: ["workflows", "workflow-runs"], portability: "carry" },
|
|
285
305
|
/* The SAVED loops, which are a manifest and so the opposite of the ledger below them: a handful of entries
|
|
286
306
|
* a person authors, read by two surfaces at once — the workflows page that owns them, and every chat
|
|
287
307
|
* composer's loop picker. Those two are in different windows as often as not (a popped-out chat is its own
|
|
288
308
|
* window), so an edit made on the page has to reach a picker nobody is going to think to reopen. A CORE key
|
|
289
309
|
* rather than the workflows extension's, for the reason the workflow designs beside it are: the composer
|
|
290
310
|
* lists saved loops whether or not that extension is switched on. */
|
|
291
|
-
{ path: ".intentic/loop-designs.json", invalidates: ["loop-designs"], portability: "carry", versioned: true },
|
|
311
|
+
{ path: ".intentic/config/loop-designs.json", invalidates: ["loop-designs"], portability: "carry", versioned: true },
|
|
292
312
|
{
|
|
293
|
-
path: ".intentic/loops.json",
|
|
313
|
+
path: ".intentic/records/loops.json",
|
|
294
314
|
invalidates: [],
|
|
295
315
|
why: "Ralph loops and their iteration history. Nothing observes it: where a RUNNING loop stands rides on the fleet roster (AgentSummary.loop), which the /events stream already pushes about once a second, and a second source invalidating on this file could only ever disagree with the card beside it. The iteration list of an ENDED loop is an on-demand read — nothing renders it until someone opens it (web's useLoops, which holds no query for exactly this reason).",
|
|
296
316
|
portability: "carry",
|
|
@@ -303,15 +323,15 @@ const STATE_FILES = [
|
|
|
303
323
|
* query uses would put the drift this table exists to remove straight back into it. Each says which
|
|
304
324
|
* constraint would have to move first, so the next reader doesn't re-derive it. */
|
|
305
325
|
{
|
|
306
|
-
path: ".intentic/webchat-installs.json",
|
|
326
|
+
path: ".intentic/records/webchat-installs.json",
|
|
307
327
|
invalidates: [],
|
|
308
|
-
why: "Which origins have loaded a
|
|
328
|
+
why: "Which origins have loaded a Front Desk's widget, written on a 30s flush timer while a customer's site serves page views. The install panel that renders it fetches on open and polls itself while it is on screen, which is the whole window in which the answer changes for anyone. Pushing instead would bill every connected browser a refetch per flush, for a panel almost nobody has open.",
|
|
309
329
|
portability: "carry",
|
|
310
330
|
},
|
|
311
331
|
{
|
|
312
|
-
path: ".intentic/thread-sessions.json",
|
|
332
|
+
path: ".intentic/records/thread-sessions.json",
|
|
313
333
|
invalidates: [],
|
|
314
|
-
why: "Thread bookkeeping (an inbound thread — a
|
|
334
|
+
why: "Thread bookkeeping (an inbound thread — a Front Desk visitor, a Discord or Slack channel — → sandbox conversation + provider session), written on EVERY inbound message. Nothing in the browser reads it: what a thread produces is a conversation, and the fleet board already learns about that from the agent registry's own push. Naming a key here would bill every connected browser a refetch per inbound message — the request storm this table's own note warns about — to refresh nothing it can see.",
|
|
315
335
|
portability: "carry",
|
|
316
336
|
},
|
|
317
337
|
/* SPLIT, so that "what an extension is configured to do" and "the token it does it with" stop being one file.
|
|
@@ -336,7 +356,7 @@ const STATE_FILES = [
|
|
|
336
356
|
* that carries can never show one. "Re-enter the credentials" is now the vault's instruction to give, and the
|
|
337
357
|
* vault is under `.intentic/auth/` — which is skipped, and says so there. */
|
|
338
358
|
{
|
|
339
|
-
path: ".intentic/extension-settings.json",
|
|
359
|
+
path: ".intentic/config/extension-settings.json",
|
|
340
360
|
invalidates: [],
|
|
341
361
|
why: "Held in a module-level shallowRef store per extension (web's extensionSettingsStore) with no query observer, and deliberately so: api.settings.get must answer SYNCHRONOUSLY from an extension's first activate() line, and the store outlives every component scope. A module-level QueryObserver is the one shape that would make invalidation refetch, and this app already ruled it out — it detaches on the queryClient.clear() at logout (see useSandbox's sandbox-list mirror). So a remote member's setting edit reaches this browser on its next load, not live.",
|
|
342
362
|
portability: "carry",
|
|
@@ -347,7 +367,7 @@ const STATE_FILES = [
|
|
|
347
367
|
* file) shows up here live. It does not re-run the host: activating or retiring an extension is the loader's
|
|
348
368
|
* reconcile, which the tab's own toggle triggers, so a remote flip takes effect on this browser's next load. */
|
|
349
369
|
{
|
|
350
|
-
path: ".intentic/extension-enablement.json",
|
|
370
|
+
path: ".intentic/config/extension-enablement.json",
|
|
351
371
|
invalidates: ["extensions"],
|
|
352
372
|
portability: "carry",
|
|
353
373
|
versioned: true,
|
|
@@ -371,18 +391,18 @@ const STATE_FILES = [
|
|
|
371
391
|
* Not a ledger and not bulk: a handful of small authored files per extension, written when someone edits them.
|
|
372
392
|
* The daemon restarts the backend host on a change here, so an edit is already a consequential event — this
|
|
373
393
|
* makes it a legible one. */
|
|
374
|
-
{ path: ".intentic/workspace-extensions/", invalidates: ["extensions"], portability: "carry", versioned: true, authored: true },
|
|
394
|
+
{ path: ".intentic/config/workspace-extensions/", invalidates: ["extensions"], portability: "carry", versioned: true, authored: true },
|
|
375
395
|
/* What the registry comparison found per installed extension (update available / advisory / post-update
|
|
376
396
|
* health), written by the periodic check and by the update/revert transactions — pushed to the tab because
|
|
377
397
|
* an advisory that auto-disabled something must not wait for a reload to be seen. */
|
|
378
|
-
{ path: ".intentic/extension-updates.json", invalidates: ["extensions"], portability: "carry" },
|
|
398
|
+
{ path: ".intentic/records/extension-updates.json", invalidates: ["extensions"], portability: "carry" },
|
|
379
399
|
/* The owner's per-extension update posture (notify / agent / auto, and the advisory opt-out). Carried:
|
|
380
400
|
* it is a decision about the extension, not about this machine. */
|
|
381
|
-
{ path: ".intentic/extension-update-policy.json", invalidates: ["extensions"], portability: "carry", versioned: true },
|
|
401
|
+
{ path: ".intentic/config/extension-update-policy.json", invalidates: ["extensions"], portability: "carry", versioned: true },
|
|
382
402
|
/* Carried, because the evidence is about the extension rather than about the machine: an export that dropped
|
|
383
403
|
* it would arrive claiming every permission was unused, which is worse than arriving with no figures at all. */
|
|
384
404
|
{
|
|
385
|
-
path: ".intentic/extension-usage.json",
|
|
405
|
+
path: ".intentic/records/extension-usage.json",
|
|
386
406
|
invalidates: [],
|
|
387
407
|
why: "Which of the routes each extension DECLARED it has actually called — the evidence behind the permissions list on its row. The one entry here whose empty set is a RATE decision rather than an architectural one: every browser with the app open reports its batch on a timer, so wiring this to the `extensions` query would refetch the whole list every few seconds for a figure nobody is watching change. The tab reads it when it loads, which is when anyone is reading it.",
|
|
388
408
|
portability: "carry",
|
|
@@ -399,7 +419,7 @@ const STATE_FILES = [
|
|
|
399
419
|
* let a source sandbox hand itself the target's ownership. Widening the guard for this single entry is the
|
|
400
420
|
* worse trade — it protects every `identity` entry, and most of those ARE credentials. */
|
|
401
421
|
{
|
|
402
|
-
path: ".intentic/members.json",
|
|
422
|
+
path: ".intentic/identity/members.json",
|
|
403
423
|
invalidates: [],
|
|
404
424
|
why: "Not this view's source at all: SandboxAccess renders the PLATFORM's invite records (apiClient.invite.list), and this file is the daemon's ENFORCED copy — written first so a grant the enforcer never got is never recorded, then never read back. A change here means the two disagreed, which the write order makes fail-closed rather than stale.",
|
|
405
425
|
portability: "identity",
|
|
@@ -419,7 +439,7 @@ const STATE_FILES = [
|
|
|
419
439
|
* therefore the only place the owner is told what to re-enter: the manifests that name those connections
|
|
420
440
|
* travel, and would otherwise arrive looking complete. */
|
|
421
441
|
{
|
|
422
|
-
path: ".intentic/auth/",
|
|
442
|
+
path: ".intentic/secrets/auth/",
|
|
423
443
|
invalidates: [],
|
|
424
444
|
why: "AI-provider credentials and runtime homes, plus the capability and extension-settings secret vaults; each account is rendered through owner-gated provider routes.",
|
|
425
445
|
portability: "secret",
|
|
@@ -430,25 +450,25 @@ const STATE_FILES = [
|
|
|
430
450
|
* The memory notes under it (`projects/<slug>/memory/**`) ARE user-facing and the /memory view polls them
|
|
431
451
|
* every 30s, which is the one place in this table where a poll survives a real change feed being available.
|
|
432
452
|
* It stays a poll deliberately: the watcher's exclusion is a DESCENT filter, so reaching those notes means
|
|
433
|
-
* letting it walk `.intentic/sessions/claude` → `projects` → every project slug. Measured on the live
|
|
453
|
+
* letting it walk `.intentic/records/sessions/claude` → `projects` → every project slug. Measured on the live
|
|
434
454
|
* workspace that is +119 watched directories against ~593 today (a fifth more), with 314 continuously-
|
|
435
455
|
* rewritten transcripts inside the newly-watched set, to make ONE memory directory live. Notes change at
|
|
436
456
|
* agent-turn cadence, so the poll costs a request a minute and the alternative costs a permanent 20% on the
|
|
437
457
|
* watcher. */
|
|
438
458
|
{
|
|
439
|
-
path: ".intentic/sessions/claude/",
|
|
459
|
+
path: ".intentic/records/sessions/claude/",
|
|
440
460
|
invalidates: [],
|
|
441
461
|
why: "Agent session transcripts — see the note above on why the memory notes under it stay polled.",
|
|
442
462
|
portability: "carry",
|
|
443
463
|
},
|
|
444
464
|
{
|
|
445
|
-
path: ".intentic/artifacts/",
|
|
465
|
+
path: ".intentic/records/artifacts/",
|
|
446
466
|
invalidates: [],
|
|
447
467
|
why: "Durable outputs owned by conversations and extension runs: attachments, browser captures, generated images, acceptance reports, workflow step reports, voice transcripts, and loop ledgers.",
|
|
448
468
|
portability: "carry",
|
|
449
469
|
},
|
|
450
470
|
{
|
|
451
|
-
path: ".intentic/cache/",
|
|
471
|
+
path: ".intentic/local/cache/",
|
|
452
472
|
invalidates: [],
|
|
453
473
|
why: "Rebuildable indexes and caches — the iq index and its vector sidecar, the whisper model; ignored by the watcher and recreated from carried workspace content.",
|
|
454
474
|
portability: "derived",
|
|
@@ -459,14 +479,14 @@ const STATE_FILES = [
|
|
|
459
479
|
* hour-tokens, gateway discovery state: all of it either expires or re-establishes itself, and classifying
|
|
460
480
|
* the root once is what keeps a token an extension caches tomorrow out of bundles without a second edit. */
|
|
461
481
|
{
|
|
462
|
-
path: ".intentic/runtime/",
|
|
482
|
+
path: ".intentic/local/runtime/",
|
|
463
483
|
invalidates: [],
|
|
464
484
|
why: "Extension runtime scratch (watermarks, cached short-lived tokens); nothing renders it and gateways re-derive it.",
|
|
465
485
|
portability: "derived",
|
|
466
486
|
outsideWriter: "extensions, through extensionRuntimeDir below",
|
|
467
487
|
},
|
|
468
488
|
{
|
|
469
|
-
path: ".intentic/tmp/",
|
|
489
|
+
path: ".intentic/local/tmp/",
|
|
470
490
|
invalidates: [],
|
|
471
491
|
why: "Scratch that agents and tools leave behind (build logs, demo checkouts); nothing reads it after the turn that wrote it. The state janitor empties it at boot.",
|
|
472
492
|
portability: "derived",
|
|
@@ -477,59 +497,68 @@ const STATE_FILES = [
|
|
|
477
497
|
* sources a fresh install rebuilds, which an export must not ship (it reached 1.3 GB on the workspace this
|
|
478
498
|
* entry was written against). */
|
|
479
499
|
{
|
|
480
|
-
path: ".intentic/.pnpm-store/",
|
|
500
|
+
path: ".intentic/local/.pnpm-store/",
|
|
481
501
|
invalidates: [],
|
|
482
502
|
why: "pnpm's content-addressable store, auto-created by installs run from under .intentic; the next install rebuilds it.",
|
|
483
503
|
portability: "derived",
|
|
484
504
|
outsideWriter: "pnpm itself, when an install runs from under .intentic",
|
|
485
505
|
},
|
|
486
506
|
{
|
|
487
|
-
path: ".intentic/newest-run.json",
|
|
507
|
+
path: ".intentic/local/newest-run.json",
|
|
488
508
|
invalidates: [],
|
|
489
509
|
why: "The newest daemon version that ever ran this workspace (store/newest-run.ts) — a downgrade tripwire, about THIS sandbox the way rule-firings is.",
|
|
490
510
|
portability: "derived",
|
|
491
511
|
note: "The target stamps its own daemon version on first boot.",
|
|
492
512
|
},
|
|
493
513
|
{
|
|
494
|
-
path: ".intentic/verify.json",
|
|
514
|
+
path: ".intentic/records/verify.json",
|
|
495
515
|
invalidates: [],
|
|
496
516
|
why: "The dependency verifier's verdict memory; nothing renders it directly — outcomes reach the owner as activity entries and workspace events.",
|
|
497
517
|
portability: "carry",
|
|
498
518
|
},
|
|
499
519
|
{
|
|
500
|
-
path: ".intentic/verify/",
|
|
520
|
+
path: ".intentic/local/verify/",
|
|
501
521
|
invalidates: [],
|
|
502
522
|
why: "A running check's wrapper artifacts (log + exit status), read once by the daemon when the panel finishes.",
|
|
503
523
|
portability: "derived",
|
|
504
524
|
},
|
|
505
525
|
{
|
|
506
|
-
path: ".intentic/ci.json",
|
|
526
|
+
path: ".intentic/secrets/ci.json",
|
|
507
527
|
invalidates: [],
|
|
508
528
|
why: "Webhook secret + conclusion memory; the Pipelines view reads it through /ci/runs, not off disk.",
|
|
509
529
|
portability: "secret",
|
|
510
530
|
note: "Re-add the CI webhook on the Pipelines view — its secret is per-sandbox.",
|
|
511
531
|
},
|
|
532
|
+
/* THE ONE ENTRY THAT OPTS OUT OF THE BACKUP, and the reason the flag exists rather than the rule simply
|
|
533
|
+
* reading `portability !== "derived"`. It is `identity` like the three below it, so the derived answer would
|
|
534
|
+
* copy it down with them — but where those are a name, a workspace id and a role per row, these are tokens
|
|
535
|
+
* that AUTHENTICATE against this sandbox from outside it. Hashed, which lowers the stakes and does not
|
|
536
|
+
* settle them: the point of a backup is to be readable after the thing it backs up is gone, and a file whose
|
|
537
|
+
* only purpose is to admit callers has no business sitting in one. Nothing is lost by leaving it out — the
|
|
538
|
+
* entry's own note already says the tokens must be re-minted on any new sandbox, so a copy could never have
|
|
539
|
+
* been restored anyway. */
|
|
512
540
|
{
|
|
513
|
-
path: ".intentic/control-tokens.json",
|
|
541
|
+
path: ".intentic/identity/control-tokens.json",
|
|
514
542
|
invalidates: [],
|
|
515
543
|
why: "Hashed control tokens (the ACP editor bridge, and anything else driving this sandbox from outside), listed on demand by the owner.",
|
|
516
544
|
portability: "identity",
|
|
545
|
+
backup: false,
|
|
517
546
|
note: "Mint fresh control tokens — the old ones authenticate against the source sandbox.",
|
|
518
547
|
},
|
|
519
548
|
{
|
|
520
|
-
path: ".intentic/owner.json",
|
|
549
|
+
path: ".intentic/identity/owner.json",
|
|
521
550
|
invalidates: [],
|
|
522
551
|
why: "Bound once on first use; a change here means the sandbox was re-owned, which re-authenticates anyway.",
|
|
523
552
|
portability: "identity",
|
|
524
553
|
},
|
|
525
554
|
{
|
|
526
|
-
path: ".intentic/workspace.json",
|
|
555
|
+
path: ".intentic/identity/workspace.json",
|
|
527
556
|
invalidates: [],
|
|
528
557
|
why: "The workspace identity, read from the /events hello frame rather than as a file.",
|
|
529
558
|
portability: "identity",
|
|
530
559
|
},
|
|
531
560
|
{
|
|
532
|
-
path: ".intentic/templates.json",
|
|
561
|
+
path: ".intentic/config/templates.json",
|
|
533
562
|
invalidates: [],
|
|
534
563
|
why: "Scaffold templates, read when the scaffold dialog opens.",
|
|
535
564
|
portability: "carry",
|
|
@@ -540,20 +569,20 @@ const STATE_FILES = [
|
|
|
540
569
|
* constantly and versions against its own build, so carrying them ships bulk that the target's Chromium may
|
|
541
570
|
* refuse anyway. The note is what keeps the loss visible instead of silent. */
|
|
542
571
|
{
|
|
543
|
-
path: ".intentic/browser/",
|
|
572
|
+
path: ".intentic/local/browser/",
|
|
544
573
|
invalidates: [],
|
|
545
574
|
why: "Browser-login profiles: Chromium rewrites these constantly. Descent-ignored by the watcher outright.",
|
|
546
575
|
portability: "derived",
|
|
547
576
|
note: "Log the agent's browser back into any site it needs — profiles do not travel.",
|
|
548
577
|
},
|
|
549
578
|
{
|
|
550
|
-
path: ".intentic/extensions/",
|
|
579
|
+
path: ".intentic/local/extensions/",
|
|
551
580
|
invalidates: [],
|
|
552
581
|
why: "Extension checkouts — whole git clones. The `extensions` query is driven by the capability manifest above, not by their contents.",
|
|
553
582
|
portability: "derived",
|
|
554
583
|
note: "Extensions re-clone from the capability manifest on the target's next reconcile.",
|
|
555
584
|
},
|
|
556
|
-
{ path: ".intentic/plugins/", invalidates: [], why: "Agent plugin dirs, read by the SDK's loader each turn.", portability: "carry" },
|
|
585
|
+
{ path: ".intentic/records/plugins/", invalidates: [], why: "Agent plugin dirs, read by the SDK's loader each turn.", portability: "carry" },
|
|
557
586
|
/* THE SKILLS THE OWNER WROTE THEMSELVES, one directory per skill — the source of truth the reconciler copies
|
|
558
587
|
* into `.agents/skills` for the ones currently switched on (settings.json's `skills` list). It is here rather
|
|
559
588
|
* than in the loaded folder for the reason the plugin dirs are: that tree holds only what is currently ON,
|
|
@@ -562,7 +591,7 @@ const STATE_FILES = [
|
|
|
562
591
|
* `versioned`, like the rest of the config slice: a skill changes how the agent behaves, so it earns a diff
|
|
563
592
|
* in the Changes review and a line in `git log` the same way a rule or a persona does. `carry` for the same
|
|
564
593
|
* reason — it is text the owner wrote, with no credential in it and nothing about this machine. */
|
|
565
|
-
{ path: ".intentic/skills/", invalidates: ["skills"], portability: "carry", versioned: true },
|
|
594
|
+
{ path: ".intentic/config/skills/", invalidates: ["skills"], portability: "carry", versioned: true },
|
|
566
595
|
/* ONE FOLDER PER PERSONA — what a session wearing that card is told, and the skills and tools only it gets.
|
|
567
596
|
* Laid out as a Claude Code plugin (`.claude-plugin/plugin.json`, `skills/`, `agents/`, `commands/`,
|
|
568
597
|
* `hooks/`, `.mcp.json`) so the runtime's own loader reads it and this daemon parses none of it, exactly as
|
|
@@ -576,7 +605,7 @@ const STATE_FILES = [
|
|
|
576
605
|
* `versioned` and `carry` for the same reasons the card and the skills above are: it changes how the agent
|
|
577
606
|
* behaves, it holds no credential, and it belongs in a pull request — which is also what makes it
|
|
578
607
|
* searchable, since every versioned entry already is. */
|
|
579
|
-
{ path: ".intentic/personas/", invalidates: ["personas"], portability: "carry", versioned: true },
|
|
608
|
+
{ path: ".intentic/config/personas/", invalidates: ["personas"], portability: "carry", versioned: true },
|
|
580
609
|
] as const satisfies readonly WorkspaceStateFile[];
|
|
581
610
|
|
|
582
611
|
export const WORKSPACE_STATE_FILES: readonly WorkspaceStateFile[] = STATE_FILES;
|
|
@@ -608,6 +637,112 @@ export const SEARCHABLE_STATE_PATHS: readonly string[] = WORKSPACE_STATE_FILES.f
|
|
|
608
637
|
(file) => file.path,
|
|
609
638
|
);
|
|
610
639
|
|
|
640
|
+
/* WHAT KIND OF THING THIS IS, in the one word a person browsing the state dir would use — and the axis the
|
|
641
|
+
* DIRECTORY LAYOUT is built on, so the folder you open explains itself before you read a table about it.
|
|
642
|
+
*
|
|
643
|
+
* It is DERIVED, and that is the whole reason it can be trusted. Forty-nine entries already answer three
|
|
644
|
+
* questions between them (is it reviewed, is it authored, does it travel), and those answers turn out to nest
|
|
645
|
+
* perfectly rather than cut across each other: every `versioned` entry is `carry`, every `authored` entry is
|
|
646
|
+
* `carry`, and nothing is both a credential and a thing a person edits. A nested set of answers is exactly what
|
|
647
|
+
* a directory tree can express, which is why five folders can carry rules that used to take five hand-kept path
|
|
648
|
+
* lists — the git exclude, the search allow-list, the sync ignore, the watcher skip, the export bundle.
|
|
649
|
+
*
|
|
650
|
+
* Declaring the group on each entry instead would have made it a fourth independent fact to keep in step with
|
|
651
|
+
* the other three, which is the failure this file exists to argue against. Adding a store still means answering
|
|
652
|
+
* the same three questions it always did; the group, the folder it belongs in, and every rule that reads them
|
|
653
|
+
* follow with no further edit. */
|
|
654
|
+
export type StateGroup =
|
|
655
|
+
/* Reviewed and reviewable: settings, personas, skills, drafts, staged docs, the environment overlay. Tracked
|
|
656
|
+
* by the root repo, searchable, backed up, and carried into a new sandbox. Two of its members (drafts, staged
|
|
657
|
+
* docs) are authored content rather than configuration, and the folder is still called `config` — the word
|
|
658
|
+
* that makes seventeen of the nineteen instantly clear beats one that makes all nineteen vague. */
|
|
659
|
+
| "config"
|
|
660
|
+
/* What HAPPENED here — run ledgers, approvals, chores, transcripts, artifacts. Machine-written, so untracked
|
|
661
|
+
* and unsearchable, but the owner's history all the same: backed up and carried. */
|
|
662
|
+
| "records"
|
|
663
|
+
/* Rebuildable from something that does travel: caches, indexes, extension checkouts, scratch, the composed
|
|
664
|
+
* overlay, browser profiles. Neither backed up nor carried, and the janitor may delete it. `local` in the
|
|
665
|
+
* sense every other tool uses it — belongs to this machine, is not shared, and losing it costs nothing. */
|
|
666
|
+
| "local"
|
|
667
|
+
/* Who owns this sandbox and who may drive it. Backed up so the owner keeps a copy of their own access, never
|
|
668
|
+
* carried — a list that travelled would let a source sandbox claim the target. */
|
|
669
|
+
| "identity"
|
|
670
|
+
/* Credentials. Never backed up; carried only when the owner opts in at export and the bundle records it. */
|
|
671
|
+
| "secrets";
|
|
672
|
+
|
|
673
|
+
/* THE FOLDER EACH GROUP LIVES IN.
|
|
674
|
+
*
|
|
675
|
+
* The group name IS the directory name — one vocabulary, not a name and a translation of it. That is what lets
|
|
676
|
+
* the guard in workspace-state.test.ts check the whole layout with one rule ("every entry sits under its own
|
|
677
|
+
* group's folder") rather than trusting forty-nine literals to have been typed correctly, and it is why renaming
|
|
678
|
+
* a folder is an edit here plus the literals the compiler then points at, with nothing able to half-move.
|
|
679
|
+
*
|
|
680
|
+
* WHICH RULES THE LAYOUT ACTUALLY CARRIES, stated plainly because it is fewer than the tidy version of this
|
|
681
|
+
* story. The sync backup collapses to two folder names (BACKUP_IGNORES) and workspace search to one, because
|
|
682
|
+
* "may the owner keep this" and "is this authored text" are exactly what the grouping sorts on. Two others do
|
|
683
|
+
* NOT collapse, and both are worth knowing about before someone tries:
|
|
684
|
+
* - THE GIT EXCLUDE tracks `versioned`, which is eighteen of `config`'s nineteen. The exception is the staged
|
|
685
|
+
* docs tree: searchable, deliberately untracked (publishing copies those pages into the repo, so tracking
|
|
686
|
+
* the staging copy too would double every one of them). A `config/` prefix would quietly start tracking it.
|
|
687
|
+
* - THE WATCHER skips what churns, which is most of `local` but not all of it: the composed overlay and the
|
|
688
|
+
* rule-firing stamps are `derived` and therefore `local`, and both still feed a view. A `local/` prefix
|
|
689
|
+
* would stop the environment page refreshing when the overlay is recomposed.
|
|
690
|
+
* Both stay derived from the flags instead, which costs a longer generated list and no correctness. The folders
|
|
691
|
+
* are the layout; the flags are still the authority. */
|
|
692
|
+
export const STATE_GROUP_DIR: Readonly<Record<StateGroup, string>> = {
|
|
693
|
+
config: `${STATE_DIR}/config`,
|
|
694
|
+
records: `${STATE_DIR}/records`,
|
|
695
|
+
local: `${STATE_DIR}/local`,
|
|
696
|
+
identity: `${STATE_DIR}/identity`,
|
|
697
|
+
secrets: `${STATE_DIR}/secrets`,
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
// Every group, derived off the folder map so the two can never disagree about how many there are. Declaration
|
|
701
|
+
// order is the order a person should read them in: what you wrote, what happened, what can be thrown away, who
|
|
702
|
+
// owns this, and the keys.
|
|
703
|
+
export const STATE_GROUPS = Object.keys(STATE_GROUP_DIR) as readonly StateGroup[];
|
|
704
|
+
|
|
705
|
+
/* The group each entry falls in. Ordered most-specific-first: a credential is a credential whatever else it is,
|
|
706
|
+
* and only once those are out of the way does "did a person write this" separate the two `carry` groups. */
|
|
707
|
+
export const stateGroupOf = (file: WorkspaceStateFile): StateGroup => {
|
|
708
|
+
switch (file.portability) {
|
|
709
|
+
case "secret":
|
|
710
|
+
return "secrets";
|
|
711
|
+
case "identity":
|
|
712
|
+
return "identity";
|
|
713
|
+
case "derived":
|
|
714
|
+
return "local";
|
|
715
|
+
case "carry":
|
|
716
|
+
return file.versioned === true || file.authored === true ? "config" : "records";
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
// The entries of one group, workspace-root-relative and in declaration order — what each rule that used to keep
|
|
721
|
+
// its own path list now asks for instead.
|
|
722
|
+
export const stateGroupPaths = (group: StateGroup): readonly string[] =>
|
|
723
|
+
WORKSPACE_STATE_FILES.filter((file) => stateGroupOf(file) === group).map((file) => file.path);
|
|
724
|
+
|
|
725
|
+
/* THE SLICE DESKTOP-SYNC COPIES DOWN — ordinary state and the records that bind this sandbox to its owner,
|
|
726
|
+
* minus anything that opted out (see `backup` on the interface).
|
|
727
|
+
*
|
|
728
|
+
* The sync used to ignore `.intentic` WHOLE, which is the same conflation the `backup` flag exists to undo: the
|
|
729
|
+
* dir holds credentials, so the dir was excluded, so a sandbox going away also took every persona, skill,
|
|
730
|
+
* automation, draft and transcript the owner had. This is the list that makes the owner's machine an actual
|
|
731
|
+
* backup instead of a copy of the source tree only.
|
|
732
|
+
*
|
|
733
|
+
* It is deliberately NOT the same as the export bundle. A bundle asks what may be reconstituted somewhere else;
|
|
734
|
+
* this asks what the owner may keep. `ownership` is the entries where those differ, and it is in here. */
|
|
735
|
+
export const BACKED_UP_STATE_PATHS: readonly string[] = WORKSPACE_STATE_FILES.filter(
|
|
736
|
+
(file) => file.backup !== false && (file.portability === "carry" || file.portability === "identity"),
|
|
737
|
+
).map((file) => file.path);
|
|
738
|
+
|
|
739
|
+
/* Its complement, which is what a sync ignore list actually needs: everything under the state dir that must NOT
|
|
740
|
+
* come down. Derived from the same predicate rather than listed, so a store added tomorrow is excluded until its
|
|
741
|
+
* class says otherwise — the same default-deny the search floor and the portability classes are built on. */
|
|
742
|
+
export const UNBACKED_STATE_PATHS: readonly string[] = WORKSPACE_STATE_FILES.filter((file) => !BACKED_UP_STATE_PATHS.includes(file.path)).map(
|
|
743
|
+
(file) => file.path,
|
|
744
|
+
);
|
|
745
|
+
|
|
611
746
|
/* THE ONE WAY AN EXTENSION NAMES ITS SCRATCH HOME — `.intentic/runtime/extensions/<id>`, workspace-relative
|
|
612
747
|
* and forward-slash so the browser bundle can hold it too; callers join it onto whatever root is in force.
|
|
613
748
|
*
|
|
@@ -616,7 +751,8 @@ export const SEARCHABLE_STATE_PATHS: readonly string[] = WORKSPACE_STATE_FILES.f
|
|
|
616
751
|
* classified it. An extension that composes through this helper cannot land outside its own directory, so the
|
|
617
752
|
* runtime/ entry's `derived` covers whatever it writes tomorrow. Extension ids are validated slugs already;
|
|
618
753
|
* the replace is defence in depth against a path ever being built from something else. */
|
|
619
|
-
export const extensionRuntimeDir = (extension: string): string =>
|
|
754
|
+
export const extensionRuntimeDir = (extension: string): string =>
|
|
755
|
+
`${STATE_GROUP_DIR.local}/runtime/extensions/${extension.replaceAll(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
620
756
|
|
|
621
757
|
/* The manifests whose problems the unreadable-manifest notice SHOWS — the handful a person hand-edits — and the
|
|
622
758
|
* one fact that decides it is already in the table above.
|
|
@@ -651,9 +787,24 @@ export const isReportedManifest = (relPath: string): boolean => REPORTED_MANIFES
|
|
|
651
787
|
* quarantine doing half its job. Secret and artifact roots stay until an owner removes them by hand: deleting
|
|
652
788
|
* content is not the janitor's call, only deleting what the class already says is disposable. */
|
|
653
789
|
export const RETIRED_WORKSPACE_STATE_DIRS = {
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
790
|
+
/* The flat spellings, from before the state dir was grouped into its five folders. Every entry in the table
|
|
791
|
+
* used to sit directly under `.intentic/`, and a sandbox that predates the move still has those directories
|
|
792
|
+
* on disk with real contents in them. They are quarantined rather than migrated — the workspace rule is fresh
|
|
793
|
+
* state and no compatibility layers — but quarantine is exactly what they need, because the alternative is
|
|
794
|
+
* worse than leaving them: `auth` at the old spelling is a real credential store that the classifier no
|
|
795
|
+
* longer recognises, so without these names it would read as ordinary workspace content and be indexed by
|
|
796
|
+
* search, carried by an export and copied down by the backup. Being listed here keeps the old copy as
|
|
797
|
+
* untouchable as the new one, and lets the janitor delete the rebuildable half. */
|
|
798
|
+
secret: ["claude", "codex", "kimi", "opencode", "cliproxy", "auth", "ci.json"],
|
|
799
|
+
/* The ownership records at their OLD flat spelling, and a bucket of their own rather than a few more names
|
|
800
|
+
* in `secret` — because the two classes differ on exactly the thing that matters for a leftover. A `secret`
|
|
801
|
+
* travels when the owner opts in at export; an `identity` never travels at all, since a list of who may
|
|
802
|
+
* drive this sandbox arriving in another one is how a source hands itself the target's ownership. Filed under
|
|
803
|
+
* `secret` these would have become carryable by ticking a box, which is the one outcome their live entries
|
|
804
|
+
* are written to prevent. */
|
|
805
|
+
identity: ["owner.json", "members.json", "workspace.json", "control-tokens.json"],
|
|
806
|
+
derived: ["iq", "extensions-runtime", "whisper", "browser/output", "browser", "cache", "runtime", "tmp", ".pnpm-store", "extensions", "verify"],
|
|
807
|
+
artifacts: ["attachments", "acceptance", "loops", "workflow-runs", "transcripts", "artifacts", "sessions"],
|
|
657
808
|
} as const;
|
|
658
809
|
|
|
659
810
|
/* THE DAEMON'S OWN CONTROL PLANE — the entries directly under the workspace root's `.intentic/` that the file
|
|
@@ -668,16 +819,26 @@ export const RETIRED_WORKSPACE_STATE_DIRS = {
|
|
|
668
819
|
*
|
|
669
820
|
* Naming these to the browser gives nothing away that the tree did not already publish — it listed them, sizes
|
|
670
821
|
* and all. What stays behind the guard is the only thing that ever mattered: the bytes. */
|
|
822
|
+
/* GROUP-RELATIVE NOW, and the one rule the regrouping did NOT simplify — worth saying because every other rule
|
|
823
|
+
* over this tree collapsed to a prefix and this one could not. What the file API refuses to open cuts ACROSS the
|
|
824
|
+
* groups: the capability manifest is `config`, the transcripts are `records`, the browser profiles are `local`,
|
|
825
|
+
* and all of `identity` and `secrets` is in. That is not an accident of the grouping, it is a different question
|
|
826
|
+
* — "would showing the bytes hand someone something" rather than "what kind of thing is this" — so it keeps an
|
|
827
|
+
* explicit list, just one that now names the folder each entry lives in. */
|
|
671
828
|
const LOCKED_STATE_ENTRIES: ReadonlySet<string> = new Set([
|
|
672
|
-
"owner.json",
|
|
673
|
-
"members.json",
|
|
674
|
-
"
|
|
675
|
-
"
|
|
829
|
+
"identity/owner.json",
|
|
830
|
+
"identity/members.json",
|
|
831
|
+
"identity/control-tokens.json",
|
|
832
|
+
"config/capabilities.json",
|
|
833
|
+
"secrets/ci.json",
|
|
834
|
+
"secrets/auth",
|
|
835
|
+
"records/sessions",
|
|
836
|
+
"local/browser",
|
|
837
|
+
/* The provider CLI's own home, which this table does not declare and so has no group to move into — it is
|
|
838
|
+
* written by the agent's runtime rather than by any daemon store. It stays at the state dir's root, and the
|
|
839
|
+
* two-segment match below still reaches it because a bare name joins to itself. Locked for the reason the
|
|
840
|
+
* credential entries are: it holds a live session for whatever the agent is signed into. */
|
|
676
841
|
"claude.json",
|
|
677
|
-
"auth",
|
|
678
|
-
"sessions",
|
|
679
|
-
"browser",
|
|
680
|
-
...RETIRED_WORKSPACE_STATE_DIRS.secret,
|
|
681
842
|
]);
|
|
682
843
|
|
|
683
844
|
/* Whether a workspace-root-relative path lands in that control plane — and so is shown locked rather than
|
|
@@ -694,7 +855,18 @@ export const isLockedWorkspacePath = (relPath: string): boolean => {
|
|
|
694
855
|
if (segments[0] === ".git") {
|
|
695
856
|
return true;
|
|
696
857
|
}
|
|
697
|
-
|
|
858
|
+
if (segments[0] !== STATE_DIR) {
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
861
|
+
/* Two shapes, because two layouts exist on disk. A current entry is `<group>/<name>`; a leftover from before
|
|
862
|
+
* the regrouping is a bare name directly under the state dir, and the quarantine record is what still knows
|
|
863
|
+
* those are credentials. Both are locked — a stale `auth/` full of real tokens is no safer to open than the
|
|
864
|
+
* live one, and the whole point of quarantining the old spellings was that nothing downstream reclassifies
|
|
865
|
+
* them as ordinary content. */
|
|
866
|
+
return (
|
|
867
|
+
LOCKED_STATE_ENTRIES.has(segments.slice(1, 3).join("/")) ||
|
|
868
|
+
(segments.length >= 2 && RETIRED_WORKSPACE_STATE_DIRS.secret.includes(segments[1] as never))
|
|
869
|
+
);
|
|
698
870
|
};
|
|
699
871
|
|
|
700
872
|
/* THE LOCKED ENTRIES THE ROOT REPO TRACKS — refused by the file API, and diffable anyway.
|