@intentic/sandbox-contract 1.224.0 → 1.226.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/README.md +14 -14
- package/dist/agent-catalog.d.ts +2 -0
- package/dist/agent-catalog.d.ts.map +1 -1
- package/dist/agent-catalog.js +19 -3
- package/dist/agent-catalog.js.map +1 -1
- package/dist/chores/chores.js +42 -42
- package/dist/chores/chores.js.map +1 -1
- package/dist/chores/extension-update.js +2 -2
- package/dist/chores/extension-update.js.map +1 -1
- package/dist/chores/fix-deps.js +1 -1
- package/dist/chores/fix-deps.js.map +1 -1
- package/dist/chores/probes.js +1 -1
- package/dist/chores/probes.js.map +1 -1
- package/dist/chores/verdict.js +2 -2
- package/dist/chores/verdict.js.map +1 -1
- package/dist/contracts/capabilities.contract.d.ts +41 -0
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/exit.contract.d.ts +80 -0
- package/dist/contracts/exit.contract.d.ts.map +1 -0
- package/dist/contracts/exit.contract.js +13 -0
- package/dist/contracts/exit.contract.js.map +1 -0
- package/dist/contracts/settings.contract.d.ts +12 -0
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/events.d.ts +4 -4
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +5 -5
- package/dist/events.js.map +1 -1
- package/dist/fast-tier.d.ts +9 -0
- package/dist/fast-tier.d.ts.map +1 -0
- package/dist/fast-tier.js +19 -0
- package/dist/fast-tier.js.map +1 -0
- package/dist/history-state.js +3 -3
- package/dist/history-state.js.map +1 -1
- package/dist/index.d.ts +203 -68
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/model-order.d.ts +1 -0
- package/dist/model-order.d.ts.map +1 -1
- package/dist/model-order.js +5 -0
- package/dist/model-order.js.map +1 -1
- package/dist/output-fields.d.ts.map +1 -1
- package/dist/output-fields.js +2 -2
- package/dist/output-fields.js.map +1 -1
- package/dist/prompt-complexity.d.ts +19 -0
- package/dist/prompt-complexity.d.ts.map +1 -0
- package/dist/prompt-complexity.js +83 -0
- package/dist/prompt-complexity.js.map +1 -0
- package/dist/publish-drafts.js +2 -2
- package/dist/publish-drafts.js.map +1 -1
- package/dist/schemas.d.ts +214 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +113 -2
- package/dist/schemas.js.map +1 -1
- package/dist/workflow-faults.js +3 -3
- package/dist/workflow-faults.js.map +1 -1
- package/dist/workspace-state.d.ts +20 -20
- package/dist/workspace-state.d.ts.map +1 -1
- package/dist/workspace-state.js +20 -20
- package/dist/workspace-state.js.map +1 -1
- package/package.json +5 -5
- package/src/agent-catalog.test.ts +72 -27
- package/src/agent-catalog.ts +91 -3
- package/src/agent-run-model.test.ts +3 -3
- package/src/capability-ledger.test.ts +34 -15
- package/src/chores/chores.test.ts +1 -1
- package/src/chores/chores.ts +42 -42
- package/src/chores/digest.test.ts +1 -1
- package/src/chores/extension-update.ts +2 -2
- package/src/chores/fix-deps.ts +1 -1
- package/src/chores/probes.test.ts +6 -6
- package/src/chores/probes.ts +1 -1
- package/src/chores/stack.test.ts +3 -3
- package/src/chores/verdict.test.ts +20 -20
- package/src/chores/verdict.ts +2 -2
- package/src/contract-lock.test.ts +1 -1
- package/src/contracts/exit.contract.ts +42 -0
- package/src/contracts/providers.contract.ts +1 -1
- package/src/conversation-ids.ts +1 -1
- package/src/events.test.ts +3 -3
- package/src/events.ts +6 -6
- package/src/fast-tier.test.ts +88 -0
- package/src/fast-tier.ts +72 -0
- package/src/history-state.ts +3 -3
- package/src/hostnames.test.ts +1 -1
- package/src/index.ts +5 -0
- package/src/model-order.test.ts +11 -11
- package/src/model-order.ts +22 -0
- package/src/output-fields.ts +2 -2
- package/src/path-refs.test.ts +4 -4
- package/src/prompt-complexity.test.ts +160 -0
- package/src/prompt-complexity.ts +271 -0
- package/src/publish-drafts.ts +2 -2
- package/src/quick-model.test.ts +11 -11
- package/src/routes.test.ts +11 -5
- package/src/runtime-state.test.ts +1 -1
- package/src/schemas.test.ts +8 -8
- package/src/schemas.ts +311 -6
- package/src/search-globs.test.ts +2 -2
- package/src/share-paths.test.ts +1 -1
- package/src/title.test.ts +9 -9
- package/src/title.ts +1 -1
- package/src/tunnel-ids.test.ts +3 -3
- package/src/versions.test.ts +3 -3
- package/src/versions.ts +1 -1
- package/src/workflow-faults.test.ts +6 -6
- package/src/workflow-faults.ts +3 -3
- package/src/workspace-state.test.ts +32 -32
- package/src/workspace-state.ts +20 -20
package/src/versions.test.ts
CHANGED
|
@@ -25,15 +25,15 @@ test("isBehind stays quiet whenever it cannot be sure", () => {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
/* A version with a segment that will not parse is read by its numeric prefix, and the failure is one-directional:
|
|
28
|
-
* it can withhold a nag, never invent one. Both halves are pinned because only the second is a safety property
|
|
28
|
+
* it can withhold a nag, never invent one. Both halves are pinned because only the second is a safety property:
|
|
29
29
|
* the first is just the prefix doing its job. */
|
|
30
30
|
test("a version that isn't dotted-numeric can only ever withhold the nudge", () => {
|
|
31
31
|
expect(isBehind("1.2.0-rc.1", "1.183.0")).toBe(true); // the prefix already decides it: 2 is behind 183
|
|
32
32
|
expect(isBehind("1.2.0-rc.1", "1.2.0")).toBe(false); // the unparseable segment is where they differ — silence
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
// The version every agent shipped before the release stamp existed. It is genuinely behind
|
|
36
|
-
// release that has one
|
|
35
|
+
// The version every agent shipped before the release stamp existed. It is genuinely behind: it predates every
|
|
36
|
+
// release that has one, and it must read that way, because those are the installs this whole signal is for.
|
|
37
37
|
test("isBehind flags the hand-written version agents used to carry", () => {
|
|
38
38
|
expect(isBehind("0.1.0", "1.183.0")).toBe(true);
|
|
39
39
|
});
|
package/src/versions.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* that one" is one question with one answer, and it lives here because both ends ask it: the daemon compares its
|
|
4
4
|
* own build against the latest published release, and the browser compares a computer's agent against the same.
|
|
5
5
|
*
|
|
6
|
-
* Shared rather than copied because the two copies would not disagree until the day it mattered
|
|
6
|
+
* Shared rather than copied because the two copies would not disagree until the day it mattered: 1.9.0 against
|
|
7
7
|
* 1.10.0 is where a hand-rolled comparator goes wrong, and it goes wrong by reporting "up to date". */
|
|
8
8
|
|
|
9
9
|
// Release versions are plain dotted numerics (semantic-release picks them), so there is no semver dependency to
|
|
@@ -3,7 +3,7 @@ import { type Workflow, WorkflowSchema, type WorkflowStep } from "./schemas.js";
|
|
|
3
3
|
import { workflowFaults, workflowRunFaults } from "./workflow-faults.js";
|
|
4
4
|
|
|
5
5
|
/* The GATE rules. The graph rules beside them are exercised by the scheduler's own integration tests, which
|
|
6
|
-
* have a real run to check them against; these have none to check, which is the point
|
|
6
|
+
* have a real run to check them against; these have none to check, which is the point: every fault here is one
|
|
7
7
|
* the workflow has to be refused for BEFORE a run, because the failure it prevents costs a full fan-out of
|
|
8
8
|
* sessions and then reports nothing anybody can act on.
|
|
9
9
|
*/
|
|
@@ -78,7 +78,7 @@ test("a gate on a field the step does not declare is refused", () => {
|
|
|
78
78
|
test("a gate on a list field is refused", () => {
|
|
79
79
|
const steps = [judge({ output: { kind: "json", fields: [{ name: "release", type: "string[]", description: "the verdicts", required: true }] } })];
|
|
80
80
|
const faults = workflowFaults(gated({ steps }));
|
|
81
|
-
expect(faults).toEqual([`The gate reads "release", which is a list
|
|
81
|
+
expect(faults).toEqual([`The gate reads "release", which is a list: a release decision has to be one value.`]);
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
// The expensive one to discover at run time: it passes every save, then blocks a release on the one commit
|
|
@@ -86,10 +86,10 @@ test("a gate on a list field is refused", () => {
|
|
|
86
86
|
test("a gate on an optional field is refused", () => {
|
|
87
87
|
const steps = [judge({ output: { kind: "json", fields: [{ name: "release", type: "string", description: "pass | fail", required: false }] } })];
|
|
88
88
|
const faults = workflowFaults(gated({ steps }));
|
|
89
|
-
expect(faults).toEqual([`The gate reads "release", which "Judge" declares optional
|
|
89
|
+
expect(faults).toEqual([`The gate reads "release", which "Judge" declares optional, it has to be required.`]);
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
-
/* THE RUN-TIME RULE, kept apart from every rule above it because it is not about the graph
|
|
92
|
+
/* THE RUN-TIME RULE, kept apart from every rule above it because it is not about the graph, and the graph is
|
|
93
93
|
* what gets saved. A design whose steps take their goal and instruction from the request is the ordinary shape
|
|
94
94
|
* and must save cleanly; it is only unrunnable on the particular run that brought no request.
|
|
95
95
|
*/
|
|
@@ -102,7 +102,7 @@ test("a design whose steps inherit is a perfectly good design", () => {
|
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
/* Refused at the door rather than discovered by the first step, because this is the one combination with
|
|
105
|
-
* nothing to tell the model at all
|
|
105
|
+
* nothing to tell the model at all, and by the time a step found out, the run has already opened a session per
|
|
106
106
|
* root and started paying for them.
|
|
107
107
|
*/
|
|
108
108
|
test("running an inheriting design with no request is refused", () => {
|
|
@@ -120,7 +120,7 @@ test("a step that declares only one of the two still needs a request", () => {
|
|
|
120
120
|
expect(workflowRunFaults(gated({ steps: [judge({ prompt: undefined })] }), undefined)).toHaveLength(1);
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
-
// A design that says everything itself is startable from anywhere, with no composer behind it
|
|
123
|
+
// A design that says everything itself is startable from anywhere, with no composer behind it, which is what
|
|
124
124
|
// keeps the gate's webhook and the workflows page working for the designs written that way.
|
|
125
125
|
test("a design that declares everything runs with no request at all", () => {
|
|
126
126
|
expect(workflowRunFaults(gated(), undefined)).toEqual([]);
|
package/src/workflow-faults.ts
CHANGED
|
@@ -38,7 +38,7 @@ const stepFaults = (step: WorkflowStep, ids: ReadonlySet<string>): string[] => {
|
|
|
38
38
|
if (step.handoff === "continue" && !continuesOneSession(step)) {
|
|
39
39
|
faults.push(
|
|
40
40
|
step.needs.length === 0
|
|
41
|
-
? `"${step.title}" continues a session but starts the run
|
|
41
|
+
? `"${step.title}" continues a session but starts the run: there is nothing to continue.`
|
|
42
42
|
: `"${step.title}" continues a session but waits for ${step.needs.length} steps; it can only continue one.`,
|
|
43
43
|
);
|
|
44
44
|
}
|
|
@@ -136,7 +136,7 @@ const gateFaults = (workflow: Pick<Workflow, "steps" | "gate">): string[] => {
|
|
|
136
136
|
return [`The gate reads "${gate.field}", which "${step.title}" does not declare.`];
|
|
137
137
|
}
|
|
138
138
|
if (field.type === "string[]") {
|
|
139
|
-
return [`The gate reads "${gate.field}", which is a list
|
|
139
|
+
return [`The gate reads "${gate.field}", which is a list: a release decision has to be one value.`];
|
|
140
140
|
}
|
|
141
141
|
// The schema refuses this on save (pass is min(1)), but the designer edits drafts the schema never sees,
|
|
142
142
|
// and an empty allowlist is a gate no run could ever answer "pass", which deserves a sentence, not a save error.
|
|
@@ -145,7 +145,7 @@ const gateFaults = (workflow: Pick<Workflow, "steps" | "gate">): string[] => {
|
|
|
145
145
|
}
|
|
146
146
|
// A field the step may legally omit is a gate that answers `blocked` whenever it does, which is a release
|
|
147
147
|
// stuck on a technicality rather than on the product.
|
|
148
|
-
return field.required ? [] : [`The gate reads "${gate.field}", which "${step.title}" declares optional
|
|
148
|
+
return field.required ? [] : [`The gate reads "${gate.field}", which "${step.title}" declares optional, it has to be required.`];
|
|
149
149
|
};
|
|
150
150
|
|
|
151
151
|
/* Why the graph is not runnable, as a list of sentences. Empty ⇒ it is. Shared by the save route (which
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
// What the automations extension declares in its manifest, and what the memory extension WOULD declare if the
|
|
22
22
|
// watcher reported its files. Literals rather than the real manifests: an extension package importing this one is
|
|
23
23
|
// the dependency direction, so reaching back for them here would invert it. The real manifests are checked
|
|
24
|
-
// against this rule where they are loaded
|
|
24
|
+
// against this rule where they are loaded: web's fileBindings.test.ts and the daemon's file-bindings.test.ts.
|
|
25
25
|
const AUTOMATIONS: readonly FileContribution[] = [
|
|
26
26
|
{ path: `${STATE_DIR}/config/automations.json`, invalidates: [`automations`] },
|
|
27
27
|
{ path: `${STATE_DIR}/records/approvals/`, invalidates: [`automation-approvals`] },
|
|
@@ -45,14 +45,14 @@ describe(`staleQueryKeys`, () => {
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
it(`matches a name family and a one-file-per-entry directory through one prefix each`, () => {
|
|
48
|
-
// environment.Dockerfile, environment.custom.Dockerfile, environment.approved.Dockerfile
|
|
48
|
+
// environment.Dockerfile, environment.custom.Dockerfile, environment.approved.Dockerfile: one entry.
|
|
49
49
|
expect(staleQueryKeys([`.intentic/config/environment.custom.Dockerfile`], [])).toEqual([`environment`]);
|
|
50
50
|
expect(staleQueryKeys([`.intentic/config/drafts/post-1.json`], [])).toEqual([`drafts`]);
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
it(`refreshes the Drafts view when the AGENT writes a draft`, () => {
|
|
54
54
|
// The regression this table was reorganized around: the drafts skill writes these files directly, so
|
|
55
|
-
// there is no browser mutation to hang an invalidate on
|
|
55
|
+
// there is no browser mutation to hang an invalidate on: the watcher push is the only signal, and it
|
|
56
56
|
// used to be dropped on the floor.
|
|
57
57
|
expect(staleQueryKeys([`.intentic/config/drafts/post-1.json`], [])).toEqual([`drafts`]);
|
|
58
58
|
});
|
|
@@ -75,7 +75,7 @@ describe(`staleQueryKeys`, () => {
|
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
it(`dedupes keys across a batch that touches several manifests`, () => {
|
|
78
|
-
// A capability add recomposes the overlay, so both entries claim `environment
|
|
78
|
+
// A capability add recomposes the overlay, so both entries claim `environment`: one refetch, not two.
|
|
79
79
|
expect(staleQueryKeys([`.intentic/config/capabilities.json`, `.intentic/config/environment.Dockerfile`], [])).toEqual([
|
|
80
80
|
`capabilities`,
|
|
81
81
|
`environment`,
|
|
@@ -129,7 +129,7 @@ describe(`isReportedManifest`, () => {
|
|
|
129
129
|
|
|
130
130
|
it(`stays quiet about daemon-written state the owner cannot repair`, () => {
|
|
131
131
|
// The regression this exists for: the workflow ledger's records predated a schema that gained a required
|
|
132
|
-
// field, so every read reported the whole file ignored
|
|
132
|
+
// field, so every read reported the whole file ignored: advice ("fix the file") addressed to nobody,
|
|
133
133
|
// about sixty kilobytes of machine JSON, and refreshed by no write because the ledger feeds no query, so
|
|
134
134
|
// it sat on the card until the daemon restarted. A ledger recovers on its own next write instead.
|
|
135
135
|
expect(isReportedManifest(`.intentic/records/workflow-runs.json`)).toBe(false);
|
|
@@ -142,7 +142,7 @@ describe(`isReportedManifest`, () => {
|
|
|
142
142
|
});
|
|
143
143
|
|
|
144
144
|
it(`does not report a file outside the workspace`, () => {
|
|
145
|
-
// What `relative` hands the daemon for the manifests it keeps under /history
|
|
145
|
+
// What `relative` hands the daemon for the manifests it keeps under /history: never nameable on screen.
|
|
146
146
|
expect(isReportedManifest(`../../history/settings.json`)).toBe(false);
|
|
147
147
|
});
|
|
148
148
|
});
|
|
@@ -158,7 +158,7 @@ describe(`WORKSPACE_STATE_FILES`, () => {
|
|
|
158
158
|
it(`states a reason for every entry that invalidates nothing`, () => {
|
|
159
159
|
// An empty `invalidates` is a real answer (daemon machine state, a deliberately-polled surface, a path
|
|
160
160
|
// whose query keys belong to an extension), but a SILENT one is indistinguishable from the omission this
|
|
161
|
-
// table exists to prevent
|
|
161
|
+
// table exists to prevent, which is exactly how drafts went missing. Requiring the reason is what makes
|
|
162
162
|
// the difference visible at review time.
|
|
163
163
|
for (const file of WORKSPACE_STATE_FILES) {
|
|
164
164
|
if (file.invalidates.length === 0) {
|
|
@@ -179,7 +179,7 @@ describe(`WORKSPACE_STATE_FILES`, () => {
|
|
|
179
179
|
});
|
|
180
180
|
|
|
181
181
|
it(`only nests under an entry that invalidates nothing, so one write can't be billed twice`, () => {
|
|
182
|
-
// Entries may nest when a subtree needs a different portability class
|
|
182
|
+
// Entries may nest when a subtree needs a different portability class: stateFileFor's longest match
|
|
183
183
|
// keeps that unambiguous. Invalidation has no longest-match rule: staleQueryKeys unions every
|
|
184
184
|
// matching entry, so a nest under an entry that DOES invalidate would bill the outer view's queries for
|
|
185
185
|
// a write that belongs to the inner one. Nesting is therefore only legal beneath an empty `invalidates`.
|
|
@@ -247,7 +247,7 @@ describe(`isReviewableLockedPath`, () => {
|
|
|
247
247
|
it(`admits the locked entry the root repo tracks, and nothing else locked`, () => {
|
|
248
248
|
expect(isReviewableLockedPath(`.intentic/config/capabilities.json`)).toBe(true);
|
|
249
249
|
// Every other locked entry is a credential, an identity binding or private runtime state. None is
|
|
250
|
-
// versioned, so none is reachable through a diff
|
|
250
|
+
// versioned, so none is reachable through a diff: the carve-out cannot widen without the flag.
|
|
251
251
|
expect(isReviewableLockedPath(`.intentic/identity/owner.json`)).toBe(false);
|
|
252
252
|
expect(isReviewableLockedPath(`.intentic/identity/members.json`)).toBe(false);
|
|
253
253
|
expect(isReviewableLockedPath(`.intentic/secrets/ci.json`)).toBe(false);
|
|
@@ -257,7 +257,7 @@ describe(`isReviewableLockedPath`, () => {
|
|
|
257
257
|
expect(isReviewableLockedPath(`.git/config`)).toBe(false);
|
|
258
258
|
});
|
|
259
259
|
|
|
260
|
-
it(`answers only for the locked set
|
|
260
|
+
it(`answers only for the locked set: an ordinary path was never refused to begin with`, () => {
|
|
261
261
|
// Tracked, but not locked: the guards never ask this of them, and a `true` here would read as "this
|
|
262
262
|
// path needed a carve-out", which is a different and wrong statement.
|
|
263
263
|
expect(isReviewableLockedPath(`.intentic/config/settings.json`)).toBe(false);
|
|
@@ -285,22 +285,22 @@ describe(`VERSIONED_STATE_PATHS`, () => {
|
|
|
285
285
|
*
|
|
286
286
|
* `versioned` carves an entry out of the root repo's wholesale `.intentic` exclusion, so marking one is the
|
|
287
287
|
* difference between a file the owner reviews and a file the next baseline commit publishes into `git log`
|
|
288
|
-
* forever. A credential marked by a hurried hand is not recoverable by unmarking it later
|
|
289
|
-
* already written
|
|
288
|
+
* forever. A credential marked by a hurried hand is not recoverable by unmarking it later: the commit is
|
|
289
|
+
* already written, which is why the refusal is mechanical here rather than a rule in a comment. */
|
|
290
290
|
it(`never tracks a credential or an identity binding`, () => {
|
|
291
291
|
const leaked = WORKSPACE_STATE_FILES.filter((file) => file.versioned && (file.portability === `secret` || file.portability === `identity`));
|
|
292
292
|
expect(leaked.map((file) => file.path)).toEqual([]);
|
|
293
293
|
});
|
|
294
294
|
|
|
295
|
-
/* Narrower than `carry` ON PURPOSE, and this is where that stays true. The two answer different questions
|
|
296
|
-
* carry is "does it move to a new sandbox", versioned is "should a human review it changing"
|
|
295
|
+
/* Narrower than `carry` ON PURPOSE, and this is where that stays true. The two answer different questions:
|
|
296
|
+
* carry is "does it move to a new sandbox", versioned is "should a human review it changing", so the
|
|
297
297
|
* ledgers and the bulk are `carry` and deliberately absent: the run ledger is rewritten several times per
|
|
298
298
|
* workflow step, the usage batch every few seconds a browser is open, and the transcripts run to hundreds of
|
|
299
299
|
* megabytes. Tracking any of them buries the owner's code review under machine noise. */
|
|
300
300
|
it(`leaves the ledgers and the bulk out even though they travel`, () => {
|
|
301
301
|
for (const path of [
|
|
302
302
|
`.intentic/records/workflow-runs.json`,
|
|
303
|
-
// Split out of the tracked automations manifest precisely so a fire stops dirtying it
|
|
303
|
+
// Split out of the tracked automations manifest precisely so a fire stops dirtying it: the one
|
|
304
304
|
// entry here that would be a REGRESSION rather than an oversight if it ever went tracked.
|
|
305
305
|
`.intentic/records/automation-runs.json`,
|
|
306
306
|
`.intentic/records/loops.json`,
|
|
@@ -315,11 +315,11 @@ describe(`VERSIONED_STATE_PATHS`, () => {
|
|
|
315
315
|
});
|
|
316
316
|
|
|
317
317
|
/* Spelled out rather than derived, so ADDING a tracked entry is a visible edit to this list and not a silent
|
|
318
|
-
* consequence of editing the table above
|
|
318
|
+
* consequence of editing the table above: the review the flag itself exists to force. */
|
|
319
319
|
it(`tracks exactly the configuration slice plus the agent's own authored output`, () => {
|
|
320
320
|
expect(VERSIONED_STATE_PATHS.toSorted()).toEqual([
|
|
321
321
|
`.intentic/config/automations.json`,
|
|
322
|
-
/* The connections themselves, and the entry that took the longest to earn its place
|
|
322
|
+
/* The connections themselves, and the entry that took the longest to earn its place: it was classed
|
|
323
323
|
* `secret` on the strength of holding each capability's credential, which stopped being true when the
|
|
324
324
|
* vault took the values out and left the shape behind. Connecting a deployment orchestrator, or
|
|
325
325
|
* granting a connected computer shell and screen control, is the largest change made to what this
|
|
@@ -327,9 +327,9 @@ describe(`VERSIONED_STATE_PATHS`, () => {
|
|
|
327
327
|
`.intentic/config/capabilities.json`,
|
|
328
328
|
`.intentic/config/capability-dismissals.json`,
|
|
329
329
|
/* The two entries the AGENT authors on its own initiative, and the reason `versioned` is not read as
|
|
330
|
-
* config-only. Both are the sandbox acting outward
|
|
330
|
+
* config-only. Both are the sandbox acting outward: a draft publishes words under the owner's name,
|
|
331
331
|
* a workspace extension is code that runs in the app and can serve HTTP with the workspace under
|
|
332
|
-
* node:fs
|
|
332
|
+
* node:fs, and both used to reach that far with no diff anywhere. Kept rather than consumed, one
|
|
333
333
|
* small file at a time, so tracking them yields a record instead of churn. */
|
|
334
334
|
`.intentic/config/drafts/`,
|
|
335
335
|
`.intentic/config/environment.Dockerfile`,
|
|
@@ -340,12 +340,12 @@ describe(`VERSIONED_STATE_PATHS`, () => {
|
|
|
340
340
|
* behind it was not, so a commit could record turning an extension on and say nothing about what it
|
|
341
341
|
* was told to do. Tracked once its declared-secret values moved to the vault. */
|
|
342
342
|
`.intentic/config/extension-settings.json`,
|
|
343
|
-
// The owner's per-extension update posture (notify / agent / auto)
|
|
343
|
+
// The owner's per-extension update posture (notify / agent / auto): a standing decision about
|
|
344
344
|
// what may run unattended, which is exactly the kind of edit worth a line in `git log`.
|
|
345
345
|
`.intentic/config/extension-update-policy.json`,
|
|
346
346
|
`.intentic/config/loop-designs.json`,
|
|
347
347
|
`.intentic/config/personas.json`,
|
|
348
|
-
// A persona's own kit
|
|
348
|
+
// A persona's own kit: the prompt it runs on and the skills only its turns reach. Tracked for the
|
|
349
349
|
// reason its card is, one step further: this is the text that decides how that persona behaves.
|
|
350
350
|
`.intentic/config/personas/`,
|
|
351
351
|
`.intentic/config/settings.json`,
|
|
@@ -360,7 +360,7 @@ describe(`VERSIONED_STATE_PATHS`, () => {
|
|
|
360
360
|
|
|
361
361
|
/* THE ASYMMETRY THIS CLOSED, kept as its own assertion because it is the failure rather than a detail of it:
|
|
362
362
|
* the switch was tracked and the thing it switched was not, so a commit could record turning on an extension
|
|
363
|
-
* whose code nobody else could read
|
|
363
|
+
* whose code nobody else could read, and a workspace extension has no install moment to review at instead. */
|
|
364
364
|
it(`tracks a workspace extension's code, not just the switch that enables it`, () => {
|
|
365
365
|
expect(VERSIONED_STATE_PATHS).toContain(`.intentic/config/extension-enablement.json`);
|
|
366
366
|
expect(VERSIONED_STATE_PATHS).toContain(`.intentic/config/workspace-extensions/`);
|
|
@@ -375,14 +375,14 @@ describe(`VERSIONED_STATE_PATHS`, () => {
|
|
|
375
375
|
for (const path of [`.intentic/records/approvals/`, `.intentic/config/docs/`]) {
|
|
376
376
|
expect([path, VERSIONED_STATE_PATHS.includes(path)]).toEqual([path, false]);
|
|
377
377
|
}
|
|
378
|
-
// Both still reach workspace search
|
|
378
|
+
// Both still reach workspace search: searchability is a property of the content, not of tracking.
|
|
379
379
|
for (const path of [`.intentic/records/approvals/`, `.intentic/config/docs/`]) {
|
|
380
380
|
const entry = WORKSPACE_STATE_FILES.find((file) => file.path === path);
|
|
381
381
|
expect([path, entry?.versioned === true || entry?.authored === true]).toEqual([path, path === `.intentic/config/docs/`]);
|
|
382
382
|
}
|
|
383
383
|
});
|
|
384
384
|
|
|
385
|
-
/* The composed overlay is `derived
|
|
385
|
+
/* The composed overlay is `derived`: recomposed on every boot from the custom file that IS tracked, against
|
|
386
386
|
* whatever base image this container happens to be on. Tracking it would put a rewritten-at-startup file in
|
|
387
387
|
* front of the owner as a change they made. */
|
|
388
388
|
it(`tracks the environment overlay's source but not its composed output`, () => {
|
|
@@ -395,7 +395,7 @@ describe(`VERSIONED_STATE_PATHS`, () => {
|
|
|
395
395
|
*
|
|
396
396
|
* `stateGroupOf` reads three existing answers instead of adding a fourth, which is only sound while those answers
|
|
397
397
|
* nest: reviewed and authored entries must all be `carry`, so "did a person write this" never has to be asked of
|
|
398
|
-
* a credential. Nothing in the type system says so
|
|
398
|
+
* a credential. Nothing in the type system says so, `versioned: true` on a `secret` entry compiles, and the
|
|
399
399
|
* failure would be quiet in the worst way: the entry would group as `secrets` (correctly kept out of the backup)
|
|
400
400
|
* while the git exclude and the search floor, which read `versioned` directly, went on tracking and indexing it.
|
|
401
401
|
* A credential in the owner's diff and in the search index, from one plausible-looking flag. */
|
|
@@ -439,8 +439,8 @@ describe(`state groups`, () => {
|
|
|
439
439
|
|
|
440
440
|
/* THE LAYOUT GUARD, and the reason the folders can carry the rules at all.
|
|
441
441
|
*
|
|
442
|
-
* Five prefixes replaced five hand-kept path lists
|
|
443
|
-
* the watcher skip, the export bundle
|
|
442
|
+
* Five prefixes replaced five hand-kept path lists: the git exclude, the search allow-list, the sync backup,
|
|
443
|
+
* the watcher skip, the export bundle, and every one of them is now only as true as the claim that an entry
|
|
444
444
|
* physically SITS in the folder its class puts it in. Nothing else checks that: the paths are literals, and a
|
|
445
445
|
* credential typed into `config/` by mistake would compile, track in git, index in search and copy to the
|
|
446
446
|
* owner's laptop, with each of those rules behaving exactly as designed. This is the one assertion standing
|
|
@@ -453,7 +453,7 @@ describe(`state groups`, () => {
|
|
|
453
453
|
});
|
|
454
454
|
|
|
455
455
|
/* The browser profiles' path is copied into @intentic/workspace-ignore (isBrowserProfilePath), which cannot
|
|
456
|
-
* import this package
|
|
456
|
+
* import this package: it is the browser-safe half, deliberately free of zod and the contract surface, so
|
|
457
457
|
* the platform's web bundle can take it. This is the guard that keeps the copy honest. If it fails, the
|
|
458
458
|
* profiles moved and `BROWSER_PROFILE_GROUP` in _sandbox/workspace-ignore/src/constants.ts must move too, or
|
|
459
459
|
* the tree will start eagerly walking a Chromium user-data dir and the watcher will report its churn. */
|
|
@@ -481,7 +481,7 @@ describe(`state groups`, () => {
|
|
|
481
481
|
});
|
|
482
482
|
|
|
483
483
|
/* WHAT THE OWNER'S MACHINE KEEPS. The sync ignored the whole state dir, so losing a sandbox lost every persona,
|
|
484
|
-
* skill, automation, draft and transcript in it
|
|
484
|
+
* skill, automation, draft and transcript in it: a backup that held only the source tree. These pin the two
|
|
485
485
|
* halves of the fix: the slice that now comes down, and the credentials that still must not. */
|
|
486
486
|
describe(`BACKED_UP_STATE_PATHS`, () => {
|
|
487
487
|
it(`splits the table in two with nothing falling between`, () => {
|
|
@@ -503,14 +503,14 @@ describe(`BACKED_UP_STATE_PATHS`, () => {
|
|
|
503
503
|
}
|
|
504
504
|
});
|
|
505
505
|
|
|
506
|
-
it(`leaves the rebuildable bulk behind
|
|
506
|
+
it(`leaves the rebuildable bulk behind: it is size, not secrecy`, () => {
|
|
507
507
|
for (const path of stateGroupPaths(`local`)) {
|
|
508
508
|
expect([path, BACKED_UP_STATE_PATHS.includes(path)]).toEqual([path, false]);
|
|
509
509
|
}
|
|
510
510
|
});
|
|
511
511
|
|
|
512
512
|
/* The distinction `portability` alone cannot draw, and the reason the `backup` flag exists. Ownership records
|
|
513
|
-
* may never RESTORE into another sandbox
|
|
513
|
+
* may never RESTORE into another sandbox: that is what `identity` means, and members.json's own entry argues
|
|
514
514
|
* why. None of that stops the owner keeping a copy of who could drive their own machine. */
|
|
515
515
|
it(`copies the ownership records that may never travel`, () => {
|
|
516
516
|
for (const path of [`.intentic/identity/owner.json`, `.intentic/identity/members.json`, `.intentic/identity/workspace.json`]) {
|
|
@@ -520,7 +520,7 @@ describe(`BACKED_UP_STATE_PATHS`, () => {
|
|
|
520
520
|
|
|
521
521
|
it(`still withholds the tokens that authenticate against this sandbox`, () => {
|
|
522
522
|
expect(BACKED_UP_STATE_PATHS).not.toContain(`.intentic/identity/control-tokens.json`);
|
|
523
|
-
// And it is the ONLY entry that opts out by hand
|
|
523
|
+
// And it is the ONLY entry that opts out by hand: everything else follows from its class.
|
|
524
524
|
expect(WORKSPACE_STATE_FILES.filter((file) => file.backup === false).map((file) => file.path)).toEqual([
|
|
525
525
|
`.intentic/identity/control-tokens.json`,
|
|
526
526
|
]);
|
package/src/workspace-state.ts
CHANGED
|
@@ -253,7 +253,7 @@ const STATE_FILES = [
|
|
|
253
253
|
{
|
|
254
254
|
path: ".intentic/config/automations.json",
|
|
255
255
|
invalidates: [],
|
|
256
|
-
why: "Declared by the intentic.automations extension's contributes.files
|
|
256
|
+
why: "Declared by the intentic.automations extension's contributes.files, `automations` is its query key, not core's.",
|
|
257
257
|
portability: "carry",
|
|
258
258
|
versioned: true,
|
|
259
259
|
},
|
|
@@ -273,13 +273,13 @@ const STATE_FILES = [
|
|
|
273
273
|
{
|
|
274
274
|
path: ".intentic/records/automation-runs.json",
|
|
275
275
|
invalidates: [],
|
|
276
|
-
why: "Declared by the intentic.automations extension's contributes.files
|
|
276
|
+
why: "Declared by the intentic.automations extension's contributes.files, `automations` is its query key, not core's.",
|
|
277
277
|
portability: "carry",
|
|
278
278
|
},
|
|
279
279
|
{
|
|
280
280
|
path: ".intentic/records/approvals/",
|
|
281
281
|
invalidates: [],
|
|
282
|
-
why: "Declared by the intentic.automations extension's contributes.files
|
|
282
|
+
why: "Declared by the intentic.automations extension's contributes.files, `automation-approvals` is its query key, not core's.",
|
|
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
|
|
@@ -290,7 +290,7 @@ const STATE_FILES = [
|
|
|
290
290
|
{
|
|
291
291
|
path: ".intentic/records/chores/",
|
|
292
292
|
invalidates: [],
|
|
293
|
-
why: "Declared by the intentic.maintenance extension's contributes.files
|
|
293
|
+
why: "Declared by the intentic.maintenance extension's contributes.files, `maintenance-report`/`maintenance-runs` are its query keys, not core's.",
|
|
294
294
|
portability: "carry",
|
|
295
295
|
},
|
|
296
296
|
/* The documentation STAGING tree (documentation extension's paths.ts): generation writes here, the owner
|
|
@@ -300,7 +300,7 @@ const STATE_FILES = [
|
|
|
300
300
|
{
|
|
301
301
|
path: ".intentic/config/docs/",
|
|
302
302
|
invalidates: [],
|
|
303
|
-
why: "Declared by the intentic.documentation extension's contributes.files
|
|
303
|
+
why: "Declared by the intentic.documentation extension's contributes.files, `documentation`/`documentation-runs` are its query keys, not core's.",
|
|
304
304
|
portability: "carry",
|
|
305
305
|
authored: true,
|
|
306
306
|
outsideWriter: "the intentic.documentation extension's staging writes (its paths.ts)",
|
|
@@ -324,7 +324,7 @@ const STATE_FILES = [
|
|
|
324
324
|
{
|
|
325
325
|
path: ".intentic/records/loops.json",
|
|
326
326
|
invalidates: [],
|
|
327
|
-
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
|
|
327
|
+
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).",
|
|
328
328
|
portability: "carry",
|
|
329
329
|
},
|
|
330
330
|
|
|
@@ -343,7 +343,7 @@ const STATE_FILES = [
|
|
|
343
343
|
{
|
|
344
344
|
path: ".intentic/records/thread-sessions.json",
|
|
345
345
|
invalidates: [],
|
|
346
|
-
why: "Thread bookkeeping (an inbound thread
|
|
346
|
+
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.",
|
|
347
347
|
portability: "carry",
|
|
348
348
|
},
|
|
349
349
|
/* SPLIT, so that "what an extension is configured to do" and "the token it does it with" stop being one file.
|
|
@@ -370,7 +370,7 @@ const STATE_FILES = [
|
|
|
370
370
|
{
|
|
371
371
|
path: ".intentic/config/extension-settings.json",
|
|
372
372
|
invalidates: [],
|
|
373
|
-
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
|
|
373
|
+
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.",
|
|
374
374
|
portability: "carry",
|
|
375
375
|
versioned: true,
|
|
376
376
|
},
|
|
@@ -416,7 +416,7 @@ const STATE_FILES = [
|
|
|
416
416
|
{
|
|
417
417
|
path: ".intentic/records/extension-usage.json",
|
|
418
418
|
invalidates: [],
|
|
419
|
-
why: "Which of the routes each extension DECLARED it has actually called
|
|
419
|
+
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.",
|
|
420
420
|
portability: "carry",
|
|
421
421
|
},
|
|
422
422
|
/* THE ONE ENTRY WHERE "HOLDS NO CREDENTIAL" IS TRUE AND `versioned` IS STILL WRONG, which is worth stating
|
|
@@ -433,9 +433,9 @@ const STATE_FILES = [
|
|
|
433
433
|
{
|
|
434
434
|
path: ".intentic/identity/members.json",
|
|
435
435
|
invalidates: [],
|
|
436
|
-
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
|
|
436
|
+
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.",
|
|
437
437
|
portability: "identity",
|
|
438
|
-
note: "Re-invite collaborators from the Access tab
|
|
438
|
+
note: "Re-invite collaborators from the Access tab, a grant is the platform's record, and the target enforces its own copy.",
|
|
439
439
|
},
|
|
440
440
|
|
|
441
441
|
// ---- daemon-owned, nothing derives from watching them ----
|
|
@@ -455,7 +455,7 @@ const STATE_FILES = [
|
|
|
455
455
|
invalidates: [],
|
|
456
456
|
why: "AI-provider credentials and runtime homes, plus the capability and extension-settings secret vaults; each account is rendered through owner-gated provider routes.",
|
|
457
457
|
portability: "secret",
|
|
458
|
-
note: "Sign the agent's AI accounts in again on the Agent tab, then re-enter each connection's credential on Capabilities and each extension's secret settings on Extensions
|
|
458
|
+
note: "Sign the agent's AI accounts in again on the Agent tab, then re-enter each connection's credential on Capabilities and each extension's secret settings on Extensions, both arrived listed but unauthenticated.",
|
|
459
459
|
},
|
|
460
460
|
/* Agent session transcripts, rewritten on every streamed token.
|
|
461
461
|
*
|
|
@@ -470,7 +470,7 @@ const STATE_FILES = [
|
|
|
470
470
|
{
|
|
471
471
|
path: ".intentic/records/sessions/claude/",
|
|
472
472
|
invalidates: [],
|
|
473
|
-
why: "Agent session transcripts
|
|
473
|
+
why: "Agent session transcripts, see the note above on why the memory notes under it stay polled.",
|
|
474
474
|
portability: "carry",
|
|
475
475
|
},
|
|
476
476
|
{
|
|
@@ -482,7 +482,7 @@ const STATE_FILES = [
|
|
|
482
482
|
{
|
|
483
483
|
path: ".intentic/local/cache/",
|
|
484
484
|
invalidates: [],
|
|
485
|
-
why: "Rebuildable indexes and caches
|
|
485
|
+
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.",
|
|
486
486
|
portability: "derived",
|
|
487
487
|
},
|
|
488
488
|
/* Connector and extension scratch, one directory per extension under runtime/extensions/<id>
|
|
@@ -518,14 +518,14 @@ const STATE_FILES = [
|
|
|
518
518
|
{
|
|
519
519
|
path: ".intentic/local/newest-run.json",
|
|
520
520
|
invalidates: [],
|
|
521
|
-
why: "The newest daemon version that ever ran this workspace (store/newest-run.ts)
|
|
521
|
+
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.",
|
|
522
522
|
portability: "derived",
|
|
523
523
|
note: "The target stamps its own daemon version on first boot.",
|
|
524
524
|
},
|
|
525
525
|
{
|
|
526
526
|
path: ".intentic/records/verify.json",
|
|
527
527
|
invalidates: [],
|
|
528
|
-
why: "The dependency verifier's verdict memory; nothing renders it directly
|
|
528
|
+
why: "The dependency verifier's verdict memory; nothing renders it directly, outcomes reach the owner as activity entries and workspace events.",
|
|
529
529
|
portability: "carry",
|
|
530
530
|
},
|
|
531
531
|
{
|
|
@@ -539,7 +539,7 @@ const STATE_FILES = [
|
|
|
539
539
|
invalidates: [],
|
|
540
540
|
why: "Webhook secret + conclusion memory; the Pipelines view reads it through /ci/runs, not off disk.",
|
|
541
541
|
portability: "secret",
|
|
542
|
-
note: "Re-add the CI webhook on the Pipelines view
|
|
542
|
+
note: "Re-add the CI webhook on the Pipelines view, its secret is per-sandbox.",
|
|
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
|
|
@@ -555,7 +555,7 @@ const STATE_FILES = [
|
|
|
555
555
|
why: "Hashed control tokens (the ACP editor bridge, and anything else driving this sandbox from outside), listed on demand by the owner.",
|
|
556
556
|
portability: "identity",
|
|
557
557
|
backup: false,
|
|
558
|
-
note: "Mint fresh control tokens
|
|
558
|
+
note: "Mint fresh control tokens, the old ones authenticate against the source sandbox.",
|
|
559
559
|
},
|
|
560
560
|
{
|
|
561
561
|
path: ".intentic/identity/owner.json",
|
|
@@ -585,12 +585,12 @@ const STATE_FILES = [
|
|
|
585
585
|
invalidates: [],
|
|
586
586
|
why: "Browser-login profiles: Chromium rewrites these constantly. Descent-ignored by the watcher outright.",
|
|
587
587
|
portability: "derived",
|
|
588
|
-
note: "Log the agent's browser back into any site it needs
|
|
588
|
+
note: "Log the agent's browser back into any site it needs, profiles do not travel.",
|
|
589
589
|
},
|
|
590
590
|
{
|
|
591
591
|
path: ".intentic/local/extensions/",
|
|
592
592
|
invalidates: [],
|
|
593
|
-
why: "Extension checkouts
|
|
593
|
+
why: "Extension checkouts, whole git clones. The `extensions` query is driven by the capability manifest above, not by their contents.",
|
|
594
594
|
portability: "derived",
|
|
595
595
|
note: "Extensions re-clone from the capability manifest on the target's next reconcile.",
|
|
596
596
|
},
|