@intentic/sandbox-contract 1.328.0 → 1.330.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 (126) hide show
  1. package/dist/chores/probes.d.ts.map +1 -1
  2. package/dist/chores/probes.js +1 -2
  3. package/dist/chores/probes.js.map +1 -1
  4. package/dist/contracts/agents.contract.d.ts +69 -0
  5. package/dist/contracts/agents.contract.d.ts.map +1 -1
  6. package/dist/contracts/capabilities.contract.d.ts +8 -0
  7. package/dist/contracts/capabilities.contract.d.ts.map +1 -1
  8. package/dist/contracts/device.contract.d.ts +42 -0
  9. package/dist/contracts/device.contract.d.ts.map +1 -1
  10. package/dist/contracts/device.contract.js +3 -0
  11. package/dist/contracts/device.contract.js.map +1 -1
  12. package/dist/contracts/runs.contract.d.ts +35 -0
  13. package/dist/contracts/runs.contract.d.ts.map +1 -0
  14. package/dist/contracts/runs.contract.js +22 -0
  15. package/dist/contracts/runs.contract.js.map +1 -0
  16. package/dist/contracts/settings.contract.d.ts +8 -0
  17. package/dist/contracts/settings.contract.d.ts.map +1 -1
  18. package/dist/contracts/system.contract.d.ts +42 -30
  19. package/dist/contracts/system.contract.d.ts.map +1 -1
  20. package/dist/contracts/system.contract.js +10 -1
  21. package/dist/contracts/system.contract.js.map +1 -1
  22. package/dist/contracts/workspace.contract.d.ts +37 -0
  23. package/dist/contracts/workspace.contract.d.ts.map +1 -1
  24. package/dist/contracts/workspace.contract.js +11 -1
  25. package/dist/contracts/workspace.contract.js.map +1 -1
  26. package/dist/events/system-events.d.ts +8 -0
  27. package/dist/events/system-events.d.ts.map +1 -1
  28. package/dist/index.d.ts +204 -31
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +8 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/protocol/doors/listener-protocol.d.ts +0 -1
  33. package/dist/protocol/doors/listener-protocol.d.ts.map +1 -1
  34. package/dist/protocol/doors/listener-protocol.js +3 -2
  35. package/dist/protocol/doors/listener-protocol.js.map +1 -1
  36. package/dist/protocol/raw/raw-routes.d.ts +29 -0
  37. package/dist/protocol/raw/raw-routes.d.ts.map +1 -1
  38. package/dist/protocol/raw/raw-routes.js +8 -0
  39. package/dist/protocol/raw/raw-routes.js.map +1 -1
  40. package/dist/schemas/agents-list.d.ts +4 -0
  41. package/dist/schemas/agents-list.d.ts.map +1 -1
  42. package/dist/schemas/agents.d.ts +13 -0
  43. package/dist/schemas/agents.d.ts.map +1 -1
  44. package/dist/schemas/agents.js +4 -3
  45. package/dist/schemas/agents.js.map +1 -1
  46. package/dist/schemas/capabilities.d.ts +12 -0
  47. package/dist/schemas/capabilities.d.ts.map +1 -1
  48. package/dist/schemas/capabilities.js +1 -0
  49. package/dist/schemas/capabilities.js.map +1 -1
  50. package/dist/schemas/device-artifacts.d.ts +67 -0
  51. package/dist/schemas/device-artifacts.d.ts.map +1 -0
  52. package/dist/schemas/device-artifacts.js +61 -0
  53. package/dist/schemas/device-artifacts.js.map +1 -0
  54. package/dist/schemas/hosts.d.ts +2 -0
  55. package/dist/schemas/hosts.d.ts.map +1 -1
  56. package/dist/schemas/hosts.js +2 -1
  57. package/dist/schemas/hosts.js.map +1 -1
  58. package/dist/schemas/run-targets.d.ts +82 -0
  59. package/dist/schemas/run-targets.d.ts.map +1 -0
  60. package/dist/schemas/run-targets.js +49 -0
  61. package/dist/schemas/run-targets.js.map +1 -0
  62. package/dist/schemas/settings.d.ts +8 -0
  63. package/dist/schemas/settings.d.ts.map +1 -1
  64. package/dist/schemas/settings.js +2 -1
  65. package/dist/schemas/settings.js.map +1 -1
  66. package/dist/schemas/speech.d.ts +75 -0
  67. package/dist/schemas/speech.d.ts.map +1 -0
  68. package/dist/schemas/speech.js +66 -0
  69. package/dist/schemas/speech.js.map +1 -0
  70. package/dist/schemas/terminal.d.ts +4 -0
  71. package/dist/schemas/terminal.d.ts.map +1 -1
  72. package/dist/schemas/terminal.js +4 -0
  73. package/dist/schemas/terminal.js.map +1 -1
  74. package/dist/schemas/turn-break.d.ts +7 -1
  75. package/dist/schemas/turn-break.d.ts.map +1 -1
  76. package/dist/schemas/turn-break.js +4 -2
  77. package/dist/schemas/turn-break.js.map +1 -1
  78. package/dist/schemas/workspace/workspace-graph.d.ts +133 -0
  79. package/dist/schemas/workspace/workspace-graph.d.ts.map +1 -0
  80. package/dist/schemas/workspace/workspace-graph.js +83 -0
  81. package/dist/schemas/workspace/workspace-graph.js.map +1 -0
  82. package/dist/schemas/workspace/workspace-repos.d.ts +0 -39
  83. package/dist/schemas/workspace/workspace-repos.d.ts.map +1 -1
  84. package/dist/schemas/workspace/workspace-repos.js +0 -15
  85. package/dist/schemas/workspace/workspace-repos.js.map +1 -1
  86. package/dist/state/definition.d.ts +12 -0
  87. package/dist/state/definition.d.ts.map +1 -1
  88. package/dist/state/workspace-state.d.ts +2 -1
  89. package/dist/state/workspace-state.d.ts.map +1 -1
  90. package/dist/state/workspace-state.js +5 -1
  91. package/dist/state/workspace-state.js.map +1 -1
  92. package/dist/text/transcript-page.d.ts +5 -0
  93. package/dist/text/transcript-page.d.ts.map +1 -0
  94. package/dist/text/transcript-page.js +17 -0
  95. package/dist/text/transcript-page.js.map +1 -0
  96. package/package.json +5 -5
  97. package/src/chores/probes.ts +1 -2
  98. package/src/contracts/device.contract.ts +10 -0
  99. package/src/contracts/runs.contract.ts +26 -0
  100. package/src/contracts/system.contract.ts +13 -0
  101. package/src/contracts/workspace.contract.ts +12 -1
  102. package/src/index.ts +8 -1
  103. package/src/models/agent-runtimes.ts +2 -2
  104. package/src/protocol/doors/listener-protocol.ts +5 -4
  105. package/src/protocol/raw/raw-routes.ts +15 -0
  106. package/src/schemas/agents.ts +6 -3
  107. package/src/schemas/capabilities.ts +4 -0
  108. package/src/schemas/device-artifacts.ts +99 -0
  109. package/src/schemas/hosts.ts +4 -1
  110. package/src/schemas/run-targets.ts +93 -0
  111. package/src/schemas/settings.ts +6 -1
  112. package/src/schemas/speech.ts +96 -0
  113. package/src/schemas/terminal.ts +4 -0
  114. package/src/schemas/turn-break.ts +18 -9
  115. package/src/schemas/workspace/workspace-graph.ts +116 -0
  116. package/src/schemas/workspace/workspace-repos.ts +0 -22
  117. package/src/state/workspace-state.test.ts +42 -1
  118. package/src/state/workspace-state.ts +12 -1
  119. package/src/text/transcript-page.test.ts +39 -0
  120. package/src/text/transcript-page.ts +37 -0
  121. package/dist/text/whisper.d.ts +0 -3
  122. package/dist/text/whisper.d.ts.map +0 -1
  123. package/dist/text/whisper.js +0 -11
  124. package/dist/text/whisper.js.map +0 -1
  125. package/src/text/whisper.test.ts +0 -18
  126. package/src/text/whisper.ts +0 -17
package/src/index.ts CHANGED
@@ -28,6 +28,7 @@ import { loopsContract } from "./contracts/loops.contract.js";
28
28
  import { pagesContract } from "./contracts/pages.contract.js";
29
29
  import { panelsContract } from "./contracts/panels.contract.js";
30
30
  import { portsContract } from "./contracts/ports.contract.js";
31
+ import { runsContract } from "./contracts/runs.contract.js";
31
32
  import { publicContract } from "./contracts/public.contract.js";
32
33
  import { netdiskContract } from "./contracts/netdisk.contract.js";
33
34
  import { providersContract } from "./contracts/providers.contract.js";
@@ -96,6 +97,7 @@ export * from "./protocol/vitals.js";
96
97
  export { loopsContract } from "./contracts/loops.contract.js";
97
98
  export { panelsContract } from "./contracts/panels.contract.js";
98
99
  export { portsContract } from "./contracts/ports.contract.js";
100
+ export { runsContract } from "./contracts/runs.contract.js";
99
101
  export { pagesContract, PageAppCallResultSchema, PageAppCallSchema, PageErrorsResultSchema, PageErrorsSchema } from "./contracts/pages.contract.js";
100
102
  export { publicContract } from "./contracts/public.contract.js";
101
103
  export { providersContract, type RunnableProviders, RunnableProvidersSchema } from "./contracts/providers.contract.js";
@@ -135,6 +137,7 @@ export * from "./events/child-run.js";
135
137
  export * from "./policy/turned-away.js";
136
138
  export * from "./text/mentions.js";
137
139
  export * from "./text/pages.js";
140
+ export * from "./text/transcript-page.js";
138
141
  export * from "./protocol/sse.js";
139
142
  export * from "./protocol/routes.js";
140
143
  export { type ControlReach, defaultFloor, type RouteAccess, type RouteMeta, routeAccess } from "./protocol/route-meta.js";
@@ -200,6 +203,7 @@ export * from "./policy/fence-paths.js";
200
203
  export * from "./policy/persona-home.js";
201
204
  export * from "./schemas/agents.js";
202
205
  export * from "./schemas/speaker.js";
206
+ export * from "./schemas/speech.js";
203
207
  export * from "./schemas/approvals.js";
204
208
  export * from "./schemas/automations.js";
205
209
  export * from "./schemas/agents-list.js";
@@ -211,6 +215,8 @@ export * from "./schemas/codebase-health.js";
211
215
  export * from "./schemas/context-trim.js";
212
216
  export * from "./schemas/devices.js";
213
217
  export * from "./schemas/project-delivery.js";
218
+ export * from "./schemas/device-artifacts.js";
219
+ export * from "./schemas/run-targets.js";
214
220
  export * from "./schemas/engines.js";
215
221
  export * from "./schemas/environment.js";
216
222
  export * from "./schemas/exit.js";
@@ -264,6 +270,7 @@ export * from "./schemas/offload.js";
264
270
  export * from "./schemas/webext.js";
265
271
  export * from "./schemas/phone.js";
266
272
  export * from "./schemas/workflows.js";
273
+ export * from "./schemas/workspace/workspace-graph.js";
267
274
  export * from "./schemas/workspace/workspace-repos.js";
268
275
  export * from "./schemas/workspace/workspace-search.js";
269
276
  export * from "./schemas/workspace/workspace-setup.js";
@@ -276,7 +283,6 @@ export * from "./text/title.js";
276
283
  export * from "./state/versions.js";
277
284
  export * from "./text/model-answer.js";
278
285
  export * from "./text/shell-regions.js";
279
- export * from "./text/whisper.js";
280
286
  export * from "./text/workflow-faults.js";
281
287
  export * from "./text/emoji.js";
282
288
  export * from "./time/zone.js";
@@ -315,6 +321,7 @@ export const sandboxContract = {
315
321
  pages: pagesContract,
316
322
  panels: panelsContract,
317
323
  ports: portsContract,
324
+ runs: runsContract,
318
325
  public: publicContract,
319
326
  providers: providersContract,
320
327
  push: pushContract,
@@ -121,7 +121,7 @@ export const OPENCODE: AgentCapabilities = {
121
121
  steering: false,
122
122
  permissions: "plan",
123
123
  questions: false,
124
- // Added at runtime under the conversation's own names, each prompt showing its session only its own (opencode-mcp.ts).
124
+ // Added at runtime under the conversation's own names, each session's own rules showing it only its own (opencode-mcp.ts).
125
125
  mcp: "http",
126
126
  execution: ["shell"],
127
127
  effort: false,
@@ -131,7 +131,7 @@ export const OPENCODE: AgentCapabilities = {
131
131
  terminals: false,
132
132
  recovery: false,
133
133
  warm: false,
134
- // `system` on the prompt body, per message; adds to OpenCode's own prompt, with no seam to replace it.
134
+ // The session's own instruction entry, set per message; adds to OpenCode's own prompt, with no seam to replace it.
135
135
  instructions: "append",
136
136
  skillDiscovery: "prompt",
137
137
  // OpenCode raises its permission asks for the commands its config names; the daemon answers from the rulebook, a
@@ -9,7 +9,9 @@ import { rawRouteUrl } from "../raw/raw-routes.js";
9
9
  // The path of one of the four listener routes for a provider, built from RAW_ROUTES so the gateway and the daemon
10
10
  // cannot spell it two ways.
11
11
  export const listenerRouteUrl = (route: "state" | "dispatch" | "failure" | "status", provider: string): string =>
12
- route === "state" ? rawRouteUrl("GET /listeners/{provider}/state", { provider }) : rawRouteUrl(`POST /listeners/{provider}/${route}`, { provider });
12
+ route === "state"
13
+ ? rawRouteUrl("GET /listeners/{provider}/state", { provider })
14
+ : rawRouteUrl(`POST /listeners/{provider}/${route}`, { provider });
13
15
 
14
16
  // The reconcile feed GET /listeners/<provider>/state answers: the enabled automations listening to this provider and
15
17
  // the connectors the asking extension contributes, each with its whole config, secrets included, since the gateway
@@ -86,10 +88,9 @@ export const ListenerPairingSchema = z.object({
86
88
  export type ListenerPairing = z.infer<typeof ListenerPairingSchema>;
87
89
 
88
90
  // Push-based status: a gateway POSTs its connection/voice snapshot to /listeners/<provider>/status, since the daemon
89
- // holds no provider connection to probe itself. Extends ActivityStatus with whisper's voice-pending flag and each
90
- // unpaired capability's pairing ceremony by id.
91
+ // holds no provider connection to probe itself. Extends ActivityStatus with each unpaired capability's pairing ceremony
92
+ // by id.
91
93
  export const ListenerStatusSchema = ActivityStatusSchema.extend({
92
- whisperReady: z.boolean().optional(),
93
94
  pairing: z.record(z.string(), ListenerPairingSchema).optional(),
94
95
  });
95
96
  export type ListenerStatus = z.infer<typeof ListenerStatusSchema>;
@@ -16,7 +16,11 @@ export const RAW_ROUTES = {
16
16
  "GET /diff/raw": { lane: "bulk" },
17
17
  // Arming the dictation model is a read any tier makes; dictating is writing a message, the collaborator's grant.
18
18
  "GET /speech/status": { guest: true },
19
+ "POST /speech/prepare": { guest: true },
19
20
  "POST /speech/transcribe": { floor: "collaborator", guest: true },
21
+ // The live phrase stream (schemas/speech.ts): a WebSocket, so the query ticket, floored at the collaborator in the
22
+ // handler like the transcribe it streams.
23
+ "GET /speech/stream": { auth: "door" },
20
24
  // The bytes behind the workspace reads; each applies the caller's fence itself.
21
25
  "GET /workspace/raw": { guest: true, lane: "bulk" },
22
26
  // No door, unlike media: the page fetches a thumbnail itself, so the request carries the header.
@@ -124,6 +128,17 @@ export const RAW_ROUTES = {
124
128
  "GET /fleet": { agent: true },
125
129
  "POST /fleet/message": { agent: true },
126
130
  "GET /fleet/{handle}": { agent: true },
131
+ // The `devices push` CLI: a program the agent built, carried to one of the owner's computers in chunks; held to the
132
+ // devices the calling conversation's turn mounts, and refused by the machine itself unless "Run programs" is on.
133
+ "POST /devices/{name}/artifacts": { agent: true, control: "never" },
134
+ // The rest of the `devices` command, held to the same rule (hosts/device-door.routes.ts): the device's app_start for a
135
+ // shell, and its loopback ports reached from the sandbox (hosts/device-tunnels.ts).
136
+ "POST /devices/{name}/apps": { agent: true, control: "never" },
137
+ "POST /devices/{name}/tunnels": { agent: true, control: "never" },
138
+ "DELETE /devices/{name}/tunnels/{port}": { agent: true, control: "never" },
139
+ "GET /devices/tunnels": { agent: true },
140
+ // Where a device dials back with a tunnel's one-time ticket; the ticket is the whole credential, checked by the route.
141
+ "GET /system/hosts/tunnel": { auth: "door", control: "never" },
127
142
  // An extension gateway's realtime-listener control: /state hands back its connectors' stored credentials, so only the
128
143
  // extension token of the extension declaring that `listener.provider` reaches them, checked in the handler too.
129
144
  "GET /listeners/{provider}/state": { floor: "maintainer", panel: false, control: "never" },
@@ -3,7 +3,7 @@ import { LandingDeliverySchema } from "./project-delivery.js";
3
3
  import { z } from "zod";
4
4
  import { AgentHarnessSchema, AgentOriginSchema, AgentProviderSchema, ConversationQueueSchema, ForkedFromSchema } from "./agent.js";
5
5
  import { LoopStateSchema } from "./loops.js";
6
- import { LimitPolicySchema, RetryPolicySchema, TurnBreakPolicySchema, TurnBreakSchema } from "./turn-break.js";
6
+ import { LimitPolicySchema, RetryPolicySchema, RoomPolicySchema, TurnBreakPolicySchema, TurnBreakSchema } from "./turn-break.js";
7
7
  import { KeepWarmSchema } from "./keep-warm.js";
8
8
  import { AgentNeedSchema } from "./needs.js";
9
9
  import { EMOJI_MAX_LENGTH, isSingleEmoji } from "../text/emoji.js";
@@ -362,6 +362,7 @@ export const AgentSummarySchema = z.object({
362
362
  limitPolicy: LimitPolicySchema.optional(),
363
363
  outagePolicy: RetryPolicySchema.optional(),
364
364
  stopPolicy: RetryPolicySchema.optional(),
365
+ memoryPolicy: RoomPolicySchema.optional(),
365
366
  // A collaborator's ask to land (collaborators can't merge themselves); cleared by whichever merge or discard
366
367
  // answers it.
367
368
  landRequested: z
@@ -907,9 +908,11 @@ export const AgentUnsentSchema = z.object({
907
908
  // verbs is how the surfaces drifted apart in the first place.
908
909
  export const AgentBreakPolicySchema = z.object({
909
910
  id: z.string().min(1).describe("Which conversation."),
910
- ending: TurnBreakSchema.describe("Which wall this answers for: a spent usage limit, a provider outage, or a turn that stopped short."),
911
+ ending: TurnBreakSchema.describe(
912
+ "Which wall this answers for: a spent usage limit, a provider outage, a turn that stopped short, or a message the sandbox held because it was short of memory.",
913
+ ),
911
914
  policy: TurnBreakPolicySchema.nullable().describe(
912
- "What happens next for that ending. `wait` holds the turn for a press; `retry` re-runs it on a bounded ladder (outage, stop); `resend` sends it again at the published reset and `move` also tries another account with room (limit only). An answer the ending does not allow is refused. Null clears the override and goes back to following the sandbox-wide policy, so a conversation does not sit holding a frozen copy of a default it has quietly stopped following.",
915
+ "What happens next for that ending. `wait` holds the turn for a press; `retry` re-runs it on a bounded ladder (outage, stop); `resend` sends it again at the published reset (limit) or once memory frees up (memory), and `move` also tries another account with room (limit only). An answer the ending does not allow is refused. Null clears the override and goes back to following the sandbox-wide policy, so a conversation does not sit holding a frozen copy of a default it has quietly stopped following.",
913
916
  ),
914
917
  });
915
918
  export const AgentFileDiffQuerySchema = z.object({
@@ -215,6 +215,10 @@ export const DeviceScopesSchema = z.object({
215
215
  // Its own switch under `shell`, default off while `shell` defaults on: a laptop has no disposable image to recreate
216
216
  // from.
217
217
  destructive: hostScope.default("off"),
218
+ // Its own switch, default off: receiving a program this sandbox built (`devices push`) and running it under the
219
+ // agent's supervision (app_start). Narrower than `shell` in what it reaches and wider in what it trusts: the program
220
+ // is the sandbox's, not one already on the machine.
221
+ programs: hostScope.default("off"),
218
222
  // One directory per line. Empty ⇒ the machine's home directory, which is what the agent reports at connect.
219
223
  roots: z.string().optional(),
220
224
  });
@@ -0,0 +1,99 @@
1
+ import { z } from "zod";
2
+
3
+ // A PROGRAM THIS SANDBOX BUILT, CARRIED TO A CONNECTED COMPUTER TO RUN THERE (`devices push`, then the device's app_start).
4
+ // The agent's CLI hands the daemon the bytes as the agent's own shell sees them; the daemon carries them over the device
5
+ // link in chunks through `stageArtifact`, and the machine files them under its own runs folder, one folder per content
6
+ // hash, so a second push of the same build is answered from what is already there and a running copy is never
7
+ // overwritten. Behind the device's "Run programs this sandbox sends" switch (`programs`), checked on the machine.
8
+
9
+ // One chunk's decoded size: small enough that a tunnel never sees a frame it would refuse, large enough that a 100 MB
10
+ // build is fifty calls.
11
+ export const ARTIFACT_CHUNK_BYTES = 2 * 1024 * 1024;
12
+ // The most one push carries, decoded. A build bigger than this is a disk image, not a program to try.
13
+ export const ARTIFACT_MAX_BYTES = 4 * 1024 * 1024 * 1024;
14
+
15
+ const Sha256Schema = z.string().regex(/^[0-9a-f]{64}$/, "a lowercase hex sha256");
16
+ // Names the folder a push lands in, so it is one plain path segment on every OS: no separators, no dots alone, nothing
17
+ // Windows reserves.
18
+ export const ArtifactNameSchema = z
19
+ .string()
20
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/, "letters, digits, dot, dash and underscore, starting with a letter or digit, at most 64");
21
+ // The file's own name as it was in the sandbox, kept so a Windows program still ends in `.exe`.
22
+ export const ArtifactFileNameSchema = z
23
+ .string()
24
+ .regex(/^[A-Za-z0-9 ._()+-]{1,200}$/, "a plain file name")
25
+ .refine((name) => name !== "." && name !== ".." && !name.endsWith(".") && !name.endsWith(" "), "a plain file name");
26
+ // `file` is one file, filed as it is; `tar` a folder the CLI packed, unpacked on the machine.
27
+ export const ArtifactKindSchema = z.enum(["file", "tar"]);
28
+ export type ArtifactKind = z.infer<typeof ArtifactKindSchema>;
29
+ const UploadIdSchema = z.string().regex(/^[0-9a-f]{32}$/);
30
+
31
+ export const StageArtifactSchema = z.discriminatedUnion("op", [
32
+ // Whether this exact build is already staged here: the answer carries its path when it is, and nothing is sent.
33
+ z.object({ op: z.literal("have"), name: ArtifactNameSchema, sha256: Sha256Schema, kind: ArtifactKindSchema, fileName: ArtifactFileNameSchema }).strict(),
34
+ // One piece, at its offset; pieces arrive in order and a gap is refused.
35
+ z
36
+ .object({
37
+ op: z.literal("chunk"),
38
+ upload: UploadIdSchema,
39
+ offset: z.int().nonnegative(),
40
+ data: z.string().max(Math.ceil(ARTIFACT_CHUNK_BYTES / 3) * 4),
41
+ })
42
+ .strict(),
43
+ // The last word: the machine checks size and hash, then files it. A mismatch throws the upload away.
44
+ z
45
+ .object({
46
+ op: z.literal("commit"),
47
+ upload: UploadIdSchema,
48
+ name: ArtifactNameSchema,
49
+ kind: ArtifactKindSchema,
50
+ fileName: ArtifactFileNameSchema,
51
+ size: z.int().nonnegative().max(ARTIFACT_MAX_BYTES),
52
+ sha256: Sha256Schema,
53
+ })
54
+ .strict(),
55
+ // A push the daemon gave up on; what arrived is deleted.
56
+ z.object({ op: z.literal("abort"), upload: UploadIdSchema }).strict(),
57
+ ]);
58
+ export type StageArtifact = z.infer<typeof StageArtifactSchema>;
59
+
60
+ export const StageArtifactResultSchema = z.object({
61
+ // Where the build is on the machine: the file itself, or the folder a `tar` was unpacked into. Absent for a `have`
62
+ // that found nothing, a chunk and an abort.
63
+ path: z.string().optional(),
64
+ // How many bytes of this upload the machine holds, answered to every chunk.
65
+ received: z.int().nonnegative().optional(),
66
+ });
67
+ export type StageArtifactResult = z.infer<typeof StageArtifactResultSchema>;
68
+
69
+ // What `POST /devices/{name}/artifacts` answers the CLI.
70
+ export const PushedArtifactSchema = z.object({
71
+ device: z.string(),
72
+ path: z.string(),
73
+ size: z.int().nonnegative(),
74
+ sha256: Sha256Schema,
75
+ // True when the machine already had this exact build and nothing was sent.
76
+ reused: z.boolean(),
77
+ });
78
+ export type PushedArtifact = z.infer<typeof PushedArtifactSchema>;
79
+
80
+ // A DEVICE'S LOOPBACK PORT, REACHED FROM THE SANDBOX (`devices reach`, hosts/device-tunnels.ts). The sandbox listens on
81
+ // its own 127.0.0.1:<localPort>; each connection asks the device to dial its own 127.0.0.1:<devicePort> and open a
82
+ // WebSocket back to /system/hosts/tunnel carrying the ticket below, and the two are pumped together.
83
+ export const DialLoopbackSchema = z
84
+ .object({
85
+ ticket: z.string().regex(/^[0-9a-f]{48}$/),
86
+ port: z.int().min(1).max(65535),
87
+ })
88
+ .strict();
89
+ export type DialLoopback = z.infer<typeof DialLoopbackSchema>;
90
+
91
+ export const DeviceTunnelSchema = z.object({
92
+ device: z.string(),
93
+ devicePort: z.int().min(1).max(65535),
94
+ localPort: z.int().min(1).max(65535),
95
+ // What a program in the sandbox opens to reach the device's port.
96
+ url: z.string(),
97
+ openedAt: z.string(),
98
+ });
99
+ export type DeviceTunnel = z.infer<typeof DeviceTunnelSchema>;
@@ -94,9 +94,11 @@ export type DeviceFacts = z.infer<typeof DeviceFactsSchema>;
94
94
  // - `background-prepare`: the `prepare-background` op, the next update downloaded under the unattended rules of the
95
95
  // agent's own timer (`ic sandbox prepare --auto`), which the update card sends when it opens; advertised only when
96
96
  // the agent's `ic sandbox prepare` takes `--auto`.
97
+ // - `programs`: the `stageArtifact` procedure and the app_* tools, a program this sandbox built pushed to the machine and
98
+ // run under the agent's supervision (device-artifacts.ts); an agent without it is told to update rather than sent one.
97
99
  // - `project-delivery`: the `deliverProject` procedure, landed work written into a folder attached to this computer's
98
100
  // sandbox (project-delivery.ts); an agent without it is never sent one, and the land waits for Bring back.
99
- export const DeviceFeatureSchema = z.enum(["reshape-later", "set-shape", "rollback-to", "loopback-catch", "background-prepare", "project-delivery"]);
101
+ export const DeviceFeatureSchema = z.enum(["reshape-later", "set-shape", "rollback-to", "loopback-catch", "background-prepare", "project-delivery", "programs"]);
100
102
  export type DeviceFeature = z.infer<typeof DeviceFeatureSchema>;
101
103
  export const DEVICE_FEATURE_RESHAPE_LATER: DeviceFeature = "reshape-later";
102
104
  export const DEVICE_FEATURE_SET_SHAPE: DeviceFeature = "set-shape";
@@ -104,6 +106,7 @@ export const DEVICE_FEATURE_ROLLBACK_TO: DeviceFeature = "rollback-to";
104
106
  export const DEVICE_FEATURE_LOOPBACK_CATCH: DeviceFeature = "loopback-catch";
105
107
  export const DEVICE_FEATURE_BACKGROUND_PREPARE: DeviceFeature = "background-prepare";
106
108
  export const DEVICE_FEATURE_PROJECT_DELIVERY: DeviceFeature = "project-delivery";
109
+ export const DEVICE_FEATURE_PROGRAMS: DeviceFeature = "programs";
107
110
  // The features an agent advertised that this build knows; an unknown one is a newer agent's and means nothing here.
108
111
  export const deviceFeatures = (facts: Pick<DeviceFacts, "features"> | undefined): DeviceFeature[] =>
109
112
  (facts?.features ?? []).flatMap((feature) => {
@@ -0,0 +1,93 @@
1
+ import { z } from "zod";
2
+ import { ArtifactNameSchema } from "./device-artifacts.js";
3
+
4
+ // WHAT A REPOSITORY SAYS IT CAN BE RUN AS, ON ONE OF THE OWNER'S COMPUTERS (`<repo>/.intentic/run.json`). Beside the
5
+ // build scripts it names and tracked with them, like `.intentic/checks.json`. One file serves two doors that must do the
6
+ // same thing: an agent's `devices run <target>` in its own shell, and the editor's "Run on <device>" button, which runs
7
+ // that very command in a terminal the owner watches. Each run builds in the sandbox, carries the build to the device
8
+ // (`devices push`), opens the device ports the target says the sandbox needs (`devices reach`), and starts the program
9
+ // there (the device's app_start). Nothing here runs by itself: a target runs when someone runs it.
10
+ //
11
+ // {
12
+ // "targets": [{
13
+ // "name": "desktop",
14
+ // "device": "rog",
15
+ // "build": "bash _tools/scripts/desktop/build-desktop.sh 0.0.0 --windows-only",
16
+ // "artifact": {
17
+ // "intentic-desktop.exe": "_editor/desktop-app/src-tauri/target/x86_64-pc-windows-msvc/release/intentic-desktop.exe",
18
+ // "intentic-files.exe": "_editor/desktop-app/src-tauri/binaries/intentic-files-x86_64-pc-windows-msvc.exe"
19
+ // },
20
+ // "program": "intentic-desktop.exe"
21
+ // }]
22
+ // }
23
+
24
+ export const RUN_TARGETS_FILE = ".intentic/run.json";
25
+
26
+ const RepoPathSchema = z
27
+ .string()
28
+ .min(1)
29
+ .max(500)
30
+ .refine((path) => !path.startsWith("/") && !path.split(/[\\/]/).includes(".."), "a path inside the repository, relative to its root");
31
+ const BundleNameSchema = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9 ._()+-]{0,199}(\/[A-Za-z0-9][A-Za-z0-9 ._()+-]{0,199})*$/, "a relative name, folders by /");
32
+
33
+ export const RunTargetSchema = z
34
+ .object({
35
+ // What `devices run <name>` and the button call it.
36
+ name: ArtifactNameSchema,
37
+ description: z.string().max(300).optional(),
38
+ // The computer it runs on unless one is named at the run; the device's name as its tools are mounted.
39
+ device: z.string().min(1).max(100).optional(),
40
+ // A shell command run from the repository's root, in the sandbox, before every run; absent means the artifact
41
+ // is already there.
42
+ build: z.string().min(1).max(4000).optional(),
43
+ // What is carried: one file or one folder, or a set of files and folders gathered into one folder under names of
44
+ // their own (an .exe beside the sidecars, DLLs and resource folders it looks for next to itself).
45
+ artifact: z.union([RepoPathSchema, z.record(BundleNameSchema, RepoPathSchema)]),
46
+ // Which file to start when the artifact is a folder or a set; a lone file is started as itself.
47
+ program: BundleNameSchema.optional(),
48
+ args: z.array(z.string().max(4000)).max(100).default([]),
49
+ env: z.record(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), z.string().max(4000)).default({}),
50
+ // Run it inside Windows Sandbox, a throwaway VM that sees the build read-only and nothing else of the machine.
51
+ isolated: z.boolean().default(false),
52
+ // The device's own loopback ports the sandbox needs to reach while it runs (its local API, a dev server), each
53
+ // opened at the same port in the sandbox.
54
+ reach: z.array(z.int().min(1).max(65535)).max(16).default([]),
55
+ })
56
+ .strict();
57
+ export type RunTarget = z.infer<typeof RunTargetSchema>;
58
+
59
+ export const RunTargetsFileSchema = z
60
+ .object({
61
+ targets: z
62
+ .array(RunTargetSchema)
63
+ .min(1)
64
+ .max(50)
65
+ .refine((targets) => new Set(targets.map((target) => target.name)).size === targets.length, "two targets share a name"),
66
+ })
67
+ .strict();
68
+
69
+ // One repository's file as the editor lists it: its targets, or what is wrong with the file.
70
+ export const RepoRunTargetsSchema = z.object({
71
+ repo: z.string(),
72
+ path: z.string(),
73
+ targets: z.array(RunTargetSchema),
74
+ error: z.string().optional(),
75
+ });
76
+
77
+ export const RunTargetsListSchema = z.object({
78
+ repos: z.array(RepoRunTargetsSchema),
79
+ // The owner's computers: whether each is connected, whether its agent is new enough to take a program, and whether
80
+ // its card's "Run programs this sandbox sends" switch is on. A run needs all three.
81
+ devices: z.array(z.object({ id: z.string(), online: z.boolean(), programs: z.boolean(), allowed: z.boolean() })),
82
+ });
83
+ export type RunTargetsList = z.infer<typeof RunTargetsListSchema>;
84
+
85
+ export const RunStartSchema = z.object({
86
+ repo: z.string().min(1),
87
+ target: ArtifactNameSchema,
88
+ device: z.string().min(1).max(100),
89
+ });
90
+ export type RunStart = z.infer<typeof RunStartSchema>;
91
+
92
+ // The terminal session the run is in, for the editor to open.
93
+ export const RunStartedSchema = z.object({ session: z.string() });
@@ -5,7 +5,7 @@ import { z } from "zod";
5
5
  import { CommandJudgeModeSchema, ProjectInstallModeSchema } from "../policy/safety-policy.js";
6
6
  import { ModelRoleSchema } from "../models/model-roles.js";
7
7
  import { AdmissionPolicySchema, AdmissionRuleSchema, ModelPinSchema } from "./agent.js";
8
- import { LimitPolicySchema, RetryPolicySchema } from "./turn-break.js";
8
+ import { LimitPolicySchema, RetryPolicySchema, RoomPolicySchema } from "./turn-break.js";
9
9
  import { ZoneSchema } from "../time/zone.js";
10
10
  // Which prompt base the agent runs before this turn composes anything on top: Intentic's own (default), Claude Code's
11
11
  // preset, or the owner's text. Declared out here since both the daemon and the browser branch on it.
@@ -452,6 +452,11 @@ export const SandboxSettingsSchema = z.object({
452
452
  stopPolicy: RetryPolicySchema.default("wait").describe(
453
453
  "What happens to a turn that stopped short with nothing to repair — a hung runtime, a crashed harness. `wait` holds it for a press. `retry` re-runs the held turn on a short ladder, standing down after three tries that got nowhere rather than looping forever.",
454
454
  ),
455
+ // The one ending that defaults to going by itself: nothing ran, so sending it once there is room spends nothing twice,
456
+ // and it is what the person meant when they pressed Send.
457
+ memoryPolicy: RoomPolicySchema.default("resend").describe(
458
+ "What happens to a message the sandbox held because it was short of memory: nothing ran, so nothing was spent. `resend` sends it by itself once memory has stayed free for a while, one held conversation at a time. `wait` holds it until somebody presses Send anyway. The sandbox-wide default; any one conversation can say otherwise.",
459
+ ),
455
460
  limitMoveCarryUnder: z
456
461
  .number()
457
462
  .int()
@@ -0,0 +1,96 @@
1
+ import { z } from "zod";
2
+
3
+ // Dictation on this sandbox: which local model hears a language, how far it is from ready, and the live wire the
4
+ // composer streams a spoken phrase over. The daemon's engine is src/speech; the composer's half is
5
+ // _editor/web/src/features/chat/composer/voice.
6
+
7
+ // The models a sandbox can hear with, by what they are for:
8
+ // - `parakeet` (NVIDIA Parakeet TDT 0.6B v3): 25 European languages, punctuated, ~30x faster than real time on two
9
+ // cores. Baked into the standard image, and the default for any language it covers.
10
+ // - `whisper` (OpenAI Whisper large-v3-turbo): the other ~75 languages, slower, fetched the first time one is asked for.
11
+ export const SpeechEngineSchema = z.enum(["parakeet", "whisper"]);
12
+ export type SpeechEngine = z.infer<typeof SpeechEngineSchema>;
13
+
14
+ // The 25 Parakeet hears, as ISO 639-1 primary subtags: it detects which on its own, so a browser in English and a
15
+ // speaker in Polish still land on it.
16
+ export const PARAKEET_LANGUAGES: ReadonlySet<string> = new Set([
17
+ "bg",
18
+ "cs",
19
+ "da",
20
+ "de",
21
+ "el",
22
+ "en",
23
+ "es",
24
+ "et",
25
+ "fi",
26
+ "fr",
27
+ "hr",
28
+ "hu",
29
+ "it",
30
+ "lt",
31
+ "lv",
32
+ "mt",
33
+ "nl",
34
+ "pl",
35
+ "pt",
36
+ "ro",
37
+ "ru",
38
+ "sk",
39
+ "sl",
40
+ "sv",
41
+ "uk",
42
+ ]);
43
+
44
+ // A locale's primary subtag, lowercased, or undefined for anything that is not one (`""`, `*`, `auto`).
45
+ export const speechLanguage = (locale: string | undefined): string | undefined => {
46
+ const primary = (locale ?? "").trim().toLowerCase().split(/[-_]/u)[0] ?? "";
47
+ return /^[a-z]{2,3}$/u.test(primary) && primary !== "auto" ? primary : undefined;
48
+ };
49
+
50
+ // Which model hears a locale: Parakeet for its own languages and for anything unreadable, Whisper for the rest.
51
+ export const speechEngineFor = (locale: string | undefined): SpeechEngine => {
52
+ const language = speechLanguage(locale);
53
+ return language === undefined || PARAKEET_LANGUAGES.has(language) ? "parakeet" : "whisper";
54
+ };
55
+
56
+ export const SpeechStatusSchema = z.object({
57
+ provisioned: z.boolean().describe("This sandbox can run speech recognition at all."),
58
+ model: z
59
+ .enum(["absent", "downloading", "ready", "failed"])
60
+ .describe("Whether the model for the asked language is on disk: baked into the image, fetched, being fetched, or its fetch failed."),
61
+ engine: SpeechEngineSchema.optional().describe("Which model hears the asked language."),
62
+ loaded: z.boolean().optional().describe("The model is in memory, so the next phrase is heard without a load first."),
63
+ received: z.number().optional().describe("Bytes of the model fetched so far, while it downloads."),
64
+ total: z.number().optional().describe("The model's whole size in bytes, while it downloads."),
65
+ error: z.string().optional().describe("Why the last fetch failed, when it did."),
66
+ });
67
+ export type SpeechStatus = z.infer<typeof SpeechStatusSchema>;
68
+
69
+ // GET /speech/stream, a WebSocket: the composer cuts the microphone into phrases itself (its silence segmenter) and
70
+ // streams each one as it is spoken. A text frame `begin` opens phrase `id`; binary frames that follow are its audio,
71
+ // 16 kHz mono s16le; `end` closes it, keeping its first `keep` samples (the trailing pause trimmed off); `drop`
72
+ // abandons it. The daemon answers with `partial` text while the phrase is still being spoken, one `final` per ended
73
+ // phrase, in order, and `status` whenever the model's readiness moves. The language is the socket's `lang` query.
74
+ export const SpeechClientMessageSchema = z.discriminatedUnion("type", [
75
+ z.object({ type: z.literal("begin"), id: z.number() }),
76
+ z.object({ type: z.literal("end"), id: z.number(), keep: z.number() }),
77
+ z.object({ type: z.literal("drop"), id: z.number() }),
78
+ ]);
79
+ export type SpeechClientMessage = z.infer<typeof SpeechClientMessageSchema>;
80
+
81
+ export const SpeechServerMessageSchema = z.discriminatedUnion("type", [
82
+ z.object({ type: z.literal("status"), status: SpeechStatusSchema }),
83
+ z.object({ type: z.literal("partial"), id: z.number(), text: z.string() }),
84
+ z.object({
85
+ type: z.literal("final"),
86
+ id: z.number(),
87
+ text: z.string().describe("Empty when the phrase held no words."),
88
+ decodeMs: z.number().describe("How long hearing it took once its audio was in."),
89
+ }),
90
+ z.object({ type: z.literal("error"), id: z.number().optional(), message: z.string() }),
91
+ ]);
92
+ export type SpeechServerMessage = z.infer<typeof SpeechServerMessageSchema>;
93
+
94
+ // The longest phrase either door takes: a minute at 16 kHz. The composer's segmenter cuts a monologue at the same
95
+ // length, so nothing it sends is ever refused.
96
+ export const MAX_SPEECH_SAMPLES = 60 * 16_000;
@@ -140,6 +140,10 @@ export const BrowsersListSchema = z.object({
140
140
  });
141
141
  export type BrowsersList = z.infer<typeof BrowsersListSchema>;
142
142
  export const BrowserNameParamSchema = z.object({ name: z.string().describe("Which browser.") });
143
+ export const BrowserPageParamSchema = z.object({
144
+ name: z.string().describe("Which browser."),
145
+ pageId: z.string().describe("Which of its tabs: a page id from the roster."),
146
+ });
143
147
  export const OpenBrowserInputSchema = z.object({
144
148
  url: z.string().optional().describe("Where the new tab goes. Absent opens a blank tab, the way a browser's own new tab does."),
145
149
  });
@@ -7,20 +7,23 @@
7
7
 
8
8
  import { z } from "zod";
9
9
 
10
- // The three endings that leave finished work behind a live session and can be picked back up. Spelled exactly as
11
- // `TurnEndingSchema.reason` spells them, so the ending a surface reads and the question it asks are one word. A restart
12
- // is handled apart (SandboxSettings.autoResumeOnRestart): nobody is watching a restart, so it has no in-chat question.
13
- export const TurnBreakSchema = z.enum(["limit", "outage", "stopped"]);
10
+ // The walls a turn can stop at that take a standing answer. The first three end a turn that leaves finished work behind
11
+ // a live session, spelled exactly as `TurnEndingSchema.reason` spells them, so the ending a surface reads and the
12
+ // question it asks are one word. `memory` is the door: the sandbox too short of memory to start the turn at all, which
13
+ // holds the message (or the turn the sandbox kept) until a press or room comes back. A restart is handled apart
14
+ // (SandboxSettings.autoResumeOnRestart): nobody is watching a restart, so it has no in-chat question.
15
+ export const TurnBreakSchema = z.enum(["limit", "outage", "stopped", "memory"]);
14
16
  export type TurnBreak = z.infer<typeof TurnBreakSchema>;
15
17
 
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"]);
18
+ // Every ending that leaves a held turn behind: a spent allowance, an outage, a turn that stopped short, plus the
19
+ // provider's safety classifier stopping a turn partway. That one has no standing answer and never re-runs by itself: the
20
+ // person picks, each time, between the same model again and another one (2026-09-30, the owner's call over an automatic
21
+ // model switch). A low-memory hold is no ending: nothing ran, so it is never one of these.
22
+ export const HeldEndingSchema = z.enum(["limit", "outage", "stopped", "flagged"]);
20
23
  export type HeldEnding = z.infer<typeof HeldEndingSchema>;
21
24
 
22
25
  /** 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";
26
+ export const isTurnBreak = (ending: HeldEnding): ending is Exclude<HeldEnding, "flagged"> => ending !== "flagged";
24
27
 
25
28
  // `move` implies `resend`: an account with room is tried at once, and the reset appointment stands as its fallback.
26
29
  // There is deliberately no "move, else hold" — a reader willing to spend a second account is willing to wait.
@@ -31,6 +34,11 @@ export type LimitPolicy = z.infer<typeof LimitPolicySchema>;
31
34
  export const RetryPolicySchema = z.enum(["wait", "retry"]);
32
35
  export type RetryPolicy = z.infer<typeof RetryPolicySchema>;
33
36
 
37
+ // The memory wall's two answers: hold for a press, or send once the sandbox has room again. Nothing ran, so sending it
38
+ // then spends nothing twice.
39
+ export const RoomPolicySchema = z.enum(["wait", "resend"]);
40
+ export type RoomPolicy = z.infer<typeof RoomPolicySchema>;
41
+
34
42
  // One answer, whichever ending asked. Read against the ending: only `limit` can be `move`.
35
43
  export const TurnBreakPolicySchema = z.enum(["wait", "resend", "move", "retry"]);
36
44
  export type TurnBreakPolicy = z.infer<typeof TurnBreakPolicySchema>;
@@ -40,6 +48,7 @@ export const TURN_BREAK_POLICIES: Readonly<Record<TurnBreak, readonly TurnBreakP
40
48
  limit: ["wait", "resend", "move"],
41
49
  outage: ["wait", "retry"],
42
50
  stopped: ["wait", "retry"],
51
+ memory: ["wait", "resend"],
43
52
  };
44
53
 
45
54
  /** Whether this ending may be answered this way; the one gate every writer goes through. */