@dynamicagents/plugins 0.12.0 → 0.13.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 (130) hide show
  1. package/README.md +58 -77
  2. package/dist/browser/index.d.ts +0 -2
  3. package/dist/browser/index.js +21 -27
  4. package/dist/claude-code/cards.d.ts +53 -0
  5. package/dist/claude-code/cards.js +272 -0
  6. package/dist/claude-code/config.d.ts +28 -63
  7. package/dist/claude-code/config.js +0 -11
  8. package/dist/claude-code/egress.d.ts +8 -1
  9. package/dist/claude-code/egress.js +42 -4
  10. package/dist/claude-code/events.d.ts +106 -9
  11. package/dist/claude-code/events.js +201 -56
  12. package/dist/claude-code/github.d.ts +39 -0
  13. package/dist/claude-code/github.js +82 -0
  14. package/dist/claude-code/index.d.ts +16 -90
  15. package/dist/claude-code/index.js +14 -276
  16. package/dist/claude-code/model.d.ts +152 -0
  17. package/dist/claude-code/model.js +342 -0
  18. package/dist/claude-code/run.d.ts +209 -112
  19. package/dist/claude-code/run.js +339 -138
  20. package/dist/claude-code/session.d.ts +131 -0
  21. package/dist/claude-code/session.js +285 -0
  22. package/dist/claude-code/spec.d.ts +46 -0
  23. package/dist/claude-code/spec.js +83 -0
  24. package/dist/claude-code/verified.d.ts +15 -0
  25. package/dist/claude-code/verified.js +15 -0
  26. package/dist/computer/context.d.ts +1 -5
  27. package/dist/computer/context.js +7 -10
  28. package/dist/computer/file-lock.js +5 -4
  29. package/dist/computer/gate.d.ts +4 -4
  30. package/dist/computer/gate.js +5 -5
  31. package/dist/computer/index.d.ts +40 -258
  32. package/dist/computer/index.js +71 -324
  33. package/dist/computer/paths.d.ts +36 -16
  34. package/dist/computer/paths.js +42 -29
  35. package/dist/computer/proxy.d.ts +45 -0
  36. package/dist/computer/proxy.js +234 -0
  37. package/dist/computer/render.d.ts +1 -40
  38. package/dist/computer/render.js +2 -68
  39. package/dist/computer/tools-exec.js +12 -13
  40. package/dist/computer/tools-file.d.ts +9 -1
  41. package/dist/computer/tools-file.js +22 -82
  42. package/dist/computer/{tools-find.d.ts → tools-grep.d.ts} +1 -1
  43. package/dist/computer/tools-grep.js +129 -0
  44. package/dist/repo/checkout.d.ts +4 -5
  45. package/dist/repo/checkout.js +31 -8
  46. package/dist/repo/context.d.ts +35 -9
  47. package/dist/repo/context.js +101 -49
  48. package/dist/repo/index.d.ts +73 -28
  49. package/dist/repo/index.js +112 -96
  50. package/dist/repo/tools-checks.d.ts +3 -0
  51. package/dist/repo/tools-checks.js +112 -0
  52. package/dist/repo/tools-clone.js +1 -1
  53. package/dist/repo/tools-pr.d.ts +14 -0
  54. package/dist/repo/{tools-forge.js → tools-pr.js} +53 -36
  55. package/dist/repo/tools-review.d.ts +9 -0
  56. package/dist/repo/tools-review.js +67 -47
  57. package/dist/repo/tools-worktree.js +144 -60
  58. package/dist/repo/tools-worktrees.d.ts +12 -0
  59. package/dist/repo/tools-worktrees.js +48 -0
  60. package/dist/repo/url.d.ts +2 -2
  61. package/dist/repo/url.js +7 -7
  62. package/dist/scratch/index.d.ts +4 -4
  63. package/dist/scratch/index.js +12 -11
  64. package/dist/{computer → workspace}/advisory.d.ts +4 -4
  65. package/dist/{computer → workspace}/advisory.js +2 -2
  66. package/dist/{computer/host → workspace}/ca-trust.d.ts +8 -1
  67. package/dist/{computer/host → workspace}/ca-trust.js +12 -3
  68. package/dist/{computer/host → workspace}/container-fault.d.ts +3 -3
  69. package/dist/{computer/host → workspace}/container-fault.js +23 -8
  70. package/dist/workspace/exec.d.ts +94 -0
  71. package/dist/workspace/exec.js +153 -0
  72. package/dist/workspace/format.d.ts +36 -0
  73. package/dist/workspace/format.js +57 -0
  74. package/dist/{computer/host → workspace}/git-host.d.ts +21 -4
  75. package/dist/{computer/host → workspace}/git-host.js +72 -2
  76. package/dist/{computer/host → workspace}/git-identity.d.ts +1 -1
  77. package/dist/{computer/host → workspace}/git-identity.js +1 -1
  78. package/dist/workspace/index.d.ts +43 -47
  79. package/dist/workspace/index.js +52 -112
  80. package/dist/{computer/host → workspace}/install-job.d.ts +4 -4
  81. package/dist/{computer/host → workspace}/install-job.js +276 -176
  82. package/dist/{computer → workspace}/install.d.ts +8 -8
  83. package/dist/{computer → workspace}/install.js +3 -3
  84. package/dist/{computer/host/workspace.d.ts → workspace/object.d.ts} +105 -27
  85. package/dist/{computer/host/workspace.js → workspace/object.js} +217 -48
  86. package/dist/workspace/open.d.ts +95 -0
  87. package/dist/workspace/open.js +63 -0
  88. package/dist/workspace/read.d.ts +16 -0
  89. package/dist/workspace/read.js +22 -0
  90. package/dist/{computer → workspace}/shell.d.ts +5 -5
  91. package/dist/{computer → workspace}/shell.js +6 -6
  92. package/dist/{computer/host → workspace}/wake.d.ts +1 -1
  93. package/package.json +16 -36
  94. package/dist/arc-agi/analysis.d.ts +0 -322
  95. package/dist/arc-agi/analysis.js +0 -802
  96. package/dist/arc-agi/client.d.ts +0 -82
  97. package/dist/arc-agi/client.js +0 -113
  98. package/dist/arc-agi/game-tools.d.ts +0 -24
  99. package/dist/arc-agi/game-tools.js +0 -26
  100. package/dist/arc-agi/index.d.ts +0 -40
  101. package/dist/arc-agi/index.js +0 -175
  102. package/dist/arc-agi/main-agent.d.ts +0 -24
  103. package/dist/arc-agi/main-agent.js +0 -62
  104. package/dist/arc-agi/recipe.d.ts +0 -48
  105. package/dist/arc-agi/recipe.js +0 -97
  106. package/dist/arc-agi/scorecard.d.ts +0 -118
  107. package/dist/arc-agi/scorecard.js +0 -163
  108. package/dist/arc-agi/soul.d.ts +0 -29
  109. package/dist/arc-agi/soul.js +0 -79
  110. package/dist/arc-agi/store.d.ts +0 -153
  111. package/dist/arc-agi/store.js +0 -215
  112. package/dist/arc-agi/tools.d.ts +0 -36
  113. package/dist/arc-agi/tools.js +0 -574
  114. package/dist/arc-agi/types.d.ts +0 -211
  115. package/dist/arc-agi/types.js +0 -19
  116. package/dist/claude-code/recipe.d.ts +0 -53
  117. package/dist/claude-code/recipe.js +0 -100
  118. package/dist/computer/read.d.ts +0 -133
  119. package/dist/computer/read.js +0 -200
  120. package/dist/computer/tools-find.js +0 -216
  121. package/dist/recall/index.d.ts +0 -105
  122. package/dist/recall/index.js +0 -181
  123. package/dist/repo/tools-forge.d.ts +0 -4
  124. package/dist/triage/index.d.ts +0 -360
  125. package/dist/triage/index.js +0 -311
  126. /package/dist/{computer/host → workspace}/container-deps.d.ts +0 -0
  127. /package/dist/{computer/host → workspace}/container-deps.js +0 -0
  128. /package/dist/{computer/host → workspace}/sync.d.ts +0 -0
  129. /package/dist/{computer/host → workspace}/sync.js +0 -0
  130. /package/dist/{computer/host → workspace}/wake.js +0 -0
package/dist/repo/url.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * naming it": every pattern here turns away a string that resolves to something
7
7
  * other than what it appears to name.
8
8
  */
9
- /** The forges a clone URL may name. `RepoConfig.allowedHosts` overrides it. */
9
+ /** The hosts a clone URL may name. `RepoConfig.allowedHosts` overrides it. */
10
10
  export const DEFAULT_ALLOWED_HOSTS = ["github.com"];
11
11
  /** Branch names a push must never target, whatever the model believes. */
12
12
  export const PROTECTED_BRANCHES = new Set([
@@ -88,7 +88,7 @@ export function sameRepoUrl(a, b) {
88
88
  return canonical(a) === canonical(b);
89
89
  }
90
90
  /**
91
- * A name a forge would actually issue — and one that is safe to use as a path
91
+ * A name GitHub would actually issue — and one that is safe to use as a path
92
92
  * segment and as a key.
93
93
  *
94
94
  * GitHub's rule for an owner and a repository alike is alphanumerics, `-`, `_`
@@ -101,17 +101,17 @@ export function sameRepoUrl(a, b) {
101
101
  * is a separator in somebody else's key format, which is how two callers end up
102
102
  * sharing one workspace.
103
103
  */
104
- const FORGE_NAME = /^[A-Za-z0-9._-]+$/;
105
- function isForgeName(name) {
104
+ const GITHUB_NAME = /^[A-Za-z0-9._-]+$/;
105
+ function isGitHubName(name) {
106
106
  // `.` and `..` pass the character class and are exactly the two that must not.
107
- return name !== "." && name !== ".." && FORGE_NAME.test(name);
107
+ return name !== "." && name !== ".." && GITHUB_NAME.test(name);
108
108
  }
109
109
  /**
110
110
  * `owner/repo` out of a repository URL, or `undefined` if it is not one.
111
111
  *
112
112
  * Anchored on the **parsed host** rather than matched in the string, so
113
113
  * `https://evil.example.com/github.com/owner/repo` is not a github.com URL.
114
- * Both names are then checked against {@link isForgeName}, so a name that could
114
+ * Both names are then checked against {@link isGitHubName}, so a name that could
115
115
  * traverse a path or split somebody else's key never gets as far as being one.
116
116
  *
117
117
  * `undefined` is a refusal at every caller, never a reason to carry on with a
@@ -125,7 +125,7 @@ export function parseRepo(url, allowedHosts = DEFAULT_ALLOWED_HOSTS) {
125
125
  if (!match)
126
126
  return undefined;
127
127
  const [, owner, repo] = match;
128
- if (!isForgeName(owner) || !isForgeName(repo))
128
+ if (!isGitHubName(owner) || !isGitHubName(repo))
129
129
  return undefined;
130
130
  return { owner, repo };
131
131
  }
@@ -42,7 +42,7 @@ import type { AgentPlugin } from "@dynamicagents/core";
42
42
  /** Where a scratchpad lives, unless the host says otherwise. */
43
43
  export declare const DEFAULT_SCRATCH_DIR = "/workspace/scratch";
44
44
  /**
45
- * Run one command in the host's container. Matches `computerExec`'s shape.
45
+ * Run one command in the host's container. Matches `workspaceExec`'s shape.
46
46
  *
47
47
  * Injected rather than imported, like `/repo`'s: `npm run verify:exports` fails
48
48
  * any subpath whose module graph reaches a sibling's, and a scratchpad that
@@ -54,7 +54,7 @@ export type ScratchExec = (command: string, options?: {
54
54
  env?: Record<string, string | undefined>;
55
55
  timeout?: number;
56
56
  /**
57
- * The executing subtask's runtime state, forwarded **opaquely** — the same
57
+ * The running sub-agent's `runtime()`, forwarded **opaquely** — the same
58
58
  * pass-through `/repo` does, for the same reason. This plugin never looks
59
59
  * inside it.
60
60
  */
@@ -96,7 +96,7 @@ export interface ScratchConfig {
96
96
  * Runs commands in the container holding the workspace.
97
97
  *
98
98
  * Uncredentialed, and nothing here ever needs a credential: a scratchpad has
99
- * no remote, so no operation in this plugin talks to a forge.
99
+ * no remote, so no operation in this plugin talks to GitHub.
100
100
  */
101
101
  exec: ScratchExec;
102
102
  /** Where the scratchpad lives. Defaults to {@link DEFAULT_SCRATCH_DIR}. */
@@ -153,6 +153,6 @@ export interface ScratchConfig {
153
153
  * carries the same copy for the same reason.
154
154
  */
155
155
  export declare function truncateOutput(text: string, max: number): string;
156
- /** The tool name, exported so a host restricting its main agent can name it. */
156
+ /** The tool name, exported so a host restricting the plugin can name it. */
157
157
  export declare const SCRATCH_OPEN_TOOL = "scratch_open";
158
158
  export declare function scratch(config: ScratchConfig): AgentPlugin;
@@ -68,10 +68,10 @@ export function truncateOutput(text, max) {
68
68
  return text.slice(0, Math.max(0, max));
69
69
  return (text.slice(0, half) + marker(text.length - half * 2) + text.slice(-half));
70
70
  }
71
- /** The tool name, exported so a host restricting its main agent can name it. */
71
+ /** The tool name, exported so a host restricting the plugin can name it. */
72
72
  export const SCRATCH_OPEN_TOOL = "scratch_open";
73
73
  /**
74
- * What the delegating agent is told.
74
+ * What the model is told.
75
75
  *
76
76
  * The mixing rule at the end is the line worth its tokens. A host that keys one
77
77
  * workspace selection per caller — which is what makes a scratchpad routable at
@@ -294,19 +294,20 @@ export function scratch(config) {
294
294
  .filter(Boolean)
295
295
  .join(" "), maxChars);
296
296
  };
297
+ const context = capabilityFor(dir);
297
298
  return definePlugin({
298
- key: "scratch",
299
+ name: "scratch",
299
300
  /**
300
- * The main agent's, and no tool family — which is a decision rather than an
301
- * omission.
301
+ * For a parent, never a sub-agent — which the host decides by installing it
302
+ * on one and not the other.
302
303
  *
303
304
  * Opening a scratchpad *selects a workspace*, exactly as `repo_clone` does.
304
- * A subagent holding this could re-point the workspace its parent prepared
305
+ * A sub-agent holding this could re-point the workspace its parent prepared
305
306
  * half-way through its own run, which is the hazard a host's per-caller
306
- * selection already has to document. A delegated run works in whatever it
307
- * was given.
307
+ * selection already has to document. A sub-agent works in whatever it was
308
+ * given.
308
309
  */
309
- mainAgentTools: () => ({
310
+ tools: (ctx) => ({
310
311
  [SCRATCH_OPEN_TOOL]: tool({
311
312
  description: "Open a scratchpad: a git repository with no remote, in your container, " +
312
313
  "for work that does not need a cloned repository. Use it before " +
@@ -318,9 +319,9 @@ export function scratch(config) {
318
319
  .optional()
319
320
  .describe("Discard everything in the scratchpad first, including files an earlier task left")
320
321
  }),
321
- execute: ({ reset }) => open(reset, undefined)
322
+ execute: ({ reset }) => open(reset, ctx.runtime())
322
323
  })
323
324
  }),
324
- capability: capabilityFor(dir)
325
+ context: [{ provider: { get: async () => context } }]
325
326
  });
326
327
  }
@@ -17,9 +17,9 @@ import type { InstallState } from "./install.js";
17
17
  * A ceiling is a hard wall about the Durable Object, and the two are then told
18
18
  * apart only by reading prose.
19
19
  * - Capacity must not travel through a channel gated by
20
- * {@link file://./gate.ts needsDependencies}, or a full workspace says nothing
20
+ * {@link file://../computer/gate.ts needsDependencies}, or a full workspace says nothing
21
21
  * at all to `echo hi > file.txt` — the command whose write is being lost.
22
- * - "The tree is fine now", after a subagent installed by hand, has no honest
22
+ * - "The tree is fine now", after an agent installed by hand, has no honest
23
23
  * spelling as a job record: fabricating a `done` rests on the presence of a
24
24
  * `node_modules` directory, which the wreckage of the very install being
25
25
  * overridden already satisfies.
@@ -56,7 +56,7 @@ export type WorkspaceAdvisory = {
56
56
  * Reported rather than acted on, because its presence proves nothing in
57
57
  * either direction. A half-finished `npm ci` leaves the directory behind
58
58
  * with an incomplete tree, so treating existence as success hides the
59
- * common failure outright; and a subagent that re-ran the install by hand
59
+ * common failure outright; and an agent that re-ran the install by hand
60
60
  * leaves a record the host cannot update, since the host cannot see an
61
61
  * install it did not start. Both look identical from here, so the reader
62
62
  * is told which way the ambiguity runs rather than being handed a guess.
@@ -181,7 +181,7 @@ export declare function deriveAdvisories(input: AdvisoryInput): readonly Workspa
181
181
  /**
182
182
  * Everything a session should know before it starts, or `undefined` for silence.
183
183
  *
184
- * The counterpart to {@link file://./gate.ts execGate}, for the reader that is a
184
+ * The counterpart to {@link file://../computer/gate.ts execGate}, for the reader that is a
185
185
  * whole run rather than one command. It cannot be blocked and does not need to
186
186
  * be: it can install dependencies itself, and the only thing it must not do is
187
187
  * work silently against a workspace that is not what it assumes.
@@ -1,4 +1,4 @@
1
- import { humanMs } from "./render.js";
1
+ import { humanMs } from "./format.js";
2
2
  /** What each advisory *means*. The one exhaustive home for policy. */
3
3
  export function shapeOf(advisory) {
4
4
  switch (advisory.kind) {
@@ -188,7 +188,7 @@ export function deriveAdvisories(input) {
188
188
  /**
189
189
  * Everything a session should know before it starts, or `undefined` for silence.
190
190
  *
191
- * The counterpart to {@link file://./gate.ts execGate}, for the reader that is a
191
+ * The counterpart to {@link file://../computer/gate.ts execGate}, for the reader that is a
192
192
  * whole run rather than one command. It cannot be blocked and does not need to
193
193
  * be: it can install dependencies itself, and the only thing it must not do is
194
194
  * work silently against a workspace that is not what it assumes.
@@ -70,7 +70,14 @@ export declare class ContainerTrust {
70
70
  #private;
71
71
  private readonly deps;
72
72
  constructor(deps: ContainerTrustDeps);
73
- /** Forget that any container was trusted. */
73
+ /**
74
+ * Forget that any container was trusted.
75
+ *
76
+ * Drops the standing watch too, because this is also reached for a container
77
+ * that keeps running — a `running` read, a lost exec. The watch it leaves
78
+ * behind ignores that container's real exit, so without a fresh one nothing
79
+ * would clear the flag again and the replacement would be vouched for.
80
+ */
74
81
  forget(): void;
75
82
  /**
76
83
  * Trust the CA if this container has not been trusted yet.
@@ -127,7 +127,7 @@ export class ContainerTrust {
127
127
  #trusted = false;
128
128
  /** Whether an exit watch is standing for the container now running. */
129
129
  #watching = false;
130
- /** Bumped by {@link forget}, so a watch settling after it is ignored. */
130
+ /** Bumped by {@link forget}, so a watch armed before it is ignored. */
131
131
  #generation = 0;
132
132
  /**
133
133
  * Whether this container's deployment fault has already been reported.
@@ -141,10 +141,18 @@ export class ContainerTrust {
141
141
  constructor(deps) {
142
142
  this.deps = deps;
143
143
  }
144
- /** Forget that any container was trusted. */
144
+ /**
145
+ * Forget that any container was trusted.
146
+ *
147
+ * Drops the standing watch too, because this is also reached for a container
148
+ * that keeps running — a `running` read, a lost exec. The watch it leaves
149
+ * behind ignores that container's real exit, so without a fresh one nothing
150
+ * would clear the flag again and the replacement would be vouched for.
151
+ */
145
152
  forget() {
146
153
  this.#generation += 1;
147
154
  this.#trusted = false;
155
+ this.#watching = false;
148
156
  this.#faultReported = false;
149
157
  }
150
158
  /**
@@ -171,8 +179,9 @@ export class ContainerTrust {
171
179
  return;
172
180
  const generation = this.#generation;
173
181
  try {
182
+ // A stale watch returns before touching anything: `#watching` belongs to
183
+ // whichever watch was armed since.
174
184
  const done = () => {
175
- this.#watching = false;
176
185
  if (generation !== this.#generation)
177
186
  return;
178
187
  this.forget();
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * Everything else this host catches is the kind of thing a retry clears: a
5
5
  * container that is starting, a connection that dropped, a command that died
6
- * with it. These two are not. They describe a Worker and a container
7
- * application that cannot work together no matter how many times they are
8
- * asked, and they end only when an operator deploys.
6
+ * with it. These are not. They describe a Worker and a container application
7
+ * that cannot work together no matter how many times they are asked, and they
8
+ * end only when an operator deploys.
9
9
  *
10
10
  * **This changes what is said, not what is done.** No caller branches its
11
11
  * control flow on the answer, and none should start: the match is on a string
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * Everything else this host catches is the kind of thing a retry clears: a
5
5
  * container that is starting, a connection that dropped, a command that died
6
- * with it. These two are not. They describe a Worker and a container
7
- * application that cannot work together no matter how many times they are
8
- * asked, and they end only when an operator deploys.
6
+ * with it. These are not. They describe a Worker and a container application
7
+ * that cannot work together no matter how many times they are asked, and they
8
+ * end only when an operator deploys.
9
9
  *
10
10
  * **This changes what is said, not what is done.** No caller branches its
11
11
  * control flow on the answer, and none should start: the match is on a string
@@ -21,14 +21,18 @@
21
21
  /**
22
22
  * Match on the shape the backend formats, not on the prose after it.
23
23
  *
24
- * `[stage=auth]` is the stage label `CloudflareContainerBackend` writes into
25
- * every message it throws from that stage, and the container-application
26
- * sentence is the runtime's own, surfaced verbatim through the backend's
27
- * `priorExit=` field. Both are stabler than the explanations they introduce, and
28
- * neither has a `code` to key on the way a lost exec does.
24
+ * `[stage=auth]` is the stage label `ContainerBackend` writes into every
25
+ * message it throws from that stage, and the container-application sentence is
26
+ * the runtime's own, surfaced verbatim through the backend's `priorExit=` field.
27
+ * `prepared images` is in both of the backend's refusals to start without an
28
+ * image — none prepared at all, and none under the key it was asked for — and
29
+ * survives the reconnect wrapper's truncation of either. All are stabler than
30
+ * the explanations they introduce, and none has a `code` to key on the way a
31
+ * lost exec does.
29
32
  */
30
33
  const AUTH_STAGE = "[stage=auth]";
31
34
  const NO_APPLICATION = "no container application";
35
+ const NO_IMAGE = "prepared images";
32
36
  /**
33
37
  * Whether this error says the deployment is wrong, and what to say about it.
34
38
  *
@@ -54,5 +58,16 @@ export function deploymentFault(err) {
54
58
  "creates it — deleting it does not cause it to be rebuilt — so this " +
55
59
  "clears when an operator redeploys, and not before."
56
60
  };
61
+ if (text.includes(NO_IMAGE))
62
+ return {
63
+ summary: "this Worker prepared no image for the workspace container",
64
+ remedy: "the container application has no image under the name this workspace " +
65
+ "starts, so no container can start. The workspace schedules its own " +
66
+ 'containers, which takes `scheduling_policy: "durable_object"` and an ' +
67
+ "`images` entry for that name in the Worker's containers block, and an " +
68
+ "application created under another policy is deleted before that " +
69
+ "deploy rather than switched. This clears when an operator redeploys " +
70
+ "with both in place, and not before."
71
+ };
57
72
  return undefined;
58
73
  }
@@ -0,0 +1,94 @@
1
+ import { type WorkspaceHost } from "./open.js";
2
+ /**
3
+ * Running a command in a workspace's container, from outside the workspace
4
+ * object: what `/computer`'s tools and `/repo`'s git share, and how a host
5
+ * reaches its own container.
6
+ */
7
+ /** A command that has not finished in this long is a hung command. */
8
+ export declare const DEFAULT_TIMEOUT_MS: number;
9
+ /** Where checkouts live, in the container and in the workspace alike. */
10
+ export declare const DEFAULT_CWD = "/workspace";
11
+ /** Which workspace a caller reaches, and how its commands run there. */
12
+ export interface WorkspaceClientConfig {
13
+ /**
14
+ * The Durable Object namespace holding workspaces, closed over at
15
+ * instantiation so it is never model input.
16
+ */
17
+ binding: DurableObjectNamespace<WorkspaceHost>;
18
+ /**
19
+ * Which workspace this agent gets, by name. A thunk because the useful name —
20
+ * caller plus repository — does not exist when the plugin list is built.
21
+ *
22
+ * One workspace is one container is one repository: `@cloudflare/computer`
23
+ * pairs a Durable Object with exactly one container, so "never mix two
24
+ * repositories" is structural here rather than a convention to maintain.
25
+ */
26
+ workspaceName: () => string;
27
+ /** Working directory for every command. Defaults to `/workspace`. */
28
+ cwd?: string;
29
+ /**
30
+ * Run every command through this shell, e.g. `"bash"`. Unset, the command
31
+ * string goes to the runtime as-is and lands on whatever `/bin/sh` is.
32
+ *
33
+ * Worth setting, because "whatever `/bin/sh` is" is **dash** on Debian and
34
+ * Ubuntu, and a model writing shell writes *bash*. `${PIPESTATUS[0]}` is the
35
+ * one that costs most — a bash builtin a model reaches for to read a piped exit
36
+ * code, which dash fails with exit 2, after the command it wrapped has already
37
+ * run. It is one member of a family (`[[ ]]`, arrays, `set -o pipefail`,
38
+ * process substitution), so the fix is the shell rather than a note in a prompt
39
+ * telling the model to write POSIX.
40
+ *
41
+ * Set it only if the image actually has that shell: a missing one fails *every*
42
+ * command rather than the bash-flavoured ones.
43
+ */
44
+ shell?: string;
45
+ /** Per-command timeout. Defaults to ten minutes. */
46
+ timeoutMs?: number;
47
+ }
48
+ /**
49
+ * An `exec` bound to this workspace, for plugins that need a shell but should
50
+ * not own a container.
51
+ *
52
+ * `@dynamicagents/plugins/repo` is the reason this exists: it needs `git` on a real
53
+ * shell, but depending on this module would weld the two together and stop a
54
+ * host from pointing it at its own container. Structurally typed for the same
55
+ * reason — the two plugins compose without either importing the other.
56
+ *
57
+ * ## The agent tools' `env` is deliberately **not** merged here
58
+ *
59
+ * `/computer`'s `ComputerConfig.env` is merged into every command its tools
60
+ * run; this takes no environment of its own.
61
+ *
62
+ * What this hands out is a raw shell to *another plugin*, whose commands this
63
+ * module neither writes nor sees. `/repo` runs its unauthenticated git through
64
+ * it and pins the environment those need — `GIT_TERMINAL_PROMPT=0` plus the
65
+ * model-authored values it passes as variables. A host environment merged
66
+ * underneath lets through every key it does *not* pin: `http_proxy`,
67
+ * `GIT_PROXY_COMMAND`, `GIT_SSL_CAINFO`, `GIT_CONFIG_GLOBAL`, `GIT_EXEC_PATH` —
68
+ * the last two redirect the config git reads and relocate its helper binaries.
69
+ *
70
+ * No token is at stake, since the credentialed operations do not run here at
71
+ * all. The point is that changing what git does inside another plugin's commands
72
+ * should not be something a tools plugin's config field does by accident,
73
+ * from a host that only meant to set a registry. A host that wants it composes
74
+ * it where the merge is visible:
75
+ *
76
+ * ```ts
77
+ * exec: (command, options) =>
78
+ * workspaceExec(config)(command, {
79
+ * ...options,
80
+ * env: { ...mine, ...options?.env }
81
+ * });
82
+ * ```
83
+ */
84
+ export declare function workspaceExec(config: WorkspaceClientConfig): (command: string, options?: {
85
+ cwd?: string;
86
+ env?: Record<string, string | undefined>;
87
+ timeout?: number;
88
+ runtime?: unknown;
89
+ }) => Promise<{
90
+ success: boolean;
91
+ stdout: string;
92
+ stderr: string;
93
+ exitCode: number;
94
+ }>;
@@ -0,0 +1,153 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
53
+ import { cancelledNote } from "./format.js";
54
+ import { openWorkspace, workspaceNameFromRuntime } from "./open.js";
55
+ import { withShell } from "./shell.js";
56
+ /**
57
+ * Running a command in a workspace's container, from outside the workspace
58
+ * object: what `/computer`'s tools and `/repo`'s git share, and how a host
59
+ * reaches its own container.
60
+ */
61
+ /** A command that has not finished in this long is a hung command. */
62
+ export const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
63
+ /** Where checkouts live, in the container and in the workspace alike. */
64
+ export const DEFAULT_CWD = "/workspace";
65
+ /**
66
+ * An `exec` bound to this workspace, for plugins that need a shell but should
67
+ * not own a container.
68
+ *
69
+ * `@dynamicagents/plugins/repo` is the reason this exists: it needs `git` on a real
70
+ * shell, but depending on this module would weld the two together and stop a
71
+ * host from pointing it at its own container. Structurally typed for the same
72
+ * reason — the two plugins compose without either importing the other.
73
+ *
74
+ * ## The agent tools' `env` is deliberately **not** merged here
75
+ *
76
+ * `/computer`'s `ComputerConfig.env` is merged into every command its tools
77
+ * run; this takes no environment of its own.
78
+ *
79
+ * What this hands out is a raw shell to *another plugin*, whose commands this
80
+ * module neither writes nor sees. `/repo` runs its unauthenticated git through
81
+ * it and pins the environment those need — `GIT_TERMINAL_PROMPT=0` plus the
82
+ * model-authored values it passes as variables. A host environment merged
83
+ * underneath lets through every key it does *not* pin: `http_proxy`,
84
+ * `GIT_PROXY_COMMAND`, `GIT_SSL_CAINFO`, `GIT_CONFIG_GLOBAL`, `GIT_EXEC_PATH` —
85
+ * the last two redirect the config git reads and relocate its helper binaries.
86
+ *
87
+ * No token is at stake, since the credentialed operations do not run here at
88
+ * all. The point is that changing what git does inside another plugin's commands
89
+ * should not be something a tools plugin's config field does by accident,
90
+ * from a host that only meant to set a registry. A host that wants it composes
91
+ * it where the merge is visible:
92
+ *
93
+ * ```ts
94
+ * exec: (command, options) =>
95
+ * workspaceExec(config)(command, {
96
+ * ...options,
97
+ * env: { ...mine, ...options?.env }
98
+ * });
99
+ * ```
100
+ */
101
+ export function workspaceExec(config) {
102
+ return async (command, options) => {
103
+ const env_1 = { stack: [], error: void 0, hasError: false };
104
+ try {
105
+ // The caller forwards its sub-agent's runtime opaquely; only this side
106
+ // knows the key it might carry. That is what lets `/repo` reach a sub-agent's
107
+ // shared workspace without importing anything from here.
108
+ const name = workspaceNameFromRuntime(options?.runtime) ?? config.workspaceName();
109
+ const ws = __addDisposableResource(env_1, await openWorkspace(config.binding.get(config.binding.idFromName(name))), false);
110
+ const env = options?.env
111
+ ? Object.fromEntries(Object.entries(options.env).filter((entry) => entry[1] !== undefined))
112
+ : undefined;
113
+ // Named rather than inlined, because the note below has to state it: a
114
+ // ceiling the model cannot see is one it cannot work within.
115
+ const timeoutMs = options?.timeout ?? config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
116
+ // `withShell`, never `withShellTranscript`: the caller reads this result in
117
+ // code. `/repo` compares `stdout` against a URL, tests it for emptiness to
118
+ // decide a tree is clean, and reads a sha out of it to push — all of which
119
+ // need `stdout` to carry the answer alone, with git's diagnostics on the
120
+ // channel git put them on.
121
+ const handle = __addDisposableResource(env_1, await ws.runtime.exec(withShell(command, config.shell), {
122
+ cwd: options?.cwd ?? config.cwd ?? DEFAULT_CWD,
123
+ encoding: "utf8",
124
+ timeoutMs,
125
+ ...(env ? { env } : {})
126
+ }), false);
127
+ const result = await handle.result();
128
+ const killed = cancelledNote(result.status, result.exitCode, timeoutMs);
129
+ return {
130
+ // `/repo` branches on `success`, which this runtime does not report — it
131
+ // has `status` and `exitCode`. Derived from the exit code rather than the
132
+ // status, because a command that runs and fails is `completed` here.
133
+ success: result.exitCode === 0,
134
+ stdout: result.stdout,
135
+ // Appended rather than substituted: a command killed at the ceiling may
136
+ // well have written plenty first, and that output is the more useful half.
137
+ // This is the only field the note can travel on — `stdout` is a data
138
+ // channel the caller parses, and the four fields are the whole contract.
139
+ stderr: killed
140
+ ? [result.stderr, killed].filter(Boolean).join("\n")
141
+ : result.stderr,
142
+ exitCode: result.exitCode
143
+ };
144
+ }
145
+ catch (e_1) {
146
+ env_1.error = e_1;
147
+ env_1.hasError = true;
148
+ }
149
+ finally {
150
+ __disposeResources(env_1);
151
+ }
152
+ };
153
+ }
@@ -0,0 +1,36 @@
1
+ import type { WorkspaceRuntimeStatus } from "@cloudflare/computer";
2
+ /**
3
+ * Text shared by the workspace and the tools over it: the output bound, the
4
+ * elapsed-time phrase, and what a killed command is told.
5
+ */
6
+ /**
7
+ * Middle-out truncation, so both the first error and the final summary survive.
8
+ *
9
+ * The guard is not defensive padding. Without it a `max` at or below the marker's
10
+ * own length makes `half` zero or negative, and `slice(-0)` is `slice(0)` — the
11
+ * *whole* string — so the function returns more than it was given: 500 characters
12
+ * in, 543 out at `max: 80`. A silent inversion of the one thing it does,
13
+ * reachable from a public config field.
14
+ */
15
+ export declare function truncateOutput(text: string, max: number): string;
16
+ /** Elapsed time in the shape a sentence wants. */
17
+ export declare function humanMs(ms: number): string;
18
+ /**
19
+ * Why a command has nothing to show for itself.
20
+ *
21
+ * {@link renderResult} states a non-`completed` status on its verdict line;
22
+ * {@link file://./exec.ts workspaceExec} has no verdict line, only the four fields its caller
23
+ * branches on. A *killed* process writes nothing, so without this a `git clone`
24
+ * that hit the ten-minute ceiling reaches the model through
25
+ * `@dynamicagents/plugins/repo` as `clone failed:` with an empty reason — which reads
26
+ * like a bug in the plugin rather than a limit it can work within.
27
+ *
28
+ * Only `cancelled`. A `failed` status is an ordinary non-zero exit, where the
29
+ * command's own stderr is the better explanation and this would be noise on top
30
+ * of it.
31
+ *
32
+ * Both plausible causes are named because the exit code cannot separate them:
33
+ * 137 is SIGKILL, which is what the timeout sends *and* what the kernel sends a
34
+ * container that ran out of memory.
35
+ */
36
+ export declare function cancelledNote(status: WorkspaceRuntimeStatus | undefined, exitCode: number, timeoutMs: number): string | undefined;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Text shared by the workspace and the tools over it: the output bound, the
3
+ * elapsed-time phrase, and what a killed command is told.
4
+ */
5
+ /**
6
+ * Middle-out truncation, so both the first error and the final summary survive.
7
+ *
8
+ * The guard is not defensive padding. Without it a `max` at or below the marker's
9
+ * own length makes `half` zero or negative, and `slice(-0)` is `slice(0)` — the
10
+ * *whole* string — so the function returns more than it was given: 500 characters
11
+ * in, 543 out at `max: 80`. A silent inversion of the one thing it does,
12
+ * reachable from a public config field.
13
+ */
14
+ export function truncateOutput(text, max) {
15
+ if (text.length <= max)
16
+ return text;
17
+ const marker = (dropped) => `\n\n… [${dropped} characters omitted from the middle] …\n\n`;
18
+ // No budget for two halves plus the marker: keep the head, which is where the
19
+ // first error is, and say nothing clever.
20
+ const half = Math.floor((max - marker(text.length).length) / 2);
21
+ if (half < 1)
22
+ return text.slice(0, Math.max(0, max));
23
+ return (text.slice(0, half) + marker(text.length - half * 2) + text.slice(-half));
24
+ }
25
+ /** Elapsed time in the shape a sentence wants. */
26
+ export function humanMs(ms) {
27
+ const s = Math.round(ms / 1000);
28
+ return s < 60
29
+ ? `${s}s`
30
+ : `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s`;
31
+ }
32
+ /**
33
+ * Why a command has nothing to show for itself.
34
+ *
35
+ * {@link renderResult} states a non-`completed` status on its verdict line;
36
+ * {@link file://./exec.ts workspaceExec} has no verdict line, only the four fields its caller
37
+ * branches on. A *killed* process writes nothing, so without this a `git clone`
38
+ * that hit the ten-minute ceiling reaches the model through
39
+ * `@dynamicagents/plugins/repo` as `clone failed:` with an empty reason — which reads
40
+ * like a bug in the plugin rather than a limit it can work within.
41
+ *
42
+ * Only `cancelled`. A `failed` status is an ordinary non-zero exit, where the
43
+ * command's own stderr is the better explanation and this would be noise on top
44
+ * of it.
45
+ *
46
+ * Both plausible causes are named because the exit code cannot separate them:
47
+ * 137 is SIGKILL, which is what the timeout sends *and* what the kernel sends a
48
+ * container that ran out of memory.
49
+ */
50
+ export function cancelledNote(status, exitCode, timeoutMs) {
51
+ if (status !== "cancelled")
52
+ return undefined;
53
+ return (`the command was killed rather than exiting on its own (exit ${exitCode}), so ` +
54
+ `anything it had not yet written is gone. The two usual causes are the ` +
55
+ `per-command time limit — ${humanMs(timeoutMs)} here — and the container ` +
56
+ `running out of memory.`);
57
+ }