@intentic/sandbox-contract 1.245.0 → 1.246.1

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 (205) hide show
  1. package/README.md +17 -1
  2. package/dist/command-classes.d.ts +6 -3
  3. package/dist/command-classes.d.ts.map +1 -1
  4. package/dist/command-classes.js +43 -18
  5. package/dist/command-classes.js.map +1 -1
  6. package/dist/contracts/{cursor.contract.d.ts → accounts.contract.d.ts} +102 -3
  7. package/dist/contracts/accounts.contract.d.ts.map +1 -0
  8. package/dist/contracts/accounts.contract.js +61 -0
  9. package/dist/contracts/accounts.contract.js.map +1 -0
  10. package/dist/contracts/agents.contract.d.ts +121 -0
  11. package/dist/contracts/agents.contract.d.ts.map +1 -1
  12. package/dist/contracts/agents.contract.js +4 -4
  13. package/dist/contracts/agents.contract.js.map +1 -1
  14. package/dist/contracts/ci.contract.d.ts +2 -0
  15. package/dist/contracts/ci.contract.d.ts.map +1 -1
  16. package/dist/contracts/host.contract.d.ts +35 -0
  17. package/dist/contracts/host.contract.d.ts.map +1 -1
  18. package/dist/contracts/host.contract.js +3 -2
  19. package/dist/contracts/host.contract.js.map +1 -1
  20. package/dist/contracts/personas.contract.d.ts +4 -2
  21. package/dist/contracts/personas.contract.d.ts.map +1 -1
  22. package/dist/contracts/runner.contract.d.ts +2 -2
  23. package/dist/contracts/settings.contract.d.ts +2 -51
  24. package/dist/contracts/settings.contract.d.ts.map +1 -1
  25. package/dist/contracts/system.contract.d.ts +80 -30
  26. package/dist/contracts/system.contract.d.ts.map +1 -1
  27. package/dist/contracts/system.contract.js +26 -17
  28. package/dist/contracts/system.contract.js.map +1 -1
  29. package/dist/definition.d.ts +4 -8
  30. package/dist/definition.d.ts.map +1 -1
  31. package/dist/documents.d.ts +0 -1
  32. package/dist/documents.d.ts.map +1 -1
  33. package/dist/documents.js +1 -2
  34. package/dist/documents.js.map +1 -1
  35. package/dist/embed.d.ts +23 -0
  36. package/dist/embed.d.ts.map +1 -0
  37. package/dist/embed.js +84 -0
  38. package/dist/embed.js.map +1 -0
  39. package/dist/events.d.ts +21 -0
  40. package/dist/events.d.ts.map +1 -1
  41. package/dist/events.js +5 -2
  42. package/dist/events.js.map +1 -1
  43. package/dist/history-state.d.ts.map +1 -1
  44. package/dist/history-state.js +2 -0
  45. package/dist/history-state.js.map +1 -1
  46. package/dist/index.d.ts +354 -335
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +5 -13
  49. package/dist/index.js.map +1 -1
  50. package/dist/peer-dial.d.ts +33 -0
  51. package/dist/peer-dial.d.ts.map +1 -0
  52. package/dist/peer-dial.js +79 -0
  53. package/dist/peer-dial.js.map +1 -0
  54. package/dist/peer-mcp-server.d.ts +36 -0
  55. package/dist/peer-mcp-server.d.ts.map +1 -0
  56. package/dist/peer-mcp-server.js +71 -0
  57. package/dist/peer-mcp-server.js.map +1 -0
  58. package/dist/provider-specs.d.ts +38 -20
  59. package/dist/provider-specs.d.ts.map +1 -1
  60. package/dist/provider-specs.js +39 -13
  61. package/dist/provider-specs.js.map +1 -1
  62. package/dist/quick-model.d.ts.map +1 -1
  63. package/dist/quick-model.js +2 -1
  64. package/dist/quick-model.js.map +1 -1
  65. package/dist/runtime-state.d.ts +1 -1
  66. package/dist/runtime-state.js +1 -1
  67. package/dist/runtime-state.js.map +1 -1
  68. package/dist/safety-policy.d.ts +12 -3
  69. package/dist/safety-policy.d.ts.map +1 -1
  70. package/dist/safety-policy.js +30 -5
  71. package/dist/safety-policy.js.map +1 -1
  72. package/dist/schemas/agent.d.ts +6 -6
  73. package/dist/schemas/agent.d.ts.map +1 -1
  74. package/dist/schemas/agent.js +6 -2
  75. package/dist/schemas/agent.js.map +1 -1
  76. package/dist/schemas/agents.d.ts +42 -0
  77. package/dist/schemas/agents.d.ts.map +1 -1
  78. package/dist/schemas/agents.js +25 -4
  79. package/dist/schemas/agents.js.map +1 -1
  80. package/dist/schemas/automations.d.ts +11 -2
  81. package/dist/schemas/automations.d.ts.map +1 -1
  82. package/dist/schemas/automations.js +1 -1
  83. package/dist/schemas/automations.js.map +1 -1
  84. package/dist/schemas/ci.d.ts +6 -0
  85. package/dist/schemas/ci.d.ts.map +1 -1
  86. package/dist/schemas/ci.js +3 -2
  87. package/dist/schemas/ci.js.map +1 -1
  88. package/dist/schemas/context.d.ts +30 -0
  89. package/dist/schemas/context.d.ts.map +1 -0
  90. package/dist/schemas/context.js +34 -0
  91. package/dist/schemas/context.js.map +1 -0
  92. package/dist/schemas/{computers.d.ts → devices.d.ts} +154 -60
  93. package/dist/schemas/devices.d.ts.map +1 -0
  94. package/dist/schemas/devices.js +157 -0
  95. package/dist/schemas/devices.js.map +1 -0
  96. package/dist/schemas/hosts.d.ts +12 -0
  97. package/dist/schemas/hosts.d.ts.map +1 -1
  98. package/dist/schemas/hosts.js +1 -0
  99. package/dist/schemas/hosts.js.map +1 -1
  100. package/dist/schemas/issues.d.ts +0 -5
  101. package/dist/schemas/issues.d.ts.map +1 -1
  102. package/dist/schemas/issues.js +0 -1
  103. package/dist/schemas/issues.js.map +1 -1
  104. package/dist/schemas/personas.d.ts +5 -3
  105. package/dist/schemas/personas.d.ts.map +1 -1
  106. package/dist/schemas/personas.js +3 -2
  107. package/dist/schemas/personas.js.map +1 -1
  108. package/dist/schemas/provider-oauth.d.ts +48 -16
  109. package/dist/schemas/provider-oauth.d.ts.map +1 -1
  110. package/dist/schemas/provider-oauth.js +22 -20
  111. package/dist/schemas/provider-oauth.js.map +1 -1
  112. package/dist/schemas/settings.d.ts +1 -52
  113. package/dist/schemas/settings.d.ts.map +1 -1
  114. package/dist/schemas/settings.js +8 -11
  115. package/dist/schemas/settings.js.map +1 -1
  116. package/dist/schemas/terminal.js +9 -9
  117. package/dist/schemas/terminal.js.map +1 -1
  118. package/dist/schemas/usage.d.ts +0 -2
  119. package/dist/schemas/usage.d.ts.map +1 -1
  120. package/dist/schemas/usage.js +0 -2
  121. package/dist/schemas/usage.js.map +1 -1
  122. package/dist/shell-regions.d.ts +4 -0
  123. package/dist/shell-regions.d.ts.map +1 -0
  124. package/dist/shell-regions.js +156 -0
  125. package/dist/shell-regions.js.map +1 -0
  126. package/dist/workspace-state.d.ts +8 -0
  127. package/dist/workspace-state.d.ts.map +1 -1
  128. package/dist/workspace-state.js +13 -5
  129. package/dist/workspace-state.js.map +1 -1
  130. package/package.json +37 -4
  131. package/src/agent-catalog.ts +1 -1
  132. package/src/arrival.ts +3 -3
  133. package/src/command-classes.test.ts +195 -71
  134. package/src/command-classes.ts +148 -46
  135. package/src/contracts/accounts.contract.ts +94 -0
  136. package/src/contracts/agents.contract.ts +4 -3
  137. package/src/contracts/exit.contract.ts +2 -2
  138. package/src/contracts/host.contract.ts +17 -5
  139. package/src/contracts/settings.contract.ts +1 -1
  140. package/src/contracts/system.contract.ts +43 -24
  141. package/src/contracts/vpn.contract.ts +2 -2
  142. package/src/documents.test.ts +2 -1
  143. package/src/documents.ts +7 -11
  144. package/src/embed.test.ts +68 -0
  145. package/src/embed.ts +164 -0
  146. package/src/events.ts +30 -3
  147. package/src/history-state.ts +12 -3
  148. package/src/host-protocol.ts +2 -2
  149. package/src/index.ts +6 -14
  150. package/src/model-order.ts +1 -1
  151. package/src/peer-dial.test.ts +203 -0
  152. package/src/peer-dial.ts +163 -0
  153. package/src/peer-mcp-server.test.ts +104 -0
  154. package/src/peer-mcp-server.ts +144 -0
  155. package/src/provider-specs.test.ts +44 -17
  156. package/src/provider-specs.ts +147 -67
  157. package/src/quick-model.ts +10 -3
  158. package/src/routes.test.ts +6 -3
  159. package/src/runner-protocol.ts +1 -1
  160. package/src/runtime-state.ts +2 -2
  161. package/src/safety-policy.test.ts +88 -0
  162. package/src/safety-policy.ts +84 -14
  163. package/src/schemas/agent.ts +41 -13
  164. package/src/schemas/agents.ts +65 -4
  165. package/src/schemas/automations.ts +6 -4
  166. package/src/schemas/capabilities.ts +4 -4
  167. package/src/schemas/ci.ts +23 -6
  168. package/src/schemas/context.ts +87 -0
  169. package/src/schemas/{computers.ts → devices.ts} +190 -107
  170. package/src/schemas/hosts.ts +5 -1
  171. package/src/schemas/issues.ts +0 -4
  172. package/src/schemas/personas.ts +8 -3
  173. package/src/schemas/provider-oauth.ts +49 -52
  174. package/src/schemas/settings.ts +30 -54
  175. package/src/schemas/terminal.ts +12 -12
  176. package/src/schemas/usage.ts +3 -27
  177. package/src/schemas/version-seam.test.ts +0 -1
  178. package/src/shell-regions.ts +289 -0
  179. package/src/versions.ts +2 -2
  180. package/src/webext-links.ts +2 -2
  181. package/src/webext-protocol.ts +2 -2
  182. package/src/workspace-state.test.ts +48 -1
  183. package/src/workspace-state.ts +48 -11
  184. package/dist/contracts/claude.contract.d.ts +0 -91
  185. package/dist/contracts/claude.contract.d.ts.map +0 -1
  186. package/dist/contracts/claude.contract.js +0 -50
  187. package/dist/contracts/claude.contract.js.map +0 -1
  188. package/dist/contracts/cursor.contract.d.ts.map +0 -1
  189. package/dist/contracts/cursor.contract.js +0 -50
  190. package/dist/contracts/cursor.contract.js.map +0 -1
  191. package/dist/contracts/grok.contract.d.ts +0 -36
  192. package/dist/contracts/grok.contract.d.ts.map +0 -1
  193. package/dist/contracts/grok.contract.js +0 -31
  194. package/dist/contracts/grok.contract.js.map +0 -1
  195. package/dist/contracts/keys.contract.d.ts +0 -81
  196. package/dist/contracts/keys.contract.d.ts.map +0 -1
  197. package/dist/contracts/keys.contract.js +0 -51
  198. package/dist/contracts/keys.contract.js.map +0 -1
  199. package/dist/schemas/computers.d.ts.map +0 -1
  200. package/dist/schemas/computers.js +0 -135
  201. package/dist/schemas/computers.js.map +0 -1
  202. package/src/contracts/claude.contract.ts +0 -71
  203. package/src/contracts/cursor.contract.ts +0 -74
  204. package/src/contracts/grok.contract.ts +0 -41
  205. package/src/contracts/keys.contract.ts +0 -79
@@ -1,4 +1,4 @@
1
- // computers: what ONE of the user's own machines is running
1
+ // devices: what ONE of the user's own machines is running
2
2
  import { z } from "zod";
3
3
  import { HostFactsSchema } from "./hosts.js";
4
4
  // The one sentinel every non-release build carries, so a locally compiled agent is never told it is behind.
@@ -26,8 +26,47 @@ import { DEV_VERSION } from "../versions.js";
26
26
  * `mirror` enrollment, a collaborator's own laptop, drops `localDir` with it. So a member who mirrors one
27
27
  * dev-server port does not hand the sandbox's owner a map of their machine. */
28
28
 
29
+ /* ONE SANDBOX'S SHARE OF ITS MACHINE, as docker enforces it right now: read off the container's HostConfig
30
+ * and env by the machine agent, never asked of the sandbox (which cannot see its own cgroup ceiling as a
31
+ * docker flag, only as a number in /sys/fs/cgroup).
32
+ *
33
+ * The two `*Runtime` lists are the same directive vocabulary the run contract allowlists
34
+ * (@intentic/sandbox-run RUNTIME_DIRECTIVES), split by WHO asked: `overlayRuntime` is what the approved
35
+ * environment demands (a view draws those locked — "required by the Docker capability"), `hostRuntime` is
36
+ * what the owner asked for on top and may withdraw. `privileged` and `gpu` are the docker-side truth of the
37
+ * union: what the container actually got, after a host without the nvidia runtime dropped the GPU. */
38
+ export const SandboxResourcesSchema = z.object({
39
+ // The cgroup memory ceiling in bytes; absent when docker imposes none (the hosted shape).
40
+ memoryBytes: z.number().optional(),
41
+ // The CFS quota as whole cores; absent when the container may use every core (the default).
42
+ cpus: z.number().optional(),
43
+ privileged: z.boolean(),
44
+ gpu: z.boolean(),
45
+ hostRuntime: z.array(z.string()),
46
+ overlayRuntime: z.array(z.string()),
47
+ });
48
+ export type SandboxResources = z.infer<typeof SandboxResourcesSchema>;
49
+
50
+ /* WHAT A RESHAPE ASKS FOR: the Resources dialog's answer, the `reshape` op's payload, and the shape the
51
+ * machine turns into `ic sandbox reshape` flags. Every key is "leave it" when absent; the two caps take
52
+ * `null` for "back to the default" (the machine-derived memory share; every core), which `ic` spells as
53
+ * `default` and the run contract as an empty seed. At least one key, because a reshape with nothing to change
54
+ * is a restart for nothing, and the machine refuses it before anything is spawned. */
55
+ export const SandboxResourcesAskFieldsSchema = z.object({
56
+ memoryGib: z.int().positive().nullable().optional(),
57
+ cpus: z.int().positive().nullable().optional(),
58
+ privileged: z.boolean().optional(),
59
+ gpu: z.boolean().optional(),
60
+ });
61
+ // The fields alone are exported too, for a caller that composes them into a wider object (the machine's MCP
62
+ // tool adds the slug beside them) and applies the at-least-one rule itself.
63
+ export const SandboxResourcesAskSchema = SandboxResourcesAskFieldsSchema.refine((ask) => Object.values(ask).some((value) => value !== undefined), {
64
+ message: "a reshape must change at least one thing",
65
+ });
66
+ export type SandboxResourcesAsk = z.infer<typeof SandboxResourcesAskSchema>;
67
+
29
68
  // One sandbox container on the machine, the docker half, filled in by the reader, never by the sync agent.
30
- export const MachineSandboxSchema = z.object({
69
+ export const DeviceSandboxSchema = z.object({
31
70
  slug: z.string(),
32
71
  container: z.string(),
33
72
  // The display name, when the machine has one recorded. Docker knows only the container name.
@@ -37,9 +76,12 @@ export const MachineSandboxSchema = z.object({
37
76
  // Absent when the sandbox has no cloudflared sidecar AT ALL (reached over the user's own proxy), which is
38
77
  // not the same fact as a sidecar that is down, and must not render as one.
39
78
  tunnelRunning: z.boolean().optional(),
79
+ // Its share of the machine (above). Absent from a reader that did not inspect the container: the cheap
80
+ // `docker ps` listing the machine's own fleet reads use carries none of it.
81
+ resources: SandboxResourcesSchema.optional(),
40
82
  });
41
- export type MachineSandbox = z.infer<typeof MachineSandboxSchema>;
42
- /* ONE OPERATION ON ONE SANDBOX ON ONE MACHINE, the Computers view's buttons, and the only thing that changes a
83
+ export type DeviceSandbox = z.infer<typeof DeviceSandboxSchema>;
84
+ /* ONE OPERATION ON ONE SANDBOX ON ONE MACHINE, the Devices view's buttons, and the only thing that changes a
43
85
  * machine's fleet from a browser.
44
86
  *
45
87
  * All nine ops travel one route because they are one decision to the person clicking, however differently they
@@ -56,6 +98,11 @@ export type MachineSandbox = z.infer<typeof MachineSandboxSchema>;
56
98
  * same row as the others, on a container that may be too broken to answer any other way, and the stream shape
57
99
  * already carries "many lines, then an outcome" exactly as a log tail wants to arrive.
58
100
  *
101
+ * `reshape` is the one that changes the CONTAINER without changing its image: its share of the machine (memory
102
+ * and CPU caps) and its privileges (privileged, the host's GPU), recreated onto the same image so the values
103
+ * live on the container and outlive every later swap. It carries `resources`, the only op with a payload of
104
+ * its own besides `rebuild`'s hash.
105
+ *
59
106
  * The machine enforces which of them it will do: `sandboxes` covers everything but removal, which takes its own
60
107
  * switch, and a refusal comes back as the machine's own sentence naming the control to flip. */
61
108
  /* `runner-up` / `runner-remove` are the same door for a container that belongs to THIS SANDBOX rather than to
@@ -64,7 +111,7 @@ export type MachineSandbox = z.infer<typeof MachineSandboxSchema>;
64
111
  * clicking they are the same row of buttons. Both take the `sandboxes` switch and neither takes the removal
65
112
  * one: a runner holds no workspace of its own, only a mirror of the parent's git, so removing it destroys
66
113
  * nothing the parent does not still have. */
67
- export const MachineSandboxOpSchema = z.enum([
114
+ export const DeviceSandboxOpSchema = z.enum([
68
115
  "start",
69
116
  "stop",
70
117
  "restart",
@@ -72,20 +119,23 @@ export const MachineSandboxOpSchema = z.enum([
72
119
  "update",
73
120
  "rebuild",
74
121
  "rollback",
122
+ "reshape",
75
123
  "remove",
76
124
  "logs",
77
125
  "runner-up",
78
126
  "runner-remove",
79
127
  ]);
80
- export type MachineSandboxOp = z.infer<typeof MachineSandboxOpSchema>;
81
- export const MachineSandboxFlowSchema = z.object({
82
- op: MachineSandboxOpSchema,
128
+ export type DeviceSandboxOp = z.infer<typeof DeviceSandboxOpSchema>;
129
+ export const DeviceSandboxFlowSchema = z.object({
130
+ op: DeviceSandboxOpSchema,
83
131
  // Which sandbox, or, for the two runner ops, which RUNNER: the name it is known by at both ends, the
84
132
  // parent's `/system/runners` list and the machine's `ic runner list`.
85
133
  slug: z.string().min(1),
86
134
  // The approved overlay's sha256, required by `rebuild` and meaningless to the rest. It is the trust anchor:
87
135
  // only content that still hashes to what the owner reviewed is ever built.
88
136
  hash: z.string().optional(),
137
+ // What `reshape` should change, required by it and meaningless to the rest.
138
+ resources: SandboxResourcesAskSchema.optional(),
89
139
  /* `runner-up` only, and both are filled in by the DAEMON, never by the caller: where the runner dials
90
140
  * (this sandbox's public URL) and the single-use pairing it redeems there. The browser asks for a runner
91
141
  * on a machine; it never holds the credential that makes one, which is what keeps a pairing out of every
@@ -105,29 +155,53 @@ export const MachineSandboxFlowSchema = z.object({
105
155
  overlay: z.string().optional(),
106
156
  overlayHash: z.string().optional(),
107
157
  });
108
- export type MachineSandboxFlow = z.infer<typeof MachineSandboxFlowSchema>;
158
+ export type DeviceSandboxFlow = z.infer<typeof DeviceSandboxFlowSchema>;
109
159
  // The same input plus which machine it is for, the browser's half, since the daemon reaches the machine by id.
110
- export const MachineSandboxFlowInputSchema = MachineSandboxFlowSchema.extend({ id: z.string().min(1) });
111
- export type MachineSandboxFlowInput = z.infer<typeof MachineSandboxFlowInputSchema>;
160
+ export const DeviceSandboxFlowInputSchema = DeviceSandboxFlowSchema.extend({ id: z.string().min(1) });
161
+ export type DeviceSandboxFlowInput = z.infer<typeof DeviceSandboxFlowInputSchema>;
112
162
  /* What a running operation says, in the one line shape every streamed flow in this product already uses
113
163
  * (IntenticLineSchema, which the browser's reader parses): `line` as the machine prints it, then exactly one
114
164
  * terminal frame, `result` when it worked, `error` when it did not, carrying the machine's own words either
115
165
  * way rather than a code this side invented. */
116
- export const MachineFlowLineSchema = z.discriminatedUnion("kind", [
166
+ export const DeviceFlowLineSchema = z.discriminatedUnion("kind", [
117
167
  z.object({ kind: z.literal("line"), text: z.string() }),
118
168
  z.object({ kind: z.literal("result"), message: z.string() }),
119
169
  z.object({ kind: z.literal("error"), message: z.string() }),
120
170
  ]);
121
- export type MachineFlowLine = z.infer<typeof MachineFlowLineSchema>;
122
- /* RUNNING ONE OF THIS PRODUCT'S OWN CLIs ON A CONNECTED COMPUTER, FROM A BUTTON, with no agent in the loop.
171
+ export type DeviceFlowLine = z.infer<typeof DeviceFlowLineSchema>;
172
+ /* UPDATING OR RESTARTING THE AGENT ITSELF, from the row that reports its version.
173
+ *
174
+ * THE OPERATION THAT KILLS ITS OWN TRANSPORT, and that is the whole reason it is a flow of its own rather than
175
+ * another entry in the DeviceCommand set below. `run_command` runs as a child of the resident process, and both
176
+ * of these stop that process: `upgrade` calls stopResident() before it swaps the binary, and `run` stops before
177
+ * it starts. So the socket carrying the request dies mid-command, and the child's stdout pipe dies with it —
178
+ * an EPIPE on `process.stdout` is an uncaught exception in Node. An upgrade killed between
179
+ * `swap(agentPath, previous)` and `swap(staged, agentPath)` leaves a device with NO `intentic-machine` binary
180
+ * and a login entry pointing at a missing file, which is the one outcome worse than being out of date.
123
181
  *
124
- * A machine that is connected as a computer can already be told things: the ops above drive its docker, and an
182
+ * The agent therefore runs the work DETACHED (spawnDetached, exactly as it launches its own loop) and streams
183
+ * its log while it can. The stream ending with no terminal frame is the NORMAL case here, not a failure, and
184
+ * confirmation is the version moving on the view's next poll — which is also the only honest confirmation: the
185
+ * CLI's own `loop-behind` outcome exists because "a process came up" does not mean the new build is serving.
186
+ *
187
+ * `restart` needs no new CLI verb: bare `intentic-machine run` is a restart (reconcileResidency stops before it
188
+ * starts), and it is the remedy for both a stalled loop and one serving an older build than the file beside it. */
189
+ export const DeviceAgentOpSchema = z.enum(["upgrade", "restart"]);
190
+ export type DeviceAgentOp = z.infer<typeof DeviceAgentOpSchema>;
191
+ export const DeviceAgentFlowSchema = z.object({ op: DeviceAgentOpSchema });
192
+ export type DeviceAgentFlow = z.infer<typeof DeviceAgentFlowSchema>;
193
+ // The same input plus which device it is for, the browser's half.
194
+ export const DeviceAgentFlowInputSchema = DeviceAgentFlowSchema.extend({ id: z.string().min(1) });
195
+ export type DeviceAgentFlowInput = z.infer<typeof DeviceAgentFlowInputSchema>;
196
+ /* RUNNING ONE OF THIS PRODUCT'S OWN CLIs ON A CONNECTED DEVICE, FROM A BUTTON, with no agent in the loop.
197
+ *
198
+ * A machine that is connected as a device can already be told things: the ops above drive its docker, and an
125
199
  * agent with the `host` capability can run whatever it likes through `run_command`. What had no door was the
126
200
  * ordinary case in between — the user wants the thing the CLI on their machine already does, and the sandbox is
127
201
  * where they are looking. Their alternatives were to go and find a terminal, or to ask an agent to do it, which
128
202
  * spends a turn and a model's judgement on a decision that has none in it.
129
203
  *
130
- * So: a CLOSED SET OF NAMES, and the argv is built on the daemon from the name alone (hosts/machine-commands.ts).
204
+ * So: a CLOSED SET OF NAMES, and the argv is built on the daemon from the name alone (hosts/device-commands.ts).
131
205
  * The browser sends `mirror-off`, never a command line. That is the whole security property, and it is the
132
206
  * reason this is an enum rather than a string: the same socket carries `run_command`, so a route that forwarded
133
207
  * caller-supplied text would hand every browser session a shell on the user's laptop, which is a grant the
@@ -137,7 +211,7 @@ export type MachineFlowLine = z.infer<typeof MachineFlowLineSchema>;
137
211
  * own words, naming the control to flip — exactly as it does for the sandbox ops. */
138
212
  /* THE SET, and why the file-sync half of it is here beside the mirroring half.
139
213
  *
140
- * Both are the same gesture to the person clicking: something this computer is doing for this sandbox, turned
214
+ * Both are the same gesture to the person clicking: something this device is doing for this sandbox, turned
141
215
  * off or on from the row that describes it. They were split for a while by nothing but which one had been built
142
216
  * — mirroring had a button and pausing a file sync had a paragraph telling you to go and find a terminal — and
143
217
  * that is exactly the gap this door exists to close.
@@ -147,8 +221,8 @@ export type MachineFlowLine = z.infer<typeof MachineFlowLineSchema>;
147
221
  * pairing and self-revokes its enrollment on the way out, so the machine cleans up after itself rather than
148
222
  * leaving a sandbox to guess what it managed to do. Revoking from the SANDBOX side (an unreachable machine, a
149
223
  * laptop that is never coming back) is a different act and a different route, see the enrollment revoke. */
150
- export const MachineCommandSchema = z.enum(["mirror-off", "mirror-on", "sync-pause", "sync-resume", "sync-unpair"]);
151
- export type MachineCommand = z.infer<typeof MachineCommandSchema>;
224
+ export const DeviceCommandSchema = z.enum(["mirror-off", "mirror-on", "sync-pause", "sync-resume", "sync-unpair"]);
225
+ export type DeviceCommand = z.infer<typeof DeviceCommandSchema>;
152
226
  /* Which paired sandbox the command acts on: the machine's own id for it, as it appears in that machine's report,
153
227
  * so nothing here has to re-derive the sanitizing the agent applied. Absent means every sandbox that machine
154
228
  * pairs, which is what the CLI does when it is run bare.
@@ -156,42 +230,42 @@ export type MachineCommand = z.infer<typeof MachineCommandSchema>;
156
230
  * Pattern-bound because it becomes an argv token. It must start with an alphanumeric, not merely consist of id
157
231
  * characters: a value like `--takeover` is made only of legal id characters and is a FLAG by the time the CLI on
158
232
  * the machine parses it. Real ids are `sandbox-<hex>-<zone>`-shaped, so nothing legitimate leads with a dash. */
159
- export const MachineSandboxIdSchema = z
233
+ export const DeviceSandboxIdSchema = z
160
234
  .string()
161
235
  .max(200)
162
236
  .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/);
163
- export const MachineCommandInputSchema = z.object({
237
+ export const DeviceCommandInputSchema = z.object({
164
238
  id: z.string().min(1),
165
- command: MachineCommandSchema,
166
- sandboxId: MachineSandboxIdSchema.optional(),
239
+ command: DeviceCommandSchema,
240
+ sandboxId: DeviceSandboxIdSchema.optional(),
167
241
  });
168
- export type MachineCommandInput = z.infer<typeof MachineCommandInputSchema>;
242
+ export type DeviceCommandInput = z.infer<typeof DeviceCommandInputSchema>;
169
243
  /* What came back. `ok` is the command's own exit status, not this route's: a machine that refused the call, or a
170
244
  * CLI that exited non-zero, is a real answer to show the person who clicked, not an exception to convert into
171
245
  * one. Only an unreachable machine throws, because then there is nothing to report at all.
172
246
  *
173
247
  * `output` is what the command printed, kept because the CLI's own sentences ("Port mirroring OFF for: …") are
174
248
  * better than anything this side would write over them. */
175
- export const MachineCommandResultSchema = z.object({
249
+ export const DeviceCommandResultSchema = z.object({
176
250
  ok: z.boolean(),
177
251
  message: z.string(),
178
252
  output: z.string().optional(),
179
253
  });
180
- export type MachineCommandResult = z.infer<typeof MachineCommandResultSchema>;
254
+ export type DeviceCommandResult = z.infer<typeof DeviceCommandResultSchema>;
181
255
  // One paired sandbox as the local agent holds it. `localDir` is the answer to the question the Desktop sync card
182
- // has never been able to answer: which folder on that computer this sandbox's /work actually is.
183
- export const MachinePairingSchema = z.object({
256
+ // has never been able to answer: which folder on that device this sandbox's /work actually is.
257
+ export const DevicePairingSchema = z.object({
184
258
  sandboxId: z.string(),
185
259
  mode: z.enum(["sync", "mirror"]),
186
260
  // Set only for mode "sync", and only for the sandbox being reported to, see the redaction note above.
187
261
  localDir: z.string().optional(),
188
- /* Whether that computer is putting this sandbox's ports on its own localhost, which is a switch its owner
262
+ /* Whether that device is putting this sandbox's ports on its own localhost, which is a switch its owner
189
263
  * holds and not a state this sandbox can read off anything else. An empty port list means two opposite
190
264
  * things — nothing is listening in the sandbox, or the machine was told to keep them off — and only the
191
265
  * second is worth a word on screen or a button to undo.
192
266
  *
193
267
  * The MACHINE owns the flag (the agent's `sync mirror off`), because the localhost being written to is
194
- * there: a computer told to keep ports off must keep them off while this sandbox is asleep, unreachable, or
268
+ * there: a device told to keep ports off must keep them off while this sandbox is asleep, unreachable, or
195
269
  * arguing. A browser asks for it by running that same command over the machine's `host` capability, so the
196
270
  * button and the CLI are one gesture rather than two mechanisms that can disagree.
197
271
  *
@@ -213,103 +287,107 @@ export const MachinePairingSchema = z.object({
213
287
  * running is only dangerous while nobody knows, so it gets its own word on the line. */
214
288
  backupStatus: z.string().optional(),
215
289
  });
216
- export type MachinePairing = z.infer<typeof MachinePairingSchema>;
290
+ export type DevicePairing = z.infer<typeof DevicePairingSchema>;
217
291
  /* One sandbox port and what became of it on this machine's localhost. The rows that did NOT make it are the
218
- * reason this carries a state rather than being a list of live forwards: two sandboxes on one computer routinely
292
+ * reason this carries a state rather than being a list of live forwards: two sandboxes on one device routinely
219
293
  * serve the same dev-server port and only one can own localhost:6480, so the loser's port is simply missing from
220
294
  * localhost with nothing anywhere saying why. Today that fact exists only as a line in mirror.log. */
221
- export const MachinePortStateSchema = z.enum([
295
+ export const DevicePortStateSchema = z.enum([
222
296
  // Forwarded: the sandbox's listener answers on this machine's localhost at the same number.
223
297
  "mirrored",
224
298
  // Another PAIRED SANDBOX got there first (first paired wins), `heldBy` names it, because "busy on this
225
299
  // machine" sends people hunting for a process that does not exist.
226
300
  "held-by-sandbox",
227
- // Something else on this computer already binds the port, a local dev server, another tool. Not ours to
301
+ // Something else on this device already binds the port, a local dev server, another tool. Not ours to
228
302
  // name, and not ours to take.
229
303
  "busy",
230
304
  ]);
231
- export const MachinePortSchema = z.object({
305
+ export const DevicePortSchema = z.object({
232
306
  port: z.number().int().min(1).max(65535),
233
307
  host: z.enum(["127.0.0.1", "::1"]),
234
308
  // The sandbox serving the port, whose /ports listed it, not whoever ended up holding the local bind.
235
309
  sandboxId: z.string(),
236
- state: MachinePortStateSchema,
310
+ state: DevicePortStateSchema,
237
311
  // Set only for "held-by-sandbox": the sandbox id that owns the local bind instead.
238
312
  heldBy: z.string().optional(),
239
313
  // What is listening on the sandbox side ("node …/vite"), for a row the user has to recognise to act on.
240
314
  command: z.string().optional(),
241
315
  });
242
- export type MachinePort = z.infer<typeof MachinePortSchema>;
243
- /* The resident watcher's liveness. This is the field that decides whether everything ELSE in the report is still
244
- * true: a healthy session list under a dead watcher means new dev-server ports stop appearing on localhost and
245
- * commits stop arriving in the local clones, while every other row keeps reading exactly as it did. */
246
- export const MachineWatcherSchema = z.object({
316
+ export type DevicePort = z.infer<typeof DevicePortSchema>;
317
+ /* THE AGENT ON THIS DEVICE, AS ONE BLOCK, because it is one binary and one process.
318
+ *
319
+ * It used to be two: `agents: { sync, host }` up in the report, and a `watcher` beside it. That split was a
320
+ * fiction two merges out of date — `resident.ts` runs a SINGLE process serving the outbound socket per linked
321
+ * sandbox and the mirror watcher together, from a single `intentic-machine` on disk — and it cost the Devices
322
+ * view its version chip. The chip read `agents.sync` and labelled it with the ENROLLMENT MODE ("desktop sync
323
+ * 1.243.0", or the same number as "ports only 1.243.0" on a mirror), so one binary's version wore two product
324
+ * names and appeared on no row that had never been paired for sync. `agents.host` was never filled by the
325
+ * report at all: the daemon fills `Device.agentVersion` from the hello frame it already holds.
326
+ *
327
+ * So: one block, and every version question about a device is answered inside it.
328
+ *
329
+ * `running` also decides whether everything ELSE in the report is still true — a healthy session list under a
330
+ * dead loop means new dev-server ports stop appearing on localhost and commits stop arriving in the local
331
+ * clones, while every other row reads exactly as it did the moment before. */
332
+ export const DeviceAgentSchema = z.object({
247
333
  running: z.boolean(),
248
334
  pid: z.number().int().optional(),
249
- /* WHICH BUILD IS ACTUALLY SERVING, stamped into the pidfile by the loop that claimed it, which is the only
250
- * place the fact exists: replacing the binary does not touch the running process, so a machine can hold a
251
- * current agent and go on serving a months-old one indefinitely. `agents.sync` is the file, this is the
252
- * process, and the two differing is a restart somebody is owed (see watcherBuildSkew). Absent when no loop
253
- * is running, and when the one running predates the stamp. */
335
+ /* THE BUILD ON DISK: the file at `~/.intentic/bin/intentic-machine`, asked what it is rather than assumed
336
+ * (the agent's own installed.ts). Absent on a device with no installed agent at all — a dev run, an `npx`
337
+ * one, a binary somebody put elsewhere on their PATH which is "not known", never a version. */
338
+ installed: z.string().optional(),
339
+ /* THE BUILD ACTUALLY SERVING, stamped into the pidfile by the loop that claimed it, which is the only place
340
+ * the fact exists: replacing the binary does not touch the running process, so a device can hold a current
341
+ * agent and go on serving a months-old one indefinitely. The two differing is a restart somebody is owed
342
+ * (see agentBuildSkew). Absent when no loop is running, and when the one running predates the stamp. */
254
343
  build: z.string().optional(),
255
- /* When the watcher last FINISHED a pass, the field that makes `running` mean something. The agent holds its
344
+ /* When the loop last FINISHED a pass, the field that makes `running` mean something. The agent holds its
256
345
  * SSH transport listeners on its own event loop, so a failure that escapes the loop leaves a process that is
257
346
  * alive and a loop that is gone: pid present, unit "active", mirroring and the git bridge stopped. Absent
258
347
  * means the agent has not reported one (too old to stamp, or its first pass has not landed), which is not
259
348
  * the same as stalled, and readers must not treat it as either state. */
260
349
  lastTickAt: z.number().optional(),
261
350
  });
262
- export type MachineWatcher = z.infer<typeof MachineWatcherSchema>;
263
- /* How long a watcher may go without finishing a pass before "running" stops being the honest word for it. Its
264
- * loop polls every 5s and its slowest step is bounded by two 10s network timeouts per pairing, so a minute is
265
- * several passes of slack, the same yardstick the Computers view already ages a whole report by.
351
+ export type DeviceAgent = z.infer<typeof DeviceAgentSchema>;
352
+ /* How long the loop may go without finishing a pass before "running" stops being the honest word for it. It
353
+ * polls every 5s and its slowest step is bounded by two 10s network timeouts per pairing, so a minute is
354
+ * several passes of slack, the same yardstick the Devices view already ages a whole report by.
266
355
  *
267
356
  * The rule lives HERE, next to the field, because the terminal and the browser both answer this question and a
268
- * machine that is "running" in one and "stalled" in the other is worse than either answer alone. */
269
- export const WATCHER_STALL_AFTER_MS = 60_000;
270
- export const watcherStalled = (watcher: MachineWatcher, now: number): boolean =>
271
- watcher.running && watcher.lastTickAt !== undefined && now - watcher.lastTickAt > WATCHER_STALL_AFTER_MS;
272
- export const MachineReportSchema = z.object({
357
+ * device that is "running" in one and "stalled" in the other is worse than either answer alone. */
358
+ export const AGENT_STALL_AFTER_MS = 60_000;
359
+ export const agentStalled = (agent: DeviceAgent, now: number): boolean =>
360
+ agent.running && agent.lastTickAt !== undefined && now - agent.lastTickAt > AGENT_STALL_AFTER_MS;
361
+ export const DeviceReportSchema = z.object({
273
362
  /* The OS hostname, and the JOIN KEY. A machine can arrive here two ways at once, volunteered by its sync
274
363
  * agent, and read through its `host` capability, and those two know it by different names (the enrolled
275
364
  * key's comment vs. the capability id the user typed). The hostname is the one thing both can state about
276
365
  * the same box, so it is what dedupes them into a single row. */
277
366
  hostname: z.string(),
278
367
  os: z.string(),
279
- /* Which agents this machine has, and at what version, so one on an old build is visible rather than
280
- * mysteriously lacking a field. Same argument as HostSummary.version.
281
- *
282
- * `sync` is the agent INSTALLED here — the file on disk — and `watcher.build` beside it is the loop running
283
- * from that file. It used to be neither: whichever process happened to build the report stamped its own
284
- * version here, so the same machine answered its running build to a sandbox its loop posted to and its
285
- * installed build to one that ran `status --json` over a host capability. One field, two meanings, and the
286
- * gap between them — a machine updated but never restarted — invisible in both.
287
- *
288
- * `host` is what the live socket announced (so: what is running), filled by the daemon at merge time from
289
- * the hello frame it already holds, never by the sync agent, which would have to go reading another agent's
290
- * config to guess at it. */
291
- agents: z.object({ sync: z.string().optional(), host: z.string().optional() }),
292
368
  // Filled by the READER, never the agent (see above). Empty is the resting state: no Docker on the machine,
293
369
  // or nothing has looked. Neither is an error, and neither means "no sandboxes exist".
294
- sandboxes: z.array(MachineSandboxSchema),
295
- pairings: z.array(MachinePairingSchema),
296
- ports: z.array(MachinePortSchema),
297
- watcher: MachineWatcherSchema,
370
+ sandboxes: z.array(DeviceSandboxSchema),
371
+ pairings: z.array(DevicePairingSchema),
372
+ ports: z.array(DevicePortSchema),
373
+ // The one agent this device runs, on disk and in flight, in one block (see DeviceAgentSchema for why it is
374
+ // one and not the `agents` map plus a `watcher` it replaced).
375
+ agent: DeviceAgentSchema,
298
376
  // When the machine took this reading. NOT when the daemon received it. A report is a snapshot from a box
299
377
  // that may since have gone to sleep, and the UI ages it against this rather than presenting it as now.
300
378
  capturedAt: z.number(),
301
379
  });
302
- export type MachineReport = z.infer<typeof MachineReportSchema>;
380
+ export type DeviceReport = z.infer<typeof DeviceReportSchema>;
303
381
 
304
382
  /* THE AGENT THIS MACHINE INSTALLED AND THE ONE IT IS RUNNING, when they are not the same build — the whole of
305
383
  * "you updated the agent and nothing changed", as a value.
306
384
  *
307
- * It is one comparison, and it lives HERE for the same reason watcherStalled does: the terminal (`intentic-machine
308
- * status`) and the browser (the Computers row) both answer this question, and a machine that is behind in one and
385
+ * It is one comparison, and it lives HERE for the same reason agentStalled does: the terminal (`intentic-machine
386
+ * status`) and the browser (the Devices row) both answer this question, and a machine that is behind in one and
309
387
  * fine in the other is worse than either answer alone. The remedy is the same in both: restart the loop.
310
388
  *
311
389
  * AN UNSTAMPED LOOP IS THE LOUDEST CASE, not a missing one, and reading it as "nothing to say" is what let this
312
- * whole check miss the machines it was written for. `watcher.build` is stamped into the pidfile by the loop that
390
+ * whole check miss the machines it was written for. `agent.build` is stamped into the pidfile by the loop that
313
391
  * claimed it, so a loop old enough to predate the stamp reports none — and it is running, and something newer is
314
392
  * installed beside it, which is a skew by definition and a wider one than any it could have named. Every surface
315
393
  * therefore went quiet on precisely the machines furthest behind: upgrade, see the new number everywhere, watch
@@ -317,20 +395,19 @@ export type MachineReport = z.infer<typeof MachineReportSchema>;
317
395
  *
318
396
  * So the running build is OPTIONAL in the answer and the question is asked of the installed one. Still silent
319
397
  * whenever the honest answer is "no idea": a loop that is stopped (nothing is serving, and every surface already
320
- * says so in louder words), a machine with no installed agent to compare against, and a working-tree build, which
398
+ * says so in louder words), a device with no installed agent to compare against, and a working-tree build, which
321
399
  * is not a version and must not be told it is behind. */
322
- export const watcherBuildSkew = (report: MachineReport): { readonly running: string | undefined; readonly installed: string } | undefined => {
323
- const running = report.watcher.build;
324
- const installed = report.agents.sync;
325
- if (!report.watcher.running || installed === undefined || installed === DEV_VERSION || running === installed) {
400
+ export const agentBuildSkew = (agent: DeviceAgent): { readonly running: string | undefined; readonly installed: string } | undefined => {
401
+ const { build: running, installed } = agent;
402
+ if (!agent.running || installed === undefined || installed === DEV_VERSION || running === installed) {
326
403
  return undefined;
327
404
  }
328
405
  return { running, installed };
329
406
  };
330
407
 
331
- // Why a computer that is plainly THERE has no report to show. Each is a different errand for the reader, which is
408
+ // Why a device that is plainly THERE has no report to show. Each is a different errand for the reader, which is
332
409
  // the whole reason they are not collapsed into one "unavailable".
333
- export const ComputerGapSchema = z.enum([
410
+ export const DeviceGapSchema = z.enum([
334
411
  // A host capability that is enrolled but has no socket right now. Laptops sleep; this is not a fault.
335
412
  "offline",
336
413
  // Connected, but "Run commands" is switched off on its capability card, so the daemon may not ask it
@@ -342,8 +419,8 @@ export const ComputerGapSchema = z.enum([
342
419
  // machine reports. Distinct from "no-agent" because the agent IS there and the folders ARE syncing.
343
420
  "unreported",
344
421
  ]);
345
- export type ComputerGap = z.infer<typeof ComputerGapSchema>;
346
- /* ONE COMPUTER, however the sandbox happens to be able to see it, and it may be both ways at once.
422
+ export type DeviceGap = z.infer<typeof DeviceGapSchema>;
423
+ /* ONE DEVICE, however the sandbox happens to be able to see it, and it may be both ways at once.
347
424
  *
348
425
  * A machine reaches a sandbox through two independent doors: a desktop-sync enrollment (which volunteers its own
349
426
  * report) and a `host` capability (which the daemon can ask). They know the same box by different names, the
@@ -357,14 +434,14 @@ export type ComputerGap = z.infer<typeof ComputerGapSchema>;
357
434
  * ever used the enrollment, and how to name it when they want it gone. Those three lived on /system/sync
358
435
  * instead, as one machine's worth of `syncingFrom` plus a list of `mirroredBy` names, which is the sandbox-level
359
436
  * shape this view exists to stop being: one card claiming a sandbox has A desktop sync, over a list of the
360
- * several computers that actually do.
437
+ * several devices that actually do.
361
438
  *
362
439
  * `machine` is the enrollment's own name for the box (the ssh key's comment). It is what the reports are filed
363
440
  * under, and it is the id the revoke route takes — the same string, so a row can revoke exactly the enrollment
364
441
  * it is drawn from. Two machines that present the same comment share one enrollment identity throughout the
365
442
  * daemon (reports included); that is a pre-existing property of naming machines by their key comment, and this
366
443
  * field inherits it rather than inventing a second identity that would disagree with the first. */
367
- export const ComputerSyncSchema = z.object({
444
+ export const DeviceSyncSchema = z.object({
368
445
  machine: z.string(),
369
446
  /* Which half. "sync" is files AND ports and is SINGLE-HOLDER for the sandbox; "mirror" is ports only and any
370
447
  * number of machines may hold one. The row says which, because "your laptop is paired" is read as the first
@@ -374,24 +451,24 @@ export const ComputerSyncSchema = z.object({
374
451
  // has, which is exactly what a setup that did not finish leaves behind, and must not read as healthy.
375
452
  seenAt: z.number().optional(),
376
453
  });
377
- export type ComputerSync = z.infer<typeof ComputerSyncSchema>;
378
- export const ComputerSchema = z.object({
454
+ export type DeviceSync = z.infer<typeof DeviceSyncSchema>;
455
+ export const DeviceSchema = z.object({
379
456
  // Stable row key: the reported hostname when either door produced one, else the name that door knows it by.
380
457
  key: z.string(),
381
458
  // What to call it on screen, the user's own name for the machine wherever one exists.
382
459
  label: z.string(),
383
- // The desktop-sync enrollment this machine holds with this sandbox, absent when it has none (a computer
460
+ // The desktop-sync enrollment this machine holds with this sandbox, absent when it has none (a device
384
461
  // reached only through its `host` capability).
385
- sync: ComputerSyncSchema.optional(),
386
- // The host capability's id, when this machine is also a connected computer. Absent otherwise.
462
+ sync: DeviceSyncSchema.optional(),
463
+ // The host capability's id, when this machine is also a connected device. Absent otherwise.
387
464
  hostId: z.string().optional(),
388
465
  // Host-capability liveness. Absent when there is no host capability, which is NOT the same as offline.
389
466
  online: z.boolean().optional(),
390
- /* WHAT THE COMPUTER IS, as distinct from how it is reachable, the half a row used to leave out entirely,
467
+ /* WHAT THE DEVICE IS, as distinct from how it is reachable, the half a row used to leave out entirely,
391
468
  * so a Windows laptop and a Linux desktop were two identical lines of text with different names on them.
392
469
  *
393
470
  * It is carried BESIDE the report rather than inside it because the rows that need it most are the ones with
394
- * no report: a connected computer with no sync agent, or one that is asleep, still knows its own OS. Nothing
471
+ * no report: a connected device with no sync agent, or one that is asleep, still knows its own OS. Nothing
395
472
  * here depends on an agent being installed, and the daemon has held all of it since the machine connected.
396
473
  *
397
474
  * `platform` is the slug this side classifies the machine by, the host capability's own card ("windows",
@@ -399,24 +476,30 @@ export const ComputerSchema = z.object({
399
476
  * machine's connect-time description of ITSELF, which is what says which Windows and which shell. */
400
477
  platform: z.string().optional(),
401
478
  facts: HostFactsSchema.optional(),
402
- // The host agent's version and when the machine last held a socket, how a connected computer AGES. An old
403
- // agent explains a row that lacks something newer machines have, and "last seen" is the one honest thing an
404
- // offline row can still say about itself.
405
- hostAgent: z.string().optional(),
479
+ /* The agent version the socket ANNOUNCED at connect, and when the device last held one: how a connected
480
+ * device AGES. An old agent explains a row that lacks something newer devices have, and "last seen" is the
481
+ * one honest thing an offline row can still say about itself.
482
+ *
483
+ * It is the same number as `report.agent.build` whenever both are known — one resident process stamps its
484
+ * `MACHINE_VERSION` into the pidfile and sends it in the hello frame — and it is kept because it is the
485
+ * ONLY version a row with no report has: a device whose "Run commands" switch is off, or which has no
486
+ * agent to answer `status --json`, still told us what it was when it dialled. That is what stops the
487
+ * Devices view's agent chip going blank on exactly the rows that need explaining. */
488
+ agentVersion: z.string().optional(),
406
489
  lastSeen: z.number().optional(),
407
- report: MachineReportSchema.optional(),
408
- gap: ComputerGapSchema.optional(),
490
+ report: DeviceReportSchema.optional(),
491
+ gap: DeviceGapSchema.optional(),
409
492
  });
410
- export type Computer = z.infer<typeof ComputerSchema>;
411
- export const ComputersListSchema = z.object({ computers: z.array(ComputerSchema) });
493
+ export type Device = z.infer<typeof DeviceSchema>;
494
+ export const DevicesListSchema = z.object({ devices: z.array(DeviceSchema) });
412
495
  /* GET /system/sync: what desktop sync is doing for this sandbox, WITHOUT naming any one machine as the answer.
413
496
  *
414
497
  * It used to carry `syncingFrom` + `syncSeenAt` + `mirroredBy`, which is the enrollment list flattened into one
415
498
  * holder and a list of everybody else — the shape a card that believed a sandbox has A desktop sync needed, and
416
- * the reason that card kept restating facts the Computers list beside it already had per machine. Every one of
417
- * those now rides on the machine's own row (ComputerSync), where a reader can act on it.
499
+ * the reason that card kept restating facts the Devices list beside it already had per machine. Every one of
500
+ * those now rides on the machine's own row (DeviceSync), where a reader can act on it.
418
501
  *
419
- * What is left is what is genuinely about the SANDBOX rather than about any computer: whether sync is possible
502
+ * What is left is what is genuinely about the SANDBOX rather than about any device: whether sync is possible
420
503
  * here at all, whether anything at all is enrolled, and the raw reports, which is the cheap ambient read the
421
504
  * rail's badge lives on (it must never fan out to somebody's laptop just to decide whether to draw a chip). */
422
505
  export const SyncStatusSchema = z.object({
@@ -427,6 +510,6 @@ export const SyncStatusSchema = z.object({
427
510
  * surface now, so a sandbox that can answer this read can also sync. Kept as a field rather than assumed,
428
511
  * because the card branches on it and a daemon too old to say is one that should not be offered sync. */
429
512
  available: z.boolean().optional(),
430
- machines: z.array(MachineReportSchema).optional(),
513
+ machines: z.array(DeviceReportSchema).optional(),
431
514
  });
432
515
  export type SyncStatus = z.infer<typeof SyncStatusSchema>;
@@ -1,4 +1,4 @@
1
- // hosts: the user's own connected computers (the `host` capability's live half)
1
+ // hosts: the user's own connected devices (the `host` capability's live half)
2
2
  import { z } from "zod";
3
3
  // The manifest says which machines the user INTENDS to have connected; this says which are actually holding a
4
4
  // socket right now. Nothing here is remembered across a daemon restart except the enrollment itself: a machine
@@ -18,6 +18,10 @@ export const HostFactsSchema = z.object({
18
18
  home: z.string(),
19
19
  // Roots in force right now (the capability's `roots`, or [home]), the agent sees its own boundary.
20
20
  roots: z.array(z.string()),
21
+ /* The Docker ENGINE's size — the WSL guest on Windows, the Desktop VM on macOS, the host on Linux — which
22
+ * is the ceiling a sandbox's share is bounded by, and the number a Resources dialog draws its limits from.
23
+ * Absent when the machine has no docker to ask, which is a machine that runs no sandboxes anyway. */
24
+ engine: z.object({ memoryBytes: z.number(), cpus: z.number() }).optional(),
21
25
  });
22
26
  export type HostFacts = z.infer<typeof HostFactsSchema>;
23
27
  export const HostSummarySchema = z.object({
@@ -234,10 +234,6 @@ export const IssuePublicConfigSchema = z.object({
234
234
  });
235
235
  export type IssuePublicConfig = z.infer<typeof IssuePublicConfigSchema>;
236
236
 
237
- // The proof-of-work challenge, the same shape and the same solver as the Front Desk's.
238
- export const IssueChallengeSchema = z.object({ salt: z.string(), difficulty: z.number().int().positive() });
239
- export type IssueChallenge = z.infer<typeof IssueChallengeSchema>;
240
-
241
237
  /* What the intake answers with. A short reference the reporter can be shown ("we filed this as 4f3a…"), and
242
238
  * nothing else: whether this crash is new, how often it has happened and whether it woke anybody are the
243
239
  * owner's facts, and the caller is a stranger's browser. */
@@ -44,7 +44,7 @@ import { SkillDraftSchema, SkillNameSchema, SystemPromptModeSchema } from "./set
44
44
  * what it said after the SDK renames something.
45
45
  *
46
46
  * TWO STRENGTHS, AND THE DIFFERENCE IS VISIBLE FROM HERE. Everything capability-shaped (`connectors`,
47
- * `computers`, `mcp`, and the accounts in `capabilities`) is enforced by ABSENCE, the credential is never
47
+ * `devices`, `mcp`, and the accounts in `capabilities`) is enforced by ABSENCE, the credential is never
48
48
  * injected, the server never mounted, the browser never launched, which is the same mechanism the account
49
49
  * filter already uses and needs no cooperation from the model. The plain switches are enforced by taking the
50
50
  * tools out of the turn's context, which holds for every tool the harness owns and cannot reach a program the
@@ -99,7 +99,7 @@ export const PersonaPowersSchema = z.object({
99
99
  * whole reason these are optional rather than defaulted arrays, "all" and "none" are both real answers and
100
100
  * an empty default could only spell one of them. */
101
101
  connectors: z.array(entryId).max(100).optional(),
102
- computers: z.array(entryId).max(50).optional(),
102
+ devices: z.array(entryId).max(50).optional(),
103
103
  mcp: z.array(entryId).max(50).optional(),
104
104
  });
105
105
  export type PersonaPowers = z.infer<typeof PersonaPowersSchema>;
@@ -155,6 +155,11 @@ export const PersonaSchema = z.object({
155
155
  "What a conversation wearing it may do. Absent means the full toolbox, so a card written before this existed behaves exactly as it did.",
156
156
  ),
157
157
  workspace: PersonaWorkspaceSchema.optional().describe("Where it works. Absent means the whole workspace."),
158
+ /* WHICH PART OF THE WORKSPACE A SESSION WEARING THIS CARD CARRIES, the id of a context shelf
159
+ * (schemas/context.ts). A different question from `workspace.folders`: that one fences what the file tools
160
+ * may TOUCH inside a tree that holds everything, this one decides what the tree HOLDS. Absent falls through to
161
+ * the sandbox's `contextShelf` setting, and from there to everything. */
162
+ context: entryId.optional().describe("Which context shelf a conversation wearing it opens on: the part of the workspace it carries. Absent follows the sandbox setting."),
158
163
  /* WHICH SYSTEM PROMPT A SESSION WEARING THIS CARD RUNS ON, the same three bases the sandbox chooses
159
164
  * between, asked per card. ABSENT is the fourth answer and the default: follow the sandbox, which is what
160
165
  * every card meant before this field existed and what almost every card will go on meaning.
@@ -218,7 +223,7 @@ export const personaBounds = (persona: Persona): string => {
218
223
  !resolved.delegate,
219
224
  !resolved.sandbox,
220
225
  resolved.connectors !== undefined,
221
- resolved.computers !== undefined,
226
+ resolved.devices !== undefined,
222
227
  resolved.mcp !== undefined,
223
228
  ].filter(Boolean).length;
224
229
  return limits === 0 ? "Full powers" : `${limits} limit${limits === 1 ? "" : "s"}`;