@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
package/src/command-classes.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HISTORY_ROOT, WORKSPACE_ROOT } from "@intentic/constants";
|
|
2
|
+
import { type CommandClass, CommandClassSchema, type CommandLocus } from "./schemas/agent.js";
|
|
3
|
+
import { inertRegions, isLive } from "./shell-regions.js";
|
|
2
4
|
|
|
3
5
|
/* WHICH CLASSES A SHELL COMMAND FALLS IN, the classifier behind every command gate, read before the command runs.
|
|
4
6
|
*
|
|
5
7
|
* IT LIVES IN THE CONTRACT PACKAGE because there are TWO enforcement points and they must not drift. The
|
|
6
8
|
* sandbox's own gate (sandbox/src/guard/command-gate.ts) judges what the agent types here; the machine agent's
|
|
7
|
-
* shell tool (
|
|
9
|
+
* shell tool (_devices/machine/src/device/tools/shell.ts) judges what it sends to somebody's laptop. Those answer to
|
|
8
10
|
* different policy (a rulebook with a permission card here, a scope switch on the card there) but they must
|
|
9
11
|
* agree on WHAT A COMMAND IS, or a class the owner thought they had covered turns out to mean something else on
|
|
10
12
|
* the surface where it mattered most. One table, two consults.
|
|
@@ -18,7 +20,7 @@ import { type CommandClass, CommandClassSchema } from "./schemas/agent.js";
|
|
|
18
20
|
* A creatively quoted command, a path assembled from a variable, or a script written in one call and run in the
|
|
19
21
|
* next goes past it untouched. So a gate built on this is friction and a prompt for well-behaved work, never a
|
|
20
22
|
* boundary, the boundaries are structural and elsewhere: the container, the isolated worktree, the land gate,
|
|
21
|
-
* an automation's tool allowlist, and, on somebody's own
|
|
23
|
+
* an automation's tool allowlist, and, on somebody's own device, the scope switches enforced there.
|
|
22
24
|
*
|
|
23
25
|
* WHICH IS WHY THIS NO LONGER DECIDES ANYTHING. A match used to BE the verdict: whatever fired here became the
|
|
24
26
|
* permission card, so `echo "rm -rf /"`, `rg 'rm -rf'` and an actual delete were one question with one answer.
|
|
@@ -28,6 +30,18 @@ import { type CommandClass, CommandClassSchema } from "./schemas/agent.js";
|
|
|
28
30
|
* positive now costs one model call rather than one interruption, and a miss still costs everything. Anyone
|
|
29
31
|
* tuning a pattern below should widen rather than narrow it.
|
|
30
32
|
*
|
|
33
|
+
* WITH ONE EXCEPTION, AND IT IS WHY `live` EXISTS. The hard rule (safety-policy.ts hardRuleClasses) still turns
|
|
34
|
+
* a match straight into an interruption nothing can waive, so for the classes it covers a false positive costs
|
|
35
|
+
* exactly what it always did. That left the old failure standing in the one place it could not be argued out
|
|
36
|
+
* of: `echo "rm -rf /" >> notes.md` and `rg 'rm -rf /'` were un-waivable cards over a string. So every match
|
|
37
|
+
* carries whether a shell would RUN the fragment or merely print, search or write it (shell-regions.ts), and
|
|
38
|
+
* only the hard rule reads that bit. The tables stay over-inclusive; the un-waivable tier stops firing on text.
|
|
39
|
+
*
|
|
40
|
+
* AND IT IS ASKED OF A PLACE. `rm -rf /usr` ends a laptop and costs a container nothing, because a container is
|
|
41
|
+
* rebuilt from an image; a Docker volume here is a dev database the agent made and there is the owner's data.
|
|
42
|
+
* Half the tables below therefore have two answers, chosen by CommandContext.locus, and the caller must say
|
|
43
|
+
* which machine it is asking about. schemas/agent.ts CommandLocus argues the split.
|
|
44
|
+
*
|
|
31
45
|
* Matching is deliberately UNANCHORED, substrings, not line starts. Another PreToolUse hook may have rewrapped
|
|
32
46
|
* the command by the time this reads it (agent-terminals.ts wraps every Bash call in bin/tmux-run), and the
|
|
33
47
|
* agent's own line survives verbatim inside that wrapper. Nothing the wrapper adds is in any class below.
|
|
@@ -58,16 +72,30 @@ export interface CommandSpan {
|
|
|
58
72
|
readonly end: number;
|
|
59
73
|
}
|
|
60
74
|
|
|
61
|
-
|
|
62
|
-
|
|
75
|
+
/* One class the command fell in, and the fragments that put it there. `spans` is never empty: a class with
|
|
76
|
+
* nothing to point at is a class this walk does not report. */
|
|
63
77
|
export interface CommandMatch {
|
|
64
78
|
readonly commandClass: CommandClass;
|
|
65
79
|
readonly spans: readonly CommandSpan[];
|
|
80
|
+
/* Would a shell RUN any of those fragments, or are they all text — a heredoc body, a comment, a quoted
|
|
81
|
+
* argument to echo or a grep (shell-regions.ts says how that is decided and how wrong it is allowed to be)?
|
|
82
|
+
*
|
|
83
|
+
* ONLY THE HARD RULE READS THIS, and that is the whole point of it being a flag on the match rather than a
|
|
84
|
+
* filter over the tables. A mention still puts the command in the class, still reaches the judge, and is
|
|
85
|
+
* still marked on the card: the judge is the tier that can tell a README from a delete, and taking the
|
|
86
|
+
* class away would take the question away from it. What a mention must NOT do is trip the one tier that
|
|
87
|
+
* cannot be argued with, which is what `echo "rm -rf /" >> notes.md` used to do. */
|
|
88
|
+
readonly live: boolean;
|
|
66
89
|
}
|
|
67
90
|
|
|
68
91
|
/* WHAT THE CALLER CAN CHECK THAT THE PATTERNS CANNOT. Optional everywhere: absent ⇒ every table answers from
|
|
69
92
|
* the command text alone, which is what the browser, the machine agent and every test that does not care get. */
|
|
70
93
|
export interface CommandContext {
|
|
94
|
+
/* WHERE THIS COMMAND WOULD RUN, and it is the one field with no default. Half this catalog means something
|
|
95
|
+
* different on a disposable container than on somebody's laptop — which directories are roots, whether a
|
|
96
|
+
* Docker volume is a dev database or their data — and a default would be one of those two answers applied
|
|
97
|
+
* silently to the other machine. Callers state it; schemas/agent.ts CommandLocus argues the split. */
|
|
98
|
+
readonly locus: CommandLocus;
|
|
71
99
|
/* Does the file at this path — as the command spells it, `~/.npmrc`, `.env`, `/work/app/.env.local` — hold
|
|
72
100
|
* credential material? (credential-material.ts says what that means; the caller says how to read a file.)
|
|
73
101
|
*
|
|
@@ -251,16 +279,38 @@ const parseRm = (command: string): RmInvocation[] => {
|
|
|
251
279
|
};
|
|
252
280
|
|
|
253
281
|
/* A TARGET THAT IS A ROOT RATHER THAN SOMETHING INSIDE ONE. This is the whole difference between the two
|
|
254
|
-
* deletion classes, so it is deliberately a short, closed list of whole names rather than a clever heuristic
|
|
255
|
-
* the filesystem root and the top-level directories an OS keeps, the two trees this product keeps state in, a
|
|
256
|
-
* home directory however it is spelled, and a Windows drive.
|
|
282
|
+
* deletion classes, so it is deliberately a short, closed list of whole names rather than a clever heuristic.
|
|
257
283
|
*
|
|
258
|
-
*
|
|
259
|
-
|
|
260
|
-
|
|
284
|
+
* AND IT IS A DIFFERENT LIST PER LOCUS, because "root" means "the thing nothing here brings back" and the two
|
|
285
|
+
* machines answer that very differently. Everything below is measured against one question: after this delete,
|
|
286
|
+
* what restores it?
|
|
287
|
+
*
|
|
288
|
+
* `/tmp` is deliberately absent from both. It is scratch by definition and emptying it is a chore, not an
|
|
289
|
+
* incident. */
|
|
290
|
+
|
|
291
|
+
/* IN THIS SANDBOX: two entries, and the shortness is the point rather than an oversight.
|
|
292
|
+
*
|
|
293
|
+
* `` the filesystem root itself (what trimTarget normalizes `/` to). Nothing restores it.
|
|
294
|
+
* /history every OTHER conversation's worktrees and logs. THIS turn cannot recreate them at any price,
|
|
295
|
+
* which is exactly what makes it a root here and the reason it outranks anything in /work.
|
|
296
|
+
*
|
|
297
|
+
* WHAT IS DELIBERATELY NOT HERE, and each was hard-ruled before this: `/usr`, `/etc`, `/bin`, `/var`, `/opt`
|
|
298
|
+
* and the rest of the container's OS come back with the image — recreating this sandbox is a documented
|
|
299
|
+
* operation, not a catastrophe. `/work` is a git worktree whose delta lands as uncommitted changes, and it is
|
|
300
|
+
* also the directory an agent has the most legitimate reason to clear. `/Users`, `/Applications`, `/System`,
|
|
301
|
+
* `/Library` and a Windows drive do not exist in this container at all, so holding them here bought nothing and
|
|
302
|
+
* cost a card. All of them are still files.destructive, still triaged, still judged — they have simply stopped
|
|
303
|
+
* being un-waivable. */
|
|
304
|
+
const SANDBOX_ROOTS = new Set(["", HISTORY_ROOT]);
|
|
305
|
+
|
|
306
|
+
/* ON THE OWNER'S OWN COMPUTER: the full list, because nothing there is rebuilt from an image and there is no
|
|
307
|
+
* checkpoint under any of it. The filesystem root, the top-level directories an OS keeps, both of this
|
|
308
|
+
* product's own trees (a device may be running one), a home directory however it is spelled, and a Windows
|
|
309
|
+
* drive. Unchanged from what this catalog held before the split. */
|
|
310
|
+
const DEVICE_ROOTS = new Set([
|
|
261
311
|
"",
|
|
262
|
-
|
|
263
|
-
|
|
312
|
+
WORKSPACE_ROOT,
|
|
313
|
+
HISTORY_ROOT,
|
|
264
314
|
"/home",
|
|
265
315
|
"/root",
|
|
266
316
|
"/etc",
|
|
@@ -284,6 +334,8 @@ const ROOT_DIRECTORIES = new Set([
|
|
|
284
334
|
"/Library",
|
|
285
335
|
]);
|
|
286
336
|
|
|
337
|
+
const rootsAt = (locus: CommandLocus): ReadonlySet<string> => (locus === "sandbox" ? SANDBOX_ROOTS : DEVICE_ROOTS);
|
|
338
|
+
|
|
287
339
|
const HOME_ALIAS = /^(?:~|\$HOME|\$\{HOME\}|%USERPROFILE%)$/;
|
|
288
340
|
const WINDOWS_DRIVE = /^[A-Za-z]:$/;
|
|
289
341
|
|
|
@@ -293,14 +345,16 @@ const WINDOWS_DRIVE = /^[A-Za-z]:$/;
|
|
|
293
345
|
* case this class exists to stay out of the way of. */
|
|
294
346
|
const trimTarget = (operand: string): string => operand.replace(/[/\\]\*+$/, "").replace(/[/\\]+$/, "");
|
|
295
347
|
|
|
296
|
-
const isRootTarget = (operand: string): boolean => {
|
|
348
|
+
const isRootTarget = (operand: string, locus: CommandLocus): boolean => {
|
|
297
349
|
const target = trimTarget(operand);
|
|
350
|
+
/* A home directory and a Windows drive are roots on a DEVICE only. In the container `~` is the agent's own
|
|
351
|
+
* scratch home, rebuilt with the image, and `C:` is not a path that exists. */
|
|
298
352
|
if (HOME_ALIAS.test(target) || WINDOWS_DRIVE.test(target)) {
|
|
299
|
-
return
|
|
353
|
+
return locus === "device";
|
|
300
354
|
}
|
|
301
355
|
// Only an absolute path can name a root, and `""` is the root itself. A relative path is inside whatever
|
|
302
356
|
// the shell is standing in, which this cannot know and must not guess about.
|
|
303
|
-
return target === "" ? operand.startsWith("/") || operand.startsWith("\\") :
|
|
357
|
+
return target === "" ? operand.startsWith("/") || operand.startsWith("\\") : rootsAt(locus).has(target);
|
|
304
358
|
};
|
|
305
359
|
|
|
306
360
|
/* --- the JS execution backend's own deletes -------------------------------------------------------------
|
|
@@ -355,9 +409,13 @@ const nodeDeleteTargets = (program: string): { readonly target: string; readonly
|
|
|
355
409
|
* it), `docker image prune` (pull it again), `git reset --hard` (that is git.destructive, and the reflog has
|
|
356
410
|
* it), `rm -rf node_modules` (install it again). The point of a floor is that it is rare enough to be worth
|
|
357
411
|
* stopping for; a floor that fires on ordinary work is one people learn to click through. */
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
412
|
+
/* A BLOCK DEVICE, FORMATTED, WIPED OR OVERWRITTEN. The only membership of system.destructive that does not
|
|
413
|
+
* depend on where the command runs: there is no image, checkpoint or worktree behind a disk at either locus,
|
|
414
|
+
* and this is what the shipped safety policy has always told the owner it holds unconditionally.
|
|
415
|
+
*
|
|
416
|
+
* `dd` only counts when it is pointed AT a device: reading one into a file is how an image is taken, and
|
|
417
|
+
* holding a backup would be exactly the wrong lesson. */
|
|
418
|
+
const BLOCK_DEVICE = [
|
|
361
419
|
/\bmkfs(?:\.\w+)?\b/,
|
|
362
420
|
/\bwipefs\b/,
|
|
363
421
|
/\bblkdiscard\b/,
|
|
@@ -366,11 +424,22 @@ const SYSTEM_DESTRUCTIVE = [
|
|
|
366
424
|
/\bshred\b[^|;&]*\s\/dev\//,
|
|
367
425
|
// A redirect straight onto a disk device, which is the same wipe without the ceremony.
|
|
368
426
|
/>\s*\/dev\/(?:[shv]d[a-z]|nvme\d|disk\d|mmcblk\d)/,
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
427
|
+
];
|
|
428
|
+
|
|
429
|
+
/* CONTAINER STATE THAT IS DATA RATHER THAN IMAGE, its own class (container.state) because the two loci
|
|
430
|
+
* disagree about it more sharply than about anything else in this catalog.
|
|
431
|
+
*
|
|
432
|
+
* IN THIS SANDBOX these reach the NESTED engine — the host's Docker socket is never mounted, see
|
|
433
|
+
* capabilities/handlers/docker.ts — so the volumes in reach are the ones the agent itself created, and tearing
|
|
434
|
+
* down a smoke-test stack it just brought up is ordinary work. Holding it as an un-waivable card was the
|
|
435
|
+
* concrete complaint that produced this split: `docker volume rm` on a throwaway test container is not the
|
|
436
|
+
* class of thing a person needs woken for.
|
|
437
|
+
*
|
|
438
|
+
* ON SOMEBODY'S OWN COMPUTER a named volume IS the database, and the owner's policy says never. It stays
|
|
439
|
+
* hard-ruled there (safety-policy.ts hardRuleClasses), and the machine's own `destructive` scope sits under
|
|
440
|
+
* that (machine/src/device/tools/shell.ts GATED_CLASSES), which is the part that is a boundary rather than
|
|
441
|
+
* friction. */
|
|
442
|
+
const CONTAINER_STATE = [
|
|
374
443
|
/\b(?:docker|podman)\s+volume\s+(?:rm|remove|prune)\b/,
|
|
375
444
|
/\b(?:docker|podman)\s+system\s+prune\b/,
|
|
376
445
|
/\b(?:docker(?:\s+compose|-compose)?|podman-compose)\s+down\b[^|;&]*\s(?:-v\b|--volumes\b)/,
|
|
@@ -381,25 +450,27 @@ const recursiveForceRms = (command: string): CommandSpan[] =>
|
|
|
381
450
|
.filter((invocation) => invocation.recursive && invocation.force)
|
|
382
451
|
.map((invocation) => invocation.span);
|
|
383
452
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
453
|
+
/* A recursive delete aimed at a root, in either spelling the gate can be handed: the shell's `rm -rf /` and
|
|
454
|
+
* the script's `fs.rmSync("/", { recursive: true })`. Which targets count as roots is the locus's answer, so
|
|
455
|
+
* `rm -rf /usr` is this class on a laptop and merely files.destructive in a container built from an image. */
|
|
456
|
+
const rootDeletes = (program: string, locus: CommandLocus): CommandSpan[] => [
|
|
387
457
|
...parseRm(program)
|
|
388
|
-
.filter((invocation) => invocation.recursive && invocation.force && invocation.operands.some(isRootTarget))
|
|
458
|
+
.filter((invocation) => invocation.recursive && invocation.force && invocation.operands.some((operand) => isRootTarget(operand, locus)))
|
|
389
459
|
.map((invocation) => invocation.span),
|
|
390
460
|
...nodeDeleteTargets(program)
|
|
391
|
-
.filter((delete_) => isRootTarget(delete_.target))
|
|
461
|
+
.filter((delete_) => isRootTarget(delete_.target, locus))
|
|
392
462
|
.map((delete_) => delete_.span),
|
|
393
463
|
];
|
|
394
464
|
|
|
395
465
|
// The `g` twins, built once at load rather than per call: a card is minted per held command and a classify runs
|
|
396
|
-
// per command the agent types, so recompiling
|
|
466
|
+
// per command the agent types, so recompiling seven tables of patterns each time is work with no reader.
|
|
397
467
|
const GIT_DESTRUCTIVE_G = globally(GIT_DESTRUCTIVE);
|
|
398
468
|
const SECRET_REFERENCES_G = globally(SECRET_REFERENCES);
|
|
399
469
|
const CREDENTIAL_PATHS_G = globally(CREDENTIAL_PATHS);
|
|
400
470
|
const PACKAGE_PUBLISH_G = globally(PACKAGE_PUBLISH);
|
|
401
471
|
const NETWORK_OUTBOUND_G = globally(NETWORK_OUTBOUND);
|
|
402
|
-
const
|
|
472
|
+
const BLOCK_DEVICE_G = globally(BLOCK_DEVICE);
|
|
473
|
+
const CONTAINER_STATE_G = globally(CONTAINER_STATE);
|
|
403
474
|
|
|
404
475
|
/* THE PATH A MATCHED FRAGMENT SITS IN, so the oracle is asked about the file the command would actually open
|
|
405
476
|
* rather than about the suffix that fired: `sed 's/…/' ~/.npmrc` fires on `.npmrc` and must ask about
|
|
@@ -463,20 +534,21 @@ const namesAPattern = (word: string): boolean =>
|
|
|
463
534
|
* `!== false` is the whole fact-check, and the comparison is written against `false` rather than for `true` on
|
|
464
535
|
* purpose: `undefined` (nobody could look) has to behave like `true` (there is a credential in there), or the
|
|
465
536
|
* class would evaporate on every caller without a filesystem. */
|
|
466
|
-
const credentialReads = (command: string, context: CommandContext
|
|
537
|
+
const credentialReads = (command: string, context: CommandContext): CommandSpan[] => [
|
|
467
538
|
...spansOf(SECRET_REFERENCES_G, command),
|
|
468
539
|
...spansOf(CREDENTIAL_PATHS_G, command).filter((span) => {
|
|
469
540
|
const word = enclosingPath(command, span);
|
|
470
|
-
return !namesAPattern(word) && context
|
|
541
|
+
return !namesAPattern(word) && context.holdsSecret?.(word) !== false;
|
|
471
542
|
}),
|
|
472
543
|
];
|
|
473
544
|
|
|
474
545
|
// WHERE each class fires, one entry per class. Empty ⇒ the command is not in it, so membership and evidence are
|
|
475
546
|
// the same walk and cannot disagree: there is no way to be held for a class with nothing to show for it.
|
|
476
|
-
const MATCHES: Readonly<Record<CommandClass, (command: string, context: CommandContext
|
|
547
|
+
const MATCHES: Readonly<Record<CommandClass, (command: string, context: CommandContext) => CommandSpan[]>> = {
|
|
477
548
|
"git.destructive": (command) => spansOf(GIT_DESTRUCTIVE_G, command),
|
|
478
549
|
"files.destructive": (command) => [...recursiveForceRms(command), ...recursiveDeletes(command)],
|
|
479
|
-
"system.destructive": (command) => [...spansOf(
|
|
550
|
+
"system.destructive": (command, context) => [...spansOf(BLOCK_DEVICE_G, command), ...rootDeletes(command, context.locus)],
|
|
551
|
+
"container.state": (command) => spansOf(CONTAINER_STATE_G, command),
|
|
480
552
|
"secrets.access": credentialReads,
|
|
481
553
|
"package.publish": (command) => spansOf(PACKAGE_PUBLISH_G, command),
|
|
482
554
|
"network.outbound": (command) => spansOf(NETWORK_OUTBOUND_G, command),
|
|
@@ -485,31 +557,61 @@ const MATCHES: Readonly<Record<CommandClass, (command: string, context: CommandC
|
|
|
485
557
|
/* Every class the command falls in AND the fragments that put it there, in the catalog's own order so a card and
|
|
486
558
|
* a log name them the same way twice. The primitive; classifyCommand is this with the offsets dropped.
|
|
487
559
|
*
|
|
488
|
-
* `context` is
|
|
489
|
-
*
|
|
490
|
-
|
|
491
|
-
|
|
560
|
+
* `context` is REQUIRED, unlike before: its `locus` decides what half of this catalog means (see
|
|
561
|
+
* CommandContext), and the fact-check is the optional part of it.
|
|
562
|
+
*
|
|
563
|
+
* THE INERT SCAN RUNS ONCE, here, and is handed to every class rather than being redone per table: it walks the
|
|
564
|
+
* whole command, and seven walks would be six more than the answer needs. A class with no live span is still
|
|
565
|
+
* reported — `live` rides on the match and only the hard rule reads it (shell-regions.ts argues why). */
|
|
566
|
+
export const matchCommand = (command: string, context: CommandContext): CommandMatch[] => {
|
|
567
|
+
const regions = inertRegions(command);
|
|
568
|
+
return CommandClassSchema.options.flatMap((commandClass) => {
|
|
492
569
|
const spans = mergeSpans(MATCHES[commandClass](command, context));
|
|
493
|
-
return spans.length === 0 ? [] : [{ commandClass, spans }];
|
|
570
|
+
return spans.length === 0 ? [] : [{ commandClass, spans, live: spans.some((span) => isLive(span, regions)) }];
|
|
494
571
|
});
|
|
572
|
+
};
|
|
495
573
|
|
|
496
574
|
// Every class the command falls in, for the callers that only take a verdict from it (the gate's rulebook
|
|
497
575
|
// consult, the machine agent's scope switch).
|
|
498
|
-
export const classifyCommand = (command: string, context
|
|
576
|
+
export const classifyCommand = (command: string, context: CommandContext): CommandClass[] =>
|
|
499
577
|
matchCommand(command, context).map((match) => match.commandClass);
|
|
500
578
|
|
|
501
579
|
// What the card says the command would DO. The class name is a settings key, not a sentence to show a person.
|
|
502
580
|
export const COMMAND_CLASS_LABELS: Readonly<Record<CommandClass, string>> = {
|
|
503
581
|
"git.destructive": "rewrite or discard git history",
|
|
504
582
|
"files.destructive": "delete files recursively",
|
|
505
|
-
"system.destructive": "wipe a disk,
|
|
583
|
+
"system.destructive": "wipe a disk, or delete a whole root directory",
|
|
584
|
+
"container.state": "delete a container volume or the data in it",
|
|
506
585
|
"secrets.access": "read credential material",
|
|
507
586
|
"package.publish": "publish or release a package",
|
|
508
587
|
"network.outbound": "send a request out to the internet",
|
|
509
588
|
};
|
|
510
589
|
|
|
590
|
+
/* THE PATTERNS BEHIND EACH CLASS, IN WORDS, for the one reader that is a person rather than a gate: the Safety
|
|
591
|
+
* page's "What gets stopped" panel (editor/web/.../AgentSafetyRules.vue). A card says which class fired; this
|
|
592
|
+
* says what the class is, so an owner can see the whole catalog without reading this file.
|
|
593
|
+
*
|
|
594
|
+
* PROSE RATHER THAN THE REGEXES THEMSELVES, deliberately. A `/\b(?:docker|podman)\s+volume\s+(?:rm|remove|prune)\b/`
|
|
595
|
+
* on a settings page is a worse answer to "what stops my commands" than "docker volume rm, remove, prune" is,
|
|
596
|
+
* and rendering source at somebody implies they can edit it. Pinned to the tables by the conformance test in
|
|
597
|
+
* command-classes.test.ts, so a pattern added without a line here fails the suite rather than going unlisted. */
|
|
598
|
+
export const COMMAND_CLASS_PATTERNS: Readonly<Record<CommandClass, readonly string[]>> = {
|
|
599
|
+
"git.destructive": ["git push --force / -f / --force-with-lease / --delete", "git reset --hard", "git clean -f", "git branch -D", "git filter-branch"],
|
|
600
|
+
"files.destructive": ["rm -rf <path>", "fs.rm / rmSync / rmdir with recursive: true", "rimraf(<path>)"],
|
|
601
|
+
"system.destructive": [
|
|
602
|
+
"mkfs, wipefs, blkdiscard, sgdisk --zap-all",
|
|
603
|
+
"dd of=/dev/…, shred /dev/…, > /dev/sda",
|
|
604
|
+
"rm -rf aimed at a root directory",
|
|
605
|
+
],
|
|
606
|
+
"container.state": ["docker / podman volume rm, remove, prune", "docker / podman system prune", "docker compose down -v"],
|
|
607
|
+
"secrets.access": ["a {{secret:NAME}} reference in the command", ".env, .ssh/*, id_rsa, .aws/credentials, .npmrc, .git-credentials"],
|
|
608
|
+
"package.publish": ["npm / pnpm / yarn / bun publish", "cargo publish", "gh release create", "docker push", "twine upload"],
|
|
609
|
+
"network.outbound": ["curl / wget to a non-loopback https:// host", 'fetch("https://…") in a script'],
|
|
610
|
+
};
|
|
611
|
+
|
|
511
612
|
/* No verdict set lives here any more. Which classes are worth stopping for is a POLICY question now, and it is
|
|
512
|
-
* answered in two places that are honest about being different: safety-policy.ts's
|
|
513
|
-
*
|
|
514
|
-
* its own set beside its scope switches (machine/src/
|
|
515
|
-
* the destructive switch" is a question about that capability card rather than about this
|
|
613
|
+
* answered in two places that are honest about being different: safety-policy.ts's hardRuleClasses for the
|
|
614
|
+
* things nothing recovers at a given locus, and the owner's own written policy for everything else. The machine
|
|
615
|
+
* agent keeps its own set beside its scope switches (machine/src/device/tools/shell.ts), because "which
|
|
616
|
+
* commands need the destructive switch" is a question about that capability card rather than about this
|
|
617
|
+
* catalog. */
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { oc } from "@orpc/contract";
|
|
2
|
+
import { NativeProviderParamSchema } from "../schemas/agent.js";
|
|
3
|
+
import {
|
|
4
|
+
AccountIdSchema,
|
|
5
|
+
AccountListQuerySchema,
|
|
6
|
+
AccountRenameSchema,
|
|
7
|
+
LoginCancelSchema,
|
|
8
|
+
LoginCompletedSchema,
|
|
9
|
+
LoginCompleteSchema,
|
|
10
|
+
LoginRequestSchema,
|
|
11
|
+
LoginStartSchema,
|
|
12
|
+
OauthAccountListSchema,
|
|
13
|
+
OauthAccountSchema,
|
|
14
|
+
} from "../schemas/provider-oauth.js";
|
|
15
|
+
import { OkSchema } from "../schemas/shared.js";
|
|
16
|
+
|
|
17
|
+
/* THE ACCOUNTS THIS SANDBOX HOLDS ITSELF, one route family with the provider as a PARAMETER.
|
|
18
|
+
*
|
|
19
|
+
* Four families used to serve this (Claude's, Cursor's, Grok's, and one for every provider whose sign-in mints
|
|
20
|
+
* its own key), each "the previous one's shape" with a verb renamed: `oauth/start` here, `login/start` there,
|
|
21
|
+
* an `exchange` for the flow whose proof travelled and a `complete` for the one whose proof did not. The
|
|
22
|
+
* lesson providers.contract.ts learned about model catalogs holds for accounts too: the operations are the
|
|
23
|
+
* same six for all of them, so the id is a parameter and a provider's own mechanism is its module's to declare
|
|
24
|
+
* (the daemon's ProviderModule.accounts), not a vertical slice through the contract, the router and every test
|
|
25
|
+
* double.
|
|
26
|
+
*
|
|
27
|
+
* WHY NOT THE TRANSLATOR ROUTES. Those address subscriptions CLIProxyAPI holds and re-serves behind an Anthropic
|
|
28
|
+
* endpoint; these credentials live in this daemon's own auth tree (or, for Grok, in OpenCode's), and a
|
|
29
|
+
* disconnect here removes a file rather than asking a proxy to forget one. A provider served both ways (Grok)
|
|
30
|
+
* has a row in each.
|
|
31
|
+
*
|
|
32
|
+
* Nothing redeemable is on any answer here: `accounts` is an OauthAccount list, whose shape has no field a
|
|
33
|
+
* credential could ride in, and a sign-in's proof never leaves the sandbox (provider-oauth.ts, LoginStart). */
|
|
34
|
+
export const accountsContract = {
|
|
35
|
+
start: oc
|
|
36
|
+
.route({
|
|
37
|
+
method: "POST",
|
|
38
|
+
path: "/accounts/{provider}/login/start",
|
|
39
|
+
summary: "Begin connecting an account",
|
|
40
|
+
description:
|
|
41
|
+
"Hands back the page to sign in on, and the code it will ask for where there is one. The sandbox holds the proof and finishes what it can itself: a device sign-in lands in the account list on its own, a paste or a redirect needs one thing brought back to the finishing call.",
|
|
42
|
+
})
|
|
43
|
+
.input(NativeProviderParamSchema.extend(LoginRequestSchema.shape))
|
|
44
|
+
.output(LoginStartSchema),
|
|
45
|
+
complete: oc
|
|
46
|
+
.route({
|
|
47
|
+
method: "POST",
|
|
48
|
+
path: "/accounts/{provider}/login/complete",
|
|
49
|
+
summary: "Finish a sign-in with what the page handed back",
|
|
50
|
+
description:
|
|
51
|
+
"Takes the code the page showed, or the address a redirect landed on, and finishes the attempt. Answers with the account where the exchange ends here; otherwise the sandbox still has a mint to do and the row appears in the account list.",
|
|
52
|
+
})
|
|
53
|
+
.input(NativeProviderParamSchema.extend(LoginCompleteSchema.shape))
|
|
54
|
+
.output(LoginCompletedSchema),
|
|
55
|
+
cancel: oc
|
|
56
|
+
.route({
|
|
57
|
+
method: "POST",
|
|
58
|
+
path: "/accounts/{provider}/login/cancel",
|
|
59
|
+
summary: "Abandon a sign-in",
|
|
60
|
+
description: "Stops waiting on a sign-in nobody completed. An abandoned attempt also expires on its own.",
|
|
61
|
+
})
|
|
62
|
+
.input(NativeProviderParamSchema.extend(LoginCancelSchema.shape))
|
|
63
|
+
.output(OkSchema),
|
|
64
|
+
// Each account carries its plan-limit reading where the provider publishes one. `force` re-measures before
|
|
65
|
+
// answering (AccountListQuerySchema); a provider with nothing to measure accepts it and answers at once.
|
|
66
|
+
accounts: oc
|
|
67
|
+
.route({
|
|
68
|
+
method: "GET",
|
|
69
|
+
path: "/accounts/{provider}",
|
|
70
|
+
summary: "Connected accounts of a provider",
|
|
71
|
+
description:
|
|
72
|
+
"Each connected account with how full its plan limits were when last measured, where the provider publishes any. Ask for a fresh measurement and it takes one before answering, which is slower. The credentials themselves never travel: being in this list is what connected means.",
|
|
73
|
+
})
|
|
74
|
+
.input(NativeProviderParamSchema.extend(AccountListQuerySchema.shape))
|
|
75
|
+
.output(OauthAccountListSchema),
|
|
76
|
+
rename: oc
|
|
77
|
+
.route({
|
|
78
|
+
method: "POST",
|
|
79
|
+
path: "/accounts/{provider}/rename",
|
|
80
|
+
summary: "Rename an account",
|
|
81
|
+
description: "Changes the label one account shows under, so several are tellable apart. Blank restores the one derived from the sign-in.",
|
|
82
|
+
})
|
|
83
|
+
.input(NativeProviderParamSchema.extend(AccountRenameSchema.shape))
|
|
84
|
+
.output(OauthAccountSchema),
|
|
85
|
+
disconnect: oc
|
|
86
|
+
.route({
|
|
87
|
+
method: "POST",
|
|
88
|
+
path: "/accounts/{provider}/disconnect",
|
|
89
|
+
summary: "Disconnect an account",
|
|
90
|
+
description: "Clears one stored credential, and stops any sign-in still in flight for this provider. The others stay connected.",
|
|
91
|
+
})
|
|
92
|
+
.input(NativeProviderParamSchema.extend(AccountIdSchema.shape))
|
|
93
|
+
.output(OkSchema),
|
|
94
|
+
};
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
AgentAutoLandSchema,
|
|
6
6
|
AgentFileDiffQuerySchema,
|
|
7
7
|
AgentIdSchema,
|
|
8
|
+
AgentTranscriptQuerySchema,
|
|
8
9
|
AgentIdsSchema,
|
|
9
10
|
AgentLandSchema,
|
|
10
11
|
AgentPlaceSchema,
|
|
@@ -94,11 +95,11 @@ export const agentsContract = {
|
|
|
94
95
|
.route({
|
|
95
96
|
method: "GET",
|
|
96
97
|
path: "/agents/{id}/transcript",
|
|
97
|
-
summary: "
|
|
98
|
+
summary: "One page of a conversation",
|
|
98
99
|
description:
|
|
99
|
-
"
|
|
100
|
+
"The most recent turns of one conversation, in order, including the tool calls and their results: what the chat replays and the next turn is seeded from. A page, not the whole record — pass the answer's `from` back as `before` to walk further back, until `more` reads false.",
|
|
100
101
|
})
|
|
101
|
-
.input(
|
|
102
|
+
.input(AgentTranscriptQuerySchema)
|
|
102
103
|
.output(AgentTranscriptSchema),
|
|
103
104
|
/* SPEAK AS THE AGENT, append the user's words to the conversation's record as an assistant row, with no
|
|
104
105
|
* turn behind them and no reply. The row is marked `placed` for human readers (TranscriptRowSchema); the
|
|
@@ -6,7 +6,7 @@ import { OkSchema } from "../schemas/shared.js";
|
|
|
6
6
|
// The live GEO EXIT surface. An exit is ADDED as an `exit` capability (which provider, a resting country,
|
|
7
7
|
// whether it comes up on boot); it is STARTED, MOVED and ROTATED here. Same split as the vpn contract and for
|
|
8
8
|
// the same reason: switching country is a runtime operation performed many times over one stored pool, by the
|
|
9
|
-
// operator from the
|
|
9
|
+
// operator from the capability card and by the agent through `exit` on its PATH, which calls these very routes.
|
|
10
10
|
//
|
|
11
11
|
// The difference from vpn is what "success" means. A dial succeeds when the tunnel is up; a country switch
|
|
12
12
|
// succeeds only when the egress ADDRESS has moved, which is why `use` and `rotate` end by fetching an
|
|
@@ -17,7 +17,7 @@ import { OkSchema } from "../schemas/shared.js";
|
|
|
17
17
|
// the plain uplink no matter what is up.
|
|
18
18
|
export const exitContract = {
|
|
19
19
|
// Every configured exit with its live state, where it was asked to come out and where it actually does.
|
|
20
|
-
// Drives the
|
|
20
|
+
// Drives the capability card, the browser account picker and `geo list` (the CLI is `geo`: `exit` is a shell
|
|
21
21
|
// builtin, so a binary of that name is unreachable from a command line).
|
|
22
22
|
list: oc
|
|
23
23
|
.route({
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { eventIterator, oc } from "@orpc/contract";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import { DeviceAgentFlowSchema, DeviceFlowLineSchema, DeviceSandboxFlowSchema } from "../schemas/devices.js";
|
|
4
4
|
import { HostScopesSchema } from "../schemas/capabilities.js";
|
|
5
5
|
import { HostFactsSchema } from "../schemas/hosts.js";
|
|
6
6
|
import { OkSchema } from "../schemas/shared.js";
|
|
7
7
|
|
|
8
|
-
/* What a connected
|
|
8
|
+
/* What a connected device can be ASKED, over the socket it opened to this sandbox.
|
|
9
9
|
*
|
|
10
10
|
* The direction is the unusual part: the machine is the oRPC SERVER and the daemon holds the client, even though
|
|
11
|
-
* the machine is the side that dialled. A personal
|
|
11
|
+
* the machine is the side that dialled. A personal device sits behind NAT with a closing lid, so it can only
|
|
12
12
|
* ever be the one that connects, but everything is asked OF it. oRPC's websocket adapter takes any socket-like
|
|
13
13
|
* object on either side, so the roles are free to be the opposite of who placed the call.
|
|
14
14
|
*
|
|
@@ -22,7 +22,7 @@ import { OkSchema } from "../schemas/shared.js";
|
|
|
22
22
|
* release cycle. The payload is still validated where it is understood: on the machine, against the tool's own
|
|
23
23
|
* schema, and by the agent's MCP client on the way back. */
|
|
24
24
|
export const hostContract = {
|
|
25
|
-
// What this
|
|
25
|
+
// What this device is, pulled right after the socket authenticates, and again whenever the sandbox wants
|
|
26
26
|
// it fresh. The card shows it, and the agent's skill pack is written against it.
|
|
27
27
|
describe: oc.output(HostFactsSchema),
|
|
28
28
|
// The grant, pushed down on every connect and again whenever the owner edits the card. The machine ENFORCES
|
|
@@ -43,5 +43,17 @@ export const hostContract = {
|
|
|
43
43
|
*
|
|
44
44
|
* The scope is still checked here, on the machine, by the same functions the MCP tools call, this adds a
|
|
45
45
|
* way of WATCHING an operation, never a way of skipping the switch that permits it. */
|
|
46
|
-
runSandboxFlow: oc.input(
|
|
46
|
+
runSandboxFlow: oc.input(DeviceSandboxFlowSchema).output(eventIterator(DeviceFlowLineSchema)),
|
|
47
|
+
/* Update or restart the agent on this device, narrated the same way — and the one call that expects to be
|
|
48
|
+
* cut off, because the work it starts stops the process serving it (see DeviceAgentFlowSchema).
|
|
49
|
+
*
|
|
50
|
+
* A TYPED PROCEDURE rather than another `run_command` line, and the reason is the failure mode rather than
|
|
51
|
+
* the typing: run through `run_command` this is a child of a process about to be SIGTERMed, and a swap
|
|
52
|
+
* interrupted between its two renames leaves the device with no agent binary at all. Here the agent spawns
|
|
53
|
+
* the work detached from the socket first, so nothing the daemon does to this stream can brick it.
|
|
54
|
+
*
|
|
55
|
+
* It takes "Run commands" — the same grant `run_command` takes, since this is literally a command its owner
|
|
56
|
+
* could type — rather than the sandbox switches, which are about containers. An agent too old to serve this
|
|
57
|
+
* procedure is a real answer too: the view falls back to naming the command, as it did before. */
|
|
58
|
+
runAgentFlow: oc.input(DeviceAgentFlowSchema).output(eventIterator(DeviceFlowLineSchema)),
|
|
47
59
|
};
|
|
@@ -5,7 +5,7 @@ import { DayWindowQuerySchema } from "../schemas/usage.js";
|
|
|
5
5
|
|
|
6
6
|
// Per-sandbox agent settings (.intentic/config/settings.json). `get` returns the current flags with defaults applied
|
|
7
7
|
// when the file is absent; `set` overwrites them. `savings` reports what each token-reduction mechanism was
|
|
8
|
-
// worth, the cleaners' realized per-command savings
|
|
8
|
+
// worth, the cleaners' realized per-command savings over an inclusive
|
|
9
9
|
// UTC day window, the same one the spend ledger takes, so a screen can filter both with one calendar.
|
|
10
10
|
// `builtinPrompt` returns one of the two built-in system prompts as text. Intentic's own, or Claude Code's
|
|
11
11
|
// read out of the installed CLI, so the settings page can SHOW the prompt behind a mode instead of asking the
|