@intentic/sandbox-contract 1.318.0 → 1.320.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 +4 -2
- package/dist/contracts/agent.contract.d.ts +7 -0
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +37 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/device.contract.d.ts +1 -0
- package/dist/contracts/device.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +15 -2
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +0 -2
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +14 -29
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/workspace.contract.d.ts +0 -53
- package/dist/contracts/workspace.contract.d.ts.map +1 -1
- package/dist/contracts/workspace.contract.js +1 -10
- package/dist/contracts/workspace.contract.js.map +1 -1
- package/dist/events/agent-events.d.ts +5 -0
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/agent-events.js +10 -0
- package/dist/events/agent-events.js.map +1 -1
- package/dist/events/resume.d.ts +1 -0
- package/dist/events/resume.d.ts.map +1 -1
- package/dist/events/resume.js +2 -0
- package/dist/events/resume.js.map +1 -1
- package/dist/events/sandbox-notice.d.ts +2 -0
- package/dist/events/sandbox-notice.d.ts.map +1 -1
- package/dist/events/sandbox-notice.js +1 -1
- package/dist/events/sandbox-notice.js.map +1 -1
- package/dist/events/system-events.d.ts +4 -18
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/system-events.js +2 -2
- package/dist/events/system-events.js.map +1 -1
- package/dist/events/transcript.d.ts +2 -0
- package/dist/events/transcript.d.ts.map +1 -1
- package/dist/events/transcript.js +2 -4
- package/dist/events/transcript.js.map +1 -1
- package/dist/front/generated/browser-wire.d.ts +13 -0
- package/dist/front/generated/browser-wire.d.ts.map +1 -1
- package/dist/front/generated/wire.d.ts +4 -0
- package/dist/front/generated/wire.d.ts.map +1 -1
- package/dist/index.d.ts +89 -114
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/policy/secret-hosts.d.ts.map +1 -1
- package/dist/policy/secret-hosts.js.map +1 -1
- package/dist/protocol/raw-routes.d.ts +5 -0
- package/dist/protocol/raw-routes.d.ts.map +1 -1
- package/dist/protocol/raw-routes.js +1 -0
- package/dist/protocol/raw-routes.js.map +1 -1
- package/dist/protocol/vitals.d.ts +8 -0
- package/dist/protocol/vitals.d.ts.map +1 -0
- package/dist/protocol/vitals.js +16 -0
- package/dist/protocol/vitals.js.map +1 -0
- package/dist/schemas/agent.d.ts +6 -0
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +12 -2
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/agents.d.ts +6 -0
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/automations.d.ts +2 -0
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/devices.d.ts +3 -0
- package/dist/schemas/devices.d.ts.map +1 -1
- package/dist/schemas/devices.js +1 -0
- package/dist/schemas/devices.js.map +1 -1
- package/dist/schemas/git/git.d.ts +2 -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 +2 -0
- package/dist/schemas/hosts.d.ts.map +1 -1
- package/dist/schemas/hosts.js +2 -1
- package/dist/schemas/hosts.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 +1 -0
- package/dist/schemas/settings-history.js.map +1 -1
- package/dist/schemas/settings.d.ts +0 -2
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +0 -4
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/system.d.ts +15 -0
- package/dist/schemas/system.d.ts.map +1 -1
- package/dist/schemas/system.js +10 -0
- package/dist/schemas/system.js.map +1 -1
- package/dist/schemas/turn-break.d.ts +8 -0
- package/dist/schemas/turn-break.d.ts.map +1 -1
- package/dist/schemas/turn-break.js +2 -0
- package/dist/schemas/turn-break.js.map +1 -1
- package/dist/schemas/workspace/workspace-tree.d.ts +0 -56
- package/dist/schemas/workspace/workspace-tree.d.ts.map +1 -1
- package/dist/schemas/workspace/workspace-tree.js +3 -15
- package/dist/schemas/workspace/workspace-tree.js.map +1 -1
- package/dist/state/definition.d.ts +0 -4
- 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/package.json +5 -5
- package/src/contracts/workspace.contract.ts +0 -12
- package/src/events/agent-events.ts +13 -0
- package/src/events/resume.ts +5 -0
- package/src/events/sandbox-notice.ts +1 -1
- package/src/events/system-events.ts +5 -5
- package/src/events/transcript.ts +4 -6
- package/src/front/generated/browser-wire.json +95 -0
- package/src/front/generated/browser-wire.ts +36 -0
- package/src/front/generated/front-wire.json +25 -0
- package/src/front/generated/wire.ts +2 -2
- package/src/front/wire-manifests.test.ts +7 -0
- package/src/index.ts +1 -0
- package/src/policy/secret-hosts.ts +1 -0
- package/src/policy/{batch-runs.test.ts → tests/batch-runs.test.ts} +2 -2
- package/src/policy/{command-classes.test.ts → tests/command-classes.test.ts} +1 -1
- package/src/policy/{command-run.test.ts → tests/command-run.test.ts} +2 -2
- package/src/policy/{credential-material.test.ts → tests/credential-material.test.ts} +1 -1
- package/src/policy/{fence-paths.test.ts → tests/fence-paths.test.ts} +1 -1
- package/src/policy/{overlay-lint.test.ts → tests/overlay-lint.test.ts} +1 -1
- package/src/policy/{owner-ticket.test.ts → tests/owner-ticket.test.ts} +2 -2
- package/src/policy/{persona-home.test.ts → tests/persona-home.test.ts} +1 -1
- package/src/policy/{reserved-servers.test.ts → tests/reserved-servers.test.ts} +1 -1
- package/src/policy/{safety-policy.test.ts → tests/safety-policy.test.ts} +3 -3
- package/src/policy/{search-globs.test.ts → tests/search-globs.test.ts} +1 -1
- package/src/policy/{secret-hosts.test.ts → tests/secret-hosts.test.ts} +1 -1
- package/src/protocol/raw-routes.ts +3 -0
- package/src/protocol/vitals.test.ts +46 -0
- package/src/protocol/vitals.ts +38 -0
- package/src/schemas/agent.ts +16 -2
- package/src/schemas/devices.ts +4 -0
- package/src/schemas/git/git.ts +11 -1
- package/src/schemas/hosts.ts +5 -1
- package/src/schemas/settings-history.ts +1 -0
- package/src/schemas/settings.ts +1 -7
- package/src/schemas/system.ts +25 -0
- package/src/schemas/turn-break.ts +9 -0
- package/src/schemas/workspace/workspace-tree.ts +5 -20
- package/src/state/history-state.ts +2 -0
|
@@ -5,6 +5,42 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export type EdgeVerdict = "no-tunnel" | "unknown-sandbox" | "dropped";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Where the front's control link to Node stands.
|
|
10
|
+
*/
|
|
11
|
+
export type NodeLink = "starting" | "up" | "restarting";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The share of the last 10 seconds, as a percentage, in which some of the container's tasks stalled waiting for each
|
|
15
|
+
* resource: cgroup v2's `some avg10` from `cpu.pressure`, `memory.pressure` and `io.pressure`.
|
|
16
|
+
*/
|
|
17
|
+
export type Pressure = { cpu: number, memory: number, io: number, };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The sandbox's proof of life, measured by the front and answered by it at [`VITALS_PATH`] whatever state Node is in:
|
|
21
|
+
* the daemon's own heartbeat rides Node's event loop, so a sandbox busy enough to starve it would otherwise look dead.
|
|
22
|
+
* It carries nothing of the workspace.
|
|
23
|
+
*/
|
|
24
|
+
export type SandboxVitals = { node: NodeLink,
|
|
25
|
+
/**
|
|
26
|
+
* How long Node's event loop takes to answer the front, in milliseconds: the round trip of the last ping it
|
|
27
|
+
* answered, or how long the one outstanding has waited when that is longer. The front pings every 2 s, only once
|
|
28
|
+
* the previous ping is answered. Null before the first ping, and while Node is not `up`.
|
|
29
|
+
*/
|
|
30
|
+
lagMs: number | null,
|
|
31
|
+
/**
|
|
32
|
+
* How many times the front restarted Node in the last 10 minutes.
|
|
33
|
+
*/
|
|
34
|
+
restarts: number,
|
|
35
|
+
/**
|
|
36
|
+
* Seconds since the front, and so the container, started.
|
|
37
|
+
*/
|
|
38
|
+
uptimeS: number,
|
|
39
|
+
/**
|
|
40
|
+
* The container's pressure stall; null where the cgroup's pressure files cannot be read.
|
|
41
|
+
*/
|
|
42
|
+
pressure: Pressure | null, };
|
|
43
|
+
|
|
8
44
|
/**
|
|
9
45
|
* What the browser sends on a terminal socket, each a JSON text message.
|
|
10
46
|
*/
|
|
@@ -50,6 +50,18 @@
|
|
|
50
50
|
"plan"
|
|
51
51
|
],
|
|
52
52
|
"type": "object"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"properties": {
|
|
56
|
+
"answer": {
|
|
57
|
+
"const": "pong",
|
|
58
|
+
"type": "string"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"required": [
|
|
62
|
+
"answer"
|
|
63
|
+
],
|
|
64
|
+
"type": "object"
|
|
53
65
|
}
|
|
54
66
|
]
|
|
55
67
|
},
|
|
@@ -699,6 +711,19 @@
|
|
|
699
711
|
"query"
|
|
700
712
|
],
|
|
701
713
|
"type": "object"
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
"description": "Whether Node's event loop turns, and how fast: answered `pong` at once, by the link itself. The front sends one\nevery 2 s once the previous one is answered, and reports the round trip as the vitals' lag. An older Node answers\nwith a refusal, or with an answer the front cannot read, and either still counts as answered.",
|
|
717
|
+
"properties": {
|
|
718
|
+
"question": {
|
|
719
|
+
"const": "ping",
|
|
720
|
+
"type": "string"
|
|
721
|
+
}
|
|
722
|
+
},
|
|
723
|
+
"required": [
|
|
724
|
+
"question"
|
|
725
|
+
],
|
|
726
|
+
"type": "object"
|
|
702
727
|
}
|
|
703
728
|
]
|
|
704
729
|
}
|
|
@@ -4,7 +4,7 @@ export type Answer = { "answer": "preview", route: PreviewRoute, } | { "answer":
|
|
|
4
4
|
/**
|
|
5
5
|
* The member whose ticket opened it, lowercased; absent when the daemon runs without auth.
|
|
6
6
|
*/
|
|
7
|
-
member?: string, };
|
|
7
|
+
member?: string, } | { "answer": "pong" };
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* A PEM certificate chain and its PEM private key.
|
|
@@ -61,7 +61,7 @@ export type PreviewRoute = { "to": "upstream", upstream: Upstream, } | { "to": "
|
|
|
61
61
|
/**
|
|
62
62
|
* A question the front asks Node.
|
|
63
63
|
*/
|
|
64
|
-
export type Question = { "question": "preview", host: string, probe: boolean, } | { "question": "terminal", query: string, };
|
|
64
|
+
export type Question = { "question": "preview", host: string, probe: boolean, } | { "question": "terminal", query: string, } | { "question": "ping" };
|
|
65
65
|
|
|
66
66
|
export type Scheme = "http" | "https";
|
|
67
67
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { EDGE_VERDICT_HEADER, edgeVerdictOf } from "../protocol/edge-verdict.js";
|
|
3
3
|
import { INGRESS_GRANT_HEADER, INGRESS_TUNNEL_PATH } from "../protocol/ingress-contract.js";
|
|
4
|
+
import { VITALS_PATH } from "../protocol/vitals.js";
|
|
4
5
|
import { EDGE_TRANSPORTS, TERMINAL_PATH, WEBTRANSPORT_PATH } from "./browser-wire.js";
|
|
5
6
|
import { ASK_PATIENCE_MS, FRAME_LENGTH_BYTES, FRONT_SOCKET_ENV, NODE_SOCKET_ENV } from "./front-wire.js";
|
|
6
7
|
|
|
@@ -23,6 +24,7 @@ const socket = manifest("front-wire") as {
|
|
|
23
24
|
const browser = manifest("browser-wire") as {
|
|
24
25
|
edge: { verdictHeader: string; verdicts: { oneOf: { const: string }[] } };
|
|
25
26
|
terminal: { path: string; upgrade: string };
|
|
27
|
+
vitals: { path: string; body: { $defs: { NodeLink: { oneOf: { const: string }[] } } } };
|
|
26
28
|
webTransport: { path: string };
|
|
27
29
|
};
|
|
28
30
|
|
|
@@ -58,4 +60,9 @@ describe("the wire outside oRPC, as the Rust crates define it", () => {
|
|
|
58
60
|
upgrade: browser.terminal.upgrade,
|
|
59
61
|
});
|
|
60
62
|
});
|
|
63
|
+
|
|
64
|
+
it("finds the front's vitals where browser-wire says it answers them, in the states it names", () => {
|
|
65
|
+
expect(VITALS_PATH).toBe(browser.vitals.path);
|
|
66
|
+
expect(browser.vitals.body.$defs.NodeLink.oneOf.map((state) => state.const)).toEqual(["starting", "up", "restarting"]);
|
|
67
|
+
});
|
|
61
68
|
});
|
package/src/index.ts
CHANGED
|
@@ -88,6 +88,7 @@ export { issuesContract } from "./contracts/issues.contract.js";
|
|
|
88
88
|
export { logsContract } from "./contracts/logs.contract.js";
|
|
89
89
|
export { REQUEST_ID_HEADER } from "./protocol/request-id.js";
|
|
90
90
|
export * from "./protocol/edge-verdict.js";
|
|
91
|
+
export * from "./protocol/vitals.js";
|
|
91
92
|
export { loopsContract } from "./contracts/loops.contract.js";
|
|
92
93
|
export { panelsContract } from "./contracts/panels.contract.js";
|
|
93
94
|
export { portsContract } from "./contracts/ports.contract.js";
|
|
@@ -21,6 +21,7 @@ export const normalizeHostPattern = (raw: string): string | undefined => {
|
|
|
21
21
|
if (/^[a-z][a-z0-9+.-]*:\/\//.test(text)) {
|
|
22
22
|
try {
|
|
23
23
|
text = new URL(text).hostname;
|
|
24
|
+
// allow(silent-catch): A malformed URL is refused as a secret host pattern.
|
|
24
25
|
} catch {
|
|
25
26
|
return undefined;
|
|
26
27
|
}
|
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
batchRunPrefix,
|
|
15
15
|
batchTurnBody,
|
|
16
16
|
parseBatchFile,
|
|
17
|
-
} from "
|
|
18
|
-
import { ConversationIdSchema } from "
|
|
17
|
+
} from "../batch-runs.js";
|
|
18
|
+
import { ConversationIdSchema } from "../../schemas/agent.js";
|
|
19
19
|
|
|
20
20
|
// The two real layouts this fits: one fans out over items, one where a run is the item.
|
|
21
21
|
const FANNED: BatchRunKind = { runsDir: `records/artifacts/acceptance`, prefix: `xt`, scanRuns: 10 };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { classifyCommand, type CommandContext, type CommandSpan, matchCommand } from "
|
|
1
|
+
import { classifyCommand, type CommandContext, type CommandSpan, matchCommand } from "../command-classes.js";
|
|
2
2
|
|
|
3
3
|
// Every call states a locus (now required); these default to `device`, the wider reading, so an assertion made here
|
|
4
4
|
// holds at every locus, and genuine differences get their own tests below.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { CommandRun } from "
|
|
2
|
-
import { followCommandRun } from "
|
|
1
|
+
import type { CommandRun } from "../../schemas/ci.js";
|
|
2
|
+
import { followCommandRun } from "../command-run.js";
|
|
3
3
|
|
|
4
4
|
const RUNNING: CommandRun = { status: "running", command: "pnpm test", output: "" };
|
|
5
5
|
const PASSED: CommandRun = { status: "passed", command: "pnpm test", output: "ok" };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { holdsCredentialMaterial, maskCredentialMaterial } from "
|
|
1
|
+
import { holdsCredentialMaterial, maskCredentialMaterial } from "../credential-material.js";
|
|
2
2
|
|
|
3
3
|
// The two directions aren't symmetric: a miss just raises an extra card, a wrong clear un-gates a real credential read.
|
|
4
4
|
// The "no" cases have to be exactly right; every "yes" case is a real file's real shape.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fenceAllows, fenceCovers, fenceIntersection, fenceReaches, foldPath, pathInsideFolder } from "
|
|
1
|
+
import { fenceAllows, fenceCovers, fenceIntersection, fenceReaches, foldPath, pathInsideFolder } from "../fence-paths.js";
|
|
2
2
|
|
|
3
3
|
describe("foldPath", () => {
|
|
4
4
|
test("folds the spellings of one path into one answer", () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hasOfficialBase, invalidExtensionFragment, isOfficialSandboxImage, lintOverlay, overlayBase, rewriteOverlayBase } from "
|
|
1
|
+
import { hasOfficialBase, invalidExtensionFragment, isOfficialSandboxImage, lintOverlay, overlayBase, rewriteOverlayBase } from "../overlay-lint.js";
|
|
2
2
|
|
|
3
3
|
test("accepts a RUN/ENV-only fragment, comments and blank lines included", () => {
|
|
4
4
|
const fragment = `# install the postgres client\nRUN apt-get update && apt-get install -y postgresql-client\nENV PGCLIENT=1\n`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { generateKeyPairSync } from "node:crypto";
|
|
2
|
-
import { mintReachabilityGrant, verifyReachabilityGrant } from "
|
|
3
|
-
import { isOwnerTicket, mintOwnerTicket, OWNER_TICKET_TTL_MS, publicKeyPemOf, verifyOwnerTicket } from "
|
|
2
|
+
import { mintReachabilityGrant, verifyReachabilityGrant } from "../../protocol/ingress-contract.js";
|
|
3
|
+
import { isOwnerTicket, mintOwnerTicket, OWNER_TICKET_TTL_MS, publicKeyPemOf, verifyOwnerTicket } from "../owner-ticket.js";
|
|
4
4
|
|
|
5
5
|
const pair = generateKeyPairSync("ed25519");
|
|
6
6
|
const privatePem = pair.privateKey.export({ type: "pkcs8", format: "pem" }) as string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fenceHoldsPersona, personaHome } from "
|
|
1
|
+
import { fenceHoldsPersona, personaHome } from "../persona-home.js";
|
|
2
2
|
|
|
3
3
|
// Where a card lives, and therefore who may wear it. This is the whole of the rule the Access tab draws and the
|
|
4
4
|
// daemon refuses on, so the three shapes a card can name a place in are each pinned here.
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
DAEMON_MCP_SERVERS,
|
|
5
5
|
MCP_SERVER_MINTING_KINDS,
|
|
6
6
|
RESERVED_MCP_SERVER_NAMES,
|
|
7
|
-
} from "
|
|
7
|
+
} from "../reserved-servers.js";
|
|
8
8
|
|
|
9
9
|
describe("reserved MCP server names", () => {
|
|
10
10
|
test("the reserved set is exactly the daemon server rows", () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { COMMAND_CLASS_LABELS, COMMAND_CLASS_PATTERNS } from "
|
|
2
|
-
import { COMMAND_RULE_CATALOG, DEFAULT_SAFETY_POLICY, hardRuleClasses } from "
|
|
3
|
-
import { CommandClassSchema, CommandLocusSchema } from "
|
|
1
|
+
import { COMMAND_CLASS_LABELS, COMMAND_CLASS_PATTERNS } from "../command-classes.js";
|
|
2
|
+
import { COMMAND_RULE_CATALOG, DEFAULT_SAFETY_POLICY, hardRuleClasses } from "../safety-policy.js";
|
|
3
|
+
import { CommandClassSchema, CommandLocusSchema } from "../../schemas/agent.js";
|
|
4
4
|
|
|
5
5
|
// The hard rule is the one thing on this page nobody can argue with, so it's pinned here rather than trusted to review:
|
|
6
6
|
// a class added silently is one an owner can never decide about for themselves.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hostAllowed, hostMatches, normalizeHost, normalizeHostPattern, tightensHostGuard } from "
|
|
1
|
+
import { hostAllowed, hostMatches, normalizeHost, normalizeHostPattern, tightensHostGuard } from "../secret-hosts.js";
|
|
2
2
|
|
|
3
3
|
// The pattern a person writes against the host a use would reach. Every boundary a limit turns on is pinned by value:
|
|
4
4
|
// the apex a wildcard leaves out, the look-alike that only shares a suffix, and which edits take hosts away.
|
|
@@ -28,6 +28,9 @@ export const RAW_ROUTES = {
|
|
|
28
28
|
"POST /system/ws-ticket": { beforeBoot: true, floor: "collaborator", control: "never" },
|
|
29
29
|
// A WebSocket upgrade carries no Authorization header; the terminal and both browser wires check a query ticket.
|
|
30
30
|
"GET /system/terminal": { auth: "door", beforeBoot: true, control: "never", front: true },
|
|
31
|
+
// The front's proof of life (vitals.ts): no credential and nothing of the workspace, readable by any origin, answered
|
|
32
|
+
// whatever state the daemon is in.
|
|
33
|
+
"GET /system/vitals": { auth: "door", beforeBoot: true, front: true },
|
|
31
34
|
// Desktop sync's byte pipe, guarded again by sshd's own key check against the same enrollment.
|
|
32
35
|
"GET /system/sync/ssh": { sync: "pipe", control: "never", lane: "bulk" },
|
|
33
36
|
"GET /system/browser-profile": { auth: "door", beforeBoot: true, control: "never" },
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { RAW_ROUTES, rawRoutePath } from "./raw-routes.js";
|
|
2
|
+
import { parseVitals, type SandboxVitals, VITALS_PATH } from "./vitals.js";
|
|
3
|
+
|
|
4
|
+
// The JSON browser-wire's own test pins (`vitals_are_the_camel_case_json_the_editor_parses_with_null_for_what_is_unknown`):
|
|
5
|
+
// what the front writes, the editor reads.
|
|
6
|
+
const RUST_GOLDEN_JSON = `{"node":"restarting","lagMs":null,"restarts":2,"uptimeS":3600,"pressure":{"cpu":12.34,"memory":0.0,"io":1.5}}`;
|
|
7
|
+
|
|
8
|
+
const up: SandboxVitals = { node: "up", lagMs: 12, restarts: 0, uptimeS: 60, pressure: { cpu: 0.29, memory: 0.49, io: 0.88 } };
|
|
9
|
+
|
|
10
|
+
it("reads the front's own answer as it writes it", () => {
|
|
11
|
+
expect(parseVitals(JSON.parse(RUST_GOLDEN_JSON))).toEqual({
|
|
12
|
+
node: "restarting",
|
|
13
|
+
lagMs: null,
|
|
14
|
+
restarts: 2,
|
|
15
|
+
uptimeS: 3600,
|
|
16
|
+
pressure: { cpu: 12.34, memory: 0, io: 1.5 },
|
|
17
|
+
});
|
|
18
|
+
expect(parseVitals(up)).toEqual(up);
|
|
19
|
+
expect(parseVitals({ ...up, node: "starting" })?.node).toBe("starting");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("reads a lag or pressure it cannot make out as unknown, and keeps the rest", () => {
|
|
23
|
+
const { lagMs: _lag, pressure: _pressure, ...bare } = up;
|
|
24
|
+
expect(parseVitals(bare)).toEqual({ ...bare, lagMs: null, pressure: null });
|
|
25
|
+
expect(parseVitals({ ...up, lagMs: -1, pressure: { cpu: 1 } })).toEqual({ ...up, lagMs: null, pressure: null });
|
|
26
|
+
// A later front's extra fields are not this reader's business.
|
|
27
|
+
expect(parseVitals({ ...up, since: "later" })).toEqual(up);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("is undefined for anything that is not the front's answer", () => {
|
|
31
|
+
// An older sandbox forwards the path to Node, which answers 404 as JSON, or a preview's HTML page.
|
|
32
|
+
expect(parseVitals({ error: "Not Found" })).toBeUndefined();
|
|
33
|
+
expect(parseVitals("<!doctype html><title>Not found</title>")).toBeUndefined();
|
|
34
|
+
expect(parseVitals(null)).toBeUndefined();
|
|
35
|
+
expect(parseVitals(42)).toBeUndefined();
|
|
36
|
+
expect(parseVitals([up])).toBeUndefined();
|
|
37
|
+
expect(parseVitals({ ...up, node: "crashed" })).toBeUndefined();
|
|
38
|
+
expect(parseVitals({ ...up, restarts: -1 })).toBeUndefined();
|
|
39
|
+
expect(parseVitals({ ...up, uptimeS: 1.5 })).toBeUndefined();
|
|
40
|
+
expect(parseVitals({ ...up, restarts: "2" })).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("is a route the front serves itself, with no credential, before the daemon boots", () => {
|
|
44
|
+
expect(rawRoutePath("GET /system/vitals")).toBe(VITALS_PATH);
|
|
45
|
+
expect(RAW_ROUTES["GET /system/vitals"]).toEqual({ auth: "door", beforeBoot: true, front: true });
|
|
46
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// The sandbox's proof of life, which intentic-front answers itself on the daemon's own address and never forwards to
|
|
2
|
+
// Node: whether Node is up, how long its event loop takes to answer the front's ping, how often the front restarted it,
|
|
3
|
+
// how long the container has run, and its pressure stall. The daemon's heartbeat on /events rides Node's event loop,
|
|
4
|
+
// so a busy sandbox and a dead one look alike from there; this route tells them apart. The Rust crate `browser-wire`
|
|
5
|
+
// is the definition (SandboxVitals), and wire-manifests.test.ts holds the path here to the manifest it writes.
|
|
6
|
+
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import type { NodeLink, Pressure, SandboxVitals } from "../front/generated/browser-wire.js";
|
|
9
|
+
|
|
10
|
+
export type { NodeLink, Pressure, SandboxVitals };
|
|
11
|
+
|
|
12
|
+
// A plain GET any origin may read, answered by the front whatever state Node is in. The route is registered as
|
|
13
|
+
// `front: true` in raw-routes.ts, so the daemon serves none.
|
|
14
|
+
export const VITALS_PATH = "/system/vitals";
|
|
15
|
+
|
|
16
|
+
// Every state, so a new one in the Rust enum fails this file's typecheck until it is read here too.
|
|
17
|
+
const NODE_LINKS = { starting: "starting", up: "up", restarting: "restarting" } as const satisfies { readonly [K in NodeLink]: K };
|
|
18
|
+
|
|
19
|
+
const PressureSchema = z.object({ cpu: z.number(), memory: z.number(), io: z.number() });
|
|
20
|
+
|
|
21
|
+
// Lenient where a figure is only a detail: a lag or a pressure it cannot read is unknown, and the rest still stands.
|
|
22
|
+
const VitalsSchema = z.object({
|
|
23
|
+
node: z.enum(NODE_LINKS),
|
|
24
|
+
lagMs: z.number().nonnegative().nullable().catch(null),
|
|
25
|
+
restarts: z.number().int().nonnegative(),
|
|
26
|
+
uptimeS: z.number().int().nonnegative(),
|
|
27
|
+
pressure: PressureSchema.nullable().catch(null),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// A response body as `Response.json()` hands it over: any JSON value, not yet known to be the front's answer.
|
|
31
|
+
type JsonBody = string | number | boolean | null | readonly JsonBody[] | { readonly [key: string]: JsonBody };
|
|
32
|
+
|
|
33
|
+
// The front's answer, or undefined for anything else: an older sandbox forwards the path to Node, which answers 404 or
|
|
34
|
+
// an HTML page, and an edge in between may answer for a box that is not there.
|
|
35
|
+
export const parseVitals = (body: JsonBody): SandboxVitals | undefined => {
|
|
36
|
+
const parsed = VitalsSchema.safeParse(body);
|
|
37
|
+
return parsed.success ? parsed.data : undefined;
|
|
38
|
+
};
|
package/src/schemas/agent.ts
CHANGED
|
@@ -304,6 +304,16 @@ const AgentTurnFieldsSchema = z.object({
|
|
|
304
304
|
editorContext: EditorContextSchema.optional().describe(
|
|
305
305
|
'What the user has open in their editor, folded into the prompt so that pointing words like "this" resolve.',
|
|
306
306
|
),
|
|
307
|
+
// The composer's scheduled send: a person who knows the account is spent books the message for its reopen instead of
|
|
308
|
+
// sending it into a refusal. Held in the conversation's queue, never started early; the queue's `until` is this.
|
|
309
|
+
sendAt: z
|
|
310
|
+
.number()
|
|
311
|
+
.int()
|
|
312
|
+
.positive()
|
|
313
|
+
.optional()
|
|
314
|
+
.describe(
|
|
315
|
+
"Hold this message until then (epoch milliseconds) instead of starting a turn now, for an allowance you know is spent. It waits in the conversation's queue, where it can be sent early, reworded or removed, and goes by itself at that instant. Ignored when already past, or when a turn is running.",
|
|
316
|
+
),
|
|
307
317
|
});
|
|
308
318
|
export const AgentTurnSchema = AgentTurnFieldsSchema
|
|
309
319
|
// An attachment-only send (no text) is legal; an entirely empty turn is not.
|
|
@@ -473,7 +483,7 @@ export const QueuedMessageSchema = z.object({
|
|
|
473
483
|
});
|
|
474
484
|
export type QueuedMessage = z.infer<typeof QueuedMessageSchema>;
|
|
475
485
|
// Why a queue holds its messages rather than letting them go when the conversation is free.
|
|
476
|
-
export const QueuePauseSchema = z.enum(["stopped", "refused"]);
|
|
486
|
+
export const QueuePauseSchema = z.enum(["stopped", "refused", "scheduled"]);
|
|
477
487
|
export type QueuePause = z.infer<typeof QueuePauseSchema>;
|
|
478
488
|
// What waits for a conversation's next turn: messages that arrived while a turn that could not take them ran, and
|
|
479
489
|
// held ones. Conversation state, the same for every window and kept across a restart.
|
|
@@ -481,8 +491,12 @@ export const ConversationQueueSchema = z.object({
|
|
|
481
491
|
items: z.array(QueuedMessageSchema).describe("What waits, in the order it goes out."),
|
|
482
492
|
revision: z.number().int().nonnegative().describe("Moves with every change to the queue, so of two copies the higher is the newer."),
|
|
483
493
|
paused: QueuePauseSchema.optional().describe(
|
|
484
|
-
"Why nothing goes out by itself: somebody stopped the turn,
|
|
494
|
+
"Why nothing goes out by itself: somebody stopped the turn, the turn these messages started was refused before it ran, or they were scheduled for later. Resuming lets them go, and so does sending another message unless they are scheduled.",
|
|
485
495
|
),
|
|
496
|
+
until: z
|
|
497
|
+
.number()
|
|
498
|
+
.optional()
|
|
499
|
+
.describe("When scheduled messages go out by themselves, in milliseconds. Only on a queue paused as `scheduled`."),
|
|
486
500
|
});
|
|
487
501
|
export type ConversationQueue = z.infer<typeof ConversationQueueSchema>;
|
|
488
502
|
// What resuming a queue did: started a turn with what waited, when nothing else ran.
|
package/src/schemas/devices.ts
CHANGED
|
@@ -103,6 +103,10 @@ export const DeviceSandboxOpSchema = z.enum([
|
|
|
103
103
|
"stop",
|
|
104
104
|
"restart",
|
|
105
105
|
"prepare",
|
|
106
|
+
// `prepare` under the unattended rules (`ic sandbox prepare --auto`): nobody pressed anything, so low disk is "not
|
|
107
|
+
// now" and a pinned sandbox or the version it went back from is left alone. What the update card sends by itself
|
|
108
|
+
// when it opens; sent only to an agent advertising `background-prepare`.
|
|
109
|
+
"prepare-background",
|
|
106
110
|
"update",
|
|
107
111
|
"rebuild",
|
|
108
112
|
"rollback",
|
package/src/schemas/git/git.ts
CHANGED
|
@@ -447,11 +447,21 @@ export const AgentChangesSchema = z.object({
|
|
|
447
447
|
.string()
|
|
448
448
|
.optional()
|
|
449
449
|
.describe("The branch its copy is standing on. Absent where it stands on no branch at all, which is a state git allows."),
|
|
450
|
+
carried: z
|
|
451
|
+
.boolean()
|
|
452
|
+
.optional()
|
|
453
|
+
.describe(
|
|
454
|
+
"Whether everything the conversation committed there has also been copied onto its own branch, so what is listed here and what a merge brings include it. False where a commit would not copy over cleanly. Absent from a sandbox too old to copy it, which never did.",
|
|
455
|
+
),
|
|
456
|
+
uncommitted: z
|
|
457
|
+
.boolean()
|
|
458
|
+
.optional()
|
|
459
|
+
.describe("Whether its copy there holds uncommitted changes to tracked files, which no merge brings until they are committed there."),
|
|
450
460
|
}),
|
|
451
461
|
)
|
|
452
462
|
.optional()
|
|
453
463
|
.describe(
|
|
454
|
-
"Repositories whose copy the conversation left standing on a different branch of its own. What is listed for them is this conversation's branch
|
|
464
|
+
"Repositories whose copy the conversation left standing on a different branch of its own. What is listed for them is this conversation's own branch, onto which each turn copies what it committed on the other branch unless `carried` says it could not.",
|
|
455
465
|
),
|
|
456
466
|
// Read off the live copy, so a retired one has none: its scratch went with it.
|
|
457
467
|
scratch: z
|
package/src/schemas/hosts.ts
CHANGED
|
@@ -72,12 +72,16 @@ export type DeviceFacts = z.infer<typeof DeviceFactsSchema>;
|
|
|
72
72
|
// previous one; advertised only when the agent's `ic sandbox rollback` takes `--to`.
|
|
73
73
|
// - `loopback-catch`: the `catchLoopback` procedure, a sign-in's loopback redirect caught on this machine
|
|
74
74
|
// (loopback-catch.ts); an agent without it rejects the call as an unknown procedure.
|
|
75
|
-
|
|
75
|
+
// - `background-prepare`: the `prepare-background` op, the next update downloaded under the unattended rules of the
|
|
76
|
+
// agent's own timer (`ic sandbox prepare --auto`), which the update card sends when it opens; advertised only when
|
|
77
|
+
// the agent's `ic sandbox prepare` takes `--auto`.
|
|
78
|
+
export const DeviceFeatureSchema = z.enum(["reshape-later", "set-shape", "rollback-to", "loopback-catch", "background-prepare"]);
|
|
76
79
|
export type DeviceFeature = z.infer<typeof DeviceFeatureSchema>;
|
|
77
80
|
export const DEVICE_FEATURE_RESHAPE_LATER: DeviceFeature = "reshape-later";
|
|
78
81
|
export const DEVICE_FEATURE_SET_SHAPE: DeviceFeature = "set-shape";
|
|
79
82
|
export const DEVICE_FEATURE_ROLLBACK_TO: DeviceFeature = "rollback-to";
|
|
80
83
|
export const DEVICE_FEATURE_LOOPBACK_CATCH: DeviceFeature = "loopback-catch";
|
|
84
|
+
export const DEVICE_FEATURE_BACKGROUND_PREPARE: DeviceFeature = "background-prepare";
|
|
81
85
|
// The features an agent advertised that this build knows; an unknown one is a newer agent's and means nothing here.
|
|
82
86
|
export const deviceFeatures = (facts: Pick<DeviceFacts, "features"> | undefined): DeviceFeature[] =>
|
|
83
87
|
(facts?.features ?? []).flatMap((feature) => {
|
package/src/schemas/settings.ts
CHANGED
|
@@ -372,13 +372,6 @@ export const SandboxSettingsSchema = z.object({
|
|
|
372
372
|
.describe(
|
|
373
373
|
"What share of conversations to run without the brief, so the two can be compared. Whole conversations rather than individual turns, because the brief sits in the prompt for the whole session and withholding it from one turn would not take it back.",
|
|
374
374
|
),
|
|
375
|
-
// Only the eager background pass; the `fileq` CLI itself is always on PATH regardless, gated only by its own skill.
|
|
376
|
-
sidecars: z
|
|
377
|
-
.boolean()
|
|
378
|
-
.default(false)
|
|
379
|
-
.describe(
|
|
380
|
-
"Keep an up-to-date markdown rendering of every document, image and audio file in the workspace, made in the background as files land, so the agent reads a pre-derived text instead of paying to parse the file mid-task. Costs background CPU on a document-heavy workspace, so it is a switch rather than a default.",
|
|
381
|
-
),
|
|
382
375
|
outputCleaners: z
|
|
383
376
|
.string()
|
|
384
377
|
.default("")
|
|
@@ -630,6 +623,7 @@ export const InputSavingsSchema = z.object({
|
|
|
630
623
|
});
|
|
631
624
|
export type InputSavings = z.infer<typeof InputSavingsSchema>;
|
|
632
625
|
// One arm of a turn-level experiment; mean is per turn, since the two arms never hold the same count.
|
|
626
|
+
// `mean` is over samples capped at both arms' pooled 95th percentile, so one runaway conversation cannot carry an arm.
|
|
633
627
|
export const SavingsArmSchema = z.object({ turns: z.number(), mean: z.number() });
|
|
634
628
|
// One metric's reading of a turn-level experiment (the two arms, plus the arithmetic over them). `metric` says what
|
|
635
629
|
// `mean`/`deltaPct` count:
|
package/src/schemas/system.ts
CHANGED
|
@@ -39,6 +39,26 @@ export const StagedUpdateSchema = z.object({
|
|
|
39
39
|
.describe("What the downloaded build's first boot would convert in this sandbox's stored files. Absent when no plan could be had, which says nothing either way."),
|
|
40
40
|
});
|
|
41
41
|
export type StagedUpdate = z.infer<typeof StagedUpdateSchema>;
|
|
42
|
+
// A download of the next update running on the machine right now, with how far it has got. `ic sandbox prepare` writes
|
|
43
|
+
// it onto /history while it works (update-preparing.json), refreshing `at` every few seconds, and removes it when it
|
|
44
|
+
// ends, whichever way. Advisory like the staged marker: the daemon passes it on only while `at` is fresh, so a download
|
|
45
|
+
// whose ic was killed is never drawn as one still running.
|
|
46
|
+
// It names no version: only the finished image can say which it is, and asking it costs a container; the card already
|
|
47
|
+
// knows the release it is downloading as `latest`.
|
|
48
|
+
export const PreparingUpdateSchema = z.object({
|
|
49
|
+
channel: z.string().describe("Which channel it is being taken from."),
|
|
50
|
+
startedAt: z.number().describe("When the download began, in milliseconds."),
|
|
51
|
+
at: z.number().describe("When the machine last said it is still working on it, in milliseconds."),
|
|
52
|
+
// A string rather than an enum, as UpdateOutcome's `verb` is: a newer ic's step must not make an older daemon drop
|
|
53
|
+
// the whole marker. `download`, `build` and `check` are the ones this build names.
|
|
54
|
+
phase: z
|
|
55
|
+
.string()
|
|
56
|
+
.describe(
|
|
57
|
+
"What it is doing: download (pulling the new image), build (building this sandbox's environment on it), or check (checking this sandbox's stored files against it).",
|
|
58
|
+
),
|
|
59
|
+
percent: z.number().min(0).max(100).optional().describe("How far through the download it is, from 0 to 100. Absent for a step with no measure of its own."),
|
|
60
|
+
});
|
|
61
|
+
export type PreparingUpdate = z.infer<typeof PreparingUpdateSchema>;
|
|
42
62
|
export const InfoSchema = z.object({
|
|
43
63
|
name: z.string().optional().describe("What this sandbox is called."),
|
|
44
64
|
image: z.string().optional().describe("The image it is running."),
|
|
@@ -83,6 +103,11 @@ export const InfoSchema = z.object({
|
|
|
83
103
|
staged: StagedUpdateSchema.optional().describe(
|
|
84
104
|
"An update already downloaded and built on the machine running this container, waiting only for the restart that applies it. That restart is seconds, where an unprepared update is minutes, which is a different decision entirely. Absent when nothing is waiting.",
|
|
85
105
|
),
|
|
106
|
+
// A download running on the host right now (update-preparing.json, written by ic while it prepares); absent when
|
|
107
|
+
// none is, and when the host stopped refreshing it (PREPARING_FRESH_MS in the daemon).
|
|
108
|
+
preparing: PreparingUpdateSchema.optional().describe(
|
|
109
|
+
"A download of the next update running on the machine right now, and how far it has got. Absent when nothing is downloading, or when the machine stopped saying it is.",
|
|
110
|
+
),
|
|
86
111
|
// What the host last did about this sandbox's version (update-outcome.json on /history, written by ic); absent
|
|
87
112
|
// when no host has said, which is every hosted sandbox and every host older than the file.
|
|
88
113
|
lastUpdate: UpdateOutcomeSchema.optional().describe(
|
|
@@ -13,6 +13,15 @@ import { z } from "zod";
|
|
|
13
13
|
export const TurnBreakSchema = z.enum(["limit", "outage", "stopped"]);
|
|
14
14
|
export type TurnBreak = z.infer<typeof TurnBreakSchema>;
|
|
15
15
|
|
|
16
|
+
// Every ending that leaves a held turn behind: the three above, plus the provider's safety classifier stopping a turn
|
|
17
|
+
// partway. That one has no standing answer and never re-runs by itself: the person picks, each time, between the same
|
|
18
|
+
// model again and another one (2026-09-30, the owner's call over an automatic model switch).
|
|
19
|
+
export const HeldEndingSchema = z.enum([...TurnBreakSchema.options, "flagged"]);
|
|
20
|
+
export type HeldEnding = z.infer<typeof HeldEndingSchema>;
|
|
21
|
+
|
|
22
|
+
/** Whether this ending takes a standing answer (turn-break policy), which `flagged` never does. */
|
|
23
|
+
export const isTurnBreak = (ending: HeldEnding): ending is TurnBreak => ending !== "flagged";
|
|
24
|
+
|
|
16
25
|
// `move` implies `resend`: an account with room is tried at once, and the reset appointment stands as its fallback.
|
|
17
26
|
// There is deliberately no "move, else hold" — a reader willing to spend a second account is willing to wait.
|
|
18
27
|
export const LimitPolicySchema = z.enum(["wait", "resend", "move"]);
|
|
@@ -210,29 +210,14 @@ export const WorkspaceDerivedQuerySchema = z.object({
|
|
|
210
210
|
.min(1)
|
|
211
211
|
.describe("The file you want the text of, as a workspace path. The real file, not its shadow: where the text is kept is this route's business."),
|
|
212
212
|
});
|
|
213
|
-
// Where
|
|
214
|
-
//
|
|
215
|
-
export const
|
|
216
|
-
enabled: z.boolean().describe("Whether the background pass is on (the `sidecars` setting). Off means a shadow exists only where someone asked for one."),
|
|
217
|
-
queued: z.number().describe("Files waiting for a shadow, not counting the batch being rendered right now."),
|
|
218
|
-
deriving: z
|
|
219
|
-
.array(z.string())
|
|
220
|
-
.describe("The files being rendered at this moment, as workspace paths. One batch at a time, because derivation shares the box with the agent it serves."),
|
|
221
|
-
sweeping: z.boolean().describe("Whether a whole-tree pass is running, which is what a freshly enabled setting or an unlistably large batch triggers."),
|
|
222
|
-
broken: z.boolean().describe("Whether the `fileq` binary is missing, in which case nothing renders in the background until this sandbox restarts."),
|
|
223
|
-
shadows: z.number().optional().describe("How many shadows the last whole-tree pass counted. Absent until one has run in this daemon's lifetime."),
|
|
224
|
-
sweptAt: z.string().optional().describe("When that pass finished, as an ISO timestamp."),
|
|
225
|
-
});
|
|
226
|
-
export type SidecarStatus = z.infer<typeof SidecarStatusSchema>;
|
|
227
|
-
// Where one file stands with that pass, which is the question a reader looking at a missing or stale shadow is
|
|
228
|
-
// actually asking. `idle` means the pass is on and this file is not waiting for anything: what it has is what it gets.
|
|
229
|
-
export const DerivedStateSchema = z.enum(["off", "queued", "deriving", "idle", "broken", "undeliverable"]);
|
|
213
|
+
// Where one file stands, which is the question a reader looking at a missing or stale rendering is actually asking.
|
|
214
|
+
// Nothing renders in the background: a file has text once someone asks for it, and fileq keeps it until the file changes.
|
|
215
|
+
export const DerivedStateSchema = z.enum(["deriving", "idle", "broken", "undeliverable"]);
|
|
230
216
|
export type DerivedState = z.infer<typeof DerivedStateSchema>;
|
|
231
217
|
const DerivedStandingShape = {
|
|
232
218
|
state: DerivedStateSchema.describe(
|
|
233
|
-
"Where this file stands
|
|
219
|
+
"Where this file stands: being read right now, settled (what it has is what it gets until someone asks), or unreachable because the renderer is missing. `undeliverable` is a format nothing here reads.",
|
|
234
220
|
),
|
|
235
|
-
queue: SidecarStatusSchema.describe("How the background pass as a whole is doing, so a wait can be reported as a queue rather than as nothing happening."),
|
|
236
221
|
};
|
|
237
222
|
export const WorkspaceDerivedPresentSchema = z.object({
|
|
238
223
|
...DerivedStandingShape,
|
|
@@ -257,7 +242,7 @@ export const WorkspaceDerivedPresentSchema = z.object({
|
|
|
257
242
|
});
|
|
258
243
|
export const WorkspaceDerivedAbsentSchema = z.object({
|
|
259
244
|
...DerivedStandingShape,
|
|
260
|
-
present: z.literal(false).describe("There is no derived text for that file. Read `state` before saying so to anyone: absent and
|
|
245
|
+
present: z.literal(false).describe("There is no derived text for that file. Read `state` before saying so to anyone: absent and being read are different answers."),
|
|
261
246
|
path: z.string().describe("The file, as asked for."),
|
|
262
247
|
derivable: z
|
|
263
248
|
.boolean()
|
|
@@ -82,6 +82,8 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
|
|
|
82
82
|
{ path: "engines/", portability: "derived" },
|
|
83
83
|
// An update the host downloaded and built for this sandbox (`ic sandbox prepare`); about this machine only.
|
|
84
84
|
{ path: "update-staged.json", portability: "derived" },
|
|
85
|
+
// A download the host is running for this sandbox right now (ic rewrites it every few seconds, removes it when done).
|
|
86
|
+
{ path: "update-preparing.json", portability: "derived" },
|
|
85
87
|
// What the host last did about this sandbox's version (ic writes it), the owner's skipped release, and why the
|
|
86
88
|
// last boot could not start (the daemon writes it, the host's probation reads it): all rebuilt by whoever next says.
|
|
87
89
|
{ path: "update-outcome.json", portability: "derived" },
|