@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.
Files changed (141) hide show
  1. package/README.md +14 -14
  2. package/dist/claude-code/cards.d.ts +2 -0
  3. package/dist/claude-code/cards.js +11 -0
  4. package/dist/claude-code/config.d.ts +10 -83
  5. package/dist/claude-code/config.js +0 -2
  6. package/dist/claude-code/credentials.d.ts +32 -50
  7. package/dist/claude-code/credentials.js +115 -74
  8. package/dist/claude-code/daemon/index.d.ts +24 -0
  9. package/dist/claude-code/daemon/index.js +203 -0
  10. package/dist/claude-code/egress.d.ts +35 -117
  11. package/dist/claude-code/egress.js +80 -325
  12. package/dist/claude-code/events.d.ts +14 -17
  13. package/dist/claude-code/events.js +19 -15
  14. package/dist/claude-code/harness-wire.d.ts +45 -0
  15. package/dist/claude-code/harness-wire.js +47 -0
  16. package/dist/claude-code/index.d.ts +19 -29
  17. package/dist/claude-code/index.js +17 -25
  18. package/dist/claude-code/language-model.d.ts +19 -0
  19. package/dist/claude-code/language-model.js +71 -0
  20. package/dist/claude-code/model.d.ts +77 -70
  21. package/dist/claude-code/model.js +327 -327
  22. package/dist/claude-code/placeholder.d.ts +12 -0
  23. package/dist/claude-code/placeholder.js +12 -0
  24. package/dist/claude-code/spec.d.ts +4 -4
  25. package/dist/claude-code/spec.js +4 -4
  26. package/dist/computer/client.d.ts +88 -0
  27. package/dist/computer/client.js +6 -0
  28. package/dist/computer/context.d.ts +7 -7
  29. package/dist/computer/context.js +14 -18
  30. package/dist/computer/gate.d.ts +4 -23
  31. package/dist/computer/gate.js +1 -37
  32. package/dist/computer/index.d.ts +44 -43
  33. package/dist/computer/index.js +29 -50
  34. package/dist/computer/paths.d.ts +5 -27
  35. package/dist/computer/paths.js +7 -30
  36. package/dist/computer/proxy.d.ts +17 -31
  37. package/dist/computer/proxy.js +9 -44
  38. package/dist/computer/render.d.ts +3 -40
  39. package/dist/computer/render.js +3 -36
  40. package/dist/computer/sandbox.d.ts +55 -0
  41. package/dist/computer/sandbox.js +236 -0
  42. package/dist/computer/shell.d.ts +36 -0
  43. package/dist/computer/shell.js +41 -0
  44. package/dist/computer/tools-exec.js +3 -9
  45. package/dist/computer/tools-file.d.ts +1 -1
  46. package/dist/computer/tools-file.js +1 -1
  47. package/dist/{repo → repo-github}/checkout.d.ts +3 -4
  48. package/dist/{repo → repo-github}/checkout.js +3 -4
  49. package/dist/{repo → repo-github}/context.d.ts +14 -32
  50. package/dist/{repo → repo-github}/context.js +46 -81
  51. package/dist/{repo → repo-github}/index.d.ts +30 -13
  52. package/dist/{repo → repo-github}/index.js +25 -6
  53. package/dist/repo-github/mcp.d.ts +95 -0
  54. package/dist/repo-github/mcp.js +107 -0
  55. package/dist/repo-github/sandbox-egress.d.ts +68 -0
  56. package/dist/repo-github/sandbox-egress.js +272 -0
  57. package/dist/repo-github/sandbox-git.d.ts +30 -0
  58. package/dist/repo-github/sandbox-git.js +113 -0
  59. package/dist/{repo → repo-github}/tools-checks.js +43 -4
  60. package/dist/{repo → repo-github}/tools-clone.js +2 -21
  61. package/dist/{repo → repo-github}/tools-pr.d.ts +0 -1
  62. package/dist/{repo → repo-github}/tools-pr.js +85 -20
  63. package/dist/{repo → repo-github}/tools-review.d.ts +6 -0
  64. package/dist/{repo → repo-github}/tools-review.js +62 -176
  65. package/dist/{repo → repo-github}/url.d.ts +11 -0
  66. package/dist/{repo → repo-github}/url.js +12 -1
  67. package/dist/{workspace → sandbox}/advisory.d.ts +16 -35
  68. package/dist/{workspace → sandbox}/advisory.js +2 -29
  69. package/dist/sandbox/agent-exec.d.ts +31 -0
  70. package/dist/sandbox/agent-exec.js +62 -0
  71. package/dist/sandbox/ca-trust.d.ts +9 -0
  72. package/dist/sandbox/ca-trust.js +21 -0
  73. package/dist/sandbox/constants.d.ts +22 -0
  74. package/dist/sandbox/constants.js +22 -0
  75. package/dist/sandbox/daemon/composite.d.ts +9 -0
  76. package/dist/sandbox/daemon/composite.js +35 -0
  77. package/dist/sandbox/daemon/index.d.ts +47 -0
  78. package/dist/sandbox/daemon/index.js +116 -0
  79. package/dist/sandbox/egress.d.ts +81 -0
  80. package/dist/sandbox/egress.js +109 -0
  81. package/dist/sandbox/exec.d.ts +57 -0
  82. package/dist/sandbox/exec.js +52 -0
  83. package/dist/sandbox/format.d.ts +2 -0
  84. package/dist/sandbox/format.js +7 -0
  85. package/dist/sandbox/header.d.ts +50 -0
  86. package/dist/sandbox/header.js +94 -0
  87. package/dist/sandbox/index.d.ts +17 -0
  88. package/dist/sandbox/index.js +17 -0
  89. package/dist/{workspace/install.d.ts → sandbox/install-plan.d.ts} +2 -2
  90. package/dist/{workspace/install.js → sandbox/install-plan.js} +2 -2
  91. package/dist/sandbox/name.d.ts +27 -0
  92. package/dist/sandbox/name.js +32 -0
  93. package/dist/sandbox/object.d.ts +244 -0
  94. package/dist/sandbox/object.js +1139 -0
  95. package/dist/scratch/index.d.ts +1 -9
  96. package/dist/scratch/index.js +2 -18
  97. package/package.json +24 -20
  98. package/dist/claude-code/github.d.ts +0 -39
  99. package/dist/claude-code/github.js +0 -82
  100. package/dist/claude-code/run.d.ts +0 -491
  101. package/dist/claude-code/run.js +0 -913
  102. package/dist/claude-code/session.d.ts +0 -131
  103. package/dist/claude-code/session.js +0 -288
  104. package/dist/workspace/ca-trust.d.ts +0 -95
  105. package/dist/workspace/ca-trust.js +0 -312
  106. package/dist/workspace/container-deps.d.ts +0 -58
  107. package/dist/workspace/container-deps.js +0 -249
  108. package/dist/workspace/container-fault.d.ts +0 -38
  109. package/dist/workspace/container-fault.js +0 -73
  110. package/dist/workspace/deps-snapshot.d.ts +0 -110
  111. package/dist/workspace/deps-snapshot.js +0 -424
  112. package/dist/workspace/exec.d.ts +0 -94
  113. package/dist/workspace/exec.js +0 -153
  114. package/dist/workspace/format.d.ts +0 -36
  115. package/dist/workspace/format.js +0 -57
  116. package/dist/workspace/git-host.d.ts +0 -79
  117. package/dist/workspace/git-host.js +0 -226
  118. package/dist/workspace/git-identity.d.ts +0 -57
  119. package/dist/workspace/git-identity.js +0 -147
  120. package/dist/workspace/index.d.ts +0 -50
  121. package/dist/workspace/index.js +0 -58
  122. package/dist/workspace/install-job.d.ts +0 -170
  123. package/dist/workspace/install-job.js +0 -934
  124. package/dist/workspace/object.d.ts +0 -522
  125. package/dist/workspace/object.js +0 -1541
  126. package/dist/workspace/open.d.ts +0 -95
  127. package/dist/workspace/open.js +0 -63
  128. package/dist/workspace/read.d.ts +0 -16
  129. package/dist/workspace/read.js +0 -22
  130. package/dist/workspace/shell.d.ts +0 -45
  131. package/dist/workspace/shell.js +0 -80
  132. package/dist/workspace/sync.d.ts +0 -111
  133. package/dist/workspace/sync.js +0 -124
  134. package/dist/workspace/wake.d.ts +0 -21
  135. package/dist/workspace/wake.js +0 -10
  136. /package/dist/{repo → repo-github}/tools-checks.d.ts +0 -0
  137. /package/dist/{repo → repo-github}/tools-clone.d.ts +0 -0
  138. /package/dist/{repo → repo-github}/tools-worktree.d.ts +0 -0
  139. /package/dist/{repo → repo-github}/tools-worktree.js +0 -0
  140. /package/dist/{repo → repo-github}/tools-worktrees.d.ts +0 -0
  141. /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 workspace, and how to say it.
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 WorkspaceAdvisory}
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 WorkspaceAdvisory = {
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, and the combination that
78
- * proves it is `storage-exhausted`: permanent, reaches every command, and the
79
- * only one under which writes are lost. Collapsing any pair would have hidden
80
- * exactly that case.
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. A full workspace does, because the write that is
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: WorkspaceAdvisory): AdvisoryShape;
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: WorkspaceAdvisory, to: AdvisoryAudience): string;
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 WorkspaceAdvisory} `treePresent` and the reader is told which way the
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, and that is not future-proofing. A full workspace and a failed
173
- * install are both true at once, and a single-slot record could only keep the
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 WorkspaceAdvisory[];
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 WorkspaceAdvisory[]): string | undefined;
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, and that is not future-proofing. A full workspace and a failed
104
- * install are both true at once, and a single-slot record could only keep the
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
+ }