@intentic/sandbox-contract 1.223.0 → 1.224.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/chores/chores.js +1 -1
- package/dist/chores/chores.js.map +1 -1
- package/dist/chores/prompt.d.ts.map +1 -1
- package/dist/chores/prompt.js +3 -3
- package/dist/chores/prompt.js.map +1 -1
- package/dist/contracts/ports.contract.d.ts +11 -0
- package/dist/contracts/ports.contract.d.ts.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/schemas.d.ts +22 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +3 -0
- package/dist/schemas.js.map +1 -1
- package/package.json +4 -4
- package/src/agent-catalog.ts +78 -78
- package/src/agent-run-model.ts +8 -8
- package/src/capability-env.ts +3 -3
- package/src/capability-secrets.ts +5 -5
- package/src/chores/chores.ts +67 -67
- package/src/chores/digest.ts +3 -3
- package/src/chores/extension-update.ts +3 -3
- package/src/chores/fix-deps.ts +3 -3
- package/src/chores/probes.ts +16 -16
- package/src/chores/prompt.ts +9 -9
- package/src/chores/stack.ts +17 -17
- package/src/chores/verdict.ts +23 -23
- package/src/contract-lock.ts +7 -7
- package/src/contracts/activity.contract.ts +1 -1
- package/src/contracts/agent.contract.ts +2 -2
- package/src/contracts/agents.contract.ts +11 -11
- package/src/contracts/automations.contract.ts +5 -5
- package/src/contracts/capabilities.contract.ts +7 -7
- package/src/contracts/chores.contract.ts +3 -3
- package/src/contracts/ci.contract.ts +2 -2
- package/src/contracts/claude.contract.ts +3 -3
- package/src/contracts/drafts.contract.ts +1 -1
- package/src/contracts/endpoints.contract.ts +2 -2
- package/src/contracts/extensions.contract.ts +8 -8
- package/src/contracts/git.contract.ts +6 -6
- package/src/contracts/grok.contract.ts +4 -4
- package/src/contracts/host.contract.ts +5 -5
- package/src/contracts/intentic.contract.ts +2 -2
- package/src/contracts/logs.contract.ts +1 -1
- package/src/contracts/loops.contract.ts +8 -8
- package/src/contracts/personas.contract.ts +8 -8
- package/src/contracts/ports.contract.ts +1 -1
- package/src/contracts/prepush.contract.ts +2 -2
- package/src/contracts/providers.contract.ts +4 -4
- package/src/contracts/public.contract.ts +1 -1
- package/src/contracts/push.contract.ts +2 -2
- package/src/contracts/secrets.contract.ts +2 -2
- package/src/contracts/sessions.contract.ts +2 -2
- package/src/contracts/settings.contract.ts +3 -3
- package/src/contracts/share.contract.ts +1 -1
- package/src/contracts/skills.contract.ts +2 -2
- package/src/contracts/system.contract.ts +9 -9
- package/src/contracts/usage.contract.ts +1 -1
- package/src/contracts/vpn.contract.ts +5 -5
- package/src/contracts/workflows.contract.ts +12 -12
- package/src/contracts/workspace.contract.ts +13 -13
- package/src/conversation-ids.ts +7 -7
- package/src/events.ts +136 -136
- package/src/history-state.ts +12 -12
- package/src/host-protocol.ts +7 -7
- package/src/hostnames.ts +15 -15
- package/src/index.ts +8 -8
- package/src/listener-protocol.ts +12 -12
- package/src/model-order.ts +30 -30
- package/src/output-fields.ts +9 -9
- package/src/path-refs.ts +6 -6
- package/src/publish-drafts.ts +6 -6
- package/src/quick-model.ts +15 -15
- package/src/routes.ts +19 -19
- package/src/runtime-state.ts +22 -22
- package/src/schemas.ts +930 -918
- package/src/search-globs.ts +6 -6
- package/src/session-names.ts +5 -5
- package/src/share-paths.ts +7 -7
- package/src/sse.ts +1 -1
- package/src/state-portability.ts +8 -8
- package/src/terminal-protocol.ts +3 -3
- package/src/title.ts +20 -20
- package/src/tunnel-ids.ts +7 -7
- package/src/versions.ts +5 -5
- package/src/workflow-faults.ts +11 -11
- package/src/workspace-state.ts +119 -119
package/src/workspace-state.ts
CHANGED
|
@@ -2,31 +2,31 @@ import { STATE_DIR } from "@intentic/constants";
|
|
|
2
2
|
import type { FileContribution } from "@intentic/extension-manifest";
|
|
3
3
|
import type { StateFile } from "./state-portability.js";
|
|
4
4
|
|
|
5
|
-
/* WHICH WORKSPACE FILE BACKS WHICH CORE VIEW
|
|
5
|
+
/* WHICH WORKSPACE FILE BACKS WHICH CORE VIEW, one declaration, read by both sides of the wire.
|
|
6
6
|
*
|
|
7
7
|
* The daemon's own state lives under `<workspace>/.intentic/`, the agent edits it out-of-band with its file
|
|
8
8
|
* tools, and the file watcher pushes every change as a `workspaceChanged` batch. Turning those paths back into
|
|
9
9
|
* "and therefore this view is stale" used to be a hand-written table in the BROWSER (web's systemEventRouting),
|
|
10
|
-
* maintained separately from the paths the daemon actually writes (composition.ts)
|
|
10
|
+
* maintained separately from the paths the daemon actually writes (composition.ts), two lists of the same
|
|
11
11
|
* fact, in two packages, with nothing tying them together.
|
|
12
12
|
*
|
|
13
13
|
* They drifted, exactly as that shape always does. `.intentic/drafts/` is written by the AGENT (the drafts
|
|
14
|
-
* skill puts a file there) and rendered by the Drafts view, but it was never added to the browser's table
|
|
14
|
+
* skill puts a file there) and rendered by the Drafts view, but it was never added to the browser's table, so
|
|
15
15
|
* a draft appearing on disk while the owner watched the page changed nothing until they refocused the tab.
|
|
16
16
|
* Extension settings and the members list were missing for the same reason; writing them out is what showed
|
|
17
|
-
* that neither is a drafts-shaped hole
|
|
17
|
+
* that neither is a drafts-shaped hole, see their entries.
|
|
18
18
|
*
|
|
19
19
|
* So the binding is declared HERE, once, in the package both the daemon and the browser already import, and
|
|
20
20
|
* each side derives what it needs: the daemon builds its store paths from `path`, the browser builds its
|
|
21
21
|
* invalidation table from `invalidates`. Adding a manifest without saying what it makes stale is now a change
|
|
22
|
-
* to one visible list rather than an omission in a file nobody edits
|
|
22
|
+
* to one visible list rather than an omission in a file nobody edits, and `workspace-state.test.ts` fails when
|
|
23
23
|
* a daemon store names a `.intentic` path this list doesn't carry.
|
|
24
24
|
*
|
|
25
25
|
* This mirrors what routes.ts does for the route surface ("nothing is generated and nothing is hand-maintained")
|
|
26
26
|
* one layer over: the same refusal to keep the same knowledge in two places.
|
|
27
27
|
*
|
|
28
28
|
* EXTENSIONS declare their own half in their manifest (`contributes.files`, @intentic/extension-api), in the same
|
|
29
|
-
* two fields, and the browser unions the two lists
|
|
29
|
+
* two fields, and the browser unions the two lists, see staleQueryKeys. That split is what this table is FOR:
|
|
30
30
|
* before it existed the core enumeration had to carry `automations` and `automation-approvals`, query keys owned
|
|
31
31
|
* by the automations extension, because the extension had no way to say so itself. A key belongs to whoever
|
|
32
32
|
* queries it. */
|
|
@@ -35,56 +35,56 @@ import type { StateFile } from "./state-portability.js";
|
|
|
35
35
|
* right to declare NO invalidations (for a daemon-owned file, the answer more often than not), and a
|
|
36
36
|
* portability class, because the daemon's own state is what an environment export has to reason about.
|
|
37
37
|
*
|
|
38
|
-
* `path` is workspace-root-relative, forward-slash
|
|
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
40
|
* - an exact file `.intentic/config/settings.json`
|
|
41
41
|
* - a directory `.intentic/config/drafts/` (one file per draft)
|
|
42
42
|
* - a name family `.intentic/config/environment.custom.` (…Dockerfile and anything later named beside it)
|
|
43
|
-
* A directory entry keeps its trailing slash so it can never prefix-match a sibling file. Entries may NEST
|
|
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 {
|
|
46
|
-
/* The browser query keys this file's contents feed. EMPTY is a real answer, not a gap
|
|
47
|
-
* renders nothing from, or one deliberately kept off the push path
|
|
46
|
+
/* The browser query keys this file's contents feed. EMPTY is a real answer, not a gap, a file the browser
|
|
47
|
+
* renders nothing from, or one deliberately kept off the push path, and `why` says which. Never a prefix
|
|
48
48
|
* test over `.intentic/` as a whole: one stray write must not cost every view a refetch, which is the
|
|
49
49
|
* amplification that once turned an iq index rebuild into an endless request storm. */
|
|
50
50
|
readonly invalidates: readonly string[];
|
|
51
51
|
// Why this file has no invalidations, for the entries that declare none. Absent when it has some.
|
|
52
52
|
readonly why?: string;
|
|
53
|
-
/* Whether this entry is TRACKED by the root repo
|
|
53
|
+
/* Whether this entry is TRACKED by the root repo, the third thing an entry declares, and the one an owner
|
|
54
54
|
* sees most directly: a tracked entry gets a diff in the Changes review and a line in `git log`, so a change
|
|
55
55
|
* to how this sandbox behaves can be read, reverted, and attributed.
|
|
56
56
|
*
|
|
57
57
|
* ABSENT IS THE ANSWER FOR ALMOST EVERYTHING, and deliberately so. The root repo excludes `.intentic`
|
|
58
58
|
* wholesale and this flag is the only thing that carves an entry back out, so a store added later is
|
|
59
|
-
* untracked until someone says otherwise
|
|
59
|
+
* untracked until someone says otherwise, the same default-deny the `portability` classes are built on, for
|
|
60
60
|
* the same reason. An ignore-pattern list would invert it: a credential store added next month would be
|
|
61
61
|
* committed on its first write, and nothing would have had to change for that to happen.
|
|
62
62
|
*
|
|
63
63
|
* WHAT EARNS IT is one question, asked of the entry rather than of its shape: does a change here change what
|
|
64
64
|
* this sandbox DOES? Two families answer yes.
|
|
65
|
-
* - CONFIGURATION
|
|
65
|
+
* - CONFIGURATION, the small, slow-moving files that decide how the sandbox behaves: settings, personas,
|
|
66
66
|
* skills, automations, workflow designs, the environment overlay, which extensions are on.
|
|
67
|
-
* - AUTHORED CONTENT WHOSE CONSEQUENCES LEAVE THE SANDBOX
|
|
67
|
+
* - AUTHORED CONTENT WHOSE CONSEQUENCES LEAVE THE SANDBOX, a workspace extension is code that runs in the
|
|
68
68
|
* app with a declared permission surface and a backend of its own; a post draft is words that go out
|
|
69
69
|
* under the owner's name. Neither is configuration, and reading `versioned` as config-only is what kept
|
|
70
70
|
* both of them out: an agent could write, and then run, a whole extension with a diff nowhere, and
|
|
71
71
|
* propose a public post that left no trace once declined. The rule was never "is this a setting", it was
|
|
72
|
-
* "can this be read, reverted and attributed"
|
|
72
|
+
* "can this be read, reverted and attributed", and for the two things the AGENT authors on its own,
|
|
73
73
|
* that matters more than it does for a file a person edited on purpose.
|
|
74
74
|
*
|
|
75
75
|
* Two kinds of entry stay out on purpose even though they are `carry` and hold no secret:
|
|
76
76
|
* - LEDGERS (workflow runs, loop iterations, thread bookkeeping, permission-usage batches, held-wake
|
|
77
77
|
* queues), which are rewritten on a timer or several times per step. Tracking them buries the owner's
|
|
78
|
-
* code review under machine noise
|
|
78
|
+
* code review under machine noise, one of them is written every few seconds while a browser has the app
|
|
79
79
|
* open. A queue is a ledger too: it records that something was ASKED, and is emptied when it is answered.
|
|
80
80
|
* - BULK (session transcripts, artifacts), which are hundreds of megabytes of constantly-rewritten
|
|
81
81
|
* content. They travel in a bundle; they do not belong in a diff.
|
|
82
82
|
* `versioned` is therefore NARROWER than `carry`, and the two answer different questions: carry is "does it
|
|
83
83
|
* move to a new sandbox", this is "should a human review it changing". */
|
|
84
84
|
readonly versioned?: true;
|
|
85
|
-
/* AUTHORED but not configuration
|
|
85
|
+
/* AUTHORED but not configuration, the fourth question an entry can answer, and the narrowest: is this
|
|
86
86
|
* human- or agent-written TEXT that a workspace search should surface? Every `versioned` entry already is
|
|
87
|
-
* (a setting, a persona, a skill
|
|
87
|
+
* (a setting, a persona, a skill, things the agent is asked to find and edit), so this flag exists only
|
|
88
88
|
* for the entries that are authored content without being config: a draft awaiting approval, a staged
|
|
89
89
|
* README, an extension the agent wrote in place. Two of those three are now `versioned` as well, which makes
|
|
90
90
|
* the flag redundant to `SEARCHABLE_STATE_PATHS` on them and is why it stays anyway: searchability is a
|
|
@@ -94,27 +94,27 @@ 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
|
|
97
|
+
/* WHETHER DESKTOP-SYNC COPIES THIS DOWN, the fifth question, and the one `portability` cannot answer even
|
|
98
98
|
* though it looks like it should.
|
|
99
99
|
*
|
|
100
100
|
* Portability asks whether a piece of state may RESTORE into a different sandbox. Backup asks whether it may
|
|
101
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
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
|
|
103
|
+
* was already written down, an access list that TRAVELLED would let a source sandbox hand itself the
|
|
104
104
|
* target's ownership, which is why it may never be `carry`. None of that reasoning says the owner may not
|
|
105
105
|
* hold a copy of who could drive their own sandbox. Conflating the two is what made the sync ignore the state
|
|
106
106
|
* dir wholesale, so a sandbox that went away took every persona, skill, draft and transcript with it.
|
|
107
107
|
*
|
|
108
108
|
* SO IT DERIVES, and this flag exists only to carve out the entry where the derivation is wrong. The default
|
|
109
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
|
|
111
|
-
* rebuildable and all bulk
|
|
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 exposure is the number of
|
|
112
112
|
* places it exists, and a laptop is one more place.
|
|
113
113
|
*
|
|
114
114
|
* `false` is the only value: an entry either accepts the derived answer or opts out of the copy, and there is
|
|
115
115
|
* no entry that needs opting IN against its class. Anything opting out says why on the entry. */
|
|
116
116
|
readonly backup?: false;
|
|
117
|
-
/* WHO BUILDS THIS TREE, when it is not the daemon
|
|
117
|
+
/* WHO BUILDS THIS TREE, when it is not the daemon, an extension, pnpm, another process entirely. Declared
|
|
118
118
|
* on the entry because the coverage guard's second direction ("every declared entry is built somewhere in
|
|
119
119
|
* the daemon") is only meaningful for entries the daemon owns: one it can never build must say who does, or
|
|
120
120
|
* the guard would read the entry as dead. Absent for everything the daemon writes itself. */
|
|
@@ -123,16 +123,16 @@ export interface WorkspaceStateFile extends StateFile {
|
|
|
123
123
|
|
|
124
124
|
/* Declared `as const` so the paths survive as literal types (see WorkspaceStatePath below), then published under
|
|
125
125
|
* the interface. Both bindings are needed and neither is redundant: the const is the only thing that can produce
|
|
126
|
-
* the path union, and every consumer reads entries as `WorkspaceStateFile
|
|
126
|
+
* the path union, and every consumer reads entries as `WorkspaceStateFile`, an exact-literal tuple loses the
|
|
127
127
|
* optional members (`note`, `why`) on the entries that omit them, which is a worse type for reading than the
|
|
128
128
|
* interface it satisfies. One list, two views of it. */
|
|
129
129
|
const STATE_FILES = [
|
|
130
130
|
/* A capability add/remove recomposes the environment overlay and can add or drop a repo's panel.
|
|
131
131
|
*
|
|
132
|
-
* SPLIT ALREADY
|
|
132
|
+
* SPLIT ALREADY, and this entry's classification had not caught up, which is the whole of what changed here.
|
|
133
133
|
* It read `secret` on a claim that had stopped being true: that each entry's `config` carries that
|
|
134
134
|
* capability's credential, so the manifest is a secret in full. It does not. capabilities-store.ts's
|
|
135
|
-
* withSecretVault keeps credential VALUES off /work entirely
|
|
135
|
+
* withSecretVault keeps credential VALUES off /work entirely, the manifest holds `__intentic_vaulted__`
|
|
136
136
|
* where one used to be, reads rehydrate so no caller noticed, and main.ts sweeps a hand-written value out at
|
|
137
137
|
* boot. What is left is the SHAPE of a connection: a kind, a URL, a username, a purpose, which permissions a
|
|
138
138
|
* connected computer was granted.
|
|
@@ -140,19 +140,19 @@ const STATE_FILES = [
|
|
|
140
140
|
* WHICH KEYS THOSE ARE IS DERIVED, not listed a second time: `echo` already answers "what of this config may
|
|
141
141
|
* a browser see", and the credential keys are exactly its complement (capabilities/secret-fields.ts). A kind
|
|
142
142
|
* that starts withholding a new field starts vaulting it on the same commit. That is what makes this
|
|
143
|
-
* classification a property of the code rather than a promise to re-audit it
|
|
143
|
+
* classification a property of the code rather than a promise to re-audit it, the reason the entry can be
|
|
144
144
|
* reclassified at all, and the reason a hand-kept "these fields are safe" list could not have earned it.
|
|
145
145
|
*
|
|
146
146
|
* `carry`, and this is the entry where that earns the most. composeEnvironment reads its Dockerfile fragments
|
|
147
147
|
* from here, so a bundle that dropped it arrived on a stock overlay with an import report listing every
|
|
148
148
|
* connection to re-add by hand. It now arrives listing them itself, each visibly unconnected and waiting for
|
|
149
|
-
* one credential apiece
|
|
149
|
+
* one credential apiece, the shape personas.json has had all along, for the same reason.
|
|
150
150
|
*
|
|
151
151
|
* `versioned`, which is the point. Connecting this sandbox to a deployment orchestrator, or granting a
|
|
152
152
|
* connected computer shell and screen control, is the largest change anyone makes to what it can DO, and it
|
|
153
153
|
* left a diff nowhere. One consequence worth stating rather than discovering: an identifier that pairs with a
|
|
154
|
-
* credential
|
|
155
|
-
* user"
|
|
154
|
+
* credential. Komodo's api key beside its api secret, which its own connector card calls "like a database
|
|
155
|
+
* user", is echoed, and therefore lands in the diff exactly as a database username would. */
|
|
156
156
|
{
|
|
157
157
|
path: ".intentic/config/capabilities.json",
|
|
158
158
|
invalidates: ["capabilities", "environment", "panels", "manifests"],
|
|
@@ -163,21 +163,21 @@ const STATE_FILES = [
|
|
|
163
163
|
/* Which workspace-derived recommendations the owner has said "not needed" to, and the evidence each was
|
|
164
164
|
* declined against. It rides the `capabilities` key because the catalog is what changes when one lands, and
|
|
165
165
|
* it travels because a decision about what this workspace does NOT need is as much the owner's as the
|
|
166
|
-
* connections themselves
|
|
166
|
+
* connections themselves, an export that dropped it would greet them on the target with the same
|
|
167
167
|
* suggestions they had already dismissed. Holds no credential: it is a card name and a file path. */
|
|
168
168
|
{ path: ".intentic/config/capability-dismissals.json", invalidates: ["capabilities"], portability: "carry", versioned: true },
|
|
169
169
|
|
|
170
|
-
/* The secret use ledger
|
|
170
|
+
/* The secret use ledger, one row per moment the agent's exits spent a stored secret (a `{{secret:name}}`
|
|
171
171
|
* reference resolved into a shell command, a value typed into a browser field), joined onto the secrets
|
|
172
172
|
* inventory as each entry's "last used" (sandbox's secrets/secret-uses.ts). Holds names and destinations,
|
|
173
|
-
* never values
|
|
173
|
+
* never values, which is why it may `carry`: like the automations' run ledger, a use history is about the
|
|
174
174
|
* secrets, and an export that dropped it would arrive claiming none had ever been touched. */
|
|
175
175
|
{ path: ".intentic/records/secret-uses.json", invalidates: ["secrets"], portability: "carry" },
|
|
176
176
|
|
|
177
|
-
/* The wallet's payment ledger
|
|
177
|
+
/* The wallet's payment ledger, one row per payment attempt that reached policy (sandbox's
|
|
178
178
|
* wallet/wallet-ledger.ts): what was paid, to whom, how it settled, with the onchain transaction hash
|
|
179
|
-
* when the endpoint stated one. Holds amounts, hosts and addresses, never a credential
|
|
180
|
-
* never enters the container at all
|
|
179
|
+
* when the endpoint stated one. Holds amounts, hosts and addresses, never a credential, the signing key
|
|
180
|
+
* never enters the container at all, so it may `carry`: like the secret-use ledger, a spend history is
|
|
181
181
|
* about the owner's money, and an export that dropped it would arrive claiming none was ever spent. */
|
|
182
182
|
{
|
|
183
183
|
path: ".intentic/records/wallet-ledger.json",
|
|
@@ -186,16 +186,16 @@ const STATE_FILES = [
|
|
|
186
186
|
portability: "carry",
|
|
187
187
|
},
|
|
188
188
|
|
|
189
|
-
/* The named personas this sandbox shows the outside world
|
|
189
|
+
/* The named personas this sandbox shows the outside world, which connected accounts each one speaks for,
|
|
190
190
|
* what a session wearing it may do, where it works (schemas.ts PersonaSchema). It invalidates `capabilities` as well as
|
|
191
191
|
* its own key because a card and the accounts it names are read together everywhere they are shown: connect
|
|
192
192
|
* a second Reddit and the persona list has a new candidate; remove one and a card points at nothing.
|
|
193
193
|
*
|
|
194
|
-
* It is `carry`, and that is the whole design rather than an oversight
|
|
194
|
+
* It is `carry`, and that is the whole design rather than an oversight, a card is a NAME and a list of ids,
|
|
195
195
|
* never a credential, so it travels to a new sandbox in full while the logins it refers to stay behind. What
|
|
196
196
|
* arrives is a workspace that already knows it has a work-reddit and a studio-x, both visibly unconnected,
|
|
197
197
|
* each waiting for one sign-in. It was also the FIRST file under .intentic the root repo tracked, and the
|
|
198
|
-
* argument it was carved out on
|
|
198
|
+
* argument it was carved out on, a card is configuration, holds no secret, and belongs in review, is the
|
|
199
199
|
* one `versioned` now generalises to the rest of the config slice (personas/personas-store.ts argues it at
|
|
200
200
|
* length, and its reasoning is why the flag exists rather than a second hand-kept list). */
|
|
201
201
|
{ path: ".intentic/config/personas.json", invalidates: ["personas", "capabilities", "manifests"], portability: "carry", versioned: true },
|
|
@@ -206,7 +206,7 @@ const STATE_FILES = [
|
|
|
206
206
|
* stale copy of it:
|
|
207
207
|
* - custom is the owner-approved SOURCE OF TRUTH and the only one that must travel;
|
|
208
208
|
* - approved is COMPOSED from custom + the capability fragments + this container's base image, and is
|
|
209
|
-
* rewritten on the target's first boot
|
|
209
|
+
* rewritten on the target's first boot, carrying it would ship a FROM naming an image the target may
|
|
210
210
|
* not be on (see composeEnvironment's baseImageOf);
|
|
211
211
|
* - the proposal and the per-tool drafts under environment.d/ are the agent's pending requests, which the
|
|
212
212
|
* owner has not answered yet; they travel so the question survives the move. */
|
|
@@ -230,19 +230,19 @@ const STATE_FILES = [
|
|
|
230
230
|
portability: "derived",
|
|
231
231
|
note: "Stamps of when each rule last did something; the new sandbox starts its own record.",
|
|
232
232
|
},
|
|
233
|
-
/* Written by the AGENT's file tools (the drafts skill), read by the owner's approval inbox
|
|
233
|
+
/* Written by the AGENT's file tools (the drafts skill), read by the owner's approval inbox, the one entry
|
|
234
234
|
* here whose whole point is that a change arrives from outside the browser that renders it. `authored`:
|
|
235
235
|
* a draft is text somebody wrote, and "find the reddit draft about X" is an ordinary search.
|
|
236
236
|
*
|
|
237
237
|
* `versioned` because a draft is the furthest-reaching thing the agent writes: these words go out under the
|
|
238
|
-
* owner's name, to an audience, and cannot be recalled. The approval inbox already gates that
|
|
238
|
+
* owner's name, to an audience, and cannot be recalled. The approval inbox already gates that, but a gate is
|
|
239
239
|
* not a record. Declining one used to erase it, so the question "what has this agent tried to post" had no
|
|
240
240
|
* answer at all, and an approved post's own history (what was proposed, what the owner changed, when it
|
|
241
241
|
* actually went) lived only in a file nobody could diff.
|
|
242
242
|
*
|
|
243
243
|
* It costs almost nothing to track, which is why the ledger objection does not reach it: a draft is one small
|
|
244
244
|
* file per post, it is written a handful of times across its whole life (proposed → approved → posted, with
|
|
245
|
-
* `postedAt`/`postedUrl` stamped at the end), and it is KEPT afterwards rather than consumed
|
|
245
|
+
* `postedAt`/`postedUrl` stamped at the end), and it is KEPT afterwards rather than consumed, so tracking
|
|
246
246
|
* yields a durable record instead of the add/delete churn a queue would produce. Nothing in a draft is a
|
|
247
247
|
* credential: it is a platform, a target URL and the body itself, all of it bound for publication anyway. */
|
|
248
248
|
{ path: ".intentic/config/drafts/", invalidates: ["drafts"], portability: "carry", versioned: true, authored: true },
|
|
@@ -257,17 +257,17 @@ const STATE_FILES = [
|
|
|
257
257
|
portability: "carry",
|
|
258
258
|
versioned: true,
|
|
259
259
|
},
|
|
260
|
-
/* The run history, keyed by automation id
|
|
260
|
+
/* The run history, keyed by automation id, the LEDGER half of what automations.json used to be, and split
|
|
261
261
|
* out of it for the one reason this table's `versioned` note already gives: a tracked file must be worth
|
|
262
262
|
* reviewing. A scheduled automation records a run every time it fires, so every fire dirtied the manifest
|
|
263
|
-
* the owner reviews, and the run records went into `git log` with it
|
|
263
|
+
* the owner reviews, and the run records went into `git log` with it, timestamps and conversation ids
|
|
264
264
|
* committed beside the prompt they belong to, burying an actual edit to the automation's config under
|
|
265
265
|
* machine noise. Config is now the only thing in the tracked file, and a fire touches nothing tracked.
|
|
266
266
|
*
|
|
267
267
|
* It is `carry` for the same reason the workflow ledger is: a run history is about the automation, not about
|
|
268
268
|
* the machine, and an export that dropped it would arrive claiming every automation had never run.
|
|
269
269
|
*
|
|
270
|
-
* Its invalidation is the extension's, exactly like the manifest above
|
|
270
|
+
* Its invalidation is the extension's, exactly like the manifest above, and it has to be DECLARED there
|
|
271
271
|
* rather than inherited, because the row renders its run history from this file now: without its own entry
|
|
272
272
|
* a completed run would stop refreshing the view the moment it stopped living in automations.json. */
|
|
273
273
|
{
|
|
@@ -283,7 +283,7 @@ const STATE_FILES = [
|
|
|
283
283
|
portability: "carry",
|
|
284
284
|
},
|
|
285
285
|
/* The maintenance ledger and probe evidence, written by the daemon's chores-store and rendered by the
|
|
286
|
-
* intentic.maintenance extension
|
|
286
|
+
* intentic.maintenance extension, the automations shape exactly: the path is the daemon's, the query keys
|
|
287
287
|
* (`maintenance-report`, `maintenance-runs`) are the extension's own contributes.files. Point-in-time
|
|
288
288
|
* evidence about this workspace, so `carry` like the run ledgers: an export that dropped it would arrive
|
|
289
289
|
* claiming no chore had ever been checked. */
|
|
@@ -294,7 +294,7 @@ const STATE_FILES = [
|
|
|
294
294
|
portability: "carry",
|
|
295
295
|
},
|
|
296
296
|
/* The documentation STAGING tree (documentation extension's paths.ts): generation writes here, the owner
|
|
297
|
-
* reads and approves here, publishing copies into the repo. `authored` is the whole nature of the entry
|
|
297
|
+
* reads and approves here, publishing copies into the repo. `authored` is the whole nature of the entry,
|
|
298
298
|
* these are draft READMEs, drafts-shaped in every way that matters, and "find the staged page about X" is
|
|
299
299
|
* as ordinary a search as finding a post draft. */
|
|
300
300
|
{
|
|
@@ -307,15 +307,15 @@ const STATE_FILES = [
|
|
|
307
307
|
},
|
|
308
308
|
/* The workflow designs and their run ledger became CORE keys the day runs got cards on the fleet board and
|
|
309
309
|
* a mode of the chat panel (web's useWorkflowRuns): those surfaces exist whether or not the workflows
|
|
310
|
-
* extension is enabled, so their freshness cannot ride an extension's contributes.files
|
|
310
|
+
* extension is enabled, so their freshness cannot ride an extension's contributes.files, an owner turning
|
|
311
311
|
* the extension off would have frozen the board's run cards mid-run. This push is the ONLY live feed the
|
|
312
312
|
* run surfaces have: the scheduler writes the ledger several times per step and nothing polls for it.
|
|
313
313
|
* The runs file invalidates `workflows` too, because GET /workflows embeds each design's runs
|
|
314
|
-
* (WorkflowSummary)
|
|
314
|
+
* (WorkflowSummary), a settled step changes that answer as surely as an edited design does. */
|
|
315
315
|
{ path: ".intentic/config/workflows.json", invalidates: ["workflows"], portability: "carry", versioned: true },
|
|
316
316
|
{ path: ".intentic/records/workflow-runs.json", invalidates: ["workflows", "workflow-runs"], portability: "carry" },
|
|
317
317
|
/* The SAVED loops, which are a manifest and so the opposite of the ledger below them: a handful of entries
|
|
318
|
-
* a person authors, read by two surfaces at once
|
|
318
|
+
* a person authors, read by two surfaces at once, the workflows page that owns them, and every chat
|
|
319
319
|
* composer's loop picker. Those two are in different windows as often as not (a popped-out chat is its own
|
|
320
320
|
* window), so an edit made on the page has to reach a picker nobody is going to think to reopen. A CORE key
|
|
321
321
|
* rather than the workflows extension's, for the reason the workflow designs beside it are: the composer
|
|
@@ -331,7 +331,7 @@ const STATE_FILES = [
|
|
|
331
331
|
/* ---- reached by no query, for reasons that are not oversights ----
|
|
332
332
|
*
|
|
333
333
|
* This channel's currency is a QUERY KEY, and invalidation only reaches a query something is observing.
|
|
334
|
-
* Both entries below are outside that by design, so an empty set is the honest record
|
|
334
|
+
* Both entries below are outside that by design, so an empty set is the honest record, naming a key no
|
|
335
335
|
* query uses would put the drift this table exists to remove straight back into it. Each says which
|
|
336
336
|
* constraint would have to move first, so the next reader doesn't re-derive it. */
|
|
337
337
|
{
|
|
@@ -350,23 +350,23 @@ const STATE_FILES = [
|
|
|
350
350
|
*
|
|
351
351
|
* This entry used to be `secret` and untracked, classed by what a value COULD hold: values are a primitive
|
|
352
352
|
* union an extension chooses the meaning of, and "an API key for the service I talk to" is squarely within
|
|
353
|
-
* it. That classification was honest about the risk and wrong about the file
|
|
353
|
+
* it. That classification was honest about the risk and wrong about the file, it meant an extension's whole
|
|
354
354
|
* configuration was unreviewable because one of its keys might be a credential, AND the credential was in
|
|
355
355
|
* there anyway, in a file the workspace API does not lock. A turn could simply read it.
|
|
356
356
|
*
|
|
357
357
|
* A descriptor already says which keys those are (`contributes.settings[].secret`), so the values it names
|
|
358
|
-
* now live in the vault off /work and this file keeps the rest
|
|
358
|
+
* now live in the vault off /work and this file keeps the rest, the capability manifest's split, applied to
|
|
359
359
|
* the same problem one table over (extensions/extension-settings.ts holds it, and the reasoning). Reads
|
|
360
360
|
* rehydrate, so no caller changed.
|
|
361
361
|
*
|
|
362
362
|
* What the split earns: `carry`, because what is left is an extension's configuration and a bundle should
|
|
363
363
|
* arrive with it; and `versioned`, because turning an extension's behaviour on is a decision, and the file
|
|
364
364
|
* that records it can now be read without reading anybody's token. The boot sweep is what keeps that true of
|
|
365
|
-
* a file the agent can also edit
|
|
365
|
+
* a file the agent can also edit, see vaultExtensionSettingSecrets.
|
|
366
366
|
*
|
|
367
367
|
* NO `note`, and the split is why: a note is printed by the import report beside a SKIPPED entry, so an entry
|
|
368
368
|
* that carries can never show one. "Re-enter the credentials" is now the vault's instruction to give, and the
|
|
369
|
-
* vault is under `.intentic/auth
|
|
369
|
+
* vault is under `.intentic/auth/`, which is skipped, and says so there. */
|
|
370
370
|
{
|
|
371
371
|
path: ".intentic/config/extension-settings.json",
|
|
372
372
|
invalidates: [],
|
|
@@ -374,8 +374,8 @@ const STATE_FILES = [
|
|
|
374
374
|
portability: "carry",
|
|
375
375
|
versioned: true,
|
|
376
376
|
},
|
|
377
|
-
/* Unlike the settings file above it, the on/off switch IS observed by a query
|
|
378
|
-
* which carries each row's switch position
|
|
377
|
+
/* Unlike the settings file above it, the on/off switch IS observed by a query, the Extensions tab's list,
|
|
378
|
+
* which carries each row's switch position, so a flip made elsewhere (another member, the agent writing the
|
|
379
379
|
* file) shows up here live. It does not re-run the host: activating or retiring an extension is the loader's
|
|
380
380
|
* reconcile, which the tab's own toggle triggers, so a remote flip takes effect on this browser's next load. */
|
|
381
381
|
{
|
|
@@ -384,16 +384,16 @@ const STATE_FILES = [
|
|
|
384
384
|
portability: "carry",
|
|
385
385
|
versioned: true,
|
|
386
386
|
},
|
|
387
|
-
/* Workspace extensions: one directory per extension, consumed straight from the workspace
|
|
387
|
+
/* Workspace extensions: one directory per extension, consumed straight from the workspace, no clone, no
|
|
388
388
|
* install moment. Written like drafts, by the agent's own file tools (which is the point: an agent authors
|
|
389
389
|
* an extension and it is live for the daemon and every session at once, since .intentic is shared), so this
|
|
390
390
|
* push is what makes one appearing or changing show up on the Extensions tab while the owner watches.
|
|
391
|
-
* `authored` for the same reason as drafts: this is source the agent wrote and will be asked to find
|
|
391
|
+
* `authored` for the same reason as drafts: this is source the agent wrote and will be asked to find,
|
|
392
392
|
* unlike `.intentic/extensions/` below, which is CLONES of source that lives elsewhere.
|
|
393
393
|
*
|
|
394
394
|
* `versioned` FOR THAT SAME REASON, which is the whole argument. Every other load path an extension can take
|
|
395
395
|
* is already reviewable by construction: a git-installed one is a sha in `capabilities.json` that an owner
|
|
396
|
-
* approved, a baked one shipped in the image. This one is neither
|
|
396
|
+
* approved, a baked one shipped in the image. This one is neither, it is code that appears because an agent
|
|
397
397
|
* wrote a file, runs in the app on the owner's session, may register a rail tile, and may serve HTTP from a
|
|
398
398
|
* node process with the workspace under `node:fs` and whatever `permissions.daemon` names. Untracked, the
|
|
399
399
|
* switch that turns it on was in `git log` (extension-enablement.json, below) while the thing being switched
|
|
@@ -401,11 +401,11 @@ const STATE_FILES = [
|
|
|
401
401
|
* kind with no install moment to review at, so the diff is the only review there is.
|
|
402
402
|
*
|
|
403
403
|
* Not a ledger and not bulk: a handful of small authored files per extension, written when someone edits them.
|
|
404
|
-
* The daemon restarts the backend host on a change here, so an edit is already a consequential event
|
|
404
|
+
* The daemon restarts the backend host on a change here, so an edit is already a consequential event, this
|
|
405
405
|
* makes it a legible one. */
|
|
406
406
|
{ path: ".intentic/config/workspace-extensions/", invalidates: ["extensions"], portability: "carry", versioned: true, authored: true },
|
|
407
407
|
/* What the registry comparison found per installed extension (update available / advisory / post-update
|
|
408
|
-
* health), written by the periodic check and by the update/revert transactions
|
|
408
|
+
* health), written by the periodic check and by the update/revert transactions, pushed to the tab because
|
|
409
409
|
* an advisory that auto-disabled something must not wait for a reload to be seen. */
|
|
410
410
|
{ path: ".intentic/records/extension-updates.json", invalidates: ["extensions"], portability: "carry" },
|
|
411
411
|
/* The owner's per-extension update posture (notify / agent / auto, and the advisory opt-out). Carried:
|
|
@@ -423,13 +423,13 @@ const STATE_FILES = [
|
|
|
423
423
|
* because it looks like the two above it: an email and a role per row, nothing to vault, and "who may drive
|
|
424
424
|
* this sandbox" is as consequential a fact as any this table tracks.
|
|
425
425
|
*
|
|
426
|
-
* It stays out for two reasons that are not about secrecy. It is a MIRROR
|
|
426
|
+
* It stays out for two reasons that are not about secrecy. It is a MIRROR, the platform's invite records are
|
|
427
427
|
* the grant, this is the copy the enforcer keeps so a grant it never received is never honoured, and a change
|
|
428
428
|
* here is the two disagreeing rather than anyone deciding something. Review of the decision already exists,
|
|
429
429
|
* on the Access tab, against the record that is authoritative. And tracking it would mean reclassifying it
|
|
430
430
|
* `carry` to satisfy the guard, which is the one thing it must never be: an access list that travelled would
|
|
431
431
|
* let a source sandbox hand itself the target's ownership. Widening the guard for this single entry is the
|
|
432
|
-
* worse trade
|
|
432
|
+
* worse trade, it protects every `identity` entry, and most of those ARE credentials. */
|
|
433
433
|
{
|
|
434
434
|
path: ".intentic/identity/members.json",
|
|
435
435
|
invalidates: [],
|
|
@@ -444,7 +444,7 @@ const STATE_FILES = [
|
|
|
444
444
|
* and no generic export can safely distinguish those files. The broad root also makes a newly-added provider
|
|
445
445
|
* secret by construction instead of relying on another hand-maintained provider-name list.
|
|
446
446
|
*
|
|
447
|
-
* IT IS NO LONGER ONLY THE AI LOGINS. Both credential splits put their vault here
|
|
447
|
+
* IT IS NO LONGER ONLY THE AI LOGINS. Both credential splits put their vault here, `capability-secrets.json`
|
|
448
448
|
* and `extension-secrets.json`, sited beside the provider homes precisely because this tree is already
|
|
449
449
|
* outside the file routes, the workspace walk and the search index (composition.ts sites them, and the two
|
|
450
450
|
* stores argue why). So this is now the ONE entry a secret-less bundle leaves behind, and its note is
|
|
@@ -486,7 +486,7 @@ const STATE_FILES = [
|
|
|
486
486
|
portability: "derived",
|
|
487
487
|
},
|
|
488
488
|
/* Connector and extension scratch, one directory per extension under runtime/extensions/<id>
|
|
489
|
-
* (extensionRuntimeDir below
|
|
489
|
+
* (extensionRuntimeDir below, the ONLY way an extension names a home here, so a new one lands under its
|
|
490
490
|
* own id by construction instead of minting a file at the .intentic root). Resume watermarks, cached
|
|
491
491
|
* hour-tokens, gateway discovery state: all of it either expires or re-establishes itself, and classifying
|
|
492
492
|
* the root once is what keeps a token an extension caches tomorrow out of bundles without a second edit. */
|
|
@@ -504,7 +504,7 @@ const STATE_FILES = [
|
|
|
504
504
|
portability: "derived",
|
|
505
505
|
},
|
|
506
506
|
/* Not written by the daemon at all: pnpm auto-creates its content-addressable store at the project's
|
|
507
|
-
* mountpoint, and `.intentic` is its own mount in an isolated turn
|
|
507
|
+
* mountpoint, and `.intentic` is its own mount in an isolated turn, so an install run from under it mints
|
|
508
508
|
* this. Declared anyway, because the table's job is to say what everything under `.intentic` IS: hardlink
|
|
509
509
|
* sources a fresh install rebuilds, which an export must not ship (it reached 1.3 GB on the workspace this
|
|
510
510
|
* entry was written against). */
|
|
@@ -543,10 +543,10 @@ const STATE_FILES = [
|
|
|
543
543
|
},
|
|
544
544
|
/* THE ONE ENTRY THAT OPTS OUT OF THE BACKUP, and the reason the flag exists rather than the rule simply
|
|
545
545
|
* reading `portability !== "derived"`. It is `identity` like the three below it, so the derived answer would
|
|
546
|
-
* copy it down with them
|
|
546
|
+
* copy it down with them, but where those are a name, a workspace id and a role per row, these are tokens
|
|
547
547
|
* that AUTHENTICATE against this sandbox from outside it. Hashed, which lowers the stakes and does not
|
|
548
548
|
* settle them: the point of a backup is to be readable after the thing it backs up is gone, and a file whose
|
|
549
|
-
* only purpose is to admit callers has no business sitting in one. Nothing is lost by leaving it out
|
|
549
|
+
* only purpose is to admit callers has no business sitting in one. Nothing is lost by leaving it out, the
|
|
550
550
|
* entry's own note already says the tokens must be re-minted on any new sandbox, so a copy could never have
|
|
551
551
|
* been restored anyway. */
|
|
552
552
|
{
|
|
@@ -595,61 +595,61 @@ const STATE_FILES = [
|
|
|
595
595
|
note: "Extensions re-clone from the capability manifest on the target's next reconcile.",
|
|
596
596
|
},
|
|
597
597
|
{ path: ".intentic/records/plugins/", invalidates: [], why: "Agent plugin dirs, read by the SDK's loader each turn.", portability: "carry" },
|
|
598
|
-
/* THE SKILLS THE OWNER WROTE THEMSELVES, one directory per skill
|
|
598
|
+
/* THE SKILLS THE OWNER WROTE THEMSELVES, one directory per skill, the source of truth the reconciler copies
|
|
599
599
|
* into `.agents/skills` for the ones currently switched on (settings.json's `skills` list). It is here rather
|
|
600
600
|
* than in the loaded folder for the reason the plugin dirs are: that tree holds only what is currently ON,
|
|
601
601
|
* and a skill switched off has to keep its text somewhere the loaders will not read it from.
|
|
602
602
|
*
|
|
603
603
|
* `versioned`, like the rest of the config slice: a skill changes how the agent behaves, so it earns a diff
|
|
604
604
|
* in the Changes review and a line in `git log` the same way a rule or a persona does. `carry` for the same
|
|
605
|
-
* reason
|
|
605
|
+
* reason, it is text the owner wrote, with no credential in it and nothing about this machine. */
|
|
606
606
|
{ path: ".intentic/config/skills/", invalidates: ["skills"], portability: "carry", versioned: true },
|
|
607
|
-
/* ONE FOLDER PER PERSONA
|
|
607
|
+
/* ONE FOLDER PER PERSONA, what a session wearing that card is told, and the skills and tools only it gets.
|
|
608
608
|
* Laid out as a Claude Code plugin (`.claude-plugin/plugin.json`, `skills/`, `agents/`, `commands/`,
|
|
609
609
|
* `hooks/`, `.mcp.json`) so the runtime's own loader reads it and this daemon parses none of it, exactly as
|
|
610
610
|
* the plugin checkouts above are read (personas/persona-kit.ts).
|
|
611
611
|
*
|
|
612
612
|
* A SECOND ENTRY BESIDE `personas.json` RATHER THAN A FIELD INSIDE IT, because the two are different kinds
|
|
613
|
-
* of thing to review. The card is a name, some ids and some switches
|
|
613
|
+
* of thing to review. The card is a name, some ids and some switches, a few lines that diff cleanly. This
|
|
614
614
|
* is prose and files: a system prompt, a skill, a subagent. Folding a 20k prompt into the JSON would make
|
|
615
615
|
* every persona edit an unreadable diff and put text somebody wrote inside a record nobody writes by hand.
|
|
616
616
|
*
|
|
617
617
|
* `versioned` and `carry` for the same reasons the card and the skills above are: it changes how the agent
|
|
618
|
-
* behaves, it holds no credential, and it belongs in a pull request
|
|
618
|
+
* behaves, it holds no credential, and it belongs in a pull request, which is also what makes it
|
|
619
619
|
* searchable, since every versioned entry already is. */
|
|
620
620
|
{ path: ".intentic/config/personas/", invalidates: ["personas"], portability: "carry", versioned: true },
|
|
621
621
|
] as const satisfies readonly WorkspaceStateFile[];
|
|
622
622
|
|
|
623
623
|
export const WORKSPACE_STATE_FILES: readonly WorkspaceStateFile[] = STATE_FILES;
|
|
624
624
|
|
|
625
|
-
/* The entries the root repo tracks, workspace-root-relative and in declaration order
|
|
625
|
+
/* The entries the root repo tracks, workspace-root-relative and in declaration order, what history.ts turns
|
|
626
626
|
* into the negations that carve them back out of the wholesale `.intentic` exclusion.
|
|
627
627
|
*
|
|
628
628
|
* Derived rather than written down beside the exclude rule, for the reason this whole file exists: the git rule
|
|
629
629
|
* lives in the daemon and the classification lives here, and a second hand-kept copy of "which config is
|
|
630
630
|
* reviewable" is a copy that goes stale the first time someone adds a store. Marking an entry `versioned` is now
|
|
631
|
-
* the entire change
|
|
631
|
+
* the entire change, the exclude list follows on the next boot, in both places it is written. */
|
|
632
632
|
export const VERSIONED_STATE_PATHS: readonly string[] = WORKSPACE_STATE_FILES.filter((file) => file.versioned).map((file) => file.path);
|
|
633
633
|
|
|
634
634
|
/* The `.intentic` slice a workspace SEARCH may surface: configuration a person reviews (`versioned`) plus the
|
|
635
|
-
* authored-content dirs (`authored`)
|
|
635
|
+
* authored-content dirs (`authored`), drafts, staged docs, workspace extensions. Everything else under
|
|
636
636
|
* `.intentic` is machine state, and the search engine (iq's floor) denies it BY DEFAULT off this list, the
|
|
637
637
|
* same default-deny the portability classes are built on and for the same reason: a deny list is a list a new
|
|
638
|
-
* ledger is forgotten from, and the forgetting is silent
|
|
638
|
+
* ledger is forgotten from, and the forgetting is silent, it ranked loop iteration history and cloned
|
|
639
639
|
* third-party extension source against the user's own code for months before this derivation existed.
|
|
640
640
|
*
|
|
641
641
|
* WHAT THE CREDENTIAL SPLITS MOVED ACROSS THIS LINE, since the note that used to sit here said the opposite and
|
|
642
642
|
* was worth replacing rather than deleting. `capabilities.json` was `secret` and unversioned, and the sentence
|
|
643
643
|
* celebrated that the index therefore stopped copying capability tokens into search text. It is `versioned` now
|
|
644
|
-
* and searchable
|
|
644
|
+
* and searchable, and the guarantee is unchanged, because the tokens are not in the file any more. The floor
|
|
645
645
|
* moved from "keep the index away from the file that holds credentials" to "the file holds none", which is the
|
|
646
|
-
* stronger of the two: it also holds for the shell, which never consulted this list at all. `auth
|
|
647
|
-
* values went, both vaults included
|
|
646
|
+
* stronger of the two: it also holds for the shell, which never consulted this list at all. `auth/`, where those
|
|
647
|
+
* values went, both vaults included, is still denied, and is the entry that was doing the real work all along. */
|
|
648
648
|
export const SEARCHABLE_STATE_PATHS: readonly string[] = WORKSPACE_STATE_FILES.filter((file) => file.versioned || file.authored).map(
|
|
649
649
|
(file) => file.path,
|
|
650
650
|
);
|
|
651
651
|
|
|
652
|
-
/* WHAT KIND OF THING THIS IS, in the one word a person browsing the state dir would use
|
|
652
|
+
/* WHAT KIND OF THING THIS IS, in the one word a person browsing the state dir would use, and the axis the
|
|
653
653
|
* DIRECTORY LAYOUT is built on, so the folder you open explains itself before you read a table about it.
|
|
654
654
|
*
|
|
655
655
|
* It is DERIVED, and that is the whole reason it can be trusted. Forty-nine entries already answer three
|
|
@@ -657,7 +657,7 @@ export const SEARCHABLE_STATE_PATHS: readonly string[] = WORKSPACE_STATE_FILES.f
|
|
|
657
657
|
* perfectly rather than cut across each other: every `versioned` entry is `carry`, every `authored` entry is
|
|
658
658
|
* `carry`, and nothing is both a credential and a thing a person edits. A nested set of answers is exactly what
|
|
659
659
|
* a directory tree can express, which is why five folders can carry rules that used to take five hand-kept path
|
|
660
|
-
* lists
|
|
660
|
+
* lists, the git exclude, the search allow-list, the sync ignore, the watcher skip, the export bundle.
|
|
661
661
|
*
|
|
662
662
|
* Declaring the group on each entry instead would have made it a fourth independent fact to keep in step with
|
|
663
663
|
* the other three, which is the failure this file exists to argue against. Adding a store still means answering
|
|
@@ -666,25 +666,25 @@ export const SEARCHABLE_STATE_PATHS: readonly string[] = WORKSPACE_STATE_FILES.f
|
|
|
666
666
|
export type StateGroup =
|
|
667
667
|
/* Reviewed and reviewable: settings, personas, skills, drafts, staged docs, the environment overlay. Tracked
|
|
668
668
|
* by the root repo, searchable, backed up, and carried into a new sandbox. Two of its members (drafts, staged
|
|
669
|
-
* docs) are authored content rather than configuration, and the folder is still called `config
|
|
669
|
+
* docs) are authored content rather than configuration, and the folder is still called `config`, the word
|
|
670
670
|
* that makes seventeen of the nineteen instantly clear beats one that makes all nineteen vague. */
|
|
671
671
|
| "config"
|
|
672
|
-
/* What HAPPENED here
|
|
672
|
+
/* What HAPPENED here, run ledgers, approvals, chores, transcripts, artifacts. Machine-written, so untracked
|
|
673
673
|
* and unsearchable, but the owner's history all the same: backed up and carried. */
|
|
674
674
|
| "records"
|
|
675
675
|
/* Rebuildable from something that does travel: caches, indexes, extension checkouts, scratch, the composed
|
|
676
676
|
* overlay, browser profiles. Neither backed up nor carried, and the janitor may delete it. `local` in the
|
|
677
|
-
* sense every other tool uses it
|
|
677
|
+
* sense every other tool uses it, belongs to this machine, is not shared, and losing it costs nothing. */
|
|
678
678
|
| "local"
|
|
679
679
|
/* Who owns this sandbox and who may drive it. Backed up so the owner keeps a copy of their own access, never
|
|
680
|
-
* carried
|
|
680
|
+
* carried, a list that travelled would let a source sandbox claim the target. */
|
|
681
681
|
| "identity"
|
|
682
682
|
/* Credentials. Never backed up; carried only when the owner opts in at export and the bundle records it. */
|
|
683
683
|
| "secrets";
|
|
684
684
|
|
|
685
685
|
/* THE FOLDER EACH GROUP LIVES IN.
|
|
686
686
|
*
|
|
687
|
-
* The group name IS the directory name
|
|
687
|
+
* The group name IS the directory name, one vocabulary, not a name and a translation of it. That is what lets
|
|
688
688
|
* the guard in workspace-state.test.ts check the whole layout with one rule ("every entry sits under its own
|
|
689
689
|
* group's folder") rather than trusting forty-nine literals to have been typed correctly, and it is why renaming
|
|
690
690
|
* a folder is an edit here plus the literals the compiler then points at, with nothing able to half-move.
|
|
@@ -729,12 +729,12 @@ export const stateGroupOf = (file: WorkspaceStateFile): StateGroup => {
|
|
|
729
729
|
}
|
|
730
730
|
};
|
|
731
731
|
|
|
732
|
-
// The entries of one group, workspace-root-relative and in declaration order
|
|
732
|
+
// The entries of one group, workspace-root-relative and in declaration order, what each rule that used to keep
|
|
733
733
|
// its own path list now asks for instead.
|
|
734
734
|
export const stateGroupPaths = (group: StateGroup): readonly string[] =>
|
|
735
735
|
WORKSPACE_STATE_FILES.filter((file) => stateGroupOf(file) === group).map((file) => file.path);
|
|
736
736
|
|
|
737
|
-
/* THE SLICE DESKTOP-SYNC COPIES DOWN
|
|
737
|
+
/* THE SLICE DESKTOP-SYNC COPIES DOWN, ordinary state and the records that bind this sandbox to its owner,
|
|
738
738
|
* minus anything that opted out (see `backup` on the interface).
|
|
739
739
|
*
|
|
740
740
|
* The sync used to ignore `.intentic` WHOLE, which is the same conflation the `backup` flag exists to undo: the
|
|
@@ -750,12 +750,12 @@ export const BACKED_UP_STATE_PATHS: readonly string[] = WORKSPACE_STATE_FILES.fi
|
|
|
750
750
|
|
|
751
751
|
/* Its complement, which is what a sync ignore list actually needs: everything under the state dir that must NOT
|
|
752
752
|
* come down. Derived from the same predicate rather than listed, so a store added tomorrow is excluded until its
|
|
753
|
-
* class says otherwise
|
|
753
|
+
* class says otherwise, the same default-deny the search floor and the portability classes are built on. */
|
|
754
754
|
export const UNBACKED_STATE_PATHS: readonly string[] = WORKSPACE_STATE_FILES.filter((file) => !BACKED_UP_STATE_PATHS.includes(file.path)).map(
|
|
755
755
|
(file) => file.path,
|
|
756
756
|
);
|
|
757
757
|
|
|
758
|
-
/* THE ONE WAY AN EXTENSION NAMES ITS SCRATCH HOME
|
|
758
|
+
/* THE ONE WAY AN EXTENSION NAMES ITS SCRATCH HOME, `.intentic/runtime/extensions/<id>`, workspace-relative
|
|
759
759
|
* and forward-slash so the browser bundle can hold it too; callers join it onto whatever root is in force.
|
|
760
760
|
*
|
|
761
761
|
* It exists for the reason statePath does one table over: before it, every gateway spelled the layout itself
|
|
@@ -766,23 +766,23 @@ export const UNBACKED_STATE_PATHS: readonly string[] = WORKSPACE_STATE_FILES.fil
|
|
|
766
766
|
export const extensionRuntimeDir = (extension: string): string =>
|
|
767
767
|
`${STATE_GROUP_DIR.local}/runtime/extensions/${extension.replaceAll(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
768
768
|
|
|
769
|
-
/* The manifests whose problems the unreadable-manifest notice SHOWS
|
|
769
|
+
/* The manifests whose problems the unreadable-manifest notice SHOWS, the handful a person hand-edits, and the
|
|
770
770
|
* one fact that decides it is already in the table above.
|
|
771
771
|
*
|
|
772
772
|
* Every store reads through the same `jsonFile`, so every store reports what it could not make sense of, and for
|
|
773
|
-
* a long time the notice showed all of them. That is wrong twice over. Its advice
|
|
774
|
-
* on its own
|
|
773
|
+
* a long time the notice showed all of them. That is wrong twice over. Its advice, fix the file and this clears
|
|
774
|
+
* on its own, is addressed to somebody holding an editor, which is true of `settings.json` and false of a
|
|
775
775
|
* daemon-written LEDGER nobody opens: a run history that stopped matching a schema the build tightened is not a
|
|
776
776
|
* mistake the owner made, and the card asked them to repair sixty kilobytes of machine JSON by hand. Worse, a
|
|
777
777
|
* file that reports into the notice without feeding the notice's QUERY leaves a complaint no write can refresh,
|
|
778
|
-
* so it sits on screen until the daemon restarts
|
|
778
|
+
* so it sits on screen until the daemon restarts, which is exactly how the workflow ledger's entry became
|
|
779
779
|
* permanent furniture.
|
|
780
780
|
*
|
|
781
781
|
* Both follow from one rule, which is why this derives rather than lists: a file's problems are shown IFF a write
|
|
782
782
|
* to that file refreshes the notice. Declaring `manifests` in `invalidates` is the entire opt-in, so the edit
|
|
783
783
|
* that puts a file on the card is the same edit that keeps it current, and neither can be done without the
|
|
784
784
|
* other. A ledger that breaks still falls back and still sets its unreadable bytes aside on the next write
|
|
785
|
-
* (store/json-file.ts)
|
|
785
|
+
* (store/json-file.ts), it just stops asking the owner to fix it. */
|
|
786
786
|
export const REPORTED_MANIFEST_PATHS: readonly string[] = WORKSPACE_STATE_FILES.filter((file) => file.invalidates.includes("manifests")).map(
|
|
787
787
|
(file) => file.path,
|
|
788
788
|
);
|
|
@@ -794,22 +794,22 @@ export const isReportedManifest = (relPath: string): boolean => REPORTED_MANIFES
|
|
|
794
794
|
/* Old directory names are never read or migrated. Keep that finite set in one quarantine record so access,
|
|
795
795
|
* export, and search cannot reinterpret abandoned machine state as ordinary workspace content after a rename.
|
|
796
796
|
* `artifacts` still carry; the distinction here tells portability only which retired roots are secrets or
|
|
797
|
-
* derived
|
|
797
|
+
* derived, and tells the state janitor which it may DELETE: a retired `derived` root is a rebuildable cache
|
|
798
798
|
* by its own classification, so leaving 466 MB of abandoned model where only a manual `rm` reaches it was
|
|
799
799
|
* quarantine doing half its job. Secret and artifact roots stay until an owner removes them by hand: deleting
|
|
800
800
|
* content is not the janitor's call, only deleting what the class already says is disposable. */
|
|
801
801
|
export const RETIRED_WORKSPACE_STATE_DIRS = {
|
|
802
802
|
/* The flat spellings, from before the state dir was grouped into its five folders. Every entry in the table
|
|
803
803
|
* used to sit directly under `.intentic/`, and a sandbox that predates the move still has those directories
|
|
804
|
-
* on disk with real contents in them. They are quarantined rather than migrated
|
|
805
|
-
* state and no compatibility layers
|
|
804
|
+
* on disk with real contents in them. They are quarantined rather than migrated, the workspace rule is fresh
|
|
805
|
+
* state and no compatibility layers, but quarantine is exactly what they need, because the alternative is
|
|
806
806
|
* worse than leaving them: `auth` at the old spelling is a real credential store that the classifier no
|
|
807
807
|
* longer recognises, so without these names it would read as ordinary workspace content and be indexed by
|
|
808
808
|
* search, carried by an export and copied down by the backup. Being listed here keeps the old copy as
|
|
809
809
|
* untouchable as the new one, and lets the janitor delete the rebuildable half. */
|
|
810
810
|
secret: ["claude", "codex", "kimi", "opencode", "cliproxy", "auth", "ci.json"],
|
|
811
811
|
/* The ownership records at their OLD flat spelling, and a bucket of their own rather than a few more names
|
|
812
|
-
* in `secret
|
|
812
|
+
* in `secret`, because the two classes differ on exactly the thing that matters for a leftover. A `secret`
|
|
813
813
|
* travels when the owner opts in at export; an `identity` never travels at all, since a list of who may
|
|
814
814
|
* drive this sandbox arriving in another one is how a source hands itself the target's ownership. Filed under
|
|
815
815
|
* `secret` these would have become carryable by ticking a box, which is the one outcome their live entries
|
|
@@ -819,23 +819,23 @@ export const RETIRED_WORKSPACE_STATE_DIRS = {
|
|
|
819
819
|
artifacts: ["attachments", "acceptance", "loops", "workflow-runs", "transcripts", "artifacts", "sessions"],
|
|
820
820
|
} as const;
|
|
821
821
|
|
|
822
|
-
/* THE DAEMON'S OWN CONTROL PLANE
|
|
822
|
+
/* THE DAEMON'S OWN CONTROL PLANE, the entries directly under the workspace root's `.intentic/` that the file
|
|
823
823
|
* API refuses to read, write, move or delete for anyone, the owner included (workspace/workspace-files.ts holds
|
|
824
824
|
* the enforcement and the full reasoning for each name).
|
|
825
825
|
*
|
|
826
826
|
* The list lives HERE, in the package both sides import, because the browser has to draw the same rule the
|
|
827
827
|
* daemon enforces. It didn't, and the gap was a small piece of theatre: the explorer listed `capabilities.json`
|
|
828
828
|
* like any other file, opening it flashed a tab, the read came back with nothing there, and the tab closed
|
|
829
|
-
* itself
|
|
829
|
+
* itself, a refusal acted out as a glitch. A file the app will not open should say so before it is clicked,
|
|
830
830
|
* which takes a rule the explorer can consult, not a status code it can only react to.
|
|
831
831
|
*
|
|
832
|
-
* Naming these to the browser gives nothing away that the tree did not already publish
|
|
832
|
+
* Naming these to the browser gives nothing away that the tree did not already publish, it listed them, sizes
|
|
833
833
|
* and all. What stays behind the guard is the only thing that ever mattered: the bytes. */
|
|
834
|
-
/* GROUP-RELATIVE NOW, and the one rule the regrouping did NOT simplify
|
|
834
|
+
/* GROUP-RELATIVE NOW, and the one rule the regrouping did NOT simplify, worth saying because every other rule
|
|
835
835
|
* over this tree collapsed to a prefix and this one could not. What the file API refuses to open cuts ACROSS the
|
|
836
836
|
* groups: the capability manifest is `config`, the transcripts are `records`, the browser profiles are `local`,
|
|
837
837
|
* and all of `identity` and `secrets` is in. That is not an accident of the grouping, it is a different question
|
|
838
|
-
|
|
838
|
+
*, "would showing the bytes hand someone something" rather than "what kind of thing is this", so it keeps an
|
|
839
839
|
* explicit list, just one that now names the folder each entry lives in. */
|
|
840
840
|
const LOCKED_STATE_ENTRIES: ReadonlySet<string> = new Set([
|
|
841
841
|
"identity/owner.json",
|
|
@@ -846,14 +846,14 @@ const LOCKED_STATE_ENTRIES: ReadonlySet<string> = new Set([
|
|
|
846
846
|
"secrets/auth",
|
|
847
847
|
"records/sessions",
|
|
848
848
|
"local/browser",
|
|
849
|
-
/* The provider CLI's own home, which this table does not declare and so has no group to move into
|
|
849
|
+
/* The provider CLI's own home, which this table does not declare and so has no group to move into, it is
|
|
850
850
|
* written by the agent's runtime rather than by any daemon store. It stays at the state dir's root, and the
|
|
851
851
|
* two-segment match below still reaches it because a bare name joins to itself. Locked for the reason the
|
|
852
852
|
* credential entries are: it holds a live session for whatever the agent is signed into. */
|
|
853
853
|
"claude.json",
|
|
854
854
|
]);
|
|
855
855
|
|
|
856
|
-
/* Whether a workspace-root-relative path lands in that control plane
|
|
856
|
+
/* Whether a workspace-root-relative path lands in that control plane, and so is shown locked rather than
|
|
857
857
|
* opened. Scoped deliberately tight, matching the guard: only the ROOT `.intentic` counts (a repo's own nested
|
|
858
858
|
* one is ordinary content) and only these entries within it, subtrees included, so a new provider dropped under
|
|
859
859
|
* `auth/` is covered without a second edit.
|
|
@@ -872,7 +872,7 @@ export const isLockedWorkspacePath = (relPath: string): boolean => {
|
|
|
872
872
|
}
|
|
873
873
|
/* Two shapes, because two layouts exist on disk. A current entry is `<group>/<name>`; a leftover from before
|
|
874
874
|
* the regrouping is a bare name directly under the state dir, and the quarantine record is what still knows
|
|
875
|
-
* those are credentials. Both are locked
|
|
875
|
+
* those are credentials. Both are locked, a stale `auth/` full of real tokens is no safer to open than the
|
|
876
876
|
* live one, and the whole point of quarantining the old spellings was that nothing downstream reclassifies
|
|
877
877
|
* them as ordinary content. */
|
|
878
878
|
return (
|
|
@@ -881,22 +881,22 @@ export const isLockedWorkspacePath = (relPath: string): boolean => {
|
|
|
881
881
|
);
|
|
882
882
|
};
|
|
883
883
|
|
|
884
|
-
/* THE LOCKED ENTRIES THE ROOT REPO TRACKS
|
|
884
|
+
/* THE LOCKED ENTRIES THE ROOT REPO TRACKS, refused by the file API, and diffable anyway.
|
|
885
885
|
*
|
|
886
886
|
* `capabilities.json` is the only one today and the whole reason this exists. Both of its rules are right on
|
|
887
887
|
* their own: it is `versioned`, because connecting this sandbox to a deployment orchestrator is the largest
|
|
888
888
|
* change anyone makes to what it can DO and that belongs in review; and it is locked, because a member who
|
|
889
889
|
* could PUT one through the generic file API would be granting themselves a capability the owner never
|
|
890
|
-
* approved. The lock was always about that WRITE
|
|
890
|
+
* approved. The lock was always about that WRITE, its credentials live in the vault, not in the file.
|
|
891
891
|
*
|
|
892
892
|
* Together, though, the second silently cancelled the first. The Changes panel listed the file (git tracks it,
|
|
893
|
-
* so `git status` reports it), and clicking the row asked a diff route that refuses every control-plane path
|
|
893
|
+
* so `git status` reports it), and clicking the row asked a diff route that refuses every control-plane path,
|
|
894
894
|
* a 404 on the one surface `versioned` exists to produce. The bytes were already in `git log`, in every clone
|
|
895
895
|
* of the root repo and in the workspace search; only the review was missing.
|
|
896
896
|
*
|
|
897
897
|
* So the review surfaces ask THIS instead of the flat lock, and it derives from the same flag rather than
|
|
898
898
|
* naming the file, so marking another locked entry `versioned` cannot reproduce the contradiction. Every other
|
|
899
|
-
* surface
|
|
899
|
+
* surface, read, write, move, delete, publish, still asks `isLockedWorkspacePath` and still refuses.
|
|
900
900
|
*
|
|
901
901
|
* Accepts either slash, like the rule above it. */
|
|
902
902
|
export const isReviewableLockedPath = (relPath: string): boolean => {
|
|
@@ -908,7 +908,7 @@ export const isReviewableLockedPath = (relPath: string): boolean => {
|
|
|
908
908
|
* the first sentence of this file's header TRUE rather than aspirational.
|
|
909
909
|
*
|
|
910
910
|
* "The daemon builds its store paths from `path`" was the design; the code did not. `composition.ts` and twenty
|
|
911
|
-
* files beside it spelled the same layout a SECOND way
|
|
911
|
+
* files beside it spelled the same layout a SECOND way, `join(root, ".intentic", "settings.json")`, with
|
|
912
912
|
* nothing tying the two spellings together. Rename a store's file and this table keeps declaring the old name:
|
|
913
913
|
* no error, no failing test, just a view that quietly stops refreshing, which is the exact failure the table was
|
|
914
914
|
* written to end and the exact way drafts went missing.
|
|
@@ -921,12 +921,12 @@ export type WorkspaceStatePath = (typeof STATE_FILES)[number]["path"];
|
|
|
921
921
|
* this; keeping it here rather than in the web means the rule is unit-testable without a query client, and the
|
|
922
922
|
* daemon can assert against the same table.
|
|
923
923
|
*
|
|
924
|
-
* `contributed` is what the ACTIVATED extensions declared in `contributes.files
|
|
924
|
+
* `contributed` is what the ACTIVATED extensions declared in `contributes.files`, passed in rather than
|
|
925
925
|
* imported, because which extensions are live is a browser fact this package has no way to know. It is a
|
|
926
926
|
* required argument for the same reason: an added second source that callers may forget is a source that
|
|
927
927
|
* silently does nothing, which is the failure this whole file exists to remove. Extension entries are unioned
|
|
928
928
|
* flat with the core ones, not layered over them: both lists describe the same fact about the same file, and a
|
|
929
|
-
* path can legitimately match one entry in each
|
|
929
|
+
* path can legitimately match one entry in each, a core prefix that invalidates nothing must not veto a
|
|
930
930
|
* narrower extension entry beneath it, or everything under one of the daemon's machine-state prefixes would be
|
|
931
931
|
* unreachable to extensions by construction. */
|
|
932
932
|
export const staleQueryKeys = (paths: readonly string[], contributed: readonly FileContribution[]): readonly string[] => [
|
|
@@ -937,9 +937,9 @@ export const staleQueryKeys = (paths: readonly string[], contributed: readonly F
|
|
|
937
937
|
),
|
|
938
938
|
];
|
|
939
939
|
|
|
940
|
-
/* Every query key any watched file feeds
|
|
940
|
+
/* Every query key any watched file feeds, what a NEW /events connection invalidates wholesale (core's table
|
|
941
941
|
* plus the running extensions'). The file push is these keys' ONLY live feed, and a `workspaceChanged` frame
|
|
942
|
-
* produced while the stream was down is a frame nobody will ever resend
|
|
942
|
+
* produced while the stream was down is a frame nobody will ever resend, so each key's view would sit stale
|
|
943
943
|
* until the file's NEXT write, indefinitely for anything that settled while the browser was away. Re-asking on
|
|
944
944
|
* connect bounds the damage at one cheap read per key, which is what lets those views go entirely unpolled. */
|
|
945
945
|
export const fileBoundQueryKeys = (contributed: readonly FileContribution[]): readonly string[] => [
|