@intentic/sandbox-contract 1.331.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 +11 -0
- package/dist/contracts/accounts.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.d.ts +37 -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 +103 -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 +72 -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 +17 -0
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/translator.contract.d.ts +4 -0
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/events/agent-events.d.ts +46 -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 +2 -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 +313 -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 +25 -18
- package/dist/models/model-roles.d.ts.map +1 -1
- package/dist/models/model-roles.js +23 -27
- 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 +3 -1
- package/dist/models/plan-pools.d.ts.map +1 -1
- package/dist/models/plan-pools.js +18 -6
- 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 +32 -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/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 +27 -0
- package/dist/schemas/providers/plan-limits.d.ts.map +1 -1
- package/dist/schemas/providers/plan-limits.js +8 -1
- package/dist/schemas/providers/plan-limits.js.map +1 -1
- package/dist/schemas/providers/provider-oauth.d.ts +4 -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 +1 -1
- package/dist/schemas/settings-history.d.ts.map +1 -1
- package/dist/schemas/settings-history.js +21 -0
- package/dist/schemas/settings-history.js.map +1 -1
- package/dist/schemas/settings.d.ts +67 -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 +35 -50
- package/src/models/model-setups.test.ts +100 -0
- package/src/models/model-setups.ts +151 -0
- package/src/models/plan-pools.test.ts +58 -0
- package/src/models/plan-pools.ts +38 -7
- 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/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 +14 -1
- package/src/schemas/providers/usage.ts +13 -7
- package/src/schemas/run-targets.ts +5 -2
- package/src/schemas/settings-history.ts +28 -0
- package/src/schemas/settings.test.ts +74 -21
- 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
|
@@ -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
|
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type GitChange, type GitDiffSide, type GitScope, UNATTRIBUTED_ORIGIN } from "./git.js";
|
|
2
|
+
|
|
3
|
+
// What a scope names, resolved against a repository's own status. One definition, read by the daemon when it acts on a
|
|
4
|
+
// scope and by the Changes panel when it asks the owner about one, so the question and the action cannot disagree.
|
|
5
|
+
// Sides are the verb's, not the scope's: staging reads the two not yet in the index; a scope narrows, never widens.
|
|
6
|
+
|
|
7
|
+
export interface ChangedSides {
|
|
8
|
+
readonly conflicted: readonly GitChange[];
|
|
9
|
+
readonly staged: readonly GitChange[];
|
|
10
|
+
readonly unstaged: readonly GitChange[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Who landed each path, newest first, as `RepoChanges.origins` ships it; a rename's old path is keyed too.
|
|
14
|
+
export type ChangeOrigins = Readonly<Record<string, readonly string[]>>;
|
|
15
|
+
|
|
16
|
+
// What each verb can move; `git add` on an unmerged path settles a merge, so conflicts are stageable too.
|
|
17
|
+
export const STAGEABLE_SIDES: readonly GitDiffSide[] = ["unstaged", "conflicted"];
|
|
18
|
+
export const UNSTAGEABLE_SIDES: readonly GitDiffSide[] = ["staged"];
|
|
19
|
+
// Discard rewrites the worktree, which every side has one of.
|
|
20
|
+
export const DISCARDABLE_SIDES: readonly GitDiffSide[] = ["conflicted", "staged", "unstaged"];
|
|
21
|
+
|
|
22
|
+
// The paths one row stands for on disk: a rename is two, and no verb acts on half a move.
|
|
23
|
+
export const legsOf = (change: Pick<GitChange, "path" | "from">): readonly string[] =>
|
|
24
|
+
change.from === undefined ? [change.path] : [change.path, change.from];
|
|
25
|
+
|
|
26
|
+
// Every conversation that landed either leg of a row, newest first per leg, each once.
|
|
27
|
+
export const originsOfChange = (change: Pick<GitChange, "path" | "from">, origins: ChangeOrigins): readonly string[] => [
|
|
28
|
+
...new Set(legsOf(change).flatMap((path) => origins[path] ?? [])),
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// Whether a row is that origin's work: a conversation that landed either leg of it, or for the unattributed origin a row
|
|
32
|
+
// no conversation landed any leg of. A rename out of an agent's file is the agent's row, whoever renamed it, so the
|
|
33
|
+
// panel counts it, the scope moves it and the discard question names it under the same chip.
|
|
34
|
+
export const changeIsFrom = (change: Pick<GitChange, "path" | "from">, origin: string, origins: ChangeOrigins): boolean => {
|
|
35
|
+
const ids = originsOfChange(change, origins);
|
|
36
|
+
return origin === UNATTRIBUTED_ORIGIN ? ids.length === 0 : ids.includes(origin);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Paths a scope resolves to, for a verb that moves `sides`; both legs of a rename included.
|
|
40
|
+
// Deduplicated: a path staged and edited again is two rows over one file, and a rename's legs can be named twice.
|
|
41
|
+
export const scopedPaths = (changed: ChangedSides, sides: readonly GitDiffSide[], scope: GitScope, origins: ChangeOrigins = {}): readonly string[] => {
|
|
42
|
+
const origin = scope.origin;
|
|
43
|
+
const reading = scope.side === undefined ? sides : sides.filter((side) => side === scope.side);
|
|
44
|
+
const rows = reading.flatMap((side) => changed[side]);
|
|
45
|
+
const named = origin === undefined ? rows : rows.filter((change) => changeIsFrom(change, origin, origins));
|
|
46
|
+
return [...new Set(named.flatMap(legsOf))];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// A scope that narrows nothing is the whole repository; two of the three verbs say so to git directly, unenumerated.
|
|
50
|
+
export const isWholeRepo = (scope: GitScope): boolean => scope.side === undefined && scope.origin === undefined;
|
|
51
|
+
|
|
52
|
+
// What discarding `paths` does to each, read off the same status: a path the last commit does not hold leaves the disk
|
|
53
|
+
// (an untracked file, a staged new file, a rename's new leg), every other returns to its committed state. Mirrors the
|
|
54
|
+
// daemon's discard, which unstages first and then cleans what reads untracked.
|
|
55
|
+
export const discardOutcome = (changed: ChangedSides, paths: readonly string[]): { readonly deletes: readonly string[]; readonly restores: readonly string[] } => {
|
|
56
|
+
const notInHead = new Set<string>();
|
|
57
|
+
for (const change of changed.staged) {
|
|
58
|
+
if (change.status === "added" || (change.status === "renamed" && change.from !== undefined)) {
|
|
59
|
+
notInHead.add(change.path);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Untracked, unless the index deleted it from the last commit: that path is in HEAD and comes back.
|
|
63
|
+
const indexDeleted = new Set(changed.staged.filter((change) => change.status === "deleted").map((change) => change.path));
|
|
64
|
+
for (const change of changed.unstaged) {
|
|
65
|
+
if (change.status === "added" && !indexDeleted.has(change.path)) {
|
|
66
|
+
notInHead.add(change.path);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const unique = [...new Set(paths)];
|
|
70
|
+
return { deletes: unique.filter((path) => notInHead.has(path)), restores: unique.filter((path) => !notInHead.has(path)) };
|
|
71
|
+
};
|
package/src/schemas/git/git.ts
CHANGED
|
@@ -78,9 +78,23 @@ export const DiscardSchema = RepoParamSchema.extend(GitTargetSchema.shape)
|
|
|
78
78
|
.refine(oneTarget, ONE_TARGET);
|
|
79
79
|
// Index-only moves; nothing on disk changes, so neither needs a checkpoint. An empty target means the whole repository
|
|
80
80
|
// (`git add -A`, or the entire index).
|
|
81
|
+
// `chip` is the reversible form of an origin's move (the Changes panel's origin chips): staging records what the index
|
|
82
|
+
// held at every path it moves, and unstaging the same chip writes exactly that back, so a partial `git add -p` or a
|
|
83
|
+
// merge's clean result survives one on/off. It names its own target, so it goes alone.
|
|
84
|
+
const oneIndexMove = (move: GitTarget & { readonly chip?: string | undefined }): boolean =>
|
|
85
|
+
oneTarget(move) && (move.chip === undefined || (move.paths === undefined && move.scope === undefined));
|
|
81
86
|
export const GitIndexMoveSchema = RepoParamSchema.extend(GitTargetSchema.shape)
|
|
87
|
+
.extend({
|
|
88
|
+
chip: z
|
|
89
|
+
.string()
|
|
90
|
+
.min(1)
|
|
91
|
+
.optional()
|
|
92
|
+
.describe(
|
|
93
|
+
`Stage one origin's unstaged files (a conversation id, or "${UNATTRIBUTED_ORIGIN}" for the files none landed) so the move can be taken back exactly: unstaging with the same value later restores what the index held at each of those paths before, partial staging included, and leaves alone any path staged differently since or a commit that already took them. Goes without paths or a scope.`,
|
|
94
|
+
),
|
|
95
|
+
})
|
|
82
96
|
.describe("What to move across the index. Nothing on disk changes either way.")
|
|
83
|
-
.refine(
|
|
97
|
+
.refine(oneIndexMove, { message: "name paths, a scope or a chip, not more than one" });
|
|
84
98
|
// No separate "set upstream" flag: the daemon runs `push -u` exactly when the branch has none yet, which is never
|
|
85
99
|
// destructive.
|
|
86
100
|
export const PushSchema = RepoParamSchema.extend({
|
package/src/schemas/hosts.ts
CHANGED
|
@@ -76,6 +76,13 @@ export const DeviceFactsSchema = z.object({
|
|
|
76
76
|
// The agent's last upkeep pass in this environment (DeviceUpkeepSchema): whether a release brought this machine to
|
|
77
77
|
// the current shape, read rather than assumed. Absent before the first pass, and from an agent older than the field.
|
|
78
78
|
upkeep: DeviceUpkeepSchema.optional(),
|
|
79
|
+
// How the agent before this one ended, as one sentence (watchdog kill, crash, update, reboot), so a device that
|
|
80
|
+
// dropped off says why once it is back. Absent on a first start, and from an agent older than the field.
|
|
81
|
+
previousExit: z.string().optional(),
|
|
82
|
+
// Which ic answers on this machine and what steers it: an old ic.exe installed from a leftover IC_URL looked like
|
|
83
|
+
// a daemon bug for an afternoon. Override values are the agent's own redaction (secret-looking ones by length).
|
|
84
|
+
ic: z.object({ path: z.string(), version: z.string(), modified: z.string().optional() }).optional(),
|
|
85
|
+
icOverrides: z.array(z.string()).optional(),
|
|
79
86
|
});
|
|
80
87
|
export type DeviceFacts = z.infer<typeof DeviceFactsSchema>;
|
|
81
88
|
|
package/src/schemas/keep-warm.ts
CHANGED
|
@@ -88,7 +88,8 @@ export const PromptCacheOpeningSchema = z.object({
|
|
|
88
88
|
});
|
|
89
89
|
export type PromptCacheOpening = z.infer<typeof PromptCacheOpeningSchema>;
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
/** A token count as the transcript words it: "305k", or the bare count under a thousand. */
|
|
92
|
+
export const tokensLabel = (tokens: number): string => (tokens >= 1_000 ? `${Math.round(tokens / 1_000)}k` : String(tokens));
|
|
92
93
|
|
|
93
94
|
const spanLabel = (ms: number): string => {
|
|
94
95
|
const minutes = Math.max(1, Math.round(ms / 60_000));
|
package/src/schemas/logs.ts
CHANGED
|
@@ -52,10 +52,35 @@ export const ClientDiagnosticSchema = z.object({
|
|
|
52
52
|
.describe("Whatever else was worth keeping."),
|
|
53
53
|
});
|
|
54
54
|
export type ClientDiagnostic = z.infer<typeof ClientDiagnosticSchema>;
|
|
55
|
+
// Which page a batch came from, said once per batch rather than on every event. Without it two tabs' reports read as
|
|
56
|
+
// one stream, and an agent spent dozens of calls learning a page was the owner's own dev server rather than the build
|
|
57
|
+
// it had just changed.
|
|
58
|
+
export const ClientPageSchema = z.object({
|
|
59
|
+
pageId: z.string().min(1).max(64).describe("This page load's own id: every report from one load shares it, and a reload gets a new one."),
|
|
60
|
+
tabId: z.string().max(64).optional().describe("The browser tab's id, which a reload of the same tab keeps."),
|
|
61
|
+
userAgent: z.string().max(300).optional().describe("The browser's own description of itself."),
|
|
62
|
+
viewport: z.string().max(40).optional().describe("The window's size in CSS pixels and its pixel ratio, e.g. 390x844@3."),
|
|
63
|
+
// The join key into PostHog's session replay: absent where analytics is off (dev, self-hosted) or not loaded yet.
|
|
64
|
+
replay: z.string().max(100).optional().describe("The PostHog session id, whose replay shows what the person did."),
|
|
65
|
+
origin: z.string().max(200).optional().describe("The address the page was loaded from."),
|
|
66
|
+
served: z
|
|
67
|
+
.string()
|
|
68
|
+
.max(300)
|
|
69
|
+
.optional()
|
|
70
|
+
.describe("How the page was served: `dev` (a Vite dev server, with the checkout, branch and who started it), `desktop` (the installed app, with its version), `local` (a window on a folder) or `hosted`."),
|
|
71
|
+
commit: z
|
|
72
|
+
.string()
|
|
73
|
+
.max(40)
|
|
74
|
+
.optional()
|
|
75
|
+
.describe("The short commit the page's build was made from (a dev server's: the one it started on). Absent where the build could not read it."),
|
|
76
|
+
});
|
|
77
|
+
export type ClientPage = z.infer<typeof ClientPageSchema>;
|
|
55
78
|
// Capped batch: the client already coalesces and drops its own side, and an unbounded array here is a way to fill a
|
|
56
79
|
// disk with one request.
|
|
57
80
|
export const ClientDiagnosticsReportSchema = z.object({
|
|
58
81
|
events: z.array(ClientDiagnosticSchema).min(1).max(50).describe("What the browser has to report, oldest first."),
|
|
82
|
+
// Optional: a page from before it sends none.
|
|
83
|
+
page: ClientPageSchema.optional().describe("Which page load sent the batch."),
|
|
59
84
|
});
|
|
60
85
|
export type ClientDiagnosticsReport = z.infer<typeof ClientDiagnosticsReportSchema>;
|
|
61
86
|
export const ClientDiagnosticsAcceptedSchema = z.object({
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type SandboxMetrics, SandboxMetricsSchema } from "./metrics.js";
|
|
2
|
+
|
|
3
|
+
// A daemon payload from before memoryBreakdown and the other optional accounting fields existed.
|
|
4
|
+
const legacyMetrics = {
|
|
5
|
+
at: 1_000_000,
|
|
6
|
+
sandbox: {
|
|
7
|
+
cores: 2,
|
|
8
|
+
memoryBytes: 7 * 2 ** 30,
|
|
9
|
+
memoryLimitBytes: 16 * 2 ** 30,
|
|
10
|
+
loadAverage: [1.5, 1.25, 1],
|
|
11
|
+
processes: 3,
|
|
12
|
+
},
|
|
13
|
+
daemon: { rssBytes: 300 * 2 ** 20, heapUsedBytes: 120 * 2 ** 20 },
|
|
14
|
+
sessions: { "conv-a": { processes: 2, rssBytes: 400 * 2 ** 20 } },
|
|
15
|
+
roles: {
|
|
16
|
+
agentRuntime: { processes: 1, rssBytes: 300 * 2 ** 20 },
|
|
17
|
+
terminal: { processes: 1, rssBytes: 100 * 2 ** 20 },
|
|
18
|
+
},
|
|
19
|
+
} satisfies SandboxMetrics;
|
|
20
|
+
|
|
21
|
+
describe("the metrics memory accounting contract", () => {
|
|
22
|
+
test("an older payload parses unchanged without the optional breakdown or its categories", () => {
|
|
23
|
+
const parsed = SandboxMetricsSchema.parse(legacyMetrics);
|
|
24
|
+
expect(parsed).toEqual(legacyMetrics);
|
|
25
|
+
expect(Object.hasOwn(parsed.sandbox, "memoryBreakdown")).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("a partial new breakdown preserves zero and leaves unmeasured categories absent", () => {
|
|
29
|
+
const payload = {
|
|
30
|
+
...legacyMetrics,
|
|
31
|
+
sandbox: {
|
|
32
|
+
...legacyMetrics.sandbox,
|
|
33
|
+
memoryBreakdown: { anonymousBytes: 0, inactiveFileCacheBytes: 3 * 2 ** 30 },
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
expect(SandboxMetricsSchema.parse(payload)).toEqual(payload);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("each category is independently optional, with no zero defaults for the others", () => {
|
|
40
|
+
for (const memoryBreakdown of [
|
|
41
|
+
{},
|
|
42
|
+
{ anonymousBytes: 0 },
|
|
43
|
+
{ countedFileCacheBytes: 0 },
|
|
44
|
+
{ kernelBytes: 0 },
|
|
45
|
+
{ inactiveFileCacheBytes: 0 },
|
|
46
|
+
]) {
|
|
47
|
+
const payload = { ...legacyMetrics, sandbox: { ...legacyMetrics.sandbox, memoryBreakdown } };
|
|
48
|
+
expect(SandboxMetricsSchema.parse(payload)).toEqual(payload);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("measured resident categories remain separate from a headline that also counts full swap", () => {
|
|
53
|
+
const payload = {
|
|
54
|
+
...legacyMetrics,
|
|
55
|
+
sandbox: {
|
|
56
|
+
...legacyMetrics.sandbox,
|
|
57
|
+
memoryBytes: 16.5 * 2 ** 30,
|
|
58
|
+
swapBytes: 9.5 * 2 ** 30,
|
|
59
|
+
swapLimitBytes: 10 * 2 ** 30,
|
|
60
|
+
swapFull: true,
|
|
61
|
+
memoryBreakdown: {
|
|
62
|
+
anonymousBytes: 5 * 2 ** 30,
|
|
63
|
+
countedFileCacheBytes: 2 ** 30,
|
|
64
|
+
kernelBytes: 2 ** 30,
|
|
65
|
+
inactiveFileCacheBytes: 3 * 2 ** 30,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
expect(SandboxMetricsSchema.parse(payload)).toEqual(payload);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("present categories must be byte numbers, not numeric strings or non-finite values", () => {
|
|
73
|
+
for (const field of ["anonymousBytes", "countedFileCacheBytes", "kernelBytes", "inactiveFileCacheBytes"]) {
|
|
74
|
+
for (const invalid of ["0", null, Number.NaN, Number.POSITIVE_INFINITY]) {
|
|
75
|
+
const payload = { ...legacyMetrics, sandbox: { ...legacyMetrics.sandbox, memoryBreakdown: { [field]: invalid } } };
|
|
76
|
+
expect(SandboxMetricsSchema.safeParse(payload).success).toBe(false);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
package/src/schemas/metrics.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
// What the sandbox is using right now, measured only when asked (GET /system/metrics). CPU percentages are of ONE core
|
|
4
|
-
// unless the field says otherwise; memory is
|
|
4
|
+
// unless the field says otherwise; memory is in bytes, with admission accounting and process RSS distinguished below.
|
|
5
5
|
|
|
6
6
|
// The kinds of process a sandbox runs, as the daemon classifies them by command line.
|
|
7
7
|
export const PROCESS_ROLES = [
|
|
@@ -30,7 +30,7 @@ export const ProcessGroupMetricsSchema = z.object({
|
|
|
30
30
|
rssBytes: z
|
|
31
31
|
.number()
|
|
32
32
|
.describe(
|
|
33
|
-
"Their resident memory added up, in bytes
|
|
33
|
+
"Their process resident memory (RSS) added up, in bytes, including file mappings and shared pages counted once per process. This can exceed what they cost together. It excludes unmapped file cache, kernel memory and swapped-out pages.",
|
|
34
34
|
),
|
|
35
35
|
});
|
|
36
36
|
export type ProcessGroupMetrics = z.infer<typeof ProcessGroupMetricsSchema>;
|
|
@@ -84,24 +84,46 @@ export const SandboxUsageSchema = z.object({
|
|
|
84
84
|
memoryBytes: z
|
|
85
85
|
.number()
|
|
86
86
|
.describe(
|
|
87
|
-
"Memory counted against the limit, as the daemon admits work by it: resident
|
|
87
|
+
"Memory counted against the limit, as the daemon admits work by it: the cgroup's resident charge, including the daemon, kernel memory and file cache except inactive_file, plus swapped bytes only when swap is nearly full (`swapFull`). Falls back to the machine's used memory when the budget cannot measure it.",
|
|
88
88
|
),
|
|
89
89
|
memoryLimitBytes: z
|
|
90
90
|
.number()
|
|
91
91
|
.describe(
|
|
92
92
|
"The memory limit work is admitted against: where the kernel starts throttling the container (memory.high), else its hard limit, else the machine's memory.",
|
|
93
93
|
),
|
|
94
|
+
memoryBreakdown: z
|
|
95
|
+
.object({
|
|
96
|
+
anonymousBytes: z.number().optional().describe("Anonymous resident memory charged to the cgroup (memory.stat anon), in bytes."),
|
|
97
|
+
countedFileCacheBytes: z
|
|
98
|
+
.number()
|
|
99
|
+
.optional()
|
|
100
|
+
.describe("File cache counted in resident usage (memory.stat file less inactive_file), in bytes. Absent unless both counters are valid."),
|
|
101
|
+
kernelBytes: z
|
|
102
|
+
.number()
|
|
103
|
+
.optional()
|
|
104
|
+
.describe("Kernel memory charged to the cgroup (memory.stat kernel), in bytes. Absent on kernels without that aggregate."),
|
|
105
|
+
inactiveFileCacheBytes: z
|
|
106
|
+
.number()
|
|
107
|
+
.optional()
|
|
108
|
+
.describe("Inactive file cache (memory.stat inactive_file), in bytes, excluded from memoryBytes."),
|
|
109
|
+
})
|
|
110
|
+
.optional()
|
|
111
|
+
.describe(
|
|
112
|
+
"Approximate measured memory.stat categories, not an RSS subtraction or an exact sum of memoryBytes: admission is sampled separately and may also count swap. Missing or invalid categories are omitted, not zero. Absent without cgroup memory, on machine-memory fallback, when no category is valid, and from older daemons.",
|
|
113
|
+
),
|
|
94
114
|
swapBytes: z
|
|
95
115
|
.number()
|
|
96
116
|
.optional()
|
|
97
117
|
.describe(
|
|
98
|
-
"What was pushed out to swap, counted in `memoryBytes` only
|
|
118
|
+
"What was pushed out to swap, counted in `memoryBytes` only when swap is nearly full under admission policy. Absent where the sandbox cannot see its own memory.",
|
|
99
119
|
),
|
|
100
120
|
swapLimitBytes: z.number().optional().describe("What swap can hold. Absent where nothing bounds it, and from a daemon that predates it."),
|
|
101
121
|
swapFull: z
|
|
102
122
|
.boolean()
|
|
103
123
|
.optional()
|
|
104
|
-
.describe(
|
|
124
|
+
.describe(
|
|
125
|
+
"Swap has reached the admission policy's nearly-full threshold against `swapLimitBytes`, so swapped bytes count against the memory limit too, even if swap space remains.",
|
|
126
|
+
),
|
|
105
127
|
memoryRoom: MemoryRoomSchema.optional().describe("What admission reads off the same reading. Absent from a daemon that predates it."),
|
|
106
128
|
diskBytes: z.number().optional().describe("Space used on the volume the workspace lives on. Absent when the volume would not say."),
|
|
107
129
|
diskTotalBytes: z.number().optional().describe("That volume's size. Absent when the volume would not say."),
|
|
@@ -114,7 +136,7 @@ export const SandboxUsageSchema = z.object({
|
|
|
114
136
|
.describe(
|
|
115
137
|
"Cores the machine's load average reads against: every core the sandbox may be scheduled on, before any quota. Absent from a daemon that predates it.",
|
|
116
138
|
),
|
|
117
|
-
processes: z.number().describe("How many processes are running in the sandbox."),
|
|
139
|
+
processes: z.number().describe("How many processes are running in the sandbox, including the daemon."),
|
|
118
140
|
pressure: PressureMetricsSchema.optional().describe(
|
|
119
141
|
"How much work waited on CPU, memory or disk lately. Absent where the kernel does not report it.",
|
|
120
142
|
),
|
|
@@ -140,12 +162,12 @@ export const SandboxMetricsSchema = z.object({
|
|
|
140
162
|
.number()
|
|
141
163
|
.optional()
|
|
142
164
|
.describe("How long the CPU figures were measured over, in milliseconds. Absent on a first reading, and then so is every CPU figure."),
|
|
143
|
-
sandbox: SandboxUsageSchema.describe("The sandbox as a whole."),
|
|
144
|
-
daemon: DaemonUsageSchema.describe("The daemon
|
|
165
|
+
sandbox: SandboxUsageSchema.describe("The sandbox as a whole, including the daemon."),
|
|
166
|
+
daemon: DaemonUsageSchema.describe("The daemon's own usage, included in the sandbox total and process count but excluded from roles and session sums."),
|
|
145
167
|
sessions: z
|
|
146
168
|
.record(z.string(), SessionMetricsSchema)
|
|
147
169
|
.describe(
|
|
148
|
-
"What each conversation's processes use, by conversation id: the agent's own process and everything it started. Only conversations with processes running, and only those the caller may see.",
|
|
170
|
+
"What each conversation's processes use, by conversation id: the agent's own process and everything it started, excluding the daemon. These group many of the same process readings as roles, not additional memory. Only conversations with processes running, and only those the caller may see.",
|
|
149
171
|
),
|
|
150
172
|
roles: z
|
|
151
173
|
.partialRecord(ProcessRoleSchema, ProcessGroupMetricsSchema)
|
package/src/schemas/needs.ts
CHANGED
|
@@ -57,6 +57,13 @@ export type CapabilityNeed = z.infer<typeof CapabilityNeedSchema>;
|
|
|
57
57
|
// The alphabet a `{{secret:NAME}}` reference resolves; a pasted value is stored under exactly this name.
|
|
58
58
|
export const SECRET_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
59
59
|
|
|
60
|
+
// A `{{secret:name}}` reference wherever it stands in text, capturing the name. Wider than SECRET_NAME, since a
|
|
61
|
+
// reference also names the other stores' entries (`<capability>/<field>`). Not global, so a shared instance carries no
|
|
62
|
+
// `lastIndex` between callers: a site that replaces or spans every one derives its own /g copy from it. The terminal
|
|
63
|
+
// filter (@intentic/output-cleaners, builtins only) keeps a copy, pinned to this one by the daemon's
|
|
64
|
+
// src/secrets/terminal-masking-parity.test.ts.
|
|
65
|
+
export const SECRET_REFERENCE = /\{\{secret:([A-Za-z0-9_./-]+)\}\}/;
|
|
66
|
+
|
|
60
67
|
export const SecretNeedSchema = z.object({
|
|
61
68
|
kind: z.literal("secret"),
|
|
62
69
|
name: z.string().regex(SECRET_NAME).describe("The name it is stored under, and what `{{secret:NAME}}` will resolve."),
|
|
@@ -283,6 +290,16 @@ export const AgentNeedSchema = z.object({
|
|
|
283
290
|
});
|
|
284
291
|
export type AgentNeed = z.infer<typeof AgentNeedSchema>;
|
|
285
292
|
|
|
293
|
+
// A credential gateway card's "Allow requests like this with <name> in this conversation": the `ask` rule it answered
|
|
294
|
+
// stops asking that conversation, until somebody takes it back. Kept by the rule's own content, so editing the rule
|
|
295
|
+
// asks again.
|
|
296
|
+
export const BrokerRulePassSchema = z.object({
|
|
297
|
+
capability: z.string().describe("The capability whose rule asked, by id."),
|
|
298
|
+
rule: z.string().describe("The rule's own identity (its content), so an edited rule is a different one and asks again."),
|
|
299
|
+
said: z.string().describe('The request in a person\'s words, as the card put it: "GET api.github.com/repos/x with GitHub".'),
|
|
300
|
+
});
|
|
301
|
+
export type BrokerRulePass = z.infer<typeof BrokerRulePassSchema>;
|
|
302
|
+
|
|
286
303
|
// What a person allowed one conversation beyond its persona and area, from a grant need's card
|
|
287
304
|
// (personas/conversation-grants.ts). Honoured from that conversation's next turn on, and revocable on the Grants page.
|
|
288
305
|
export const ConversationGrantSchema = z.object({
|
|
@@ -306,6 +323,11 @@ export const ConversationGrantSchema = z.object({
|
|
|
306
323
|
.boolean()
|
|
307
324
|
.default(false)
|
|
308
325
|
.describe("Whether every request an allow-once could settle is allowed without asking, until somebody takes it back."),
|
|
326
|
+
// Answered on a credential gateway card, read live by the gateway as each request its rules would ask about arrives.
|
|
327
|
+
rules: z
|
|
328
|
+
.array(BrokerRulePassSchema)
|
|
329
|
+
.default([])
|
|
330
|
+
.describe("Credential gateway rules that no longer ask before its requests, each with the words a person allowed it in."),
|
|
309
331
|
updatedAt: z.number().describe("When it last changed, in milliseconds."),
|
|
310
332
|
by: z.string().optional().describe("Who last allowed something here."),
|
|
311
333
|
});
|
|
@@ -323,6 +345,7 @@ export const StandingGrantsSchema = z.object({
|
|
|
323
345
|
installs: z.boolean().default(false).describe("Whether its own dependency installs run without asking."),
|
|
324
346
|
secrets: z.array(z.string()).default([]).describe("Secrets it may send past their host guard without asking."),
|
|
325
347
|
everything: z.boolean().default(false).describe("Whether every request an allow-once could settle is allowed without asking."),
|
|
348
|
+
rules: z.array(BrokerRulePassSchema).default([]).describe("Credential gateway rules that no longer ask before its requests."),
|
|
326
349
|
by: z.string().optional().describe("Who last allowed one of those."),
|
|
327
350
|
updatedAt: z.number().optional().describe("When one of those last changed, in milliseconds."),
|
|
328
351
|
releases: z
|
|
@@ -336,11 +359,14 @@ export type StandingGrants = z.infer<typeof StandingGrantsSchema>;
|
|
|
336
359
|
export const GrantRevokeSchema = z.object({
|
|
337
360
|
conversationId: z.string(),
|
|
338
361
|
kind: z
|
|
339
|
-
.enum(["capability", "folder", "shelf", "release", "install", "secret", "everything"])
|
|
362
|
+
.enum(["capability", "folder", "shelf", "release", "install", "secret", "everything", "rule"])
|
|
340
363
|
.describe(
|
|
341
|
-
"Which kind of yes: a grant past the persona or area, a credential's release, letting its installs run unasked, a secret sent past its host guard, or
|
|
364
|
+
"Which kind of yes: a grant past the persona or area, a credential's release, letting its installs run unasked, a secret sent past its host guard, allowing everything, or a credential gateway rule that stopped asking.",
|
|
342
365
|
),
|
|
343
|
-
what: z
|
|
366
|
+
what: z
|
|
367
|
+
.string()
|
|
368
|
+
.describe("The capability id, folder, shelf, released credential or secret it named, or the rule's identity; empty for installs and everything."),
|
|
369
|
+
capability: z.string().optional().describe("For a rule, the capability whose rule it is; a rule's identity alone does not say."),
|
|
344
370
|
});
|
|
345
371
|
export type GrantRevoke = z.infer<typeof GrantRevokeSchema>;
|
|
346
372
|
|