@intentic/sandbox-contract 1.332.0 → 1.333.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/dist/contracts/agent.contract.d.ts +1 -0
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/automations.contract.d.ts +1 -0
- package/dist/contracts/automations.contract.d.ts.map +1 -1
- package/dist/contracts/needs.contract.d.ts +29 -1
- package/dist/contracts/needs.contract.d.ts.map +1 -1
- package/dist/contracts/needs.contract.js +2 -2
- package/dist/contracts/needs.contract.js.map +1 -1
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.js +2 -2
- package/dist/contracts/runner.contract.js.map +1 -1
- package/dist/contracts/settings.contract.d.ts +34 -0
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/index.d.ts +114 -43
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/models/model-roles.d.ts +8 -0
- package/dist/models/model-roles.d.ts.map +1 -1
- package/dist/models/model-roles.js +8 -0
- package/dist/models/model-roles.js.map +1 -1
- package/dist/policy/control-scopes.d.ts +47 -0
- package/dist/policy/control-scopes.d.ts.map +1 -1
- package/dist/policy/control-scopes.js +16 -0
- package/dist/policy/control-scopes.js.map +1 -1
- package/dist/policy/device-programs.d.ts +28 -0
- package/dist/policy/device-programs.d.ts.map +1 -0
- package/dist/policy/device-programs.js +79 -0
- package/dist/policy/device-programs.js.map +1 -0
- package/dist/policy/identity-assertion.d.ts +27 -0
- package/dist/policy/identity-assertion.d.ts.map +1 -0
- package/dist/policy/identity-assertion.js +77 -0
- package/dist/policy/identity-assertion.js.map +1 -0
- package/dist/policy/sandbox-power.d.ts +34 -0
- package/dist/policy/sandbox-power.d.ts.map +1 -0
- package/dist/policy/sandbox-power.js +146 -0
- package/dist/policy/sandbox-power.js.map +1 -0
- package/dist/protocol/doors/runner-protocol.d.ts +36 -0
- package/dist/protocol/doors/runner-protocol.d.ts.map +1 -1
- package/dist/protocol/doors/runner-protocol.js +2 -0
- package/dist/protocol/doors/runner-protocol.js.map +1 -1
- package/dist/protocol/raw/raw-json-routes.d.ts +839 -1
- package/dist/protocol/raw/raw-json-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-json-routes.js +60 -2
- package/dist/protocol/raw/raw-json-routes.js.map +1 -1
- package/dist/protocol/raw/raw-routes.d.ts +9 -0
- package/dist/protocol/raw/raw-routes.d.ts.map +1 -1
- package/dist/protocol/raw/raw-routes.js +3 -0
- package/dist/protocol/raw/raw-routes.js.map +1 -1
- package/dist/schemas/agent.d.ts +3 -0
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/automations.d.ts +1 -0
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/automations.js +8 -2
- package/dist/schemas/automations.js.map +1 -1
- package/dist/schemas/device-swaps.d.ts +19 -0
- package/dist/schemas/device-swaps.d.ts.map +1 -0
- package/dist/schemas/device-swaps.js +11 -0
- package/dist/schemas/device-swaps.js.map +1 -0
- package/dist/schemas/environment.d.ts +7 -0
- package/dist/schemas/environment.d.ts.map +1 -1
- package/dist/schemas/environment.js +2 -0
- package/dist/schemas/environment.js.map +1 -1
- package/dist/schemas/members.d.ts +4 -0
- package/dist/schemas/members.d.ts.map +1 -1
- package/dist/schemas/members.js +5 -0
- package/dist/schemas/members.js.map +1 -1
- package/dist/schemas/needs.d.ts +78 -1
- package/dist/schemas/needs.d.ts.map +1 -1
- package/dist/schemas/needs.js +36 -1
- package/dist/schemas/needs.js.map +1 -1
- package/dist/schemas/restart-intent.d.ts +47 -0
- package/dist/schemas/restart-intent.d.ts.map +1 -0
- package/dist/schemas/restart-intent.js +19 -0
- package/dist/schemas/restart-intent.js.map +1 -0
- package/dist/schemas/settings.d.ts +34 -0
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +2 -0
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/short-codes.d.ts +16 -0
- package/dist/schemas/short-codes.d.ts.map +1 -1
- package/dist/schemas/short-codes.js +2 -0
- package/dist/schemas/short-codes.js.map +1 -1
- package/dist/schemas/speech.d.ts +4 -0
- package/dist/schemas/speech.d.ts.map +1 -1
- package/dist/schemas/speech.js +1 -0
- package/dist/schemas/speech.js.map +1 -1
- package/dist/schemas/system.d.ts +46 -0
- package/dist/schemas/system.d.ts.map +1 -1
- package/dist/schemas/system.js +18 -1
- package/dist/schemas/system.js.map +1 -1
- package/dist/schemas/voice.d.ts +171 -0
- package/dist/schemas/voice.d.ts.map +1 -0
- package/dist/schemas/voice.js +99 -0
- package/dist/schemas/voice.js.map +1 -0
- package/dist/schemas/workspace/workspace-upload.d.ts +14 -0
- package/dist/schemas/workspace/workspace-upload.d.ts.map +1 -0
- package/dist/schemas/workspace/workspace-upload.js +6 -0
- package/dist/schemas/workspace/workspace-upload.js.map +1 -0
- package/dist/state/arrival.d.ts +3 -0
- package/dist/state/arrival.d.ts.map +1 -1
- package/dist/state/arrival.js +1 -0
- package/dist/state/arrival.js.map +1 -1
- package/dist/state/definition.d.ts +52 -4
- package/dist/state/definition.d.ts.map +1 -1
- package/dist/state/history-state.d.ts.map +1 -1
- package/dist/state/history-state.js +2 -0
- package/dist/state/history-state.js.map +1 -1
- package/dist/state/runtime-state.d.ts +2 -2
- package/dist/state/runtime-state.d.ts.map +1 -1
- package/dist/state/runtime-state.js +2 -2
- package/dist/state/runtime-state.js.map +1 -1
- package/dist/state/update-probation.d.ts +50 -0
- package/dist/state/update-probation.d.ts.map +1 -0
- package/dist/state/update-probation.js +81 -0
- package/dist/state/update-probation.js.map +1 -0
- package/dist/state/workspace-state.d.ts +1 -1
- package/dist/state/workspace-state.d.ts.map +1 -1
- package/dist/state/workspace-state.js +1 -1
- package/dist/state/workspace-state.js.map +1 -1
- package/package.json +16 -5
- package/src/contracts/needs.contract.ts +2 -2
- package/src/contracts/runner.contract.ts +10 -2
- package/src/ids/tunnel-ids.test.ts +30 -0
- package/src/index.ts +7 -0
- package/src/models/model-roles.ts +10 -0
- package/src/policy/control-scopes.ts +27 -0
- package/src/policy/device-programs.ts +137 -0
- package/src/policy/identity-assertion.ts +130 -0
- package/src/policy/sandbox-power.ts +247 -0
- package/src/policy/tests/device-programs.test.ts +107 -0
- package/src/policy/tests/identity-assertion.test.ts +79 -0
- package/src/policy/tests/sandbox-power.test.ts +181 -0
- package/src/protocol/doors/runner-protocol.ts +7 -0
- package/src/protocol/raw/raw-json-routes.test.ts +87 -0
- package/src/protocol/raw/raw-json-routes.ts +125 -6
- package/src/protocol/raw/raw-routes.ts +6 -0
- package/src/schemas/automations.ts +10 -3
- package/src/schemas/device-swaps.test.ts +39 -0
- package/src/schemas/device-swaps.ts +26 -0
- package/src/schemas/environment.ts +4 -0
- package/src/schemas/members.ts +8 -0
- package/src/schemas/needs.ts +51 -1
- package/src/schemas/restart-intent.test.ts +45 -0
- package/src/schemas/restart-intent.ts +47 -0
- package/src/schemas/settings.ts +6 -0
- package/src/schemas/short-codes.ts +11 -0
- package/src/schemas/speech.ts +4 -0
- package/src/schemas/system.ts +43 -4
- package/src/schemas/voice.ts +161 -0
- package/src/schemas/workspace/workspace-upload.ts +11 -0
- package/src/state/arrival.ts +3 -0
- package/src/state/history-state.ts +5 -0
- package/src/state/runtime-state.test.ts +9 -0
- package/src/state/runtime-state.ts +7 -5
- package/src/state/update-probation.test.ts +45 -0
- package/src/state/update-probation.ts +169 -0
- package/src/state/workspace-state.ts +4 -1
package/src/index.ts
CHANGED
|
@@ -164,6 +164,8 @@ export * from "./models/agent-catalog.js";
|
|
|
164
164
|
export * from "./policy/capability-env.js";
|
|
165
165
|
export * from "./policy/command-classes.js";
|
|
166
166
|
export * from "./policy/command-run.js";
|
|
167
|
+
export * from "./policy/device-programs.js";
|
|
168
|
+
export * from "./policy/sandbox-power.js";
|
|
167
169
|
export * from "./policy/safety-policy.js";
|
|
168
170
|
export * from "./policy/reserved-servers.js";
|
|
169
171
|
export * from "./policy/credential-material.js";
|
|
@@ -205,6 +207,7 @@ export * from "./policy/persona-home.js";
|
|
|
205
207
|
export * from "./schemas/agents.js";
|
|
206
208
|
export * from "./schemas/speaker.js";
|
|
207
209
|
export * from "./schemas/speech.js";
|
|
210
|
+
export * from "./schemas/voice.js";
|
|
208
211
|
export * from "./schemas/approvals.js";
|
|
209
212
|
export * from "./schemas/automations.js";
|
|
210
213
|
export * from "./schemas/agents-list.js";
|
|
@@ -215,6 +218,7 @@ export * from "./schemas/providers/claude-gate.js";
|
|
|
215
218
|
export * from "./schemas/codebase-health.js";
|
|
216
219
|
export * from "./schemas/context-trim.js";
|
|
217
220
|
export * from "./schemas/devices.js";
|
|
221
|
+
export * from "./schemas/device-swaps.js";
|
|
218
222
|
export * from "./schemas/project-delivery.js";
|
|
219
223
|
export * from "./schemas/device-artifacts.js";
|
|
220
224
|
export * from "./schemas/run-targets.js";
|
|
@@ -265,6 +269,7 @@ export * from "./schemas/short-codes.js";
|
|
|
265
269
|
export * from "./schemas/system-prompt.js";
|
|
266
270
|
export * from "./schemas/system.js";
|
|
267
271
|
export * from "./schemas/state-plan.js";
|
|
272
|
+
export * from "./schemas/restart-intent.js";
|
|
268
273
|
export * from "./schemas/updates.js";
|
|
269
274
|
export * from "./schemas/terminal.js";
|
|
270
275
|
export * from "./schemas/turn-break.js";
|
|
@@ -280,11 +285,13 @@ export * from "./schemas/workspace/workspace-repos.js";
|
|
|
280
285
|
export * from "./schemas/workspace/workspace-search.js";
|
|
281
286
|
export * from "./schemas/workspace/workspace-setup.js";
|
|
282
287
|
export * from "./schemas/workspace/workspace-tree.js";
|
|
288
|
+
export * from "./schemas/workspace/workspace-upload.js";
|
|
283
289
|
export * from "./state/arrival.js";
|
|
284
290
|
export * from "./state/definition.js";
|
|
285
291
|
export * from "./policy/search-globs.js";
|
|
286
292
|
export * from "./state/starter.js";
|
|
287
293
|
export * from "./text/title.js";
|
|
294
|
+
export * from "./state/update-probation.js";
|
|
288
295
|
export * from "./state/versions.js";
|
|
289
296
|
export * from "./text/model-answer.js";
|
|
290
297
|
export * from "./text/shell-regions.js";
|
|
@@ -73,6 +73,16 @@ export const MODEL_ROLES = [
|
|
|
73
73
|
icon: "align-left",
|
|
74
74
|
setup: "low",
|
|
75
75
|
},
|
|
76
|
+
{
|
|
77
|
+
// Reads one finished reply (and the ask it answered) and writes what a person hears in hands-free voice: a few
|
|
78
|
+
// short sentences, never markdown. A fast model matters more than a strong one, since the person is waiting.
|
|
79
|
+
id: "spoken-reply",
|
|
80
|
+
label: "Spoken replies",
|
|
81
|
+
blurb: "The few sentences you hear when you talk to the agent hands-free: what it did and what it needs from you, while the full reply stays on screen.",
|
|
82
|
+
kind: "helper",
|
|
83
|
+
icon: "volume-up",
|
|
84
|
+
setup: "low",
|
|
85
|
+
},
|
|
76
86
|
{
|
|
77
87
|
// One reading answering both halves of "what does this chat open on": an offered `provider:model` line with an
|
|
78
88
|
// effort and an account, and a persona id, each a classification over a fixed list rather than free text. The
|
|
@@ -36,3 +36,30 @@ export const CONTROL_SCOPE_REACH: readonly ControlScopeReach[] = [
|
|
|
36
36
|
note: "Separate because the usual arrangement is a program that works and a person who decides.",
|
|
37
37
|
},
|
|
38
38
|
];
|
|
39
|
+
|
|
40
|
+
// One control token as the roster lists it: everything but the secret, which the daemon keeps only as a hash.
|
|
41
|
+
export const ControlTokenSummarySchema = z.object({
|
|
42
|
+
id: z.string(),
|
|
43
|
+
label: z.string(),
|
|
44
|
+
scope: ControlScopeSchema,
|
|
45
|
+
createdAt: z.number(),
|
|
46
|
+
// The owner or maintainer who minted it, so the roster says whose decision each token was.
|
|
47
|
+
createdBy: z.string().optional(),
|
|
48
|
+
// Epoch ms after which the token is refused; absent means it lives until revoked.
|
|
49
|
+
expiresAt: z.number().optional(),
|
|
50
|
+
// Epoch ms of the last authorized request, coarse on purpose; absent means never used.
|
|
51
|
+
lastUsedAt: z.number().optional(),
|
|
52
|
+
});
|
|
53
|
+
export type ControlTokenSummary = z.infer<typeof ControlTokenSummarySchema>;
|
|
54
|
+
export const ControlTokensListSchema = z.object({ tokens: z.array(ControlTokenSummarySchema) });
|
|
55
|
+
|
|
56
|
+
// POST /system/control/tokens: scope is required, never defaulted; expiry is optional and absent means never.
|
|
57
|
+
export const ControlTokenMintSchema = z.object({
|
|
58
|
+
scope: ControlScopeSchema,
|
|
59
|
+
label: z.string().optional(),
|
|
60
|
+
expiresAt: z.number().optional(),
|
|
61
|
+
});
|
|
62
|
+
export type ControlTokenMint = z.infer<typeof ControlTokenMintSchema>;
|
|
63
|
+
// The raw token, shown exactly once.
|
|
64
|
+
export const ControlTokenMintedSchema = z.object({ id: z.string(), token: z.string() });
|
|
65
|
+
export type ControlTokenMinted = z.infer<typeof ControlTokenMintedSchema>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// WHICH DEVICE TOOLS CARRY A PROGRAM (2026-10-12). A command on somebody's own computer arrives by more doors than
|
|
4
|
+
// `run_command`: `app_start` starts a program with arguments, `open` starts whatever its target names (on Linux a
|
|
5
|
+
// target with spaces is split into a program and arguments and spawned), `android_shell` hands a line to a phone's
|
|
6
|
+
// shell, and the input tools put text where a terminal could run it. The daemon's gate used to know two of those by
|
|
7
|
+
// name, the machine's own checks a different few, and `app_start program=powershell args=[…]` slipped past the judge, the
|
|
8
|
+
// card and the unattended refusal. This is the one table both ends read: the tool, and how to lift the command line
|
|
9
|
+
// out of its arguments. A device tool that carries a program is added here, or the machine's test says it is missing.
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* What a call's program is, which decides how it is read:
|
|
13
|
+
* - `command`: a command line for the machine's own shell, or a program started with arguments; judged as `run_command`.
|
|
14
|
+
* - `phone-command`: a line for an attached phone's shell, judged as the `adb shell` program the machine runs for it.
|
|
15
|
+
* - `app-launch`: an application, URL or document started with nothing to interpret; the ordinary GUI launch, low risk.
|
|
16
|
+
* - `typed`: text put where a terminal could run it (typed, pasted, set into a field).
|
|
17
|
+
*/
|
|
18
|
+
export type ProgramKind = "command" | "phone-command" | "app-launch" | "typed";
|
|
19
|
+
|
|
20
|
+
/** The program a tool call carries, as the classifier and the judge read it. */
|
|
21
|
+
export interface ProgramReading {
|
|
22
|
+
readonly tool: string;
|
|
23
|
+
readonly kind: ProgramKind;
|
|
24
|
+
/** The command line as a shell would read it. */
|
|
25
|
+
readonly commandLine: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The fields of every tool in the table below. One that is the wrong type reads as absent rather than failing the
|
|
29
|
+
// parse: such a call never runs, since the device checks its input against its own schema before anything else.
|
|
30
|
+
export const ProgramArgumentsSchema = z.looseObject({
|
|
31
|
+
command: z.string().optional().catch(undefined),
|
|
32
|
+
serial: z.string().optional().catch(undefined),
|
|
33
|
+
program: z.string().optional().catch(undefined),
|
|
34
|
+
args: z.array(z.string()).optional().catch(undefined),
|
|
35
|
+
target: z.string().optional().catch(undefined),
|
|
36
|
+
action: z.string().optional().catch(undefined),
|
|
37
|
+
text: z.string().optional().catch(undefined),
|
|
38
|
+
value: z.string().optional().catch(undefined),
|
|
39
|
+
});
|
|
40
|
+
export type ProgramArguments = z.infer<typeof ProgramArgumentsSchema>;
|
|
41
|
+
|
|
42
|
+
interface ProgramCarrier {
|
|
43
|
+
readonly tool: string;
|
|
44
|
+
/** In words: where the program is in the arguments. */
|
|
45
|
+
readonly carries: string;
|
|
46
|
+
readonly read: (args: ProgramArguments) => Omit<ProgramReading, "tool"> | undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const present = (value: string | undefined): string | undefined => (value === undefined || value.trim() === "" ? undefined : value);
|
|
50
|
+
|
|
51
|
+
// A program by the name it is known by (`rm`, `powershell`), not the path or the `.exe` it was started through.
|
|
52
|
+
const programName = (program: string): string => (program.split(/[/\\]/).at(-1) ?? program).replace(/\.exe$/i, "");
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The program and its arguments as the one command line they amount to: the program by its name, each argument quoted
|
|
56
|
+
* only where a shell would need it, so the line reads the way a person would type it.
|
|
57
|
+
*/
|
|
58
|
+
export const commandLineOf = (program: string, args: readonly string[]): string =>
|
|
59
|
+
[
|
|
60
|
+
programName(program),
|
|
61
|
+
...args.map((arg) => (arg === "" || /[\s'"|;&<>()$`\\]/.test(arg) ? (arg.includes("'") ? `"${arg}"` : `'${arg}'`) : arg)),
|
|
62
|
+
].join(" ");
|
|
63
|
+
|
|
64
|
+
// A phone's serial as adb prints one (a USB serial, `ip:port`, an mDNS name), and nothing else: a serial is spliced into
|
|
65
|
+
// the program judged below, where a `#` or a quote in it could make the command after it read as inert.
|
|
66
|
+
const ADB_SERIAL = /^[\w.:-]+$/;
|
|
67
|
+
|
|
68
|
+
// A URL, or `www.`/`mailto:` which the machine opens the same way (desktop-automation's looksLikeUrl).
|
|
69
|
+
const URL_TARGET = /^(?:[a-z][a-z0-9+.-]*:\/\/|www\.|mailto:)/i;
|
|
70
|
+
|
|
71
|
+
const typedBy = (tool: string, action: string, field: "text" | "value"): ProgramCarrier => ({
|
|
72
|
+
tool,
|
|
73
|
+
carries: `${field}, when action is ${action}`,
|
|
74
|
+
read: (args) => {
|
|
75
|
+
const typed = args.action === action ? present(args[field]) : undefined;
|
|
76
|
+
return typed === undefined ? undefined : { kind: "typed", commandLine: typed };
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/** Every device tool that carries a program. */
|
|
81
|
+
export const DEVICE_PROGRAM_TOOLS: readonly ProgramCarrier[] = [
|
|
82
|
+
{
|
|
83
|
+
tool: "run_command",
|
|
84
|
+
carries: "command",
|
|
85
|
+
read: ({ command }) => {
|
|
86
|
+
const line = present(command);
|
|
87
|
+
return line === undefined ? undefined : { kind: "command", commandLine: line };
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
tool: "android_shell",
|
|
92
|
+
carries: "command, run as `adb [-s serial] shell <command>`",
|
|
93
|
+
read: ({ command, serial }) => {
|
|
94
|
+
const line = present(command);
|
|
95
|
+
if (line === undefined) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
return { kind: "phone-command", commandLine: `adb ${serial !== undefined && ADB_SERIAL.test(serial) ? `-s ${serial} ` : ""}shell ${line}` };
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
tool: "app_start",
|
|
103
|
+
carries: "program and args",
|
|
104
|
+
read: ({ program, args }) => {
|
|
105
|
+
const started = present(program);
|
|
106
|
+
if (started === undefined) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
// With nothing to interpret it is a launch; with arguments it is a command by another door, whatever the
|
|
110
|
+
// program is: `rm -rf ~`, `curl … | sh` and `powershell -c …` read the same here as in run_command.
|
|
111
|
+
return { kind: args === undefined || args.length === 0 ? "app-launch" : "command", commandLine: commandLineOf(started, args ?? []) };
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
tool: "open",
|
|
116
|
+
carries: "target",
|
|
117
|
+
read: ({ target }) => {
|
|
118
|
+
const opened = present(target);
|
|
119
|
+
if (opened === undefined) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
// A URL or a single word (an application name, a file path) is opened. A target with spaces that is not a URL
|
|
123
|
+
// can be split into a program and arguments and spawned, so it is read as the command it may be.
|
|
124
|
+
return { kind: URL_TARGET.test(opened) || !/\s/.test(opened.trim()) ? "app-launch" : "command", commandLine: opened };
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
typedBy("device", "type", "text"),
|
|
128
|
+
typedBy("android_act", "type", "text"),
|
|
129
|
+
typedBy("clipboard", "write", "text"),
|
|
130
|
+
typedBy("ui_act", "set_value", "value"),
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
/** The program a `tools/call` of `tool` carries, or undefined for a tool that carries none or a call with nothing in the place the table looks. */
|
|
134
|
+
export const programInCall = (tool: string, args: ProgramArguments): ProgramReading | undefined => {
|
|
135
|
+
const read = DEVICE_PROGRAM_TOOLS.find((carrier) => carrier.tool === tool)?.read(args);
|
|
136
|
+
return read === undefined ? undefined : { tool, ...read };
|
|
137
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { createPrivateKey, createPublicKey, randomBytes, sign as edSign, verify as edVerify } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { SANDBOX_ID } from "../ids/tunnel-ids.js";
|
|
4
|
+
|
|
5
|
+
// The platform's signed word that a signed-in person controls an email address, spent on one sandbox's daemon. It is how
|
|
6
|
+
// an address no identity provider vouches for (a custom domain on Fastmail, Proton, a company's own mail server) reaches
|
|
7
|
+
// a sandbox: the platform confirmed the address by mailing it a code, and nobody else could. The daemon accepts it only
|
|
8
|
+
// where its owner opted in for that person (the owner record or the member row says `emailSignIn`), and on hosted
|
|
9
|
+
// machines, where the platform already holds the machine. Signed with a key of its own (IDENTITY_SIGNING_KEY), never the
|
|
10
|
+
// ingress key, so neither credential can stand in for the other.
|
|
11
|
+
|
|
12
|
+
const ASSERTION_PREFIX = "pa1";
|
|
13
|
+
|
|
14
|
+
// One round trip to the daemon and a retry; a lifted assertion is dead before anyone reads the log it sat in.
|
|
15
|
+
export const IDENTITY_ASSERTION_TTL_MS = 2 * 60_000;
|
|
16
|
+
// The longest life a daemon accepts whatever the claim says, so a platform minting with a wrong clock or a wrong ttl
|
|
17
|
+
// still cannot hand out a credential that outlives a replay window the daemon can hold in memory.
|
|
18
|
+
export const IDENTITY_ASSERTION_MAX_TTL_MS = 5 * 60_000;
|
|
19
|
+
|
|
20
|
+
// The env var that carries the platform's identity key on a hosted machine, which boots before its first announce. A
|
|
21
|
+
// sandbox on its owner's own machine learns the same key from the announce answer (`AnnounceAcceptedSchema`).
|
|
22
|
+
export const ENV_PLATFORM_IDENTITY_KEY = "PLATFORM_IDENTITY_KEY";
|
|
23
|
+
|
|
24
|
+
// How the platform confirmed the address it vouches for: a code it mailed, or a Google sign-in it verified.
|
|
25
|
+
export const IdentityAssertionMethodSchema = z.enum(["email", "google"]);
|
|
26
|
+
export type IdentityAssertionMethod = z.infer<typeof IdentityAssertionMethodSchema>;
|
|
27
|
+
|
|
28
|
+
// The signed claim, times in seconds like the owner ticket's. `nonce` is unique per mint: the daemon spends each once.
|
|
29
|
+
export interface IdentityAssertion {
|
|
30
|
+
readonly sandboxId: string;
|
|
31
|
+
readonly email: string;
|
|
32
|
+
readonly method: IdentityAssertionMethod;
|
|
33
|
+
readonly issuedAt: number;
|
|
34
|
+
readonly expiresAt: number;
|
|
35
|
+
readonly nonce: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const base64url = (bytes: Buffer): string => bytes.toString("base64url");
|
|
39
|
+
|
|
40
|
+
// The prefix is part of what is signed, so an assertion is never a valid signature over anything else the same key
|
|
41
|
+
// might one day sign, and an owner ticket or a grant (which sign their bare payload) is never an assertion.
|
|
42
|
+
const signedBytes = (payload: Buffer): Buffer => Buffer.concat([Buffer.from(`${ASSERTION_PREFIX}.`, "utf8"), payload]);
|
|
43
|
+
|
|
44
|
+
export const isIdentityAssertion = (bearer: string): boolean => bearer.startsWith(`${ASSERTION_PREFIX}.`);
|
|
45
|
+
|
|
46
|
+
export const mintIdentityAssertion = (
|
|
47
|
+
privateKeyPem: string,
|
|
48
|
+
claim: {
|
|
49
|
+
readonly sandboxId: string;
|
|
50
|
+
readonly email: string;
|
|
51
|
+
readonly method: IdentityAssertionMethod;
|
|
52
|
+
readonly issuedAtMs: number;
|
|
53
|
+
readonly ttlMs?: number;
|
|
54
|
+
},
|
|
55
|
+
): string => {
|
|
56
|
+
if (!SANDBOX_ID.test(claim.sandboxId)) {
|
|
57
|
+
throw new Error(`an identity assertion names a 12-hex sandbox id, got "${claim.sandboxId}"`);
|
|
58
|
+
}
|
|
59
|
+
if (claim.email.trim() === "") {
|
|
60
|
+
throw new Error("an identity assertion names an address");
|
|
61
|
+
}
|
|
62
|
+
const ttlMs = claim.ttlMs ?? IDENTITY_ASSERTION_TTL_MS;
|
|
63
|
+
if (ttlMs <= 0 || ttlMs > IDENTITY_ASSERTION_MAX_TTL_MS) {
|
|
64
|
+
throw new Error(`an identity assertion lives at most ${IDENTITY_ASSERTION_MAX_TTL_MS} ms`);
|
|
65
|
+
}
|
|
66
|
+
const iat = Math.floor(claim.issuedAtMs / 1000);
|
|
67
|
+
const exp = Math.floor((claim.issuedAtMs + ttlMs) / 1000);
|
|
68
|
+
const payload = Buffer.from(
|
|
69
|
+
JSON.stringify({
|
|
70
|
+
v: 1,
|
|
71
|
+
sub: claim.sandboxId,
|
|
72
|
+
email: claim.email.trim().toLowerCase(),
|
|
73
|
+
amr: claim.method,
|
|
74
|
+
iat,
|
|
75
|
+
exp,
|
|
76
|
+
jti: base64url(randomBytes(16)),
|
|
77
|
+
}),
|
|
78
|
+
"utf8",
|
|
79
|
+
);
|
|
80
|
+
const signature = edSign(null, signedBytes(payload), createPrivateKey(privateKeyPem));
|
|
81
|
+
return `${ASSERTION_PREFIX}.${base64url(payload)}.${base64url(signature)}`;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// What the signed payload must be, read only after the signature checked out.
|
|
85
|
+
const AssertionPayloadSchema = z.object({
|
|
86
|
+
v: z.literal(1),
|
|
87
|
+
sub: z.string().regex(SANDBOX_ID),
|
|
88
|
+
email: z.string().min(1),
|
|
89
|
+
amr: IdentityAssertionMethodSchema,
|
|
90
|
+
iat: z.number(),
|
|
91
|
+
exp: z.number(),
|
|
92
|
+
jti: z.string().min(16),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const parsePayload = (payload: Buffer): z.infer<typeof AssertionPayloadSchema> | undefined => {
|
|
96
|
+
try {
|
|
97
|
+
return AssertionPayloadSchema.safeParse(JSON.parse(payload.toString("utf8"))).data;
|
|
98
|
+
} catch {
|
|
99
|
+
// allow(silent-catch): a payload that is not JSON is an invalid assertion, which undefined is how this says.
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Verifies an assertion against the platform's public key at nowMs; undefined for any way of being invalid (wrong
|
|
105
|
+
// prefix, bad signature, malformed claim, expired, issued in the future, living longer than the daemon allows). The
|
|
106
|
+
// caller checks sandboxId against its own id and spends the nonce.
|
|
107
|
+
export const verifyIdentityAssertion = (publicKeyPem: string, token: string, nowMs: number): IdentityAssertion | undefined => {
|
|
108
|
+
const [prefix, encodedPayload, encodedSignature, ...rest] = token.split(".");
|
|
109
|
+
if (prefix !== ASSERTION_PREFIX || encodedPayload === undefined || encodedSignature === undefined || rest.length > 0) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
const payload = Buffer.from(encodedPayload, "base64url");
|
|
113
|
+
try {
|
|
114
|
+
if (!edVerify(null, signedBytes(payload), createPublicKey(publicKeyPem), Buffer.from(encodedSignature, "base64url"))) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// allow(silent-catch): a key or signature that does not parse verifies nothing: the assertion is invalid, said as undefined.
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
const claim = parsePayload(payload);
|
|
122
|
+
if (claim === undefined) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
// A minute of clock skew either way, as the daemon allows Google's tokens; never a claim from further ahead.
|
|
126
|
+
if (claim.exp * 1000 <= nowMs || claim.iat * 1000 > nowMs + 60_000 || (claim.exp - claim.iat) * 1000 > IDENTITY_ASSERTION_MAX_TTL_MS) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
return { sandboxId: claim.sub, email: claim.email, method: claim.amr, issuedAt: claim.iat, expiresAt: claim.exp, nonce: claim.jti };
|
|
130
|
+
};
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { inertRegions, isLive } from "../text/shell-regions.js";
|
|
2
|
+
import type { CommandSpan } from "./command-classes.js";
|
|
3
|
+
|
|
4
|
+
// A SHELL COMMAND THAT STARTS, STOPS OR REPLACES A SANDBOX CONTAINER (2026-10-12). `ic sandbox restart work` and
|
|
5
|
+
// `docker stop intentic-sandbox-work` end the same turns the device tools manage_sandbox and swap_sandbox end, but they
|
|
6
|
+
// arrive as text in run_command, where no tool name says what they are. This reads that text once, for both ends that
|
|
7
|
+
// have to answer it: the daemon, which asks the owner before an agent cuts the sandbox it runs in while others are
|
|
8
|
+
// working (hosts/host-restart-guard.ts), and the device agent, whose "Manage sandboxes" switch decides whether anything
|
|
9
|
+
// may do this at all (the machine's tools/shell.ts). The same verbs and the same wording, so a command and the tool
|
|
10
|
+
// that does the same job cannot drift apart.
|
|
11
|
+
|
|
12
|
+
/** The sandbox operations a command can name, in the words `ic sandbox` spells them. */
|
|
13
|
+
export type SandboxPowerOp =
|
|
14
|
+
| "start"
|
|
15
|
+
| "stop"
|
|
16
|
+
| "restart"
|
|
17
|
+
| "sleep"
|
|
18
|
+
| "remove"
|
|
19
|
+
| "update"
|
|
20
|
+
| "prepare"
|
|
21
|
+
| "rebuild"
|
|
22
|
+
| "rollback"
|
|
23
|
+
| "dev"
|
|
24
|
+
| "resize"
|
|
25
|
+
| "restore"
|
|
26
|
+
| "connect";
|
|
27
|
+
|
|
28
|
+
/** The owner's own word for an operation, as a card or a refusal says it ("<doing> this sandbox now?"). */
|
|
29
|
+
export const SANDBOX_POWER_DOING = {
|
|
30
|
+
start: "Start",
|
|
31
|
+
stop: "Stop",
|
|
32
|
+
restart: "Restart",
|
|
33
|
+
sleep: "Stop",
|
|
34
|
+
remove: "Remove",
|
|
35
|
+
update: "Update",
|
|
36
|
+
prepare: "Prepare an update for",
|
|
37
|
+
rebuild: "Rebuild",
|
|
38
|
+
rollback: "Roll back",
|
|
39
|
+
dev: "Update",
|
|
40
|
+
resize: "Restart with new resources",
|
|
41
|
+
restore: "Restore a backup over",
|
|
42
|
+
connect: "Reinstall",
|
|
43
|
+
} as const satisfies Record<SandboxPowerOp, string>;
|
|
44
|
+
|
|
45
|
+
/** One command that manages a sandbox container, and what it aims at. */
|
|
46
|
+
export interface SandboxPowerCommand {
|
|
47
|
+
readonly via: "ic" | "docker";
|
|
48
|
+
readonly op: SandboxPowerOp;
|
|
49
|
+
/** Whether it ends the turns of a sandbox that is running; false for a start, a download, a resource ask saved for later. */
|
|
50
|
+
readonly stops: boolean;
|
|
51
|
+
/** The slugs (`ic`) or container names (`docker`) it names, as written; an expansion (`$x`, `$(…)`, `*`) stays as typed. */
|
|
52
|
+
readonly targets: readonly string[];
|
|
53
|
+
/** Names none: `ic sandbox restart` alone (the only sandbox the device runs), `remove --all`, `docker stop` fed from a pipe. */
|
|
54
|
+
readonly unnamed: boolean;
|
|
55
|
+
/** The fragment, so a card can point at it. */
|
|
56
|
+
readonly span: CommandSpan;
|
|
57
|
+
/** Whether a shell would run it, as against a quoted argument of echo or a comment. */
|
|
58
|
+
readonly live: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// `ic sandbox <verb>`: the program may be a full path, `ic.exe`, or quoted (`& 'C:\…\ic.exe' sandbox restart work`).
|
|
62
|
+
const IC_INVOCATION = /(?<![\w.$-])ic(?:\.exe)?["']?\s+sandbox\s+([a-z][a-z-]*)(?![\w-])/gi;
|
|
63
|
+
// `docker stop`, `docker container rm`, podman alike. A global option between the program and the verb
|
|
64
|
+
// (`docker -H host stop x`) is not read: the verb then follows an option, and the command is left unrecognised.
|
|
65
|
+
const DOCKER_INVOCATION = /(?<![\w.$-])(?:docker|podman)(?:\.exe)?["']?\s+(?:container\s+)?(stop|restart|rm|remove|kill)(?![\w-])/gi;
|
|
66
|
+
|
|
67
|
+
const IC_VERBS = new Map<string, SandboxPowerOp>([
|
|
68
|
+
["start", "start"],
|
|
69
|
+
["stop", "stop"],
|
|
70
|
+
["restart", "restart"],
|
|
71
|
+
["sleep", "sleep"],
|
|
72
|
+
["remove", "remove"],
|
|
73
|
+
["rm", "remove"],
|
|
74
|
+
["update", "update"],
|
|
75
|
+
["prepare", "prepare"],
|
|
76
|
+
["rebuild", "rebuild"],
|
|
77
|
+
["rollback", "rollback"],
|
|
78
|
+
["dev", "dev"],
|
|
79
|
+
["reshape", "resize"],
|
|
80
|
+
["shape", "resize"],
|
|
81
|
+
["backup-restore", "restore"],
|
|
82
|
+
["connect", "connect"],
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
const DOCKER_VERBS = new Map<string, SandboxPowerOp>([
|
|
86
|
+
["stop", "stop"],
|
|
87
|
+
["kill", "stop"],
|
|
88
|
+
["restart", "restart"],
|
|
89
|
+
["rm", "remove"],
|
|
90
|
+
["remove", "remove"],
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
// The flags of each program that take their value as the next word, so the value is not read as a slug.
|
|
94
|
+
const IC_VALUE_FLAGS: ReadonlySet<string> = new Set([
|
|
95
|
+
"--channel",
|
|
96
|
+
"--to",
|
|
97
|
+
"--set",
|
|
98
|
+
"--when",
|
|
99
|
+
"--memory",
|
|
100
|
+
"--cpus",
|
|
101
|
+
"--privileged",
|
|
102
|
+
"--gpus",
|
|
103
|
+
"--idle",
|
|
104
|
+
"--source",
|
|
105
|
+
"--code",
|
|
106
|
+
"--snapshot",
|
|
107
|
+
"--into",
|
|
108
|
+
"--confirm",
|
|
109
|
+
"--accept",
|
|
110
|
+
"--tail",
|
|
111
|
+
]);
|
|
112
|
+
const DOCKER_VALUE_FLAGS: ReadonlySet<string> = new Set(["-t", "--time", "-s", "--signal"]);
|
|
113
|
+
|
|
114
|
+
// The simple command that starts the text: up to the first `|`, `;`, `&` or newline outside quotes. A `&` right after
|
|
115
|
+
// `>` or `<` is a redirection (`2>&1`), which ends nothing.
|
|
116
|
+
const SIMPLE_COMMAND = /^(?:[^|;&\n\r"']|(?<=[<>])&|"[^"]*"?|'[^']*'?)*/;
|
|
117
|
+
// One word, quoted pieces included. Backslashes stay as written: they are Windows path separators as often as escapes,
|
|
118
|
+
// and a slug has neither.
|
|
119
|
+
const WORD = /(?:[^\s"']|"[^"]*"?|'[^']*'?)+/g;
|
|
120
|
+
const REDIRECTION = /^\d*(?:>>?|<)/;
|
|
121
|
+
const BARE_REDIRECTION = /^\d*(?:>>?|<)$/;
|
|
122
|
+
|
|
123
|
+
const wordsOf = (text: string): string[] => [...(SIMPLE_COMMAND.exec(text)?.[0] ?? "").matchAll(WORD)].map((word) => word[0].replace(/["']/g, ""));
|
|
124
|
+
|
|
125
|
+
// A redirection (`2>&1`, `> log`, `>>out`) is not an operand; a bare `>` takes the word after it with it.
|
|
126
|
+
const withoutRedirections = (words: readonly string[]): string[] => {
|
|
127
|
+
const kept: string[] = [];
|
|
128
|
+
let skipNext = false;
|
|
129
|
+
for (const word of words) {
|
|
130
|
+
if (skipNext) {
|
|
131
|
+
skipNext = false;
|
|
132
|
+
} else if (BARE_REDIRECTION.test(word)) {
|
|
133
|
+
skipNext = true;
|
|
134
|
+
} else if (!REDIRECTION.test(word)) {
|
|
135
|
+
kept.push(word);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return kept;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
interface Operands {
|
|
142
|
+
readonly positionals: readonly string[];
|
|
143
|
+
/** Flags as written, `--flag=value` split: name → value (undefined for a switch). */
|
|
144
|
+
readonly flags: ReadonlyMap<string, string | undefined>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const operandsOf = (words: readonly string[], valueFlags: ReadonlySet<string>): Operands => {
|
|
148
|
+
const positionals: string[] = [];
|
|
149
|
+
const flags = new Map<string, string | undefined>();
|
|
150
|
+
const list = withoutRedirections(words);
|
|
151
|
+
for (const [index, word] of list.entries()) {
|
|
152
|
+
const equals = word.indexOf("=");
|
|
153
|
+
if (!word.startsWith("-") || word === "-") {
|
|
154
|
+
// The word after a flag that takes a value is that value, not an operand.
|
|
155
|
+
if (!valueFlags.has(list[index - 1] ?? "")) {
|
|
156
|
+
positionals.push(word);
|
|
157
|
+
}
|
|
158
|
+
} else if (word.startsWith("--") && equals !== -1) {
|
|
159
|
+
flags.set(word.slice(0, equals), word.slice(equals + 1));
|
|
160
|
+
} else {
|
|
161
|
+
flags.set(word, valueFlags.has(word) ? list[index + 1] : undefined);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { positionals, flags };
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
interface Aim {
|
|
168
|
+
readonly stops: boolean;
|
|
169
|
+
readonly targets: readonly string[];
|
|
170
|
+
readonly all: boolean;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// What `ic sandbox <op>` with these operands does to a running sandbox, and which sandboxes it aims at. The verbs that
|
|
174
|
+
// restart nothing say so: a start, a download, a resource ask saved for the next restart, a backup restored to a folder.
|
|
175
|
+
const icAim = (op: SandboxPowerOp, { positionals, flags }: Operands): Aim => {
|
|
176
|
+
const all = flags.has("--all") || flags.has("-a");
|
|
177
|
+
if (op === "start" || op === "prepare") {
|
|
178
|
+
return { stops: false, targets: positionals, all };
|
|
179
|
+
}
|
|
180
|
+
if (op === "resize") {
|
|
181
|
+
// `reshape --later` and `--forget` save an ask and restart nothing; `shape` restarts only with `--when now`.
|
|
182
|
+
const when = flags.get("--when");
|
|
183
|
+
const saves = flags.has("--later") || flags.has("--forget") || (when !== undefined && when !== "now");
|
|
184
|
+
return { stops: !saves, targets: positionals, all };
|
|
185
|
+
}
|
|
186
|
+
if (op === "restore") {
|
|
187
|
+
// A backup restored into a folder touches no sandbox; `--into <slug>` stops that one to write its volumes.
|
|
188
|
+
const into = flags.get("--into");
|
|
189
|
+
return into === undefined ? { stops: false, targets: [], all: false } : { stops: true, targets: [into], all: false };
|
|
190
|
+
}
|
|
191
|
+
if (op === "connect") {
|
|
192
|
+
return { stops: flags.has("--replace"), targets: [], all: false };
|
|
193
|
+
}
|
|
194
|
+
return { stops: true, targets: positionals, all };
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Every command in `command` that starts, stops or replaces a sandbox container, through `ic sandbox` or `docker`.
|
|
199
|
+
* A scan for the programs by name, not a shell parse: it is meant to over-include, since a caller that mistakes
|
|
200
|
+
* a mention for a command asks the owner once, and one that misses a command cuts a fleet's turns unasked.
|
|
201
|
+
*/
|
|
202
|
+
export const sandboxPowerCommands = (command: string): SandboxPowerCommand[] => {
|
|
203
|
+
const regions = inertRegions(command);
|
|
204
|
+
const found: SandboxPowerCommand[] = [];
|
|
205
|
+
for (const invocation of command.matchAll(IC_INVOCATION)) {
|
|
206
|
+
const op = IC_VERBS.get((invocation[1] ?? "").toLowerCase());
|
|
207
|
+
if (op !== undefined) {
|
|
208
|
+
const span = { start: invocation.index, end: invocation.index + invocation[0].length };
|
|
209
|
+
const aim = icAim(op, operandsOf(wordsOf(command.slice(span.end)), IC_VALUE_FLAGS));
|
|
210
|
+
found.push({ via: "ic", op, stops: aim.stops, targets: aim.targets, unnamed: aim.targets.length === 0 || aim.all, span, live: isLive(span, regions) });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const invocation of command.matchAll(DOCKER_INVOCATION)) {
|
|
214
|
+
const op = DOCKER_VERBS.get((invocation[1] ?? "").toLowerCase());
|
|
215
|
+
if (op !== undefined) {
|
|
216
|
+
const span = { start: invocation.index, end: invocation.index + invocation[0].length };
|
|
217
|
+
const { positionals } = operandsOf(wordsOf(command.slice(span.end)), DOCKER_VALUE_FLAGS);
|
|
218
|
+
found.push({ via: "docker", op, stops: true, targets: positionals, unnamed: positionals.length === 0, span, live: isLive(span, regions) });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return found.toSorted((left, right) => left.span.start - right.span.start);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// A word whose value exists only when the shell runs it: `$(docker ps -q)`, `$NAME`, `*`, PowerShell's `(…)` and `%X%`.
|
|
225
|
+
const isExpansion = (word: string): boolean => /[$`*?%]|^\(/.test(word);
|
|
226
|
+
|
|
227
|
+
/** The sandbox a caller means to protect: its slug, and the container name docker knows it by. */
|
|
228
|
+
export interface OwnSandbox {
|
|
229
|
+
readonly slug: string;
|
|
230
|
+
readonly container: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Whether `hit` may stop `own`. Deliberately generous: it names it, names none (the device's only sandbox, `--all`, a
|
|
235
|
+
* pipe), or names something that is only worked out when the command runs. A target spelled as the bare slug counts for
|
|
236
|
+
* `docker` too, which knows the container by a longer name.
|
|
237
|
+
*/
|
|
238
|
+
export const mayReachSandbox = (hit: SandboxPowerCommand, own: OwnSandbox): boolean =>
|
|
239
|
+
hit.unnamed || hit.targets.some((target) => target === own.slug || target === own.container || isExpansion(target));
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Whether `hit` manages a sandbox that a device's "Manage sandboxes" switch covers, whichever sandbox it is: any
|
|
243
|
+
* `ic sandbox` verb here, and a `docker` verb aimed at a container whose name starts with `containerPrefix` (a sandbox's
|
|
244
|
+
* own, or its tunnel's). A docker target worked out at run time is not read: the device cannot know what it will be.
|
|
245
|
+
*/
|
|
246
|
+
export const managesSandbox = (hit: SandboxPowerCommand, containerPrefix: string): boolean =>
|
|
247
|
+
hit.via === "ic" || hit.targets.some((target) => target.startsWith(containerPrefix));
|