@intentic/sandbox-contract 1.244.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/batch-runs.d.ts +32 -0
- package/dist/batch-runs.d.ts.map +1 -0
- package/dist/batch-runs.js +43 -0
- package/dist/batch-runs.js.map +1 -0
- 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 +52 -2
- 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} +155 -61
- 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 +15 -0
- package/dist/workspace-state.d.ts.map +1 -1
- package/dist/workspace-state.js +21 -5
- package/dist/workspace-state.js.map +1 -1
- package/package.json +48 -4
- package/src/agent-catalog.ts +1 -1
- package/src/arrival.ts +3 -3
- package/src/batch-runs.test.ts +167 -0
- package/src/batch-runs.ts +181 -0
- 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} +201 -109
- 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 +55 -1
- package/src/workspace-state.ts +76 -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 -134
- 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
|
@@ -2,11 +2,12 @@ import { eventIterator, oc } from "@orpc/contract";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { SessionTranscriptSchema, SystemEventSchema } from "../events.js";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
DeviceAgentFlowInputSchema,
|
|
6
|
+
DeviceCommandInputSchema,
|
|
7
|
+
DeviceCommandResultSchema,
|
|
8
|
+
DeviceFlowLineSchema,
|
|
9
|
+
DeviceSandboxFlowInputSchema,
|
|
10
|
+
} from "../schemas/devices.js";
|
|
10
11
|
import { PresenceReportSchema } from "../schemas/logs.js";
|
|
11
12
|
import { OkSchema } from "../schemas/shared.js";
|
|
12
13
|
import { DaemonSessionSchema, InfoSchema, ManifestProblemsSchema } from "../schemas/system.js";
|
|
@@ -160,52 +161,70 @@ export const systemContract = {
|
|
|
160
161
|
.route({
|
|
161
162
|
method: "GET",
|
|
162
163
|
path: "/system/subagents",
|
|
163
|
-
summary: "
|
|
164
|
+
summary: "Subagents the agents have started",
|
|
164
165
|
description:
|
|
165
|
-
"Every
|
|
166
|
+
"Every subagent and child agent this sandbox's conversations have delegated work to, whichever tool started it, with what each one is doing.",
|
|
166
167
|
})
|
|
167
168
|
.output(SubagentsListSchema),
|
|
168
169
|
subagentTranscript: oc
|
|
169
170
|
.route({
|
|
170
171
|
method: "GET",
|
|
171
172
|
path: "/system/subagents/{id}/transcript",
|
|
172
|
-
summary: "A
|
|
173
|
+
summary: "A subagent's record",
|
|
173
174
|
description:
|
|
174
|
-
"The full record of one delegated
|
|
175
|
+
"The full record of one delegated subagent, in the same shape as any other conversation. It comes live from the parent turn while it works, and from stored history once it has finished.",
|
|
175
176
|
})
|
|
176
177
|
.input(SubagentIdParamSchema)
|
|
177
178
|
.output(SessionTranscriptSchema),
|
|
178
|
-
/* Start, stop, restart, update, rebuild, roll back or remove a sandbox on one of the user's own
|
|
179
|
-
* the
|
|
179
|
+
/* Start, stop, restart, update, rebuild, roll back or remove a sandbox on one of the user's own devices,
|
|
180
|
+
* the Devices view's buttons, relayed to the machine over the socket it holds open to us.
|
|
180
181
|
*
|
|
181
182
|
* Streamed because the slowest of these takes minutes, and it is the same stream whatever the op: one door
|
|
182
183
|
* for one decision, so the view has one shape to render rather than one per duration. The daemon adds no
|
|
183
184
|
* judgement, the machine enforces its own switches and its refusal arrives as the terminal `error` line,
|
|
184
185
|
* in its own words, naming the control to flip. */
|
|
185
|
-
|
|
186
|
+
manageDeviceSandbox: oc
|
|
186
187
|
.route({
|
|
187
188
|
method: "POST",
|
|
188
|
-
path: "/system/
|
|
189
|
-
summary: "Drive a sandbox on one of your own
|
|
189
|
+
path: "/system/devices/{id}/sandboxes/{slug}",
|
|
190
|
+
summary: "Drive a sandbox on one of your own devices",
|
|
190
191
|
description:
|
|
191
|
-
"Start, stop, restart, update, rebuild, roll back or remove a sandbox running on a machine you own, relayed over the connection that machine holds open. The answer is a stream because the slowest of these takes minutes, and it is the same stream whichever you ask for. The daemon adds no opinion: the machine enforces its own permissions and a refusal arrives as the last line, in the machine's words, naming the switch to flip.",
|
|
192
|
+
"Start, stop, restart, update, rebuild, roll back, reshape (its memory and CPU caps, privileged, GPU) or remove a sandbox running on a machine you own, relayed over the connection that machine holds open. The answer is a stream because the slowest of these takes minutes, and it is the same stream whichever you ask for. The daemon adds no opinion: the machine enforces its own permissions and a refusal arrives as the last line, in the machine's words, naming the switch to flip.",
|
|
192
193
|
})
|
|
193
|
-
.input(
|
|
194
|
-
.output(eventIterator(
|
|
195
|
-
/* Run one of this product's own CLI actions on a connected
|
|
194
|
+
.input(DeviceSandboxFlowInputSchema)
|
|
195
|
+
.output(eventIterator(DeviceFlowLineSchema)),
|
|
196
|
+
/* Run one of this product's own CLI actions on a connected device, from a button rather than through an
|
|
196
197
|
* agent. A closed set of names, and the daemon builds the command line from the name (see the schema): the
|
|
197
198
|
* browser never sends one, because the socket underneath also carries `run_command`.
|
|
198
199
|
*
|
|
199
200
|
* Not a stream, unlike the sandbox ops beside it: these are seconds-long CLI calls whose whole answer is the
|
|
200
201
|
* sentence they print at the end, and a stream for that is a shape with nothing to put in it. */
|
|
201
|
-
|
|
202
|
+
runDeviceCommand: oc
|
|
202
203
|
.route({
|
|
203
204
|
method: "POST",
|
|
204
|
-
path: "/system/
|
|
205
|
-
summary: "Run one of your
|
|
205
|
+
path: "/system/devices/{id}/commands/{command}",
|
|
206
|
+
summary: "Run one of your device's own CLI actions",
|
|
206
207
|
description:
|
|
207
|
-
"Performs a named action on a machine you own by running its own intentic-machine command there — turning that
|
|
208
|
+
"Performs a named action on a machine you own by running its own intentic-machine command there — turning that device's port mirroring off, say — over the connection it holds open. The set of actions is fixed and the command line is built here from the name, never sent by the caller. The machine enforces its own permissions and a refusal comes back as its own sentence, naming the switch to flip.",
|
|
208
209
|
})
|
|
209
|
-
.input(
|
|
210
|
-
.output(
|
|
210
|
+
.input(DeviceCommandInputSchema)
|
|
211
|
+
.output(DeviceCommandResultSchema),
|
|
212
|
+
/* Update or restart the agent on one of the user's own devices — the remedy the Devices view used to print
|
|
213
|
+
* as a command to go and type, on the view built to replace that terminal.
|
|
214
|
+
*
|
|
215
|
+
* Streamed like the sandbox ops, and for a reason of its own: this is the one call whose transport the work
|
|
216
|
+
* destroys. Both ops stop the resident process holding the socket, so the stream ends WITHOUT a terminal
|
|
217
|
+
* frame in the ordinary, successful case, and callers must read that as "it started" rather than as a
|
|
218
|
+
* failure (DeviceAgentFlowSchema has the whole argument). The confirmation is the device's agent version
|
|
219
|
+
* moving on the next read of the fleet. */
|
|
220
|
+
runDeviceAgentFlow: oc
|
|
221
|
+
.route({
|
|
222
|
+
method: "POST",
|
|
223
|
+
path: "/system/devices/{id}/agent/{op}",
|
|
224
|
+
summary: "Update or restart the agent on one of your own devices",
|
|
225
|
+
description:
|
|
226
|
+
"Updates a machine you own to the current intentic-machine agent, or restarts the loop it is running, over the connection that machine holds open. The answer is a stream of the run's own output — and it normally stops mid-run, because the agent's loop is what carries this connection: the work is detached from it first, so it finishes regardless, and the device's reported version is what confirms it. Takes the machine's \"Run commands\" permission, the same one a command typed there would.",
|
|
227
|
+
})
|
|
228
|
+
.input(DeviceAgentFlowInputSchema)
|
|
229
|
+
.output(eventIterator(DeviceFlowLineSchema)),
|
|
211
230
|
};
|
|
@@ -5,14 +5,14 @@ import { ForticlientImportInputSchema, ForticlientImportSchema, VpnConnectInputS
|
|
|
5
5
|
|
|
6
6
|
// The live VPN surface. A VPN is ADDED as a `vpn` capability (credentials, autoConnect, capabilities.contract);
|
|
7
7
|
// it is DIALLED here. The split is deliberate: connecting is a runtime operation that both the operator (the
|
|
8
|
-
//
|
|
8
|
+
// VPN capability card) and the agent (`vpn` on its PATH, which calls these very routes) perform many times
|
|
9
9
|
// over one stored connection, so it cannot be a capability re-add, and its result is richer than a
|
|
10
10
|
// CapabilityStatus, which is why `list` returns VpnLinks instead of {state, detail}.
|
|
11
11
|
//
|
|
12
12
|
// Every route reads tunnel state back from the OS rather than from daemon memory, so the agent dropping a
|
|
13
13
|
// tunnel from a shell and the UI dropping it are the same event, and a daemon restart observes the truth.
|
|
14
14
|
export const vpnContract = {
|
|
15
|
-
// Every configured VPN with its live link state, the
|
|
15
|
+
// Every configured VPN with its live link state, the VPN card, the rail indicator, and `vpn list`.
|
|
16
16
|
list: oc
|
|
17
17
|
.route({
|
|
18
18
|
method: "GET",
|
package/src/documents.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
|
-
import { documentOf, documentTitle, isDocumentPath, isPlanDocumentPath
|
|
2
|
+
import { documentOf, documentTitle, isDocumentPath, isPlanDocumentPath } from "./documents.js";
|
|
3
|
+
import { PLAN_DOCUMENTS_DIR } from "./workspace-state.js";
|
|
3
4
|
|
|
4
5
|
const write = (path: string, newText: string, extra: { oldText?: string; truncated?: boolean } = {}) => [
|
|
5
6
|
{ type: "diff" as const, path, newText, ...extra },
|
package/src/documents.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { STATE_DIR } from "@intentic/constants";
|
|
2
1
|
import type { CardDocument, ToolCallContent } from "./events.js";
|
|
3
2
|
import { planParts } from "./title.js";
|
|
3
|
+
import { PLAN_DOCUMENTS_DIR } from "./workspace-state.js";
|
|
4
4
|
|
|
5
5
|
/* A DOCUMENT A TURN WROTE FOR A PERSON TO READ, told apart from the files it changed for the machine.
|
|
6
6
|
*
|
|
@@ -18,14 +18,6 @@ import { planParts } from "./title.js";
|
|
|
18
18
|
* prose it is a mid-sentence slice with no heading, and the DIFF is what a reader wants from an edit anyway. So
|
|
19
19
|
* a Write of a markdown file is a document, and an edit to one stays a diff. */
|
|
20
20
|
|
|
21
|
-
/* WHERE THE CLI'S PLAN FILES LAND, workspace-relative.
|
|
22
|
-
*
|
|
23
|
-
* `~/.claude/plans` is a symlink onto this directory (sessions/session-store.ts links the SDK's conversation
|
|
24
|
-
* state onto the workspace volume), which makes a plan file the one document with a HARNESS-OWNED address: no
|
|
25
|
-
* guessing whether prose is a plan, no threshold on length, the path says so. That is why plan documents get
|
|
26
|
-
* their own treatment while everything else rides the general markdown rule above. */
|
|
27
|
-
export const PLAN_DOCUMENTS_DIR = `${STATE_DIR}/records/sessions/claude/plans`;
|
|
28
|
-
|
|
29
21
|
// Prose, by extension. Deliberately short: a document is something the chat can RENDER, and markdown is what
|
|
30
22
|
// the chat's prose pipeline speaks. A `.txt` report would render as an unstyled wall and reads better as the
|
|
31
23
|
// plain text box it already gets.
|
|
@@ -36,8 +28,12 @@ export const isDocumentPath = (path: string): boolean => {
|
|
|
36
28
|
return DOCUMENT_EXTENSIONS.some((extension) => lower.endsWith(extension));
|
|
37
29
|
};
|
|
38
30
|
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
/* Whether a path is one of the CLI's plan files, which is what earns a document the plan treatment here rather
|
|
32
|
+
* than the general markdown rule above: the harness owns that address, so there is no guessing whether prose is
|
|
33
|
+
* a plan and no threshold on length. Matched on the DIRECTORY rather than on the name, which is a mint-fresh
|
|
34
|
+
* three-word phrase (`map-of-this-wiggly-spring.md`) carrying no signal at all.
|
|
35
|
+
*
|
|
36
|
+
* The directory itself is declared in workspace-state.ts, beside the lock it is the one exception to. */
|
|
41
37
|
export const isPlanDocumentPath = (path: string): boolean => path.startsWith(`${PLAN_DOCUMENTS_DIR}/`);
|
|
42
38
|
|
|
43
39
|
/* What a document is CALLED. Its opening heading when it has one, which is the line the author wrote to name
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { webcrypto } from "node:crypto";
|
|
2
|
+
import { beforeAll, expect, test, vi } from "vitest";
|
|
3
|
+
import { EmbedError, embedFailure, embedUrl, fetchEmbedJson, solveProofOfWork } from "./embed.js";
|
|
4
|
+
|
|
5
|
+
/* The wire every embed speaks before it speaks its own, against a fake fetch and the real WebCrypto. */
|
|
6
|
+
|
|
7
|
+
// jsdom ships no SubtleCrypto; node's is the same WebCrypto the browser exposes, so the solver is exercised
|
|
8
|
+
// against the real digest rather than a stub.
|
|
9
|
+
beforeAll(() => {
|
|
10
|
+
Object.defineProperty(globalThis, "crypto", { value: webcrypto, configurable: true });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// The daemon verifies exactly this: hash the whole answer string and count the leading zero bits.
|
|
14
|
+
const leadingZeroBits = async (answer: string): Promise<number> => {
|
|
15
|
+
const digest = new Uint8Array(await webcrypto.subtle.digest("SHA-256", new TextEncoder().encode(answer)));
|
|
16
|
+
let bits = 0;
|
|
17
|
+
for (const byte of digest) {
|
|
18
|
+
if (byte !== 0) {
|
|
19
|
+
return bits + Math.clz32(byte) - 24;
|
|
20
|
+
}
|
|
21
|
+
bits += 8;
|
|
22
|
+
}
|
|
23
|
+
return bits;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
test("a door's route is spelled the daemon's way, with the automation id encoded", () => {
|
|
27
|
+
expect(embedUrl({ base: "https://sandbox.example", automationId: "a b" }, "webchat", "config")).toBe("https://sandbox.example/webchat/a%20b/config");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("a refusal carries the server's own sentence and status, and a bodyless one says what it can", async () => {
|
|
31
|
+
const said = await embedFailure(new Response(JSON.stringify({ error: "origin not allowed" }), { status: 403 }));
|
|
32
|
+
expect(said).toBeInstanceOf(EmbedError);
|
|
33
|
+
expect(said).toMatchObject({ message: "origin not allowed", status: 403 });
|
|
34
|
+
expect(await embedFailure(new Response("nope", { status: 502 }))).toMatchObject({ message: "request failed (502)", status: 502 });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("a JSON fetch answers the body or throws the refusal", async () => {
|
|
38
|
+
vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({ ok: true }), { status: 200 })));
|
|
39
|
+
expect(await fetchEmbedJson<{ ok: boolean }>("https://x/y")).toEqual({ ok: true });
|
|
40
|
+
vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({ error: "rate limited" }), { status: 429 })));
|
|
41
|
+
await expect(fetchEmbedJson("https://x/y")).rejects.toMatchObject({ message: "rate limited", status: 429 });
|
|
42
|
+
vi.unstubAllGlobals();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/* The one test that pays for real work: 12 bits is ~900 awaited digests, which is what carries the solver past
|
|
46
|
+
* its 512-nonce batch and through the yield the page needs to keep painting. Every digest is a round trip to
|
|
47
|
+
* the platform's crypto, so what that costs is set by how loaded the machine is and never by this code: hence
|
|
48
|
+
* a budget that bounds a hang instead of the suite's 5s hang detector for in-memory work, which a busy runner
|
|
49
|
+
* beat. The other two prove things that need no work at all, and are cheap on purpose. */
|
|
50
|
+
test("the answer really clears the difficulty, and carries the salt back for the daemon to re-derive", async () => {
|
|
51
|
+
const answer = await solveProofOfWork({ salt: "abc123", difficulty: 12 }, "needs https");
|
|
52
|
+
expect(answer.startsWith("abc123:")).toBe(true);
|
|
53
|
+
expect(await leadingZeroBits(answer)).toBeGreaterThanOrEqual(12);
|
|
54
|
+
}, 20_000);
|
|
55
|
+
|
|
56
|
+
test("a different salt yields a different answer: a solution cannot be replayed across callers", async () => {
|
|
57
|
+
const [one, two] = await Promise.all([
|
|
58
|
+
solveProofOfWork({ salt: "salt-one", difficulty: 6 }, "needs https"),
|
|
59
|
+
solveProofOfWork({ salt: "salt-two", difficulty: 6 }, "needs https"),
|
|
60
|
+
]);
|
|
61
|
+
expect(one).not.toBe(two);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("an http:// page is told the truth, in the embed's own words, instead of hanging on a missing SubtleCrypto", async () => {
|
|
65
|
+
Object.defineProperty(globalThis, "crypto", { value: { randomUUID: webcrypto.randomUUID }, configurable: true });
|
|
66
|
+
await expect(solveProofOfWork({ salt: "abc", difficulty: 8 }, "This page must be served over HTTPS to start a chat.")).rejects.toThrow(/HTTPS to start a chat/);
|
|
67
|
+
Object.defineProperty(globalThis, "crypto", { value: webcrypto, configurable: true });
|
|
68
|
+
});
|
package/src/embed.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/* THE ZOD-FREE HALF OF AN EMBED'S WIRE: what every script a customer drops on their own page does before it
|
|
2
|
+
* does anything of its own. Two embeds exist, the Front Desk chat bubble (_sandbox/webchat-widget) and the bug
|
|
3
|
+
* reporter (_sandbox/issue-widget), and each used to carry its own copy of this: the same three requests
|
|
4
|
+
* against a public door, the same proof-of-work solver, the same localStorage-backed id, the same
|
|
5
|
+
* read-my-own-script-tag boot. The daemon's side of the same doors is one module too (automations/public-door.ts).
|
|
6
|
+
*
|
|
7
|
+
* THE RULE FOR WHAT MAY LIVE HERE: no imports, ever. This module is bundled INTO a page that belongs to someone
|
|
8
|
+
* else, so it must cost that page nothing it did not ask for; the contract's barrel would bring zod with it
|
|
9
|
+
* (webext-links.ts measured that at 1.1 MB for two strings). Types are declared here for the same reason. */
|
|
10
|
+
|
|
11
|
+
// Where an embed talks to: the daemon it came from, and the automation it is the public face of.
|
|
12
|
+
export interface EmbedEndpoint {
|
|
13
|
+
readonly base: string;
|
|
14
|
+
readonly automationId: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// What the server said when it refused. The daemon answers every refusal as {"error": "..."}, and that sentence
|
|
18
|
+
// is shown verbatim where a person can see it: "origin not allowed" tells a site owner exactly what to fix and
|
|
19
|
+
// anything invented here would not.
|
|
20
|
+
export class EmbedError extends Error {
|
|
21
|
+
constructor(
|
|
22
|
+
message: string,
|
|
23
|
+
readonly status: number,
|
|
24
|
+
) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "EmbedError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const embedFailure = async (response: Response): Promise<EmbedError> => {
|
|
31
|
+
const body = (await response.json().catch(() => undefined)) as { error?: unknown } | undefined;
|
|
32
|
+
return new EmbedError(typeof body?.error === "string" ? body.error : `request failed (${response.status})`, response.status);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// One route of a public door: `<base>/<slug>/<automation>/<path>`, the daemon's own spelling.
|
|
36
|
+
export const embedUrl = ({ base, automationId }: EmbedEndpoint, slug: string, path: string): string =>
|
|
37
|
+
`${base}/${slug}/${encodeURIComponent(automationId)}/${path}`;
|
|
38
|
+
|
|
39
|
+
// One GET against a public door, answered as JSON or refused with the server's own sentence.
|
|
40
|
+
export const fetchEmbedJson = async <T>(url: string): Promise<T> => {
|
|
41
|
+
const response = await fetch(url);
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
throw await embedFailure(response);
|
|
44
|
+
}
|
|
45
|
+
return (await response.json()) as T;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/* ---- proof of work ----
|
|
49
|
+
*
|
|
50
|
+
* The daemon issues a challenge: find a nonce whose SHA-256 of `${salt}:${nonce}` begins with `difficulty` zero
|
|
51
|
+
* BITS. It costs the person a second or so and costs a script the same per identity it wants to burn, with no
|
|
52
|
+
* third-party account anywhere. The challenge is minted FOR one caller (the daemon signs the caller's own id
|
|
53
|
+
* into the salt), so a solution cannot be carried to another thread or another reporter; hence the id in the
|
|
54
|
+
* challenge request rather than a bare GET.
|
|
55
|
+
*
|
|
56
|
+
* Solved on the main thread in yielding batches rather than in a Worker: a Worker would have to come from a
|
|
57
|
+
* blob: URL, which a host page's Content-Security-Policy is entitled to forbid, and being unable to chat or send
|
|
58
|
+
* feedback because of the SITE's CSP is a worse failure than a busy second. */
|
|
59
|
+
export interface PowChallenge {
|
|
60
|
+
readonly salt: string;
|
|
61
|
+
readonly difficulty: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const fetchEmbedChallenge = (endpoint: EmbedEndpoint, slug: string, param: string, id: string): Promise<PowChallenge> =>
|
|
65
|
+
fetchEmbedJson<PowChallenge>(`${embedUrl(endpoint, slug, "challenge")}?${param}=${encodeURIComponent(id)}`);
|
|
66
|
+
|
|
67
|
+
const BATCH = 512;
|
|
68
|
+
|
|
69
|
+
// Leading zero bits of a digest, up to `wanted`. Stops at the first non-zero byte, so a miss costs one byte.
|
|
70
|
+
const leadingZeroBits = (digest: Uint8Array, wanted: number): number => {
|
|
71
|
+
let bits = 0;
|
|
72
|
+
for (const byte of digest) {
|
|
73
|
+
if (byte !== 0) {
|
|
74
|
+
// Math.clz32 counts 32-bit leading zeros; the byte sits in the low 8, so 24 of them are structural.
|
|
75
|
+
return bits + Math.clz32(byte) - 24;
|
|
76
|
+
}
|
|
77
|
+
bits += 8;
|
|
78
|
+
if (bits >= wanted) {
|
|
79
|
+
return bits;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return bits;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/* Resolves to the ANSWER the daemon expects, `<salt>:<nonce>`, carrying back the salt it signed so it can
|
|
86
|
+
* re-derive the challenge it issued without having stored one. `insecure` is what to say on an http:// page:
|
|
87
|
+
* `crypto.subtle` is only available in a secure context, and the fix is the site's TLS, not anything the
|
|
88
|
+
* person can do, so the sentence names the thing they were trying to do. */
|
|
89
|
+
export const solveProofOfWork = async (challenge: PowChallenge, insecure: string, onProgress?: (attempts: number) => void): Promise<string> => {
|
|
90
|
+
if (crypto.subtle === undefined) {
|
|
91
|
+
throw new Error(insecure);
|
|
92
|
+
}
|
|
93
|
+
const encoder = new TextEncoder();
|
|
94
|
+
for (let nonce = 0; ; nonce += 1) {
|
|
95
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(`${challenge.salt}:${nonce}`)));
|
|
96
|
+
if (leadingZeroBits(digest, challenge.difficulty) >= challenge.difficulty) {
|
|
97
|
+
return `${challenge.salt}:${nonce}`;
|
|
98
|
+
}
|
|
99
|
+
if (nonce % BATCH === BATCH - 1) {
|
|
100
|
+
onProgress?.(nonce + 1);
|
|
101
|
+
// Hand the main thread back so the page (and the embed's own "checking…" line) keeps painting.
|
|
102
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/* ---- what an embed keeps in the visitor's browser ----
|
|
108
|
+
*
|
|
109
|
+
* localStorage throws in Safari's private mode and anywhere the site blocks storage. An embed with no storage
|
|
110
|
+
* still works; it just mints a fresh id per page load, which costs only a slightly less useful thread or
|
|
111
|
+
* rate-limit key. Silence here is deliberate: an embed must never be the thing that puts an error in the
|
|
112
|
+
* console. */
|
|
113
|
+
export const readStored = (name: string): string | undefined => {
|
|
114
|
+
try {
|
|
115
|
+
return window.localStorage.getItem(name) ?? undefined;
|
|
116
|
+
} catch {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const writeStored = (name: string, value: string): void => {
|
|
122
|
+
try {
|
|
123
|
+
window.localStorage.setItem(name, value);
|
|
124
|
+
} catch {
|
|
125
|
+
/* no storage: this page load keeps the value in memory and the next one mints another */
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/* A per-browser id under `name`, minted once and kept. NOT identity and NOT a secret: anyone can mint one, and
|
|
130
|
+
* the daemon treats it as exactly what it is, the key that threads a visitor's messages into one conversation,
|
|
131
|
+
* or that a rate window counts against so one runaway tab cannot spend the whole day's budget. */
|
|
132
|
+
export const storedId = (name: string): string => {
|
|
133
|
+
const existing = readStored(name);
|
|
134
|
+
if (existing !== undefined && existing !== "") {
|
|
135
|
+
return existing;
|
|
136
|
+
}
|
|
137
|
+
const minted = crypto.randomUUID();
|
|
138
|
+
writeStored(name, minted);
|
|
139
|
+
return minted;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/* ---- the <script> tag an embed boots from ----
|
|
143
|
+
*
|
|
144
|
+
* <script src="https://sandbox-<id>.<zone>/<slug>/…js" data-automation="support" defer></script>
|
|
145
|
+
*
|
|
146
|
+
* Everything else is derived: the daemon to talk to is the ORIGIN THIS SCRIPT CAME FROM, which is the one thing
|
|
147
|
+
* a copy-pasted snippet can't get wrong. `data-base` overrides it for a site fronting the sandbox behind its
|
|
148
|
+
* own proxy, the only case where the two legitimately differ.
|
|
149
|
+
*
|
|
150
|
+
* `document.currentScript` is only valid while the script body is executing, so it is read at module scope by
|
|
151
|
+
* the caller rather than inside an async boot. The querySelector is the fallback for a bundler or tag manager
|
|
152
|
+
* that re-executes the entry in a context where currentScript is null. */
|
|
153
|
+
export const embedScript = (srcMatch: string): HTMLScriptElement | null =>
|
|
154
|
+
(document.currentScript as HTMLScriptElement | null) ?? document.querySelector<HTMLScriptElement>(`script[src*="${srcMatch}"]`);
|
|
155
|
+
|
|
156
|
+
// The endpoint a script tag names, or undefined when it carries no automation id, the one mistake worth a
|
|
157
|
+
// console line: without it the embed is silently absent and the site owner has nothing to go on.
|
|
158
|
+
export const embedEndpointOf = (script: HTMLScriptElement): EmbedEndpoint | undefined => {
|
|
159
|
+
const automationId = script.dataset["automation"];
|
|
160
|
+
if (automationId === undefined || automationId === "") {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
return { automationId, base: (script.dataset["base"] ?? new URL(script.src, window.location.href).origin).replace(/\/$/, "") };
|
|
164
|
+
};
|
package/src/events.ts
CHANGED
|
@@ -93,7 +93,7 @@ export const PermissionAskSchema = z.object({
|
|
|
93
93
|
* the title (the judge read the owner's policy and the program, and its account of why this needs asking is
|
|
94
94
|
* the only account there is), so this is left off rather than printing the same words twice. It carries the
|
|
95
95
|
* sentence on the two cards whose title says something the sentence cannot: the hard rule's, which names the
|
|
96
|
-
* consequence that stopped it, and a machine command's, which names the
|
|
96
|
+
* consequence that stopped it, and a machine command's, which names the device.
|
|
97
97
|
*
|
|
98
98
|
* Written by the quick model from the program text and the policy, never by the agent being gated — a card
|
|
99
99
|
* whose persuasive half was authored by the thing it is stopping argues for its own approval, and the turns
|
|
@@ -557,7 +557,7 @@ export const TranscriptToolSchema: z.ZodType<TranscriptTool> = z.lazy(() =>
|
|
|
557
557
|
.array(TranscriptToolSchema)
|
|
558
558
|
.optional()
|
|
559
559
|
.describe(
|
|
560
|
-
"Calls a delegated
|
|
560
|
+
"Calls a delegated subagent made, nested under the call that started it, so a reopened conversation redraws the delegation rather than collapsing it into one result.",
|
|
561
561
|
),
|
|
562
562
|
thinking: z.string().optional().describe("What the agent was reasoning about around this call."),
|
|
563
563
|
subagent: TranscriptSubagentSchema.optional().describe(
|
|
@@ -772,7 +772,7 @@ export const TranscriptPatchSchema = z.discriminatedUnion("op", [
|
|
|
772
772
|
op: z.literal("tool").describe("A tool card, whole: new, or the latest state of one already there, matched by id wherever it nests."),
|
|
773
773
|
index: z.number().int().nonnegative(),
|
|
774
774
|
tool: TranscriptToolSchema,
|
|
775
|
-
parent: z.string().optional().describe("The card this one nests under, when it is a delegated
|
|
775
|
+
parent: z.string().optional().describe("The card this one nests under, when it is a delegated subagent's own call."),
|
|
776
776
|
}),
|
|
777
777
|
]);
|
|
778
778
|
export type TranscriptPatch = z.infer<typeof TranscriptPatchSchema>;
|
|
@@ -857,6 +857,16 @@ export const AgentTranscriptSchema = SessionTranscriptSchema.extend({
|
|
|
857
857
|
ending: TurnEndingSchema.optional().describe(
|
|
858
858
|
"How the last turn ended, when it left work behind that one press finishes. Absent for a conversation whose last turn ended on its own, and for the failures that name something to repair first.",
|
|
859
859
|
),
|
|
860
|
+
/* WHERE THIS PAGE SITS IN THE CONVERSATION. `messages` is the tail, not the whole record: a conversation
|
|
861
|
+
* that ran all week used to be served entire to every tab that opened it and to every card the board warms
|
|
862
|
+
* behind it, which is megabytes over a tunnel to redraw a screenful.
|
|
863
|
+
*
|
|
864
|
+
* `from` is the position of the first message in the WHOLE record, which makes it two things at once: the
|
|
865
|
+
* offset every `rewindIndex` in this page is counted against, and the `before` that asks for the page above
|
|
866
|
+
* it. `more` says whether there is one, so a client can offer to go back without spending a round trip
|
|
867
|
+
* finding out. */
|
|
868
|
+
from: z.number().int().nonnegative().describe("Where the first message sits in the whole record, and the `before` that asks for the page above this one."),
|
|
869
|
+
more: z.boolean().describe("Whether older messages precede this page."),
|
|
860
870
|
});
|
|
861
871
|
|
|
862
872
|
/* WHAT A PUBLISHED CONVERSATION'S PAGE IS HANDED, the whole of it, baked into the page as one JSON block.
|
|
@@ -1334,6 +1344,23 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
1334
1344
|
"unknown-command",
|
|
1335
1345
|
"grok-model-invalid",
|
|
1336
1346
|
"codex-model-invalid",
|
|
1347
|
+
/* THE MODEL IS REAL, LISTED, AND NOT THIS PLAN'S TO RUN. A routed provider's catalog is the set
|
|
1348
|
+
* the vendor publishes, not the set the connected subscription pays for, so a picker row can be
|
|
1349
|
+
* a model the upstream refuses on sight ("Your current subscription does not have access to
|
|
1350
|
+
* kimi-for-coding-highspeed. Upgrade to higher-tier Kimi Code plans.").
|
|
1351
|
+
*
|
|
1352
|
+
* Its own code because every neighbour's recovery is wrong for it. It is not `provider-outage`,
|
|
1353
|
+
* though that is what it wore: the translator answers a refused model with a 503, the harness
|
|
1354
|
+
* reads a 5xx as an outage, and rides it out for the whole in-turn retry budget — two minutes of
|
|
1355
|
+
* a spinner for a request that was refused in five milliseconds and will be refused identically
|
|
1356
|
+
* forever, followed by an auto-resume schedule for a turn that cannot come back. It is not
|
|
1357
|
+
* `*-model-invalid` either: nothing is misspelled and reloading the catalog re-offers the same
|
|
1358
|
+
* row, because the vendor really does serve it — to somebody else.
|
|
1359
|
+
*
|
|
1360
|
+
* What changes the outcome is picking another model or buying the plan, so the sentence is the
|
|
1361
|
+
* upstream's own (it names the tier), the client holds the words, and the daemon files the model
|
|
1362
|
+
* as refused so the picker stops offering it (usage/model-refusals.ts). */
|
|
1363
|
+
"model-unavailable",
|
|
1337
1364
|
/* THE MODEL CANNOT HOLD A TURN OF THIS AGENT LOOP, so the daemon refused before sending
|
|
1338
1365
|
* (agent/context-budget.ts). Its own code because none of the neighbours describes it: nothing is
|
|
1339
1366
|
* disconnected, nothing is spent, nothing comes back on a clock, and re-sending the same request
|
package/src/history-state.ts
CHANGED
|
@@ -67,6 +67,12 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
|
|
|
67
67
|
{ path: "usage.jsonl", portability: "carry" },
|
|
68
68
|
{ path: "account-usage.json", portability: "carry" },
|
|
69
69
|
{ path: "provider-refusals.json", portability: "carry" },
|
|
70
|
+
/* Which MODELS this sandbox's credentials were refused (usage/model-refusals.ts), the per-model half of the
|
|
71
|
+
* entry above and carried for the same reason: a bundle is this owner's sandbox on another machine, signing
|
|
72
|
+
* the same subscriptions back in, so a plan that does not cover a model there does not cover it here
|
|
73
|
+
* either, and the picker should arrive already clean. Cheap to be wrong about in the one case it can be
|
|
74
|
+
* (the target signs in a bigger plan): every entry is forgotten a day after it was written. */
|
|
75
|
+
{ path: "model-refusals.json", portability: "carry" },
|
|
70
76
|
// Explicit first-time dependency setup requests. Carrying the worklist preserves the owner's decision when
|
|
71
77
|
// an export interrupts the queue before its terminal starts; fulfilled entries remove themselves.
|
|
72
78
|
{ path: "dependency-requests.json", portability: "carry" },
|
|
@@ -146,15 +152,18 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
|
|
|
146
152
|
{ path: "sync-enrollments.json", portability: "identity", note: "Re-pair desktop sync from the Sync tab." },
|
|
147
153
|
{ path: "sync-pair-consumed.json", portability: "identity" },
|
|
148
154
|
{ path: "host-enrollments.json", portability: "identity" },
|
|
149
|
-
/* A connected BROWSER's enrollment (webext/webext-
|
|
155
|
+
/* A connected BROWSER's enrollment (webext/webext-peer.ts). Identity for the hosts file's reason and one
|
|
150
156
|
* more of its own: the token admits a socket into somebody's signed-in browser, and that browser was paired
|
|
151
157
|
* with THIS sandbox — carried into another one it would either be dead weight or, worse, a second sandbox
|
|
152
158
|
* holding a live key to a browser its owner never connected it to. Re-pairing is a code and one click. */
|
|
153
159
|
{ path: "webext-enrollments.json", portability: "identity", note: "Pair your browser again from its card: the extension is still installed." },
|
|
154
|
-
// The burn list
|
|
160
|
+
// The browser door's burn list: every peer door keeps one (peers/peer-store.ts), and a browser pairing
|
|
161
|
+
// redeemed here must not read as fresh in a sandbox this file travelled to. Identity, hosts' reason.
|
|
162
|
+
{ path: "webext-pair-consumed.json", portability: "identity" },
|
|
163
|
+
// The burn list for setup-time device pairings. Identity, like sync's beside it, and for a sharper reason:
|
|
155
164
|
// carrying it into another sandbox would mark that sandbox's own fresh pairing as already spent.
|
|
156
165
|
{ path: "host-pair-consumed.json", portability: "identity" },
|
|
157
|
-
// A runner's enrollment names THIS sandbox as its parent (runners/
|
|
166
|
+
// A runner's enrollment names THIS sandbox as its parent (runners/runner-peer.ts): in another sandbox the
|
|
158
167
|
// digest would admit a socket whose runner still dials the old parent. Identity, both files, hosts' reasons.
|
|
159
168
|
{ path: "runner-enrollments.json", portability: "identity" },
|
|
160
169
|
{ path: "runner-pair-consumed.json", portability: "identity" },
|
package/src/host-protocol.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
|
|
3
3
|
/* The handshake on /system/hosts/connect, the ONE message that is not oRPC.
|
|
4
4
|
*
|
|
5
|
-
* Everything a connected
|
|
5
|
+
* Everything a connected device is asked lives in `hostContract` (contracts/host.contract.ts), spoken over
|
|
6
6
|
* this socket by oRPC's websocket adapter: the machine hosts the server, the daemon holds the client. But a
|
|
7
7
|
* socket has to prove whose it is before it can be given a typed client, and that proof cannot itself be an
|
|
8
8
|
* oRPC call, the daemon has nothing to call yet, and would be attaching a link to a stranger.
|
|
@@ -12,7 +12,7 @@ import { z } from "zod";
|
|
|
12
12
|
* wire is oRPC. Anything arriving before the link exists is either this frame or a closed socket. */
|
|
13
13
|
|
|
14
14
|
// The MCP protocol revision the machine's tool server implements. Shared because the daemon answers the
|
|
15
|
-
// handshake ITSELF when the machine is asleep (
|
|
15
|
+
// handshake ITSELF when the machine is asleep (the daemon's peer bridge), two spellings of this would mean an
|
|
16
16
|
// offline machine negotiating a different protocol than the same machine awake.
|
|
17
17
|
export const MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
18
18
|
|