@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.
- package/README.md +17 -1
- package/dist/command-classes.d.ts +6 -3
- package/dist/command-classes.d.ts.map +1 -1
- package/dist/command-classes.js +43 -18
- package/dist/command-classes.js.map +1 -1
- package/dist/contracts/{cursor.contract.d.ts → accounts.contract.d.ts} +102 -3
- package/dist/contracts/accounts.contract.d.ts.map +1 -0
- package/dist/contracts/accounts.contract.js +61 -0
- package/dist/contracts/accounts.contract.js.map +1 -0
- package/dist/contracts/agents.contract.d.ts +121 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +4 -4
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/ci.contract.d.ts +2 -0
- package/dist/contracts/ci.contract.d.ts.map +1 -1
- package/dist/contracts/host.contract.d.ts +35 -0
- package/dist/contracts/host.contract.d.ts.map +1 -1
- package/dist/contracts/host.contract.js +3 -2
- package/dist/contracts/host.contract.js.map +1 -1
- package/dist/contracts/personas.contract.d.ts +4 -2
- package/dist/contracts/personas.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +2 -2
- package/dist/contracts/settings.contract.d.ts +2 -51
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +80 -30
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.js +26 -17
- package/dist/contracts/system.contract.js.map +1 -1
- package/dist/definition.d.ts +4 -8
- package/dist/definition.d.ts.map +1 -1
- package/dist/documents.d.ts +0 -1
- package/dist/documents.d.ts.map +1 -1
- package/dist/documents.js +1 -2
- package/dist/documents.js.map +1 -1
- package/dist/embed.d.ts +23 -0
- package/dist/embed.d.ts.map +1 -0
- package/dist/embed.js +84 -0
- package/dist/embed.js.map +1 -0
- package/dist/events.d.ts +21 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +5 -2
- package/dist/events.js.map +1 -1
- package/dist/history-state.d.ts.map +1 -1
- package/dist/history-state.js +2 -0
- package/dist/history-state.js.map +1 -1
- package/dist/index.d.ts +354 -335
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -13
- package/dist/index.js.map +1 -1
- package/dist/peer-dial.d.ts +33 -0
- package/dist/peer-dial.d.ts.map +1 -0
- package/dist/peer-dial.js +79 -0
- package/dist/peer-dial.js.map +1 -0
- package/dist/peer-mcp-server.d.ts +36 -0
- package/dist/peer-mcp-server.d.ts.map +1 -0
- package/dist/peer-mcp-server.js +71 -0
- package/dist/peer-mcp-server.js.map +1 -0
- package/dist/provider-specs.d.ts +38 -20
- package/dist/provider-specs.d.ts.map +1 -1
- package/dist/provider-specs.js +39 -13
- package/dist/provider-specs.js.map +1 -1
- package/dist/quick-model.d.ts.map +1 -1
- package/dist/quick-model.js +2 -1
- package/dist/quick-model.js.map +1 -1
- package/dist/runtime-state.d.ts +1 -1
- package/dist/runtime-state.js +1 -1
- package/dist/runtime-state.js.map +1 -1
- package/dist/safety-policy.d.ts +12 -3
- package/dist/safety-policy.d.ts.map +1 -1
- package/dist/safety-policy.js +30 -5
- package/dist/safety-policy.js.map +1 -1
- package/dist/schemas/agent.d.ts +6 -6
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +6 -2
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/agents.d.ts +42 -0
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +25 -4
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/automations.d.ts +11 -2
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/automations.js +1 -1
- package/dist/schemas/automations.js.map +1 -1
- package/dist/schemas/ci.d.ts +6 -0
- package/dist/schemas/ci.d.ts.map +1 -1
- package/dist/schemas/ci.js +3 -2
- package/dist/schemas/ci.js.map +1 -1
- package/dist/schemas/context.d.ts +30 -0
- package/dist/schemas/context.d.ts.map +1 -0
- package/dist/schemas/context.js +34 -0
- package/dist/schemas/context.js.map +1 -0
- package/dist/schemas/{computers.d.ts → devices.d.ts} +154 -60
- package/dist/schemas/devices.d.ts.map +1 -0
- package/dist/schemas/devices.js +157 -0
- package/dist/schemas/devices.js.map +1 -0
- package/dist/schemas/hosts.d.ts +12 -0
- package/dist/schemas/hosts.d.ts.map +1 -1
- package/dist/schemas/hosts.js +1 -0
- package/dist/schemas/hosts.js.map +1 -1
- package/dist/schemas/issues.d.ts +0 -5
- package/dist/schemas/issues.d.ts.map +1 -1
- package/dist/schemas/issues.js +0 -1
- package/dist/schemas/issues.js.map +1 -1
- package/dist/schemas/personas.d.ts +5 -3
- package/dist/schemas/personas.d.ts.map +1 -1
- package/dist/schemas/personas.js +3 -2
- package/dist/schemas/personas.js.map +1 -1
- package/dist/schemas/provider-oauth.d.ts +48 -16
- package/dist/schemas/provider-oauth.d.ts.map +1 -1
- package/dist/schemas/provider-oauth.js +22 -20
- package/dist/schemas/provider-oauth.js.map +1 -1
- package/dist/schemas/settings.d.ts +1 -52
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +8 -11
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/terminal.js +9 -9
- package/dist/schemas/terminal.js.map +1 -1
- package/dist/schemas/usage.d.ts +0 -2
- package/dist/schemas/usage.d.ts.map +1 -1
- package/dist/schemas/usage.js +0 -2
- package/dist/schemas/usage.js.map +1 -1
- package/dist/shell-regions.d.ts +4 -0
- package/dist/shell-regions.d.ts.map +1 -0
- package/dist/shell-regions.js +156 -0
- package/dist/shell-regions.js.map +1 -0
- package/dist/workspace-state.d.ts +8 -0
- package/dist/workspace-state.d.ts.map +1 -1
- package/dist/workspace-state.js +13 -5
- package/dist/workspace-state.js.map +1 -1
- package/package.json +37 -4
- package/src/agent-catalog.ts +1 -1
- package/src/arrival.ts +3 -3
- package/src/command-classes.test.ts +195 -71
- package/src/command-classes.ts +148 -46
- package/src/contracts/accounts.contract.ts +94 -0
- package/src/contracts/agents.contract.ts +4 -3
- package/src/contracts/exit.contract.ts +2 -2
- package/src/contracts/host.contract.ts +17 -5
- package/src/contracts/settings.contract.ts +1 -1
- package/src/contracts/system.contract.ts +43 -24
- package/src/contracts/vpn.contract.ts +2 -2
- package/src/documents.test.ts +2 -1
- package/src/documents.ts +7 -11
- package/src/embed.test.ts +68 -0
- package/src/embed.ts +164 -0
- package/src/events.ts +30 -3
- package/src/history-state.ts +12 -3
- package/src/host-protocol.ts +2 -2
- package/src/index.ts +6 -14
- package/src/model-order.ts +1 -1
- package/src/peer-dial.test.ts +203 -0
- package/src/peer-dial.ts +163 -0
- package/src/peer-mcp-server.test.ts +104 -0
- package/src/peer-mcp-server.ts +144 -0
- package/src/provider-specs.test.ts +44 -17
- package/src/provider-specs.ts +147 -67
- package/src/quick-model.ts +10 -3
- package/src/routes.test.ts +6 -3
- package/src/runner-protocol.ts +1 -1
- package/src/runtime-state.ts +2 -2
- package/src/safety-policy.test.ts +88 -0
- package/src/safety-policy.ts +84 -14
- package/src/schemas/agent.ts +41 -13
- package/src/schemas/agents.ts +65 -4
- package/src/schemas/automations.ts +6 -4
- package/src/schemas/capabilities.ts +4 -4
- package/src/schemas/ci.ts +23 -6
- package/src/schemas/context.ts +87 -0
- package/src/schemas/{computers.ts → devices.ts} +190 -107
- package/src/schemas/hosts.ts +5 -1
- package/src/schemas/issues.ts +0 -4
- package/src/schemas/personas.ts +8 -3
- package/src/schemas/provider-oauth.ts +49 -52
- package/src/schemas/settings.ts +30 -54
- package/src/schemas/terminal.ts +12 -12
- package/src/schemas/usage.ts +3 -27
- package/src/schemas/version-seam.test.ts +0 -1
- package/src/shell-regions.ts +289 -0
- package/src/versions.ts +2 -2
- package/src/webext-links.ts +2 -2
- package/src/webext-protocol.ts +2 -2
- package/src/workspace-state.test.ts +48 -1
- package/src/workspace-state.ts +48 -11
- package/dist/contracts/claude.contract.d.ts +0 -91
- package/dist/contracts/claude.contract.d.ts.map +0 -1
- package/dist/contracts/claude.contract.js +0 -50
- package/dist/contracts/claude.contract.js.map +0 -1
- package/dist/contracts/cursor.contract.d.ts.map +0 -1
- package/dist/contracts/cursor.contract.js +0 -50
- package/dist/contracts/cursor.contract.js.map +0 -1
- package/dist/contracts/grok.contract.d.ts +0 -36
- package/dist/contracts/grok.contract.d.ts.map +0 -1
- package/dist/contracts/grok.contract.js +0 -31
- package/dist/contracts/grok.contract.js.map +0 -1
- package/dist/contracts/keys.contract.d.ts +0 -81
- package/dist/contracts/keys.contract.d.ts.map +0 -1
- package/dist/contracts/keys.contract.js +0 -51
- package/dist/contracts/keys.contract.js.map +0 -1
- package/dist/schemas/computers.d.ts.map +0 -1
- package/dist/schemas/computers.js +0 -135
- package/dist/schemas/computers.js.map +0 -1
- package/src/contracts/claude.contract.ts +0 -71
- package/src/contracts/cursor.contract.ts +0 -74
- package/src/contracts/grok.contract.ts +0 -41
- package/src/contracts/keys.contract.ts +0 -79
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
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
|
|
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
|
|
42
|
-
/* ONE OPERATION ON ONE SANDBOX ON ONE MACHINE, the
|
|
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
|
|
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
|
|
81
|
-
export const
|
|
82
|
-
op:
|
|
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
|
|
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
|
|
111
|
-
export type
|
|
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
|
|
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
|
|
122
|
-
/*
|
|
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
|
-
*
|
|
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/
|
|
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
|
|
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
|
|
151
|
-
export type
|
|
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
|
|
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
|
|
237
|
+
export const DeviceCommandInputSchema = z.object({
|
|
164
238
|
id: z.string().min(1),
|
|
165
|
-
command:
|
|
166
|
-
sandboxId:
|
|
239
|
+
command: DeviceCommandSchema,
|
|
240
|
+
sandboxId: DeviceSandboxIdSchema.optional(),
|
|
167
241
|
});
|
|
168
|
-
export type
|
|
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
|
|
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
|
|
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
|
|
183
|
-
export const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
243
|
-
/*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
|
|
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
|
-
/*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|
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
|
|
263
|
-
/* How long
|
|
264
|
-
*
|
|
265
|
-
* several passes of slack, the same yardstick the
|
|
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
|
-
*
|
|
269
|
-
export const
|
|
270
|
-
export const
|
|
271
|
-
|
|
272
|
-
export const
|
|
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(
|
|
295
|
-
pairings: z.array(
|
|
296
|
-
ports: z.array(
|
|
297
|
-
|
|
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
|
|
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
|
|
308
|
-
* status`) and the browser (the
|
|
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. `
|
|
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
|
|
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
|
|
323
|
-
const running =
|
|
324
|
-
|
|
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
|
|
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
|
|
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
|
|
346
|
-
/* ONE
|
|
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
|
|
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
|
|
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
|
|
378
|
-
export const
|
|
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
|
|
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:
|
|
386
|
-
// The host capability's id, when this machine is also a connected
|
|
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
|
|
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
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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:
|
|
408
|
-
gap:
|
|
490
|
+
report: DeviceReportSchema.optional(),
|
|
491
|
+
gap: DeviceGapSchema.optional(),
|
|
409
492
|
});
|
|
410
|
-
export type
|
|
411
|
-
export const
|
|
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
|
|
417
|
-
* those now rides on the machine's own row (
|
|
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
|
|
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(
|
|
513
|
+
machines: z.array(DeviceReportSchema).optional(),
|
|
431
514
|
});
|
|
432
515
|
export type SyncStatus = z.infer<typeof SyncStatusSchema>;
|
package/src/schemas/hosts.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// hosts: the user's own connected
|
|
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({
|
package/src/schemas/issues.ts
CHANGED
|
@@ -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. */
|
package/src/schemas/personas.ts
CHANGED
|
@@ -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
|
-
* `
|
|
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
|
-
|
|
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.
|
|
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"}`;
|