@dynamicagents/plugins 0.14.0 → 0.16.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 +14 -14
- package/dist/claude-code/cards.d.ts +2 -0
- package/dist/claude-code/cards.js +11 -0
- package/dist/claude-code/config.d.ts +10 -83
- package/dist/claude-code/config.js +0 -2
- package/dist/claude-code/credentials.d.ts +32 -50
- package/dist/claude-code/credentials.js +115 -74
- package/dist/claude-code/daemon/index.d.ts +24 -0
- package/dist/claude-code/daemon/index.js +203 -0
- package/dist/claude-code/egress.d.ts +35 -117
- package/dist/claude-code/egress.js +80 -325
- package/dist/claude-code/events.d.ts +14 -17
- package/dist/claude-code/events.js +19 -15
- package/dist/claude-code/harness-wire.d.ts +45 -0
- package/dist/claude-code/harness-wire.js +47 -0
- package/dist/claude-code/index.d.ts +19 -29
- package/dist/claude-code/index.js +17 -25
- package/dist/claude-code/language-model.d.ts +19 -0
- package/dist/claude-code/language-model.js +71 -0
- package/dist/claude-code/model.d.ts +77 -70
- package/dist/claude-code/model.js +327 -327
- package/dist/claude-code/placeholder.d.ts +12 -0
- package/dist/claude-code/placeholder.js +12 -0
- package/dist/claude-code/spec.d.ts +4 -4
- package/dist/claude-code/spec.js +4 -4
- package/dist/computer/client.d.ts +88 -0
- package/dist/computer/client.js +6 -0
- package/dist/computer/context.d.ts +7 -7
- package/dist/computer/context.js +14 -18
- package/dist/computer/gate.d.ts +4 -23
- package/dist/computer/gate.js +1 -37
- package/dist/computer/index.d.ts +44 -43
- package/dist/computer/index.js +29 -50
- package/dist/computer/paths.d.ts +5 -27
- package/dist/computer/paths.js +7 -30
- package/dist/computer/proxy.d.ts +17 -31
- package/dist/computer/proxy.js +9 -44
- package/dist/computer/render.d.ts +3 -40
- package/dist/computer/render.js +3 -36
- package/dist/computer/sandbox.d.ts +55 -0
- package/dist/computer/sandbox.js +236 -0
- package/dist/computer/shell.d.ts +36 -0
- package/dist/computer/shell.js +41 -0
- package/dist/computer/tools-exec.js +3 -9
- package/dist/computer/tools-file.d.ts +1 -1
- package/dist/computer/tools-file.js +1 -1
- package/dist/{repo → repo-github}/checkout.d.ts +3 -4
- package/dist/{repo → repo-github}/checkout.js +3 -4
- package/dist/{repo → repo-github}/context.d.ts +14 -32
- package/dist/{repo → repo-github}/context.js +46 -81
- package/dist/{repo → repo-github}/index.d.ts +30 -13
- package/dist/{repo → repo-github}/index.js +25 -6
- package/dist/repo-github/mcp.d.ts +95 -0
- package/dist/repo-github/mcp.js +107 -0
- package/dist/repo-github/sandbox-egress.d.ts +68 -0
- package/dist/repo-github/sandbox-egress.js +272 -0
- package/dist/repo-github/sandbox-git.d.ts +30 -0
- package/dist/repo-github/sandbox-git.js +113 -0
- package/dist/{repo → repo-github}/tools-checks.js +43 -4
- package/dist/{repo → repo-github}/tools-clone.js +2 -21
- package/dist/{repo → repo-github}/tools-pr.d.ts +0 -1
- package/dist/{repo → repo-github}/tools-pr.js +85 -20
- package/dist/{repo → repo-github}/tools-review.d.ts +6 -0
- package/dist/{repo → repo-github}/tools-review.js +62 -176
- package/dist/{repo → repo-github}/url.d.ts +11 -0
- package/dist/{repo → repo-github}/url.js +12 -1
- package/dist/{workspace → sandbox}/advisory.d.ts +16 -35
- package/dist/{workspace → sandbox}/advisory.js +2 -29
- package/dist/sandbox/agent-exec.d.ts +31 -0
- package/dist/sandbox/agent-exec.js +62 -0
- package/dist/sandbox/ca-trust.d.ts +9 -0
- package/dist/sandbox/ca-trust.js +21 -0
- package/dist/sandbox/constants.d.ts +22 -0
- package/dist/sandbox/constants.js +22 -0
- package/dist/sandbox/daemon/composite.d.ts +9 -0
- package/dist/sandbox/daemon/composite.js +35 -0
- package/dist/sandbox/daemon/index.d.ts +47 -0
- package/dist/sandbox/daemon/index.js +116 -0
- package/dist/sandbox/egress.d.ts +81 -0
- package/dist/sandbox/egress.js +109 -0
- package/dist/sandbox/exec.d.ts +57 -0
- package/dist/sandbox/exec.js +52 -0
- package/dist/sandbox/format.d.ts +2 -0
- package/dist/sandbox/format.js +7 -0
- package/dist/sandbox/header.d.ts +50 -0
- package/dist/sandbox/header.js +94 -0
- package/dist/sandbox/index.d.ts +17 -0
- package/dist/sandbox/index.js +17 -0
- package/dist/{workspace/install.d.ts → sandbox/install-plan.d.ts} +2 -2
- package/dist/{workspace/install.js → sandbox/install-plan.js} +2 -2
- package/dist/sandbox/name.d.ts +27 -0
- package/dist/sandbox/name.js +32 -0
- package/dist/sandbox/object.d.ts +244 -0
- package/dist/sandbox/object.js +1139 -0
- package/dist/scratch/index.d.ts +1 -9
- package/dist/scratch/index.js +2 -18
- package/package.json +24 -20
- package/dist/claude-code/github.d.ts +0 -39
- package/dist/claude-code/github.js +0 -82
- package/dist/claude-code/run.d.ts +0 -491
- package/dist/claude-code/run.js +0 -913
- package/dist/claude-code/session.d.ts +0 -131
- package/dist/claude-code/session.js +0 -288
- package/dist/workspace/ca-trust.d.ts +0 -95
- package/dist/workspace/ca-trust.js +0 -312
- package/dist/workspace/container-deps.d.ts +0 -58
- package/dist/workspace/container-deps.js +0 -249
- package/dist/workspace/container-fault.d.ts +0 -38
- package/dist/workspace/container-fault.js +0 -73
- package/dist/workspace/deps-snapshot.d.ts +0 -110
- package/dist/workspace/deps-snapshot.js +0 -424
- package/dist/workspace/exec.d.ts +0 -94
- package/dist/workspace/exec.js +0 -153
- package/dist/workspace/format.d.ts +0 -36
- package/dist/workspace/format.js +0 -57
- package/dist/workspace/git-host.d.ts +0 -79
- package/dist/workspace/git-host.js +0 -226
- package/dist/workspace/git-identity.d.ts +0 -57
- package/dist/workspace/git-identity.js +0 -147
- package/dist/workspace/index.d.ts +0 -50
- package/dist/workspace/index.js +0 -58
- package/dist/workspace/install-job.d.ts +0 -170
- package/dist/workspace/install-job.js +0 -934
- package/dist/workspace/object.d.ts +0 -522
- package/dist/workspace/object.js +0 -1541
- package/dist/workspace/open.d.ts +0 -95
- package/dist/workspace/open.js +0 -63
- package/dist/workspace/read.d.ts +0 -16
- package/dist/workspace/read.js +0 -22
- package/dist/workspace/shell.d.ts +0 -45
- package/dist/workspace/shell.js +0 -80
- package/dist/workspace/sync.d.ts +0 -111
- package/dist/workspace/sync.js +0 -124
- package/dist/workspace/wake.d.ts +0 -21
- package/dist/workspace/wake.js +0 -10
- /package/dist/{repo → repo-github}/tools-checks.d.ts +0 -0
- /package/dist/{repo → repo-github}/tools-clone.d.ts +0 -0
- /package/dist/{repo → repo-github}/tools-worktree.d.ts +0 -0
- /package/dist/{repo → repo-github}/tools-worktree.js +0 -0
- /package/dist/{repo → repo-github}/tools-worktrees.d.ts +0 -0
- /package/dist/{repo → repo-github}/tools-worktrees.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { InstallState } from "./install.js";
|
|
1
|
+
import type { InstallState } from "./install-plan.js";
|
|
2
2
|
/**
|
|
3
|
-
* What a caller must not assume about a
|
|
3
|
+
* What a caller must not assume about a sandbox's checkout, and how to say it.
|
|
4
4
|
*
|
|
5
5
|
* ## Why this is not `InstallState`
|
|
6
6
|
*
|
|
@@ -12,13 +12,6 @@ import type { InstallState } from "./install.js";
|
|
|
12
12
|
* workspace right now?** The two diverge, and expressing the second through the
|
|
13
13
|
* first goes wrong in ways that are each easy to reintroduce:
|
|
14
14
|
*
|
|
15
|
-
* - A workspace at its storage ceiling is not `skipped`. That variant means
|
|
16
|
-
* "this checkout has nothing to install" — routine, and about the repository.
|
|
17
|
-
* A ceiling is a hard wall about the Durable Object, and the two are then told
|
|
18
|
-
* apart only by reading prose.
|
|
19
|
-
* - Capacity must not travel through a channel gated by
|
|
20
|
-
* {@link file://../computer/gate.ts needsDependencies}, or a full workspace says nothing
|
|
21
|
-
* at all to `echo hi > file.txt` — the command whose write is being lost.
|
|
22
15
|
* - "The tree is fine now", after an agent installed by hand, has no honest
|
|
23
16
|
* spelling as a job record: fabricating a `done` rests on the presence of a
|
|
24
17
|
* `node_modules` directory, which the wreckage of the very install being
|
|
@@ -30,7 +23,7 @@ import type { InstallState } from "./install.js";
|
|
|
30
23
|
*
|
|
31
24
|
* ## One home for what each kind means
|
|
32
25
|
*
|
|
33
|
-
* {@link shapeOf} is the only exhaustive `switch` over {@link
|
|
26
|
+
* {@link shapeOf} is the only exhaustive `switch` over {@link SandboxAdvisory}
|
|
34
27
|
* that decides *policy*, and {@link renderAdvisory} the only one that decides
|
|
35
28
|
* *wording*. Everything else reads {@link AdvisoryShape}. A new kind is a
|
|
36
29
|
* compile error in exactly those two places, and whoever adds it has to say what
|
|
@@ -39,7 +32,7 @@ import type { InstallState } from "./install.js";
|
|
|
39
32
|
* discipline.
|
|
40
33
|
*/
|
|
41
34
|
/** Something a caller must not assume away. An empty array is the good case. */
|
|
42
|
-
export type
|
|
35
|
+
export type SandboxAdvisory = {
|
|
43
36
|
kind: "deps-building";
|
|
44
37
|
command: string;
|
|
45
38
|
startedAt: number;
|
|
@@ -65,19 +58,15 @@ export type WorkspaceAdvisory = {
|
|
|
65
58
|
} | {
|
|
66
59
|
kind: "deps-absent";
|
|
67
60
|
reason: string;
|
|
68
|
-
} | {
|
|
69
|
-
kind: "storage-exhausted";
|
|
70
|
-
bytes: number;
|
|
71
|
-
capBytes: number;
|
|
72
61
|
};
|
|
73
62
|
/**
|
|
74
63
|
* The three axes every reader decides on, so none of them re-derives severity
|
|
75
64
|
* from a prose string.
|
|
76
65
|
*
|
|
77
|
-
* They are separate because they vary independently
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
66
|
+
* They are separate because they vary independently. Every kind a sandbox can
|
|
67
|
+
* report today is `universal: false, writesPersist: true` — its checkout is the
|
|
68
|
+
* container's own disk, with no ceiling of its own — and the axes stay because
|
|
69
|
+
* the readers decide on them rather than on a kind.
|
|
81
70
|
*/
|
|
82
71
|
export interface AdvisoryShape {
|
|
83
72
|
/**
|
|
@@ -95,15 +84,14 @@ export interface AdvisoryShape {
|
|
|
95
84
|
* Reaches commands that never read `node_modules`.
|
|
96
85
|
*
|
|
97
86
|
* A dependency problem does not; `cat README.md` is unaffected and must not
|
|
98
|
-
* queue behind an install.
|
|
99
|
-
* being lost is usually not a dependency's.
|
|
87
|
+
* queue behind an install.
|
|
100
88
|
*/
|
|
101
89
|
universal: boolean;
|
|
102
90
|
/** Whether a write made under this survives. `false` is the loudest thing here. */
|
|
103
91
|
writesPersist: boolean;
|
|
104
92
|
}
|
|
105
93
|
/** What each advisory *means*. The one exhaustive home for policy. */
|
|
106
|
-
export declare function shapeOf(advisory:
|
|
94
|
+
export declare function shapeOf(advisory: SandboxAdvisory): AdvisoryShape;
|
|
107
95
|
/**
|
|
108
96
|
* Who is being told, which decides the **verbs** rather than the facts.
|
|
109
97
|
*
|
|
@@ -125,16 +113,11 @@ export type AdvisoryAudience = "tool-call" | "session";
|
|
|
125
113
|
* this file exists to remove — one repository further out, where it is harder to
|
|
126
114
|
* see.
|
|
127
115
|
*/
|
|
128
|
-
export declare function renderAdvisory(advisory:
|
|
116
|
+
export declare function renderAdvisory(advisory: SandboxAdvisory, to: AdvisoryAudience): string;
|
|
129
117
|
/** What the host knows, in the shape {@link deriveAdvisories} reads. */
|
|
130
118
|
export interface AdvisoryInput {
|
|
131
119
|
/** The durable install record, exactly as the job wrote it. */
|
|
132
120
|
install: InstallState;
|
|
133
|
-
/** Set only when the workspace is over its ceiling. */
|
|
134
|
-
storage?: {
|
|
135
|
-
bytes: number;
|
|
136
|
-
capBytes: number;
|
|
137
|
-
};
|
|
138
121
|
/**
|
|
139
122
|
* Whether a `node_modules` directory exists — **existence only, not health**.
|
|
140
123
|
*
|
|
@@ -145,7 +128,7 @@ export interface AdvisoryInput {
|
|
|
145
128
|
* with the install having failed for real.
|
|
146
129
|
*
|
|
147
130
|
* So it never suppresses a proven failure. It travels onto the advisory as
|
|
148
|
-
* {@link
|
|
131
|
+
* {@link SandboxAdvisory} `treePresent` and the reader is told which way the
|
|
149
132
|
* ambiguity runs. The alternative — treating it as proof of success — is what
|
|
150
133
|
* made a durable record of a failed install disappear on exactly the failure
|
|
151
134
|
* mode it was recording.
|
|
@@ -169,15 +152,13 @@ export interface AdvisoryInput {
|
|
|
169
152
|
/**
|
|
170
153
|
* Everything currently true about a workspace, from what the host can see.
|
|
171
154
|
*
|
|
172
|
-
* An array,
|
|
173
|
-
*
|
|
174
|
-
* one written last — so the ceiling silently erased whatever the install had
|
|
175
|
-
* said, or the reverse.
|
|
155
|
+
* An array, so a reader handles any number the same way and a new kind that can
|
|
156
|
+
* be true beside another needs no new shape.
|
|
176
157
|
*
|
|
177
158
|
* Order is severity-descending, so a renderer that shows only the first shows
|
|
178
159
|
* the one that matters.
|
|
179
160
|
*/
|
|
180
|
-
export declare function deriveAdvisories(input: AdvisoryInput): readonly
|
|
161
|
+
export declare function deriveAdvisories(input: AdvisoryInput): readonly SandboxAdvisory[];
|
|
181
162
|
/**
|
|
182
163
|
* Everything a session should know before it starts, or `undefined` for silence.
|
|
183
164
|
*
|
|
@@ -186,4 +167,4 @@ export declare function deriveAdvisories(input: AdvisoryInput): readonly Workspa
|
|
|
186
167
|
* be: it can install dependencies itself, and the only thing it must not do is
|
|
187
168
|
* work silently against a workspace that is not what it assumes.
|
|
188
169
|
*/
|
|
189
|
-
export declare function sessionAdvisory(advisories: readonly
|
|
170
|
+
export declare function sessionAdvisory(advisories: readonly SandboxAdvisory[]): string | undefined;
|
|
@@ -8,8 +8,6 @@ export function shapeOf(advisory) {
|
|
|
8
8
|
return { transient: false, universal: false, writesPersist: true };
|
|
9
9
|
case "deps-absent":
|
|
10
10
|
return { transient: false, universal: false, writesPersist: true };
|
|
11
|
-
case "storage-exhausted":
|
|
12
|
-
return { transient: false, universal: true, writesPersist: false };
|
|
13
11
|
default: {
|
|
14
12
|
// A new kind lands here as a type error, which is the point: it cannot be
|
|
15
13
|
// added without someone stating whether it is transient, whether it
|
|
@@ -19,10 +17,6 @@ export function shapeOf(advisory) {
|
|
|
19
17
|
}
|
|
20
18
|
}
|
|
21
19
|
}
|
|
22
|
-
/** Gigabytes, because this is the one place the numbers are that large. */
|
|
23
|
-
function gb(bytes) {
|
|
24
|
-
return `${(bytes / 1e9).toFixed(1)} GB`;
|
|
25
|
-
}
|
|
26
20
|
/** A failed install's own last words, fenced so a model reads them as output. */
|
|
27
21
|
function quoted(tail) {
|
|
28
22
|
const trimmed = tail?.trim();
|
|
@@ -80,17 +74,6 @@ export function renderAdvisory(advisory, to) {
|
|
|
80
74
|
"yourself first."
|
|
81
75
|
: `${fact}\n\nIf you need \`node_modules\`, install them yourself first.`;
|
|
82
76
|
}
|
|
83
|
-
case "storage-exhausted": {
|
|
84
|
-
const fact = `This workspace holds ${gb(advisory.bytes)} against a ` +
|
|
85
|
-
`${gb(advisory.capBytes)} ceiling, so nothing further can be written ` +
|
|
86
|
-
"to it.";
|
|
87
|
-
return to === "tool-call"
|
|
88
|
-
? `${fact}\n\nNothing written to this workspace survives. Stop and ` +
|
|
89
|
-
"report this rather than retrying."
|
|
90
|
-
: `${fact}\n\n**Nothing you write here will survive**, including every ` +
|
|
91
|
-
"edit you are about to make. Report this and stop; an operator has " +
|
|
92
|
-
"to reclaim the space or point this caller at a smaller repository.";
|
|
93
|
-
}
|
|
94
77
|
default: {
|
|
95
78
|
const unhandled = advisory;
|
|
96
79
|
return unhandled;
|
|
@@ -100,24 +83,14 @@ export function renderAdvisory(advisory, to) {
|
|
|
100
83
|
/**
|
|
101
84
|
* Everything currently true about a workspace, from what the host can see.
|
|
102
85
|
*
|
|
103
|
-
* An array,
|
|
104
|
-
*
|
|
105
|
-
* one written last — so the ceiling silently erased whatever the install had
|
|
106
|
-
* said, or the reverse.
|
|
86
|
+
* An array, so a reader handles any number the same way and a new kind that can
|
|
87
|
+
* be true beside another needs no new shape.
|
|
107
88
|
*
|
|
108
89
|
* Order is severity-descending, so a renderer that shows only the first shows
|
|
109
90
|
* the one that matters.
|
|
110
91
|
*/
|
|
111
92
|
export function deriveAdvisories(input) {
|
|
112
93
|
const advisories = [];
|
|
113
|
-
// First, because it is the only one under which nothing a caller does sticks.
|
|
114
|
-
if (input.storage) {
|
|
115
|
-
advisories.push({
|
|
116
|
-
kind: "storage-exhausted",
|
|
117
|
-
bytes: input.storage.bytes,
|
|
118
|
-
capBytes: input.storage.capBytes
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
94
|
const install = input.install;
|
|
122
95
|
switch (install.state) {
|
|
123
96
|
case "running":
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running a command in the container as the agent, and only as the agent.
|
|
3
|
+
*
|
|
4
|
+
* `ctx.container.exec(argv, { user })` changes the uid and keeps root's
|
|
5
|
+
* capability set: a process started that way as `10001:10001` was measured with
|
|
6
|
+
* `CapEff 0xabac35fb`, `CAP_DAC_OVERRIDE` and `CAP_SYS_PTRACE` among it. So
|
|
7
|
+
* every host command goes through `setpriv`, which drops the inheritable and
|
|
8
|
+
* bounding sets and changes identity in one step, and the image provides it
|
|
9
|
+
* (util-linux).
|
|
10
|
+
*
|
|
11
|
+
* `setpriv` starts as root, so it is named by its absolute path and given no
|
|
12
|
+
* environment of the caller's: a `PATH` or `LD_PRELOAD` would otherwise be
|
|
13
|
+
* honoured as root before the drop. The command's environment is applied after
|
|
14
|
+
* it, by `env -i`, which also clears whatever the container's own carries.
|
|
15
|
+
*
|
|
16
|
+
* So is its directory, by `env -C`, and never as the platform's `cwd`: a `cwd`
|
|
17
|
+
* that does not exist fails the spawn, and the platform reports that as the
|
|
18
|
+
* command not being found in the container (observed). A missing directory is
|
|
19
|
+
* an answer — `/repo` probes a clone target before cloning into it — so it has
|
|
20
|
+
* to arrive as one: `env` exits 125 and says which directory.
|
|
21
|
+
*/
|
|
22
|
+
/** The image's unprivileged user, which owns `/workspace` and runs the CLI. */
|
|
23
|
+
export declare const AGENT_UID = 10001;
|
|
24
|
+
export declare const AGENT_GID = 10001;
|
|
25
|
+
export declare const AGENT_HOME = "/home/agent";
|
|
26
|
+
/** `argv`, run as the agent with no capabilities, in exactly {@link agentEnv}`(env)`, from `cwd` when given. */
|
|
27
|
+
export declare function asAgent(argv: readonly string[], env?: Readonly<Record<string, string>>, cwd?: string): string[];
|
|
28
|
+
/** The environment a command run {@link asAgent} expects of its user. */
|
|
29
|
+
export declare function agentEnv(extra?: Readonly<Record<string, string>>): Record<string, string>;
|
|
30
|
+
/** A shell string for `sh -c`, single-quoted so nothing in it is expanded. */
|
|
31
|
+
export declare function shellQuote(value: string): string;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running a command in the container as the agent, and only as the agent.
|
|
3
|
+
*
|
|
4
|
+
* `ctx.container.exec(argv, { user })` changes the uid and keeps root's
|
|
5
|
+
* capability set: a process started that way as `10001:10001` was measured with
|
|
6
|
+
* `CapEff 0xabac35fb`, `CAP_DAC_OVERRIDE` and `CAP_SYS_PTRACE` among it. So
|
|
7
|
+
* every host command goes through `setpriv`, which drops the inheritable and
|
|
8
|
+
* bounding sets and changes identity in one step, and the image provides it
|
|
9
|
+
* (util-linux).
|
|
10
|
+
*
|
|
11
|
+
* `setpriv` starts as root, so it is named by its absolute path and given no
|
|
12
|
+
* environment of the caller's: a `PATH` or `LD_PRELOAD` would otherwise be
|
|
13
|
+
* honoured as root before the drop. The command's environment is applied after
|
|
14
|
+
* it, by `env -i`, which also clears whatever the container's own carries.
|
|
15
|
+
*
|
|
16
|
+
* So is its directory, by `env -C`, and never as the platform's `cwd`: a `cwd`
|
|
17
|
+
* that does not exist fails the spawn, and the platform reports that as the
|
|
18
|
+
* command not being found in the container (observed). A missing directory is
|
|
19
|
+
* an answer — `/repo` probes a clone target before cloning into it — so it has
|
|
20
|
+
* to arrive as one: `env` exits 125 and says which directory.
|
|
21
|
+
*/
|
|
22
|
+
/** The image's unprivileged user, which owns `/workspace` and runs the CLI. */
|
|
23
|
+
export const AGENT_UID = 10001;
|
|
24
|
+
export const AGENT_GID = 10001;
|
|
25
|
+
export const AGENT_HOME = "/home/agent";
|
|
26
|
+
const SETPRIV = "/usr/bin/setpriv";
|
|
27
|
+
const ENV = "/usr/bin/env";
|
|
28
|
+
/** Where the image's tools are. A command's own `env` may replace it. */
|
|
29
|
+
const AGENT_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
30
|
+
/** `argv`, run as the agent with no capabilities, in exactly {@link agentEnv}`(env)`, from `cwd` when given. */
|
|
31
|
+
export function asAgent(argv, env = {}, cwd) {
|
|
32
|
+
if (argv.length === 0)
|
|
33
|
+
throw new Error("asAgent: an empty command");
|
|
34
|
+
return [
|
|
35
|
+
SETPRIV,
|
|
36
|
+
`--reuid=${AGENT_UID}`,
|
|
37
|
+
`--regid=${AGENT_GID}`,
|
|
38
|
+
"--init-groups",
|
|
39
|
+
"--inh-caps=-all",
|
|
40
|
+
"--bounding-set=-all",
|
|
41
|
+
"--",
|
|
42
|
+
ENV,
|
|
43
|
+
"-i",
|
|
44
|
+
...(cwd ? ["-C", cwd] : []),
|
|
45
|
+
...Object.entries(agentEnv(env)).map(([name, value]) => `${name}=${value}`),
|
|
46
|
+
...argv
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
/** The environment a command run {@link asAgent} expects of its user. */
|
|
50
|
+
export function agentEnv(extra = {}) {
|
|
51
|
+
return {
|
|
52
|
+
HOME: AGENT_HOME,
|
|
53
|
+
USER: "agent",
|
|
54
|
+
LOGNAME: "agent",
|
|
55
|
+
PATH: AGENT_PATH,
|
|
56
|
+
...extra
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** A shell string for `sh -c`, single-quoted so nothing in it is expanded. */
|
|
60
|
+
export function shellQuote(value) {
|
|
61
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
62
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trusting the runtime's interception CA, which only exists once intercepted.
|
|
3
|
+
*
|
|
4
|
+
* Intercepting HTTPS means the runtime terminates TLS with its own certificate,
|
|
5
|
+
* and it mounts that CA only after `start()` and the intercept call — so the
|
|
6
|
+
* image's entrypoint runs before there is anything to trust, and this runs from
|
|
7
|
+
* the object instead, once per container. HTTP intercepts need none of it.
|
|
8
|
+
*/
|
|
9
|
+
export declare const TRUST_CA_COMMAND: string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trusting the runtime's interception CA, which only exists once intercepted.
|
|
3
|
+
*
|
|
4
|
+
* Intercepting HTTPS means the runtime terminates TLS with its own certificate,
|
|
5
|
+
* and it mounts that CA only after `start()` and the intercept call — so the
|
|
6
|
+
* image's entrypoint runs before there is anything to trust, and this runs from
|
|
7
|
+
* the object instead, once per container. HTTP intercepts need none of it.
|
|
8
|
+
*/
|
|
9
|
+
export const TRUST_CA_COMMAND = [
|
|
10
|
+
'ls -A /etc/cloudflare/certs 2>&1 || echo "(no /etc/cloudflare/certs)"',
|
|
11
|
+
"CA=/etc/cloudflare/certs/cloudflare-containers-ca.crt",
|
|
12
|
+
'if [ -r "$CA" ]; then',
|
|
13
|
+
// Trailing operators: a newline ends a command in sh, so a leading `&&` on
|
|
14
|
+
// the next line is a syntax error rather than a continuation.
|
|
15
|
+
' install -m 644 "$CA" /usr/local/share/ca-certificates/cloudflare-containers-ca.crt &&',
|
|
16
|
+
" update-ca-certificates > /dev/null 2>&1 &&",
|
|
17
|
+
' echo TRUSTED || echo "TRUST FAILED"',
|
|
18
|
+
"else",
|
|
19
|
+
' echo "NO CA AT $CA"',
|
|
20
|
+
"fi"
|
|
21
|
+
].join("\n");
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the object and the daemon in its container must spell identically.
|
|
3
|
+
*
|
|
4
|
+
* A module of its own, with no imports, because the daemon runs in Node and
|
|
5
|
+
* cannot load anything that reaches `cloudflare:workers`.
|
|
6
|
+
*/
|
|
7
|
+
/** Bumped when an RPC of the object changes shape, so a caller can tell a busy object still on older code. */
|
|
8
|
+
export declare const SANDBOX_CONTRACT = 2;
|
|
9
|
+
/**
|
|
10
|
+
* The harness's own root session, which a sandbox keeps for control prompts:
|
|
11
|
+
* the daemon answers it without starting a CLI. The contract spec pins it to
|
|
12
|
+
* the harness's `ROOT_SESSION`.
|
|
13
|
+
*/
|
|
14
|
+
export declare const CONTROL_SESSION = "root";
|
|
15
|
+
/** A control prompt that only starts the container. */
|
|
16
|
+
export declare const CONTROL_WAKE = "control:wake";
|
|
17
|
+
/** Markers and job output in the container: made by the object, owned by the agent. */
|
|
18
|
+
export declare const RUN_DIR = "/run/da";
|
|
19
|
+
/** The marker a CLI turn waits for, holding the runtime id its egress was prepared for. */
|
|
20
|
+
export declare const EGRESS_READY = "/run/da/egress-ready";
|
|
21
|
+
/** How long a CLI turn waits for {@link EGRESS_READY} before it refuses to start. */
|
|
22
|
+
export declare const EGRESS_WAIT_MS = 60000;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the object and the daemon in its container must spell identically.
|
|
3
|
+
*
|
|
4
|
+
* A module of its own, with no imports, because the daemon runs in Node and
|
|
5
|
+
* cannot load anything that reaches `cloudflare:workers`.
|
|
6
|
+
*/
|
|
7
|
+
/** Bumped when an RPC of the object changes shape, so a caller can tell a busy object still on older code. */
|
|
8
|
+
export const SANDBOX_CONTRACT = 2;
|
|
9
|
+
/**
|
|
10
|
+
* The harness's own root session, which a sandbox keeps for control prompts:
|
|
11
|
+
* the daemon answers it without starting a CLI. The contract spec pins it to
|
|
12
|
+
* the harness's `ROOT_SESSION`.
|
|
13
|
+
*/
|
|
14
|
+
export const CONTROL_SESSION = "root";
|
|
15
|
+
/** A control prompt that only starts the container. */
|
|
16
|
+
export const CONTROL_WAKE = "control:wake";
|
|
17
|
+
/** Markers and job output in the container: made by the object, owned by the agent. */
|
|
18
|
+
export const RUN_DIR = "/run/da";
|
|
19
|
+
/** The marker a CLI turn waits for, holding the runtime id its egress was prepared for. */
|
|
20
|
+
export const EGRESS_READY = `${RUN_DIR}/egress-ready`;
|
|
21
|
+
/** How long a CLI turn waits for {@link EGRESS_READY} before it refuses to start. */
|
|
22
|
+
export const EGRESS_WAIT_MS = 60_000;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ContainerAdapter } from "agents/harness/container/daemon";
|
|
2
|
+
/**
|
|
3
|
+
* `adapter` for every session but the root one, which is the object's control
|
|
4
|
+
* channel: it answers a `control:` prompt at once and starts nothing.
|
|
5
|
+
*
|
|
6
|
+
* Runtime-agnostic, so the specs serve it from a Worker the way an image serves
|
|
7
|
+
* it from Node.
|
|
8
|
+
*/
|
|
9
|
+
export declare function withControlSession(adapter: ContainerAdapter): ContainerAdapter;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { CONTROL_SESSION, SANDBOX_CONTRACT } from "../constants.js";
|
|
2
|
+
/**
|
|
3
|
+
* `adapter` for every session but the root one, which is the object's control
|
|
4
|
+
* channel: it answers a `control:` prompt at once and starts nothing.
|
|
5
|
+
*
|
|
6
|
+
* Runtime-agnostic, so the specs serve it from a Worker the way an image serves
|
|
7
|
+
* it from Node.
|
|
8
|
+
*/
|
|
9
|
+
export function withControlSession(adapter) {
|
|
10
|
+
return {
|
|
11
|
+
id: adapter.id,
|
|
12
|
+
version: `${adapter.version}+sandbox.${SANDBOX_CONTRACT}`,
|
|
13
|
+
...(adapter.capabilities ? { capabilities: adapter.capabilities } : {}),
|
|
14
|
+
open(context) {
|
|
15
|
+
return context.session === CONTROL_SESSION
|
|
16
|
+
? controlSession()
|
|
17
|
+
: adapter.open(context);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function controlSession() {
|
|
22
|
+
return {
|
|
23
|
+
async run(turn) {
|
|
24
|
+
return textOf(turn.input).startsWith("control:")
|
|
25
|
+
? { status: "done", text: "ok" }
|
|
26
|
+
: { status: "unanswered", reason: "the control session runs no CLI" };
|
|
27
|
+
},
|
|
28
|
+
async close() { }
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function textOf(input) {
|
|
32
|
+
return typeof input === "string"
|
|
33
|
+
? input
|
|
34
|
+
: input.map((part) => (part.type === "text" ? part.text : "")).join("");
|
|
35
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dynamicagents/plugins/sandbox/daemon` — the entrypoint of a sandbox image.
|
|
3
|
+
*
|
|
4
|
+
* Node, not a Worker: this is what the container runs. A consumer bundles it
|
|
5
|
+
* with its own CLI options from its own `node_modules`, so the daemon and the
|
|
6
|
+
* object speak one `agents` version.
|
|
7
|
+
*
|
|
8
|
+
* It is the harness daemon with one adapter that wears two faces. The root
|
|
9
|
+
* session is the object's control channel: it answers at once and starts no
|
|
10
|
+
* CLI, which is how the object starts a container without spending a turn.
|
|
11
|
+
* Every other session runs the CLI, each turn launched the way its run header
|
|
12
|
+
* says and only once the object has prepared this container's egress.
|
|
13
|
+
*/
|
|
14
|
+
import { type ContainerAdapter } from "agents/harness/container/daemon";
|
|
15
|
+
import { type CliAdapterOptions, type CliCommand, type CliTurn, type DaemonServer } from "agents/harness/container/runtime";
|
|
16
|
+
import { type RunHeader } from "../header.js";
|
|
17
|
+
export { withControlSession } from "./composite.js";
|
|
18
|
+
/** One CLI turn, with its run header already taken off the prompt. */
|
|
19
|
+
export type SandboxCliTurn = CliTurn & {
|
|
20
|
+
readonly header: RunHeader | undefined;
|
|
21
|
+
};
|
|
22
|
+
export interface SandboxDaemonOptions {
|
|
23
|
+
/**
|
|
24
|
+
* The CLI the sessions run. Everything a sandbox decides about how it runs —
|
|
25
|
+
* its user, its homes, its working directory, the wait for egress — is set
|
|
26
|
+
* here and may not be passed.
|
|
27
|
+
*/
|
|
28
|
+
readonly cli: Omit<CliAdapterOptions, "command" | "user" | "homes" | "skeleton" | "cwd"> & {
|
|
29
|
+
readonly command: (turn: SandboxCliTurn) => CliCommand;
|
|
30
|
+
};
|
|
31
|
+
/** The environment the daemon was started with. Default `process.env`. */
|
|
32
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* What a session's home starts as: root-owned in the image, so a session starts
|
|
36
|
+
* from what the image says rather than from what an earlier one left.
|
|
37
|
+
*/
|
|
38
|
+
export declare const SESSION_SKELETON = "/etc/da/skel";
|
|
39
|
+
/** The adapter a sandbox image serves. Exported for specs; an image calls {@link serveSandboxDaemon}. */
|
|
40
|
+
export declare function sandboxAdapter(options: SandboxDaemonOptions, runtimeId: string): ContainerAdapter;
|
|
41
|
+
/**
|
|
42
|
+
* Serve the daemon with the identity, secret and port the object started the
|
|
43
|
+
* container with — as the agent, never root, holding {@link DAEMON_CAPABILITY}:
|
|
44
|
+
* the image starts it through `setpriv` (see the README), and this refuses to
|
|
45
|
+
* start otherwise.
|
|
46
|
+
*/
|
|
47
|
+
export declare function serveSandboxDaemon(options: SandboxDaemonOptions): Promise<DaemonServer>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dynamicagents/plugins/sandbox/daemon` — the entrypoint of a sandbox image.
|
|
3
|
+
*
|
|
4
|
+
* Node, not a Worker: this is what the container runs. A consumer bundles it
|
|
5
|
+
* with its own CLI options from its own `node_modules`, so the daemon and the
|
|
6
|
+
* object speak one `agents` version.
|
|
7
|
+
*
|
|
8
|
+
* It is the harness daemon with one adapter that wears two faces. The root
|
|
9
|
+
* session is the object's control channel: it answers at once and starts no
|
|
10
|
+
* CLI, which is how the object starts a container without spending a turn.
|
|
11
|
+
* Every other session runs the CLI, each turn launched the way its run header
|
|
12
|
+
* says and only once the object has prepared this container's egress.
|
|
13
|
+
*/
|
|
14
|
+
import { CONTAINER_ENV } from "agents/harness/container/daemon";
|
|
15
|
+
import { cliAdapter, serveFromEnv } from "agents/harness/container/runtime";
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import { AGENT_HOME, AGENT_UID } from "../agent-exec.js";
|
|
18
|
+
import { EGRESS_READY, EGRESS_WAIT_MS } from "../constants.js";
|
|
19
|
+
import { readRunHeader, SANDBOX_ROOT } from "../header.js";
|
|
20
|
+
import { withControlSession } from "./composite.js";
|
|
21
|
+
export { withControlSession } from "./composite.js";
|
|
22
|
+
/**
|
|
23
|
+
* The one capability the daemon holds, `CAP_NET_BIND_SERVICE`, which grants
|
|
24
|
+
* nothing here. It is held so the daemon's capabilities are not a subset of
|
|
25
|
+
* an agent process's: Linux then refuses same-uid processes its
|
|
26
|
+
* `/proc/<pid>/environ` and `mem`, where the harness token is. Measured: a CLI
|
|
27
|
+
* launched this way reads neither, and one that kept the capability reads the
|
|
28
|
+
* token. A same-uid process can still signal the daemon, which ends the
|
|
29
|
+
* container, as a crash would.
|
|
30
|
+
*/
|
|
31
|
+
const DAEMON_CAPABILITY = 10;
|
|
32
|
+
/** How the daemon launches the CLI: without the capability it holds. */
|
|
33
|
+
const DROP_DAEMON_CAPABILITY = [
|
|
34
|
+
"/usr/bin/setpriv",
|
|
35
|
+
"--ambient-caps=-all",
|
|
36
|
+
"--inh-caps=-all",
|
|
37
|
+
"--"
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* Wait for this container's egress, then run the CLI in the run's directory.
|
|
41
|
+
*
|
|
42
|
+
* `$1` is the runtime id the object writes into {@link EGRESS_READY} once it
|
|
43
|
+
* has registered what the harness does not; a marker naming another runtime is
|
|
44
|
+
* a snapshot's, and does not count.
|
|
45
|
+
*/
|
|
46
|
+
const LAUNCH = [
|
|
47
|
+
'rt="$1"; cwd="$2"; shift 2',
|
|
48
|
+
`n=0; while [ "$(cat ${EGRESS_READY} 2>/dev/null)" != "$rt" ]; do`,
|
|
49
|
+
` n=$((n+1)); if [ "$n" -gt ${Math.ceil(EGRESS_WAIT_MS / 100)} ]; then`,
|
|
50
|
+
' echo "the sandbox never prepared this container\'s egress" >&2; exit 97',
|
|
51
|
+
" fi; sleep 0.1",
|
|
52
|
+
"done",
|
|
53
|
+
'cd "$cwd" || exit 98',
|
|
54
|
+
'exec "$@"'
|
|
55
|
+
].join("\n");
|
|
56
|
+
/**
|
|
57
|
+
* What a session's home starts as: root-owned in the image, so a session starts
|
|
58
|
+
* from what the image says rather than from what an earlier one left.
|
|
59
|
+
*/
|
|
60
|
+
export const SESSION_SKELETON = "/etc/da/skel";
|
|
61
|
+
/** The adapter a sandbox image serves. Exported for specs; an image calls {@link serveSandboxDaemon}. */
|
|
62
|
+
export function sandboxAdapter(options, runtimeId) {
|
|
63
|
+
const cli = cliAdapter({
|
|
64
|
+
...options.cli,
|
|
65
|
+
// No `user`: the daemon is already the agent (see `serveSandboxDaemon`), so
|
|
66
|
+
// the adapter chowns nothing. Given one, it chowns each home as root, and
|
|
67
|
+
// chown follows a symlink the agent left there to whatever file it names.
|
|
68
|
+
homes: `${AGENT_HOME}/.harness/sessions`,
|
|
69
|
+
skeleton: SESSION_SKELETON,
|
|
70
|
+
command(turn) {
|
|
71
|
+
const { header, prompt } = readRunHeader(turn.prompt);
|
|
72
|
+
const inner = options.cli.command({ ...turn, prompt, header });
|
|
73
|
+
return {
|
|
74
|
+
...inner,
|
|
75
|
+
argv: [
|
|
76
|
+
...DROP_DAEMON_CAPABILITY,
|
|
77
|
+
"sh",
|
|
78
|
+
"-c",
|
|
79
|
+
LAUNCH,
|
|
80
|
+
"sandbox-cli",
|
|
81
|
+
runtimeId,
|
|
82
|
+
header?.cwd ?? SANDBOX_ROOT,
|
|
83
|
+
...inner.argv
|
|
84
|
+
]
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return withControlSession(cli);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Serve the daemon with the identity, secret and port the object started the
|
|
92
|
+
* container with — as the agent, never root, holding {@link DAEMON_CAPABILITY}:
|
|
93
|
+
* the image starts it through `setpriv` (see the README), and this refuses to
|
|
94
|
+
* start otherwise.
|
|
95
|
+
*/
|
|
96
|
+
export function serveSandboxDaemon(options) {
|
|
97
|
+
const uid = process.getuid?.();
|
|
98
|
+
if (uid !== AGENT_UID || !holdsCapability(DAEMON_CAPABILITY))
|
|
99
|
+
throw new Error(`the sandbox daemon runs as the agent (uid ${AGENT_UID}) holding net_bind_service, ` +
|
|
100
|
+
`and this is uid ${uid}: start it through setpriv, as the README's ENTRYPOINT does`);
|
|
101
|
+
const env = options.env ?? process.env;
|
|
102
|
+
// `serveFromEnv` deletes it from the env it is given, and the CLI inherits
|
|
103
|
+
// `process.env`: whatever env was given, the CLI gets no way to the daemon.
|
|
104
|
+
if (env !== process.env)
|
|
105
|
+
delete process.env[CONTAINER_ENV.token];
|
|
106
|
+
const runtimeId = env[CONTAINER_ENV.runtimeId];
|
|
107
|
+
if (!runtimeId)
|
|
108
|
+
throw new Error(`${CONTAINER_ENV.runtimeId} is not set: the sandbox object starts this image`);
|
|
109
|
+
return serveFromEnv(sandboxAdapter(options, runtimeId), env);
|
|
110
|
+
}
|
|
111
|
+
/** Whether this process's permitted set holds capability `bit`. */
|
|
112
|
+
function holdsCapability(bit) {
|
|
113
|
+
const permitted = /^CapPrm:\s*([0-9a-f]+)$/m.exec(readFileSync("/proc/self/status", "utf8"))?.[1];
|
|
114
|
+
return (permitted !== undefined &&
|
|
115
|
+
((BigInt(`0x${permitted}`) >> BigInt(bit)) & 1n) === 1n);
|
|
116
|
+
}
|