@intentic/sandbox-contract 1.318.0 → 1.319.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.
Files changed (71) hide show
  1. package/README.md +4 -2
  2. package/dist/contracts/agent.contract.d.ts +7 -0
  3. package/dist/contracts/agent.contract.d.ts.map +1 -1
  4. package/dist/contracts/agents.contract.d.ts +34 -0
  5. package/dist/contracts/agents.contract.d.ts.map +1 -1
  6. package/dist/contracts/settings.contract.d.ts +0 -2
  7. package/dist/contracts/settings.contract.d.ts.map +1 -1
  8. package/dist/contracts/system.contract.d.ts +6 -29
  9. package/dist/contracts/system.contract.d.ts.map +1 -1
  10. package/dist/contracts/workspace.contract.d.ts +0 -53
  11. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  12. package/dist/contracts/workspace.contract.js +1 -10
  13. package/dist/contracts/workspace.contract.js.map +1 -1
  14. package/dist/events/system-events.d.ts +4 -18
  15. package/dist/events/system-events.d.ts.map +1 -1
  16. package/dist/events/system-events.js +2 -2
  17. package/dist/events/system-events.js.map +1 -1
  18. package/dist/front/generated/browser-wire.d.ts +13 -0
  19. package/dist/front/generated/browser-wire.d.ts.map +1 -1
  20. package/dist/front/generated/wire.d.ts +4 -0
  21. package/dist/front/generated/wire.d.ts.map +1 -1
  22. package/dist/index.d.ts +78 -114
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/protocol/raw-routes.d.ts +5 -0
  27. package/dist/protocol/raw-routes.d.ts.map +1 -1
  28. package/dist/protocol/raw-routes.js +1 -0
  29. package/dist/protocol/raw-routes.js.map +1 -1
  30. package/dist/protocol/vitals.d.ts +8 -0
  31. package/dist/protocol/vitals.d.ts.map +1 -0
  32. package/dist/protocol/vitals.js +16 -0
  33. package/dist/protocol/vitals.js.map +1 -0
  34. package/dist/schemas/agent.d.ts +6 -0
  35. package/dist/schemas/agent.d.ts.map +1 -1
  36. package/dist/schemas/agent.js +12 -2
  37. package/dist/schemas/agent.js.map +1 -1
  38. package/dist/schemas/agents.d.ts +6 -0
  39. package/dist/schemas/agents.d.ts.map +1 -1
  40. package/dist/schemas/automations.d.ts +2 -0
  41. package/dist/schemas/automations.d.ts.map +1 -1
  42. package/dist/schemas/settings-history.d.ts +1 -1
  43. package/dist/schemas/settings-history.d.ts.map +1 -1
  44. package/dist/schemas/settings-history.js +1 -0
  45. package/dist/schemas/settings-history.js.map +1 -1
  46. package/dist/schemas/settings.d.ts +0 -2
  47. package/dist/schemas/settings.d.ts.map +1 -1
  48. package/dist/schemas/settings.js +0 -4
  49. package/dist/schemas/settings.js.map +1 -1
  50. package/dist/schemas/workspace/workspace-tree.d.ts +0 -56
  51. package/dist/schemas/workspace/workspace-tree.d.ts.map +1 -1
  52. package/dist/schemas/workspace/workspace-tree.js +3 -15
  53. package/dist/schemas/workspace/workspace-tree.js.map +1 -1
  54. package/dist/state/definition.d.ts +0 -4
  55. package/dist/state/definition.d.ts.map +1 -1
  56. package/package.json +5 -5
  57. package/src/contracts/workspace.contract.ts +0 -12
  58. package/src/events/system-events.ts +5 -5
  59. package/src/front/generated/browser-wire.json +95 -0
  60. package/src/front/generated/browser-wire.ts +36 -0
  61. package/src/front/generated/front-wire.json +25 -0
  62. package/src/front/generated/wire.ts +2 -2
  63. package/src/front/wire-manifests.test.ts +7 -0
  64. package/src/index.ts +1 -0
  65. package/src/protocol/raw-routes.ts +3 -0
  66. package/src/protocol/vitals.test.ts +46 -0
  67. package/src/protocol/vitals.ts +38 -0
  68. package/src/schemas/agent.ts +16 -2
  69. package/src/schemas/settings-history.ts +1 -0
  70. package/src/schemas/settings.ts +1 -7
  71. package/src/schemas/workspace/workspace-tree.ts +5 -20
@@ -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";
@@ -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
+ };
@@ -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, or the turn these messages started was refused before it ran. Resuming, or sending another message, lets them go.",
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.
@@ -85,6 +85,7 @@ export const SETTINGS_HISTORY = [
85
85
  "quickModel",
86
86
  "resumeAfterLimit",
87
87
  "resumeAfterOutage",
88
+ "sidecars",
88
89
  "terseHoldout",
89
90
  "terseOutput",
90
91
  "testFaultDetection",
@@ -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:
@@ -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 the background pass stands, workspace-wide. Nothing about a file on disk can say this, and without it an
214
- // unrendered file and a file queued behind forty others look identical to a reader.
215
- export const SidecarStatusSchema = z.object({
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 with the background pass: switched off, waiting its turn, being read right now, settled, or unreachable because the renderer is missing. `undeliverable` is a format nothing here reads.",
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 queued are different answers."),
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()