@intentic/sandbox-contract 1.330.0 → 1.332.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 +15 -0
- package/dist/chores/probes.d.ts.map +1 -1
- package/dist/chores/probes.js.map +1 -1
- package/dist/contracts/accounts.contract.d.ts +15 -0
- package/dist/contracts/accounts.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.d.ts +38 -0
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.js +14 -3
- package/dist/contracts/agent.contract.js.map +1 -1
- package/dist/contracts/agents.contract.d.ts +29 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/automations.contract.d.ts +2 -0
- package/dist/contracts/automations.contract.d.ts.map +1 -1
- package/dist/contracts/device.contract.d.ts +7 -0
- package/dist/contracts/device.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.d.ts +2 -0
- package/dist/contracts/git.contract.d.ts.map +1 -1
- package/dist/contracts/logs.contract.d.ts +10 -0
- package/dist/contracts/logs.contract.d.ts.map +1 -1
- package/dist/contracts/needs.contract.d.ts +7 -0
- package/dist/contracts/needs.contract.d.ts.map +1 -1
- package/dist/contracts/offload.contract.d.ts +70 -0
- package/dist/contracts/offload.contract.d.ts.map +1 -1
- package/dist/contracts/offload.contract.js +10 -1
- package/dist/contracts/offload.contract.js.map +1 -1
- package/dist/contracts/privacy.contract.d.ts +14 -0
- package/dist/contracts/privacy.contract.d.ts.map +1 -1
- package/dist/contracts/privacy.contract.js +11 -1
- package/dist/contracts/privacy.contract.js.map +1 -1
- package/dist/contracts/providers.contract.d.ts +3 -0
- package/dist/contracts/providers.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +106 -2
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +1 -0
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +74 -94
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.js +19 -1
- package/dist/contracts/settings.contract.js.map +1 -1
- package/dist/contracts/system.contract.d.ts +20 -0
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/translator.contract.d.ts +8 -0
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/events/agent-events.d.ts +47 -0
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/agent-events.js +6 -0
- package/dist/events/agent-events.js.map +1 -1
- package/dist/events/agent-words.d.ts.map +1 -1
- package/dist/events/agent-words.js +1 -1
- package/dist/events/agent-words.js.map +1 -1
- package/dist/events/resume.d.ts +3 -0
- package/dist/events/resume.d.ts.map +1 -1
- package/dist/events/resume.js +17 -2
- package/dist/events/resume.js.map +1 -1
- package/dist/events/sandbox-notice.d.ts +18 -0
- package/dist/events/sandbox-notice.d.ts.map +1 -1
- package/dist/events/sandbox-notice.js +27 -1
- package/dist/events/sandbox-notice.js.map +1 -1
- package/dist/events/system-events.d.ts +4 -0
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/transcript.d.ts +64 -0
- package/dist/events/transcript.d.ts.map +1 -1
- package/dist/events/transcript.js +17 -1
- package/dist/events/transcript.js.map +1 -1
- package/dist/index.d.ts +327 -124
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/models/agent-catalog.d.ts.map +1 -1
- package/dist/models/agent-catalog.js +8 -7
- package/dist/models/agent-catalog.js.map +1 -1
- package/dist/models/agent-runtimes.d.ts +3 -1
- package/dist/models/agent-runtimes.d.ts.map +1 -1
- package/dist/models/agent-runtimes.js +10 -0
- package/dist/models/agent-runtimes.js.map +1 -1
- package/dist/models/model-order.d.ts +1 -0
- package/dist/models/model-order.d.ts.map +1 -1
- package/dist/models/model-order.js +9 -0
- package/dist/models/model-order.js.map +1 -1
- package/dist/models/model-pins.d.ts.map +1 -1
- package/dist/models/model-pins.js.map +1 -1
- package/dist/models/model-roles.d.ts +32 -17
- package/dist/models/model-roles.d.ts.map +1 -1
- package/dist/models/model-roles.js +30 -26
- package/dist/models/model-roles.js.map +1 -1
- package/dist/models/model-setups.d.ts +46 -0
- package/dist/models/model-setups.d.ts.map +1 -0
- package/dist/models/model-setups.js +86 -0
- package/dist/models/model-setups.js.map +1 -0
- package/dist/models/plan-pools.d.ts +5 -1
- package/dist/models/plan-pools.d.ts.map +1 -1
- package/dist/models/plan-pools.js +41 -13
- package/dist/models/plan-pools.js.map +1 -1
- package/dist/models/provider-specs.d.ts +20 -0
- package/dist/models/provider-specs.d.ts.map +1 -1
- package/dist/models/provider-specs.js +13 -1
- package/dist/models/provider-specs.js.map +1 -1
- package/dist/policy/command-classes.d.ts.map +1 -1
- package/dist/policy/command-classes.js +26 -3
- package/dist/policy/command-classes.js.map +1 -1
- package/dist/policy/credential-material.d.ts +6 -0
- package/dist/policy/credential-material.d.ts.map +1 -1
- package/dist/policy/credential-material.js +37 -15
- package/dist/policy/credential-material.js.map +1 -1
- package/dist/policy/search-globs.d.ts +3 -0
- package/dist/policy/search-globs.d.ts.map +1 -1
- package/dist/policy/search-globs.js +118 -0
- package/dist/policy/search-globs.js.map +1 -1
- package/dist/policy/secret-hosts.d.ts +1 -0
- package/dist/policy/secret-hosts.d.ts.map +1 -1
- package/dist/policy/secret-hosts.js +1 -0
- package/dist/policy/secret-hosts.js.map +1 -1
- package/dist/policy/turned-away.d.ts.map +1 -1
- package/dist/policy/turned-away.js +1 -0
- package/dist/policy/turned-away.js.map +1 -1
- package/dist/protocol/doors/runner-protocol.d.ts +9 -0
- package/dist/protocol/doors/runner-protocol.d.ts.map +1 -1
- package/dist/protocol/doors/runner-protocol.js +5 -0
- package/dist/protocol/doors/runner-protocol.js.map +1 -1
- package/dist/protocol/pairing-redeem.d.ts +23 -0
- package/dist/protocol/pairing-redeem.d.ts.map +1 -0
- package/dist/protocol/pairing-redeem.js +44 -0
- package/dist/protocol/pairing-redeem.js.map +1 -0
- package/dist/protocol/peer-mcp-server.d.ts.map +1 -1
- package/dist/protocol/peer-mcp-server.js +1 -1
- package/dist/protocol/peer-mcp-server.js.map +1 -1
- package/dist/protocol/raw/raw-json-routes.d.ts +16 -0
- package/dist/protocol/raw/raw-json-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-json-routes.js +2 -0
- package/dist/protocol/raw/raw-json-routes.js.map +1 -1
- package/dist/protocol/raw/raw-routes.d.ts +8 -0
- package/dist/protocol/raw/raw-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-routes.js +2 -0
- package/dist/protocol/raw/raw-routes.js.map +1 -1
- package/dist/protocol/sse.d.ts +20 -1
- package/dist/protocol/sse.d.ts.map +1 -1
- package/dist/protocol/sse.js +112 -17
- package/dist/protocol/sse.js.map +1 -1
- package/dist/schemas/agent.d.ts +35 -0
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +12 -1
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/automations.d.ts +6 -0
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/automations.js +3 -1
- package/dist/schemas/automations.js.map +1 -1
- package/dist/schemas/devices.d.ts +17 -0
- package/dist/schemas/devices.d.ts.map +1 -1
- package/dist/schemas/devices.js +2 -1
- package/dist/schemas/devices.js.map +1 -1
- package/dist/schemas/engines.d.ts +8 -1
- package/dist/schemas/engines.d.ts.map +1 -1
- package/dist/schemas/engines.js +1 -1
- package/dist/schemas/engines.js.map +1 -1
- package/dist/schemas/environment.d.ts +30 -0
- package/dist/schemas/environment.d.ts.map +1 -1
- package/dist/schemas/environment.js +13 -0
- package/dist/schemas/environment.js.map +1 -1
- package/dist/schemas/git/git-scope.d.ts +20 -0
- package/dist/schemas/git/git-scope.d.ts.map +1 -0
- package/dist/schemas/git/git-scope.js +37 -0
- package/dist/schemas/git/git-scope.js.map +1 -0
- package/dist/schemas/git/git.d.ts +1 -0
- package/dist/schemas/git/git.d.ts.map +1 -1
- package/dist/schemas/git/git.js +9 -1
- package/dist/schemas/git/git.js.map +1 -1
- package/dist/schemas/hosts.d.ts +42 -0
- package/dist/schemas/hosts.d.ts.map +1 -1
- package/dist/schemas/hosts.js +3 -0
- package/dist/schemas/hosts.js.map +1 -1
- package/dist/schemas/keep-warm.d.ts +1 -0
- package/dist/schemas/keep-warm.d.ts.map +1 -1
- package/dist/schemas/keep-warm.js +1 -1
- package/dist/schemas/keep-warm.js.map +1 -1
- package/dist/schemas/logs.d.ts +21 -0
- package/dist/schemas/logs.d.ts.map +1 -1
- package/dist/schemas/logs.js +19 -0
- package/dist/schemas/logs.js.map +1 -1
- package/dist/schemas/metrics.d.ts +12 -0
- package/dist/schemas/metrics.d.ts.map +1 -1
- package/dist/schemas/metrics.js +26 -8
- package/dist/schemas/metrics.js.map +1 -1
- package/dist/schemas/needs.d.ts +19 -0
- package/dist/schemas/needs.d.ts.map +1 -1
- package/dist/schemas/needs.js +17 -3
- package/dist/schemas/needs.js.map +1 -1
- package/dist/schemas/offload.d.ts +154 -0
- package/dist/schemas/offload.d.ts.map +1 -1
- package/dist/schemas/offload.js +34 -0
- package/dist/schemas/offload.js.map +1 -1
- package/dist/schemas/privacy.d.ts +19 -0
- package/dist/schemas/privacy.d.ts.map +1 -1
- package/dist/schemas/privacy.js +17 -2
- package/dist/schemas/privacy.js.map +1 -1
- package/dist/schemas/project-bring-back.d.ts +172 -0
- package/dist/schemas/project-bring-back.d.ts.map +1 -0
- package/dist/schemas/project-bring-back.js +39 -0
- package/dist/schemas/project-bring-back.js.map +1 -0
- package/dist/schemas/providers/handoff.d.ts +77 -0
- package/dist/schemas/providers/handoff.d.ts.map +1 -0
- package/dist/schemas/providers/handoff.js +85 -0
- package/dist/schemas/providers/handoff.js.map +1 -0
- package/dist/schemas/providers/plan-limits.d.ts +34 -0
- package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
- package/dist/schemas/providers/plan-limits.js +13 -2
- package/dist/schemas/providers/plan-limits.js.map +1 -1
- package/dist/schemas/providers/provider-oauth.d.ts +8 -0
- package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
- package/dist/schemas/providers/usage.d.ts +3 -1
- package/dist/schemas/providers/usage.d.ts.map +1 -1
- package/dist/schemas/providers/usage.js +7 -2
- package/dist/schemas/providers/usage.js.map +1 -1
- package/dist/schemas/run-targets.d.ts.map +1 -1
- package/dist/schemas/run-targets.js +5 -2
- package/dist/schemas/run-targets.js.map +1 -1
- package/dist/schemas/settings-history.d.ts +6 -1
- package/dist/schemas/settings-history.d.ts.map +1 -1
- package/dist/schemas/settings-history.js +25 -1
- package/dist/schemas/settings-history.js.map +1 -1
- package/dist/schemas/settings.d.ts +69 -100
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +41 -24
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/short-codes.d.ts +31 -0
- package/dist/schemas/short-codes.d.ts.map +1 -0
- package/dist/schemas/short-codes.js +28 -0
- package/dist/schemas/short-codes.js.map +1 -0
- package/dist/schemas/system.d.ts +130 -0
- package/dist/schemas/system.d.ts.map +1 -1
- package/dist/schemas/system.js +37 -1
- package/dist/schemas/system.js.map +1 -1
- package/dist/state/definition.d.ts +64 -40
- package/dist/state/definition.d.ts.map +1 -1
- package/dist/state/history-state.d.ts.map +1 -1
- package/dist/state/history-state.js +1 -0
- package/dist/state/history-state.js.map +1 -1
- package/dist/state/runtime-state.d.ts +3 -0
- package/dist/state/runtime-state.d.ts.map +1 -1
- package/dist/state/runtime-state.js +1 -0
- package/dist/state/runtime-state.js.map +1 -1
- package/dist/state/versions.d.ts +6 -0
- package/dist/state/versions.d.ts.map +1 -1
- package/dist/state/versions.js +47 -0
- package/dist/state/versions.js.map +1 -1
- package/dist/text/embed.d.ts +2 -1
- package/dist/text/embed.d.ts.map +1 -1
- package/dist/text/embed.js +10 -5
- package/dist/text/embed.js.map +1 -1
- package/dist/text/title.d.ts.map +1 -1
- package/dist/text/title.js +4 -7
- package/dist/text/title.js.map +1 -1
- package/dist/text/transcript-fold.d.ts +2 -1
- package/dist/text/transcript-fold.d.ts.map +1 -1
- package/dist/text/transcript-fold.js +21 -6
- package/dist/text/transcript-fold.js.map +1 -1
- package/package.json +28 -6
- package/src/chores/probes.ts +3 -1
- package/src/contracts/agent.contract.ts +24 -3
- package/src/contracts/offload.contract.ts +21 -1
- package/src/contracts/privacy.contract.ts +13 -0
- package/src/contracts/settings.contract.ts +23 -0
- package/src/events/agent-events.ts +13 -0
- package/src/events/agent-words.ts +2 -3
- package/src/events/resume.test.ts +14 -2
- package/src/events/resume.ts +32 -3
- package/src/events/sandbox-notice.ts +31 -1
- package/src/events/transcript.ts +27 -2
- package/src/index.ts +5 -0
- package/src/models/agent-catalog.test.ts +4 -3
- package/src/models/agent-catalog.ts +10 -7
- package/src/models/agent-runtimes.ts +30 -1
- package/src/models/capability-ledger.test.ts +4 -0
- package/src/models/model-order.ts +13 -0
- package/src/models/model-pins.ts +5 -4
- package/src/models/model-roles.test.ts +9 -31
- package/src/models/model-roles.ts +42 -49
- package/src/models/model-setups.test.ts +100 -0
- package/src/models/model-setups.ts +151 -0
- package/src/models/plan-pools.test.ts +85 -0
- package/src/models/plan-pools.ts +76 -17
- package/src/models/provider-specs.ts +20 -2
- package/src/policy/command-classes.ts +34 -3
- package/src/policy/credential-material.ts +58 -16
- package/src/policy/search-globs.ts +135 -0
- package/src/policy/secret-hosts.ts +7 -0
- package/src/policy/tests/command-classes.test.ts +35 -0
- package/src/policy/tests/credential-material.test.ts +20 -0
- package/src/policy/tests/search-globs.test.ts +46 -1
- package/src/policy/tests/secret-hosts.test.ts +17 -1
- package/src/policy/turned-away.ts +2 -0
- package/src/protocol/doors/runner-protocol.ts +14 -1
- package/src/protocol/pairing-redeem.test.ts +86 -0
- package/src/protocol/pairing-redeem.ts +79 -0
- package/src/protocol/peer-mcp-server.ts +1 -2
- package/src/protocol/raw/raw-json-routes.ts +3 -0
- package/src/protocol/raw/raw-routes.ts +5 -0
- package/src/protocol/sse.test.ts +50 -1
- package/src/protocol/sse.ts +172 -26
- package/src/schemas/agent.ts +23 -2
- package/src/schemas/automations.ts +6 -2
- package/src/schemas/daemon-health.test.ts +28 -0
- package/src/schemas/devices.ts +11 -2
- package/src/schemas/engines.ts +1 -1
- package/src/schemas/environment.ts +25 -0
- package/src/schemas/git/git-scope.test.ts +91 -0
- package/src/schemas/git/git-scope.ts +71 -0
- package/src/schemas/git/git.ts +15 -1
- package/src/schemas/hosts.ts +7 -0
- package/src/schemas/keep-warm.ts +2 -1
- package/src/schemas/logs.ts +25 -0
- package/src/schemas/metrics.test.ts +80 -0
- package/src/schemas/metrics.ts +31 -9
- package/src/schemas/needs.ts +29 -3
- package/src/schemas/offload.ts +76 -0
- package/src/schemas/privacy.ts +27 -3
- package/src/schemas/project-bring-back.test.ts +15 -0
- package/src/schemas/project-bring-back.ts +78 -0
- package/src/schemas/providers/handoff.test.ts +42 -0
- package/src/schemas/providers/handoff.ts +128 -0
- package/src/schemas/providers/plan-limits.ts +23 -2
- package/src/schemas/providers/usage.ts +13 -7
- package/src/schemas/run-targets.ts +5 -2
- package/src/schemas/settings-history.ts +43 -1
- package/src/schemas/settings.test.ts +84 -6
- package/src/schemas/settings.ts +74 -44
- package/src/schemas/short-codes.ts +48 -0
- package/src/schemas/system.ts +63 -1
- package/src/state/history-state.ts +4 -0
- package/src/state/runtime-state.test.ts +4 -0
- package/src/state/runtime-state.ts +4 -0
- package/src/state/versions.test.ts +30 -1
- package/src/state/versions.ts +68 -0
- package/src/text/embed.test.ts +14 -0
- package/src/text/embed.ts +19 -10
- package/src/text/title.ts +5 -9
- package/src/text/transcript-fold.test.ts +61 -0
- package/src/text/transcript-fold.ts +31 -6
|
@@ -49,6 +49,8 @@ export const RAW_ROUTES = {
|
|
|
49
49
|
"GET /system/desktop-view": { auth: "door", beforeBoot: true, control: "never" },
|
|
50
50
|
// Deploy-target enrollment, gated by the connect token.
|
|
51
51
|
"POST /enroll": { auth: "door", control: "never" },
|
|
52
|
+
// A server-connect code for the owner's command: its single-use enroll token stands in for the connect token.
|
|
53
|
+
"POST /system/enroll/code": { floor: "owner", control: "never" },
|
|
52
54
|
// An event automation's webhook, gated by the automation's own token.
|
|
53
55
|
"POST /automations/{id}/fire": { auth: "door" },
|
|
54
56
|
// The release gate a pipeline runner waits on, gated by the workflow's minted gate token.
|
|
@@ -154,6 +156,9 @@ export const RAW_ROUTES = {
|
|
|
154
156
|
"POST /ci/webhook/{host}": { auth: "door" },
|
|
155
157
|
// Below maintainer a pairing is capped to port-mirror, so a collaborator may mint a preview tunnel.
|
|
156
158
|
"POST /system/sync/pair": { floor: "collaborator", control: "never" },
|
|
159
|
+
// A copy-paste command's short code, redeemed by the install script it was handed to: a device's first contact,
|
|
160
|
+
// gated by the code itself and a limit on failed attempts (peers/redeem-codes.ts).
|
|
161
|
+
"POST /system/redeem/{code}": { auth: "door", control: "never" },
|
|
157
162
|
// The peer doors: a device, browser or runner redeems its pairing at `enroll` and dials `connect` with the token.
|
|
158
163
|
"POST /system/hosts/pair": { control: "never" },
|
|
159
164
|
"POST /system/hosts/enroll": { auth: "door", control: "never" },
|
package/src/protocol/sse.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sseData, sseFrames } from "./sse.js";
|
|
1
|
+
import { parseSseText, type SseEvent, sseData, sseEvents, sseFrames, sseJson } from "./sse.js";
|
|
2
2
|
|
|
3
3
|
const encoder = new TextEncoder();
|
|
4
4
|
|
|
@@ -47,3 +47,52 @@ it("reads a frame's data line as JSON, and nothing from a frame without one or w
|
|
|
47
47
|
expect(sseData(`: keep-alive`)).toBeUndefined();
|
|
48
48
|
expect(sseData(`data: {not json`)).toBeUndefined();
|
|
49
49
|
});
|
|
50
|
+
|
|
51
|
+
const collectEvents = async (events: AsyncGenerator<SseEvent>): Promise<SseEvent[]> => {
|
|
52
|
+
const seen: SseEvent[] = [];
|
|
53
|
+
for await (const event of events) {
|
|
54
|
+
seen.push(event);
|
|
55
|
+
}
|
|
56
|
+
return seen;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
it("ends events at CRLF, CR and LF blank lines alike, a CRLF split across chunks included", async () => {
|
|
60
|
+
const { stream } = body([`data: 1\r\n\r`, `\ndata: 2\r\rdata: 3\n\n`], true);
|
|
61
|
+
expect((await collectEvents(sseEvents(stream))).map((event) => event.data)).toEqual([`1`, `2`, `3`]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("joins every data line of an event with a newline", () => {
|
|
65
|
+
expect(parseSseText(`data: {"a":\ndata: 1}\n\n`).map((event) => event.data)).toEqual([`{"a":\n1}`]);
|
|
66
|
+
expect(sseData(`data: {"a":\ndata: 1}`)).toEqual({ a: 1 });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("reads event, id and retry, keeps the last id across events, and skips comments", () => {
|
|
70
|
+
const events = parseSseText(`: hello\nevent: turn\nid: 7\nretry: 3000\ndata: x\n\ndata: y\n\n`);
|
|
71
|
+
expect(events.map(({ event, data, id, retry }) => ({ event, data, id, retry }))).toEqual([
|
|
72
|
+
{ event: `turn`, data: `x`, id: `7`, retry: 3000 },
|
|
73
|
+
{ event: undefined, data: `y`, id: `7`, retry: undefined },
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("drops one space after the colon and no more, and a leading byte-order mark", () => {
|
|
78
|
+
expect(parseSseText(`\uFEFFdata: two spaces\n\n`).map((event) => event.data)).toEqual([` two spaces`]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("keeps each event's raw text, its blank line included, for a reader forwarding it unchanged", () => {
|
|
82
|
+
expect(parseSseText(`event: a\r\ndata: 1\r\n\r\n: ping\n\n`).map((event) => event.raw)).toEqual([`event: a\r\ndata: 1\r\n\r\n`, `: ping\n\n`]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("dispatches an event the stream ended inside", async () => {
|
|
86
|
+
const { stream } = body([`data: last`], true);
|
|
87
|
+
expect((await collectEvents(sseEvents(stream))).map((event) => event.data)).toEqual([`last`]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("ends an event stream that goes silent past the idle window", async () => {
|
|
91
|
+
const { stream, cancelled } = body([`data: 1\n\n`], false);
|
|
92
|
+
expect((await collectEvents(sseEvents(stream, 20))).map((event) => event.data)).toEqual([`1`]);
|
|
93
|
+
expect(cancelled()).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("sseJson reads a parsed event's data", () => {
|
|
97
|
+
expect([sseJson(parseSseText(`data: [1]\n\n`)[0]!), sseJson(parseSseText(`: c\n\n`)[0]!)]).toEqual([[1], undefined]);
|
|
98
|
+
});
|
package/src/protocol/sse.ts
CHANGED
|
@@ -1,36 +1,166 @@
|
|
|
1
|
-
//
|
|
2
|
-
// a
|
|
3
|
-
//
|
|
1
|
+
// Server-Sent Events as the WHATWG event-stream grammar defines them, read off a byte stream that may be cut anywhere:
|
|
2
|
+
// inside a UTF-8 character, inside a `\r\n`, inside an event. Protocol-only (no domain shapes). Its readers: the ACP
|
|
3
|
+
// bridge and the extension SDK on the daemon's streamed routes, the privacy gateway's restorers, and the daemon's own
|
|
4
|
+
// MCP calls (an app's tool calls, a capability probe's `initialize`). Each had a parser of its own, and they disagreed:
|
|
5
|
+
// this one used to split on `\n\n` only and read the first `data:` line of an event, where the grammar allows `\r\n`
|
|
6
|
+
// and `\r` and joins every data line with `\n`. The webchat widget reads its replies with it too, through the `./sse`
|
|
7
|
+
// subpath: a bundle on customers' pages may take only modules with no imports, as this one is and must stay.
|
|
4
8
|
|
|
5
9
|
// A live daemon writes to a stream at least this often (oRPC's keep-alive comment every 5 s, a heartbeat on the rest), so
|
|
6
10
|
// this long with no bytes means the connection is dead even though nothing closed it.
|
|
7
11
|
export const SSE_IDLE_MS = 120_000;
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
export interface SseEvent {
|
|
14
|
+
// The `event:` field, undefined when the event named none (a listener reads that as "message").
|
|
15
|
+
readonly event: string | undefined;
|
|
16
|
+
// The data lines joined with `\n`; undefined for a block with none (a comment, a keep-alive, a stray blank line).
|
|
17
|
+
readonly data: string | undefined;
|
|
18
|
+
// The last event id the stream has set, as EventSource carries it: it persists across events until set again.
|
|
19
|
+
readonly id: string | undefined;
|
|
20
|
+
// The reconnection time an event asked for, in milliseconds.
|
|
21
|
+
readonly retry: number | undefined;
|
|
22
|
+
// The event's own text as it arrived, its terminating blank line included, for a reader that forwards it unchanged.
|
|
23
|
+
readonly raw: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SseParser {
|
|
27
|
+
readonly feed: (text: string) => void;
|
|
28
|
+
// The stream ended: an event it ended inside is dispatched as it stands.
|
|
29
|
+
readonly end: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Pending {
|
|
33
|
+
raw: string;
|
|
34
|
+
name: string | undefined;
|
|
35
|
+
data: string[] | undefined;
|
|
36
|
+
retry: number | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const fresh = (): Pending => ({ raw: ``, name: undefined, data: undefined, retry: undefined });
|
|
40
|
+
|
|
41
|
+
// Fields per the grammar: `field: value` (one leading space dropped), `:` starts a comment, a blank line ends the event.
|
|
42
|
+
// Every block is dispatched, a comment-only one too, so a reader forwarding the stream loses nothing; a reader of
|
|
43
|
+
// messages skips the ones whose `data` is undefined.
|
|
44
|
+
export const createSseParser = (dispatch: (event: SseEvent) => void): SseParser => {
|
|
45
|
+
let buffer = ``;
|
|
46
|
+
// How far into `buffer` is known to hold no terminator, so a long line arriving in small chunks is scanned once.
|
|
47
|
+
let scanned = 0;
|
|
48
|
+
let started = false;
|
|
49
|
+
let lastId: string | undefined;
|
|
50
|
+
let pending = fresh();
|
|
51
|
+
|
|
52
|
+
const emit = (): void => {
|
|
53
|
+
dispatch({ event: pending.name, data: pending.data?.join(`\n`), id: lastId, retry: pending.retry, raw: pending.raw });
|
|
54
|
+
pending = fresh();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const field = (name: string, value: string): void => {
|
|
58
|
+
if (name === `event`) {
|
|
59
|
+
pending.name = value;
|
|
60
|
+
} else if (name === `data`) {
|
|
61
|
+
pending.data = [...(pending.data ?? []), value];
|
|
62
|
+
} else if (name === `id` && !value.includes(`\0`)) {
|
|
63
|
+
lastId = value;
|
|
64
|
+
} else if (name === `retry` && /^\d+$/.test(value)) {
|
|
65
|
+
pending.retry = Number(value);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const line = (text: string, terminator: string): void => {
|
|
70
|
+
pending.raw += text + terminator;
|
|
71
|
+
if (text === ``) {
|
|
72
|
+
emit();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (text.startsWith(`:`)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const colon = text.indexOf(`:`);
|
|
79
|
+
field(colon < 0 ? text : text.slice(0, colon), colon < 0 ? `` : text.slice(colon + (text.charAt(colon + 1) === ` ` ? 2 : 1)));
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const lines = (final: boolean): void => {
|
|
83
|
+
const terminators = /\r\n|\r|\n/gu;
|
|
84
|
+
terminators.lastIndex = scanned;
|
|
85
|
+
let start = 0;
|
|
86
|
+
for (let found = terminators.exec(buffer); found !== null; found = terminators.exec(buffer)) {
|
|
87
|
+
// A `\r` that ends the buffer may be the first half of a `\r\n` still in flight.
|
|
88
|
+
if (found[0] === `\r` && found.index === buffer.length - 1 && !final) {
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
line(buffer.slice(start, found.index), found[0]);
|
|
92
|
+
start = found.index + found[0].length;
|
|
93
|
+
}
|
|
94
|
+
buffer = buffer.slice(start);
|
|
95
|
+
scanned = Math.max(0, buffer.length - 1);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
feed: (text) => {
|
|
100
|
+
let next = text;
|
|
101
|
+
// The grammar drops one byte-order mark at the very start; it stays in the first event's raw text.
|
|
102
|
+
if (!started && next !== ``) {
|
|
103
|
+
started = true;
|
|
104
|
+
if (next.startsWith(`\uFEFF`)) {
|
|
105
|
+
pending.raw += `\uFEFF`;
|
|
106
|
+
next = next.slice(1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
buffer += next;
|
|
110
|
+
lines(false);
|
|
111
|
+
},
|
|
112
|
+
end: () => {
|
|
113
|
+
lines(true);
|
|
114
|
+
if (buffer !== ``) {
|
|
115
|
+
line(buffer, ``);
|
|
116
|
+
buffer = ``;
|
|
117
|
+
}
|
|
118
|
+
if (pending.raw !== ``) {
|
|
119
|
+
emit();
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Every event in a body already read whole (a short MCP answer), the last one dispatched even without its blank line.
|
|
126
|
+
export const parseSseText = (text: string): SseEvent[] => {
|
|
127
|
+
const events: SseEvent[] = [];
|
|
128
|
+
const parser = createSseParser((event) => events.push(event));
|
|
129
|
+
parser.feed(text);
|
|
130
|
+
parser.end();
|
|
131
|
+
return events;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Yields each event as it arrives. Ends, cancelling the reader, after `idleMs` of silence, so a reader of a daemon that
|
|
135
|
+
// went quiet without closing (a sleeping laptop's tunnel, a wedged proxy) finishes instead of hanging.
|
|
136
|
+
export async function* sseEvents(body: ReadableStream<Uint8Array>, idleMs: number = SSE_IDLE_MS): AsyncGenerator<SseEvent> {
|
|
13
137
|
const reader = body.getReader();
|
|
14
138
|
const decoder = new TextDecoder();
|
|
15
|
-
let
|
|
139
|
+
let ready: SseEvent[] = [];
|
|
140
|
+
const parser = createSseParser((event) => ready.push(event));
|
|
16
141
|
try {
|
|
17
142
|
for (;;) {
|
|
18
143
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
19
|
-
const idle = new Promise
|
|
20
|
-
timer = setTimeout(() => resolve(
|
|
144
|
+
const idle = new Promise<`idle`>((resolve) => {
|
|
145
|
+
timer = setTimeout(() => resolve(`idle`), idleMs);
|
|
21
146
|
});
|
|
22
147
|
// oxlint-disable-next-line eslint/no-await-in-loop -- streaming reader; each chunk must be awaited in order as it arrives
|
|
23
148
|
const result = await Promise.race([reader.read(), idle]);
|
|
24
149
|
clearTimeout(timer);
|
|
25
|
-
if (result ===
|
|
150
|
+
if (result === `idle`) {
|
|
26
151
|
return;
|
|
27
152
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
153
|
+
if (result.done) {
|
|
154
|
+
parser.feed(decoder.decode());
|
|
155
|
+
parser.end();
|
|
156
|
+
} else {
|
|
157
|
+
parser.feed(decoder.decode(result.value, { stream: true }));
|
|
158
|
+
}
|
|
159
|
+
const events = ready;
|
|
160
|
+
ready = [];
|
|
161
|
+
yield* events;
|
|
162
|
+
if (result.done) {
|
|
163
|
+
return;
|
|
34
164
|
}
|
|
35
165
|
}
|
|
36
166
|
} finally {
|
|
@@ -39,20 +169,36 @@ export async function* sseFrames(body: ReadableStream<Uint8Array>, idleMs: numbe
|
|
|
39
169
|
}
|
|
40
170
|
}
|
|
41
171
|
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
export
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
return undefined;
|
|
172
|
+
// Each event's text as it arrived, without the blank line that ended it: for a reader that keeps the frame and reads
|
|
173
|
+
// it with `sseData`. The same idle window as `sseEvents`.
|
|
174
|
+
export async function* sseFrames(body: ReadableStream<Uint8Array>, idleMs: number = SSE_IDLE_MS): AsyncGenerator<string> {
|
|
175
|
+
for await (const event of sseEvents(body, idleMs)) {
|
|
176
|
+
yield event.raw.replace(/(?:\r\n|\r|\n)+$/u, ``);
|
|
48
177
|
}
|
|
49
|
-
|
|
50
|
-
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// What a data payload parses to, before a caller's schema reads it.
|
|
181
|
+
export type SseJsonValue = string | number | boolean | null | readonly SseJsonValue[] | { readonly [key: string]: SseJsonValue };
|
|
182
|
+
|
|
183
|
+
// An event's data as JSON, or undefined for an event with no data, an empty payload, or malformed JSON (all cases
|
|
184
|
+
// the callers skip).
|
|
185
|
+
export const sseJson = (event: SseEvent): SseJsonValue | undefined => {
|
|
186
|
+
if (event.data === undefined || event.data.trim() === ``) {
|
|
51
187
|
return undefined;
|
|
52
188
|
}
|
|
53
189
|
try {
|
|
54
|
-
|
|
190
|
+
// SAFETY: JSON.parse returns only JSON values; each caller's schema reads the shape it expects.
|
|
191
|
+
return JSON.parse(event.data) as SseJsonValue;
|
|
55
192
|
} catch {
|
|
56
193
|
return undefined; // Skip a malformed frame.
|
|
57
194
|
}
|
|
58
195
|
};
|
|
196
|
+
|
|
197
|
+
// The same, for a frame's text as `sseFrames` yields it.
|
|
198
|
+
export const sseData = (frame: string): SseJsonValue | undefined => {
|
|
199
|
+
const event = parseSseText(frame).find((parsed) => parsed.data !== undefined);
|
|
200
|
+
return event === undefined ? undefined : sseJson(event);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// The `event:` name of a frame's text, for a reader holding the frame rather than the parsed event.
|
|
204
|
+
export const sseEventName = (frame: string): string | undefined => parseSseText(frame)[0]?.event;
|
package/src/schemas/agent.ts
CHANGED
|
@@ -128,6 +128,10 @@ export const ForkedFromSchema = z.object({
|
|
|
128
128
|
});
|
|
129
129
|
export type ForkedFrom = z.infer<typeof ForkedFromSchema>;
|
|
130
130
|
// The turn's fields before the cross-field refinements below, so a subset (TurnProfileSchema) can be picked from them.
|
|
131
|
+
// How many files one turn may carry. A batch of a person's queued messages joins their files, so the admission splits a
|
|
132
|
+
// batch rather than build a turn past this (turn-admission.ts, together): a turn the daemon makes must parse back.
|
|
133
|
+
export const ATTACHMENT_LIMIT = 20;
|
|
134
|
+
|
|
131
135
|
const AgentTurnFieldsSchema = z.object({
|
|
132
136
|
prompt: z.string().describe("What to say to the agent. May be empty if you are only attaching files."),
|
|
133
137
|
errand: TurnErrandSchema.optional().describe(
|
|
@@ -151,7 +155,7 @@ const AgentTurnFieldsSchema = z.object({
|
|
|
151
155
|
// Already uploaded via /workspace/upload; Claude reads them via Read, Codex gets images as native input.
|
|
152
156
|
attachments: z
|
|
153
157
|
.array(z.string().min(1))
|
|
154
|
-
.max(
|
|
158
|
+
.max(ATTACHMENT_LIMIT)
|
|
155
159
|
.optional()
|
|
156
160
|
.describe("Files to hand the agent along with the prompt, as workspace paths. Upload them first."),
|
|
157
161
|
// Read out of the prompt's own `@path` tokens rather than chosen: a tokenizer over pasted text guesses, so a
|
|
@@ -450,7 +454,7 @@ export const runPickOf = (choice: {
|
|
|
450
454
|
...(choice.thinking === undefined ? {} : { thinking: choice.thinking }),
|
|
451
455
|
...(choice.fast === undefined ? {} : { fast: choice.fast }),
|
|
452
456
|
});
|
|
453
|
-
/* ONE ENTRY OF ONE
|
|
457
|
+
/* ONE ENTRY OF ONE SETUP'S MODEL LIST (settings.modelSetups): the standing version of the pick above, and not merely which model but HOW it is to be run. */
|
|
454
458
|
export const ModelPinSchema = z.object({
|
|
455
459
|
provider: AgentProviderSchema.describe("Which provider serves this work."),
|
|
456
460
|
model: z.string().min(1).describe("Which of its models. Both halves, because a model name only means anything to the provider that serves it."),
|
|
@@ -538,12 +542,29 @@ export const ConversationQueueSchema = z.object({
|
|
|
538
542
|
),
|
|
539
543
|
});
|
|
540
544
|
export type ConversationQueue = z.infer<typeof ConversationQueueSchema>;
|
|
545
|
+
// What taking a waiting message back left: the queue, and whether the conversation went with the message. A conversation
|
|
546
|
+
// its scheduled message opened, in which nothing has happened since, exists only for that message.
|
|
547
|
+
export const QueueTakenBackSchema = ConversationQueueSchema.extend({
|
|
548
|
+
unopened: z
|
|
549
|
+
.boolean()
|
|
550
|
+
.optional()
|
|
551
|
+
.describe(
|
|
552
|
+
"True when the message was the only thing in a conversation it had opened by being scheduled: no turn ran and nothing else waits. The conversation went with it, so its card is off the board and its id is free to open again. Absent otherwise, and from sandboxes older than the field.",
|
|
553
|
+
),
|
|
554
|
+
});
|
|
555
|
+
export type QueueTakenBack = z.infer<typeof QueueTakenBackSchema>;
|
|
541
556
|
// What resuming a queue did: started a turn with what waited, when nothing else ran.
|
|
542
557
|
export const QueueResumedSchema = z.object({
|
|
543
558
|
run: z.string().optional().describe("The turn the waiting messages started. Absent when a turn was already running, and they go after it."),
|
|
544
559
|
queue: ConversationQueueSchema.optional().describe(
|
|
545
560
|
"The queue the release left: what still waits once the turn took what it could. Absent from sandboxes older than the field.",
|
|
546
561
|
),
|
|
562
|
+
reason: z
|
|
563
|
+
.string()
|
|
564
|
+
.optional()
|
|
565
|
+
.describe(
|
|
566
|
+
"Why nothing went out though messages still wait, in a sentence: a spent allowance whose window is shut, a recovery the sandbox runs first, a paused queue. Absent when something went out, when they simply go after a running turn, and from sandboxes older than the field.",
|
|
567
|
+
),
|
|
547
568
|
});
|
|
548
569
|
export type QueueResumed = z.infer<typeof QueueResumedSchema>;
|
|
549
570
|
// Attaches to a conversation's newest run (live, or finished within retention); no cursor to resume from, the head
|
|
@@ -180,11 +180,15 @@ export type AutomationTarget = z.infer<typeof AutomationTargetSchema>;
|
|
|
180
180
|
export const FireOnSchema = z.enum(["pass", "change"]);
|
|
181
181
|
export type FireOn = z.infer<typeof FireOnSchema>;
|
|
182
182
|
// What the daemon remembers of a watch between checks (.intentic/records/automation-watch.json), and what the list
|
|
183
|
-
// shows under it. Values are kept whole up to a bound, so the next check can say what changed
|
|
183
|
+
// shows under it. Values are kept whole up to a bound, so the next check can say what changed; whether anything changed
|
|
184
|
+
// is decided on the digest of the whole value, so a page longer than the bound is not "changed" on every check.
|
|
184
185
|
export const WatchStateSchema = z.object({
|
|
185
186
|
armedAt: z.number().describe("When it was first checked, in milliseconds."),
|
|
186
187
|
checkedAt: z.number().describe("When it was last checked, in milliseconds."),
|
|
187
|
-
value: z.string().optional().describe("What the last passing check saw. Absent until one has passed."),
|
|
188
|
+
value: z.string().optional().describe("What the last passing check saw, cut to a bound. Absent until one has passed."),
|
|
189
|
+
digest: z.string().optional().describe("sha256 of the whole of what the last passing check saw, before any cut."),
|
|
190
|
+
// A watch edited to check something else starts over: what the old check saw says nothing about the new one.
|
|
191
|
+
condition: z.string().optional().describe("Which check this state belongs to: a digest of the guard or source it was recorded for."),
|
|
188
192
|
changedAt: z.number().optional().describe("When what it saw last changed, in milliseconds."),
|
|
189
193
|
firedAt: z.number().optional().describe("When it last set something off, in milliseconds."),
|
|
190
194
|
// Why the last check did not pass, when it did not.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type DaemonHealth, DaemonHealthReadingSchema, DaemonHealthSchema } from "./system.js";
|
|
2
|
+
|
|
3
|
+
const answer: DaemonHealth = {
|
|
4
|
+
ok: true,
|
|
5
|
+
sandboxId: "0123456789ab",
|
|
6
|
+
profile: "workspace",
|
|
7
|
+
boot: { ready: true, startedAt: 1, steps: [] },
|
|
8
|
+
state: { journal: "none", engine: 84 },
|
|
9
|
+
announce: { state: "registered", at: 2 },
|
|
10
|
+
reach: { state: "reachable", at: 3 },
|
|
11
|
+
reachedBy: "tunnel",
|
|
12
|
+
build: { version: "1.2.3" },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
test("the daemon's answer parses whole as the contract's health", () => {
|
|
16
|
+
expect(DaemonHealthSchema.parse(answer)).toEqual(answer);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("a reader takes any daemon's answer, a field of the wrong shape reading as missing", () => {
|
|
20
|
+
expect(DaemonHealthReadingSchema.parse({ ok: true, ready: false, boot: "starting", state: { journal: 7 }, reachedBy: "tunnel" })).toEqual({
|
|
21
|
+
ok: true,
|
|
22
|
+
ready: false,
|
|
23
|
+
boot: undefined,
|
|
24
|
+
state: { journal: undefined },
|
|
25
|
+
reachedBy: "tunnel",
|
|
26
|
+
});
|
|
27
|
+
expect(DaemonHealthReadingSchema.parse(answer)).toMatchObject({ sandboxId: "0123456789ab", boot: { ready: true }, announce: { state: "registered" } });
|
|
28
|
+
});
|
package/src/schemas/devices.ts
CHANGED
|
@@ -101,6 +101,9 @@ export const DeviceSandboxSchema = z.object({
|
|
|
101
101
|
// so this side adopted it for its rounds: `keptElsewhere` is left out, and these say whose it was and since when.
|
|
102
102
|
adoptedFrom: z.string().optional(),
|
|
103
103
|
keeperSilentSince: z.number().optional(),
|
|
104
|
+
// (2026-10-10) Stopped by the machine's keeper after a stretch nobody needed it (`ic sandbox sleep`), and started
|
|
105
|
+
// again by itself when somebody opens it. `running` is false while this is true.
|
|
106
|
+
asleep: z.boolean().optional(),
|
|
104
107
|
});
|
|
105
108
|
export type DeviceSandbox = z.infer<typeof DeviceSandboxSchema>;
|
|
106
109
|
// One operation on one sandbox, streamed as lines ending in a `result` or `error` frame. `prepare` builds the pending
|
|
@@ -318,12 +321,18 @@ export const DeviceSandboxIdSchema = z
|
|
|
318
321
|
.regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/);
|
|
319
322
|
// A folder on the DEVICE for `sync-install` to keep in step. The only caller-supplied string that reaches a command
|
|
320
323
|
// line, so its shape is the guard: `~`, an absolute POSIX path or a drive letter, and none of the characters that would
|
|
321
|
-
// end the argument it sits in. `~` is expanded by the daemon, which is why `$` is not allowed to arrive here.
|
|
324
|
+
// end the argument it sits in. `~` is expanded by the daemon, which is why `$` is not allowed to arrive here. The line
|
|
325
|
+
// may be PowerShell, which reads the typographic quotes “ ” „ as double quotes and ‘ ’ ‚ ‛ as single quotes beside the
|
|
326
|
+
// ASCII ones, so those are refused too: the daemon quotes the folder, and this keeps a second guard where one quoter
|
|
327
|
+
// misses a character.
|
|
322
328
|
export const DeviceLocalDirSchema = z
|
|
323
329
|
.string()
|
|
324
330
|
.min(1)
|
|
325
331
|
.max(4096)
|
|
326
|
-
|
|
332
|
+
// The curly quotes as escapes: bun's transpiler rewrites a literal one in a regex to its escape and tsgo keeps it,
|
|
333
|
+
// so spelled literally the pattern reads differently from source and from dist, and contract.lock.json can match
|
|
334
|
+
// only one of them.
|
|
335
|
+
.regex(/^(?:~|\/|[A-Za-z]:[\\/])[^"'`$;|&\n\r\u201C\u201D\u201E\u2018\u2019\u201A\u201B]*$/);
|
|
327
336
|
// A TCP port number, the one value a caller supplies that reaches a command line as a number rather than a string.
|
|
328
337
|
export const PortNumberSchema = z.number().int().min(1).max(65535);
|
|
329
338
|
export const DeviceCommandInputSchema = z.object({
|
package/src/schemas/engines.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
// An engine is the installed program a runtime (agent-catalog.ts) spawns or imports — the `claude` binary, `codex`
|
|
5
5
|
// wrapper, `@cursor/sdk`, `opencode`, `cli-proxy-api` — not the loop itself; one engine can back several runtimes.
|
|
6
6
|
|
|
7
|
-
export const ENGINE_IDS = ["claude", "codex", "cursor", "opencode", "translator"] as const;
|
|
7
|
+
export const ENGINE_IDS = ["claude", "codex", "cursor", "devin", "opencode", "translator"] as const;
|
|
8
8
|
export type EngineId = (typeof ENGINE_IDS)[number];
|
|
9
9
|
export const EngineIdSchema = z.enum(ENGINE_IDS);
|
|
10
10
|
|
|
@@ -45,12 +45,37 @@ export type RuntimeInstall = z.infer<typeof RuntimeInstallSchema>;
|
|
|
45
45
|
// (`environment propose`) clears its tool's entries.
|
|
46
46
|
export const SettledDraftSchema = z.object({ tool: z.string(), hash: z.string(), at: z.number() });
|
|
47
47
|
export type SettledDraft = z.infer<typeof SettledDraftSchema>;
|
|
48
|
+
// One overlay the daemon composed, by the hash a rebuild is stamped with, and the custom-section blocks it carried (each
|
|
49
|
+
// by name and the hash of its trimmed steps). Kept so "is this tool in the image" is answered against the overlay the
|
|
50
|
+
// running container was actually built from, which is rarely the newest one composed: every approval, capability change
|
|
51
|
+
// and daemon update composes again, and a rebuild builds whatever was newest when it started.
|
|
52
|
+
export const EnvironmentCompositionSchema = z.object({
|
|
53
|
+
hash: z.string(),
|
|
54
|
+
at: z.number(),
|
|
55
|
+
blocks: z.array(z.object({ name: z.string(), hash: z.string() })),
|
|
56
|
+
});
|
|
57
|
+
export type EnvironmentComposition = z.infer<typeof EnvironmentCompositionSchema>;
|
|
58
|
+
// A draft the daemon wrote on its own (a recurring runtime install, a cache-rule revision of an approved block), by the
|
|
59
|
+
// name its file carries and the hash of its steps. A draft that later vanishes with no answer on record was thrown away
|
|
60
|
+
// by hand (the Changes panel's discard, a git clean): that is read as a no, rather than writing it back on the next
|
|
61
|
+
// sweep. `install` is the ledger entry an auto-draft came from, which a no declines.
|
|
62
|
+
export const EnvironmentOfferSchema = z.object({
|
|
63
|
+
tool: z.string(),
|
|
64
|
+
hash: z.string(),
|
|
65
|
+
at: z.number(),
|
|
66
|
+
install: z.string().optional(),
|
|
67
|
+
});
|
|
68
|
+
export type EnvironmentOffer = z.infer<typeof EnvironmentOfferSchema>;
|
|
48
69
|
export const RuntimeInstallsFileSchema = z.object({
|
|
49
70
|
installs: z.array(RuntimeInstallSchema),
|
|
50
71
|
// The last drift snapshot, persisted so a daemon restart does not blank the card until the next sweep.
|
|
51
72
|
drift: EnvironmentDriftSchema.optional(),
|
|
52
73
|
// Drafts already answered, newest last and capped; absent on a ledger written before removal existed.
|
|
53
74
|
settled: z.array(SettledDraftSchema).optional(),
|
|
75
|
+
// Overlays composed, oldest first and capped; absent on a ledger written before they were kept.
|
|
76
|
+
compositions: z.array(EnvironmentCompositionSchema).optional(),
|
|
77
|
+
// Drafts the daemon wrote that nobody has answered yet; absent on a ledger written before they were kept.
|
|
78
|
+
offered: z.array(EnvironmentOfferSchema).optional(),
|
|
54
79
|
});
|
|
55
80
|
export type RuntimeInstallsFile = z.infer<typeof RuntimeInstallsFileSchema>;
|
|
56
81
|
// Ledger entry as the Environment card shows it: recurrence joined with whether it's present in the live container
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type GitChange, UNATTRIBUTED_ORIGIN } from "./git.js";
|
|
2
|
+
import { changeIsFrom, DISCARDABLE_SIDES, discardOutcome, isWholeRepo, scopedPaths, STAGEABLE_SIDES, UNSTAGEABLE_SIDES } from "./git-scope.js";
|
|
3
|
+
|
|
4
|
+
const change = (path: string, extra: Partial<GitChange> = {}): GitChange => ({ path, status: "modified", ...extra });
|
|
5
|
+
|
|
6
|
+
// Shape a scan produces: small enough to read, with something on every side.
|
|
7
|
+
const sides = {
|
|
8
|
+
conflicted: [change("merge.ts", { status: "conflicted" })],
|
|
9
|
+
staged: [change("staged.ts"), change("moved.ts", { status: "renamed", from: "old.ts" })],
|
|
10
|
+
unstaged: [change("edited.ts"), change("new.ts", { status: "added" })],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
test("a scope with no side reads every side the verb can move, and no others", () => {
|
|
14
|
+
// Staged is deliberately absent from stageable: re-adding it would sweep in worktree edits made after the freeze.
|
|
15
|
+
expect(scopedPaths(sides, STAGEABLE_SIDES, {})).toEqual(["edited.ts", "new.ts", "merge.ts"]);
|
|
16
|
+
expect(scopedPaths(sides, UNSTAGEABLE_SIDES, {})).toEqual(["staged.ts", "moved.ts", "old.ts"]);
|
|
17
|
+
expect(scopedPaths(sides, DISCARDABLE_SIDES, {})).toEqual(["merge.ts", "staged.ts", "moved.ts", "old.ts", "edited.ts", "new.ts"]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("a scope narrows the verb's sides and can never widen them", () => {
|
|
21
|
+
expect(scopedPaths(sides, STAGEABLE_SIDES, { side: "unstaged" })).toEqual(["edited.ts", "new.ts"]);
|
|
22
|
+
expect(scopedPaths(sides, STAGEABLE_SIDES, { side: "conflicted" })).toEqual(["merge.ts"]);
|
|
23
|
+
// Staging the one side it can't move answers empty, not everything: the honest answer to an impossible request.
|
|
24
|
+
expect(scopedPaths(sides, STAGEABLE_SIDES, { side: "staged" })).toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("both legs of a rename resolve together, so no verb acts on half a move", () => {
|
|
28
|
+
expect(scopedPaths(sides, UNSTAGEABLE_SIDES, { side: "staged" })).toContain("old.ts");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("an origin scope keeps only what that conversation landed, either leg of a rename counting", () => {
|
|
32
|
+
const origins = { "edited.ts": ["agent-a"], "new.ts": ["agent-b"], "old.ts": ["agent-a"] };
|
|
33
|
+
expect(scopedPaths(sides, STAGEABLE_SIDES, { origin: "agent-a" }, origins)).toEqual(["edited.ts"]);
|
|
34
|
+
// `moved.ts` attributes through the path it came from, the leg the land recorded.
|
|
35
|
+
expect(scopedPaths(sides, UNSTAGEABLE_SIDES, { origin: "agent-a" }, origins)).toEqual(["moved.ts", "old.ts"]);
|
|
36
|
+
// A side and an origin compose as an intersection, not either alone.
|
|
37
|
+
expect(scopedPaths(sides, DISCARDABLE_SIDES, { side: "unstaged", origin: "agent-b" }, origins)).toEqual(["new.ts"]);
|
|
38
|
+
// No matching attribution resolves to nothing, not a fallback to everyone's work.
|
|
39
|
+
expect(scopedPaths(sides, STAGEABLE_SIDES, { origin: "agent-a" })).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// The Changes panel's "You" chip sends this; it used to resolve to nothing, so its Stage all, Discard and commit
|
|
43
|
+
// each moved no file at all.
|
|
44
|
+
test("the unattributed origin keeps exactly what no conversation landed, a rename counting only when neither leg was", () => {
|
|
45
|
+
const origins = { "edited.ts": ["agent-a"], "old.ts": ["agent-a"] };
|
|
46
|
+
expect(scopedPaths(sides, STAGEABLE_SIDES, { origin: UNATTRIBUTED_ORIGIN }, origins)).toEqual(["new.ts", "merge.ts"]);
|
|
47
|
+
// `moved.ts` came from a landed path, so it is the agent's work, not the owner's.
|
|
48
|
+
expect(scopedPaths(sides, UNSTAGEABLE_SIDES, { origin: UNATTRIBUTED_ORIGIN }, origins)).toEqual(["staged.ts"]);
|
|
49
|
+
// With nothing attributed, every row is the owner's.
|
|
50
|
+
expect(scopedPaths(sides, STAGEABLE_SIDES, { side: "unstaged", origin: UNATTRIBUTED_ORIGIN })).toEqual(["edited.ts", "new.ts"]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("a path listed on two sides at once resolves to one entry", () => {
|
|
54
|
+
const both = { conflicted: [], staged: [change("a.ts")], unstaged: [change("a.ts")] };
|
|
55
|
+
expect(scopedPaths(both, DISCARDABLE_SIDES, {})).toEqual(["a.ts"]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("a scope that narrows nothing is the whole repository", () => {
|
|
59
|
+
expect(isWholeRepo({})).toBe(true);
|
|
60
|
+
expect(isWholeRepo({ side: "unstaged" })).toBe(false);
|
|
61
|
+
expect(isWholeRepo({ origin: "agent-a" })).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// The panel's count, its discard question and the daemon's scope all read this one predicate, so a rename out of an
|
|
65
|
+
// agent's file cannot be "yours" in the question and the agent's in the action.
|
|
66
|
+
test("a row is an origin's when either leg was landed by it, and the owner's only when neither was", () => {
|
|
67
|
+
const origins = { "util.ts": ["agent-a"] };
|
|
68
|
+
const renamed = change("helpers.ts", { status: "renamed", from: "util.ts" });
|
|
69
|
+
expect(changeIsFrom(renamed, "agent-a", origins)).toBe(true);
|
|
70
|
+
expect(changeIsFrom(renamed, UNATTRIBUTED_ORIGIN, origins)).toBe(false);
|
|
71
|
+
expect(changeIsFrom(change("mine.ts"), UNATTRIBUTED_ORIGIN, origins)).toBe(true);
|
|
72
|
+
expect(changeIsFrom(change("mine.ts"), "agent-a", origins)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("a discard names what leaves the disk apart from what returns to its last commit", () => {
|
|
76
|
+
const changed = {
|
|
77
|
+
conflicted: [],
|
|
78
|
+
staged: [
|
|
79
|
+
change("helpers.ts", { status: "renamed", from: "util.ts" }),
|
|
80
|
+
change("fresh.ts", { status: "added" }),
|
|
81
|
+
change("gone.ts", { status: "deleted" }),
|
|
82
|
+
],
|
|
83
|
+
// `gone.ts` deleted in the index and recreated on disk reads untracked, yet the last commit still holds it.
|
|
84
|
+
unstaged: [change("edited.ts"), change("scratch.ts", { status: "added" }), change("gone.ts", { status: "added" })],
|
|
85
|
+
};
|
|
86
|
+
const paths = scopedPaths(changed, DISCARDABLE_SIDES, {});
|
|
87
|
+
expect(discardOutcome(changed, paths)).toEqual({
|
|
88
|
+
deletes: ["helpers.ts", "fresh.ts", "scratch.ts"],
|
|
89
|
+
restores: ["util.ts", "gone.ts", "edited.ts"],
|
|
90
|
+
});
|
|
91
|
+
});
|