@dynamicagents/plugins 0.14.0 → 0.15.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 +22 -18
- 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
package/dist/workspace/exec.d.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
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
|
-
}>;
|
package/dist/workspace/exec.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
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;
|
package/dist/workspace/format.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import type { RepoGitResult } from "../repo/index.js";
|
|
2
|
-
import type { GitClient } from "@cloudflare/computer/git";
|
|
3
|
-
/**
|
|
4
|
-
* The git operations that need the GitHub token, run on this side.
|
|
5
|
-
*
|
|
6
|
-
* They are here rather than on the `WorkspaceStub` the plugin already holds
|
|
7
|
-
* because `WorkspaceGitStub` exposes only `cli(argv)` across a Durable Object
|
|
8
|
-
* boundary, and argv cannot carry an `onAuth` callback. Routed through here, the
|
|
9
|
-
* credential is read from the workspace object's own `env` and never crosses an
|
|
10
|
-
* RPC boundary, never appears in an argument list, and never enters the
|
|
11
|
-
* container.
|
|
12
|
-
*
|
|
13
|
-
* That last clause is the point, and it is why running credentialed `git` in the
|
|
14
|
-
* container is not an option however carefully it is sandboxed: git executes
|
|
15
|
-
* whatever `.git/config` and `.git/hooks` name, the model has a root shell on
|
|
16
|
-
* that filesystem, and even a disposable git dir leaves the token readable
|
|
17
|
-
* through `/proc` for the life of the process. isomorphic-git runs here and has
|
|
18
|
-
* no hooks, no `ext::` transport, no template directory and no credential
|
|
19
|
-
* helpers.
|
|
20
|
-
*
|
|
21
|
-
* **The caller owns the preconditions.** Bringing the workspace up to date is
|
|
22
|
-
* the workspace object's job and it must happen first: git here reads that
|
|
23
|
-
* object's SQLite, so a commit made in the container that has not been pulled
|
|
24
|
-
* yet is a commit this code cannot see. The workspace object's `gitClone`,
|
|
25
|
-
* `gitFetch` and `gitPush` in `./object.ts` are where that is settled.
|
|
26
|
-
*/
|
|
27
|
-
export interface GitHostDeps {
|
|
28
|
-
/** The workspace's own client — isomorphic-git over the local store. */
|
|
29
|
-
git: () => GitClient;
|
|
30
|
-
/** The GitHub credential, read at the moment it would be handed over. */
|
|
31
|
-
token: () => string | undefined;
|
|
32
|
-
tag: () => string;
|
|
33
|
-
}
|
|
34
|
-
export declare class WorkspaceGitHost {
|
|
35
|
-
#private;
|
|
36
|
-
private readonly deps;
|
|
37
|
-
constructor(deps: GitHostDeps);
|
|
38
|
-
/**
|
|
39
|
-
* Each operation takes `url` explicitly rather than a remote name: resolving
|
|
40
|
-
* `origin` would read `.git/config`, a workspace file a co-installed shell
|
|
41
|
-
* tool can write.
|
|
42
|
-
*/
|
|
43
|
-
clone(req: {
|
|
44
|
-
url: string;
|
|
45
|
-
dir: string;
|
|
46
|
-
allowedHosts: string[];
|
|
47
|
-
branch?: string;
|
|
48
|
-
depth?: number;
|
|
49
|
-
}): Promise<RepoGitResult>;
|
|
50
|
-
fetch(req: {
|
|
51
|
-
url: string;
|
|
52
|
-
dir: string;
|
|
53
|
-
allowedHosts: string[];
|
|
54
|
-
depth?: number;
|
|
55
|
-
}): Promise<RepoGitResult>;
|
|
56
|
-
/**
|
|
57
|
-
* `remote.origin.url` from the local store, or `undefined` where there is no
|
|
58
|
-
* checkout or no origin. Reads only, and needs no credential.
|
|
59
|
-
*/
|
|
60
|
-
origin(dir: string): Promise<string | undefined>;
|
|
61
|
-
push(req: {
|
|
62
|
-
url: string;
|
|
63
|
-
dir: string;
|
|
64
|
-
branch: string;
|
|
65
|
-
allowedHosts: string[];
|
|
66
|
-
}): Promise<RepoGitResult>;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* A git error's message with the chain of causes behind it.
|
|
70
|
-
*
|
|
71
|
-
* `@cloudflare/computer/git` wraps isomorphic-git's errors, and classifies some
|
|
72
|
-
* by a heuristic: any failure whose message mentions `.git` beside a missing
|
|
73
|
-
* path or a ref it could not find is reported as "not a git repository" — a
|
|
74
|
-
* sentence that is wrong about a checkout that plainly is one, and names
|
|
75
|
-
* neither the file nor the ref. What actually failed is only on `cause`, which
|
|
76
|
-
* a Durable Object boundary does not carry, so it is folded into the message
|
|
77
|
-
* here while the error is still itself.
|
|
78
|
-
*/
|
|
79
|
-
export declare function describeGitError(err: unknown): string;
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What the container's own `git init` would write, over isomorphic-git's.
|
|
3
|
-
*
|
|
4
|
-
* isomorphic-git's `init` writes `core.*` for a filesystem without symlinks or
|
|
5
|
-
* an executable bit that ignores case, and the container's git obeys it. With
|
|
6
|
-
* `symlinks = false`, a command there that rewrites the tree — starter puts a
|
|
7
|
-
* worktree on its branch with `checkout -f` — writes each link as a file holding
|
|
8
|
-
* its target, and `status` calls that clean. With `filemode = false`, a script
|
|
9
|
-
* made executable is committed as 100644. The store models all three.
|
|
10
|
-
*/
|
|
11
|
-
const CONTAINER_CORE_CONFIG = {
|
|
12
|
-
"core.symlinks": true,
|
|
13
|
-
"core.filemode": true,
|
|
14
|
-
"core.ignorecase": false
|
|
15
|
-
};
|
|
16
|
-
export class WorkspaceGitHost {
|
|
17
|
-
deps;
|
|
18
|
-
constructor(deps) {
|
|
19
|
-
this.deps = deps;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Each operation takes `url` explicitly rather than a remote name: resolving
|
|
23
|
-
* `origin` would read `.git/config`, a workspace file a co-installed shell
|
|
24
|
-
* tool can write.
|
|
25
|
-
*/
|
|
26
|
-
async clone(req) {
|
|
27
|
-
return this.#run(req.allowedHosts, async (git, onAuth) => {
|
|
28
|
-
// Spelled out rather than `git.clone`, and the reason is the credential.
|
|
29
|
-
//
|
|
30
|
-
// `clone` is the one network operation the client does not give an
|
|
31
|
-
// `onAuth` callback — it authenticates only through a `headers` option,
|
|
32
|
-
// which means attaching the token to the very first request to a URL the
|
|
33
|
-
// *model* chose. Composing the same work out of `fetch` puts every
|
|
34
|
-
// credentialed request on this side of `onAuth` instead, where the host is
|
|
35
|
-
// checked at the moment the token would be handed over. The cost is four
|
|
36
|
-
// calls instead of one; `clone` is these four.
|
|
37
|
-
await git.init({ dir: req.dir });
|
|
38
|
-
// Straight after `init`, so a clone that fails part-way leaves nothing
|
|
39
|
-
// behind that the container's git would misread.
|
|
40
|
-
for (const [path, value] of Object.entries(CONTAINER_CORE_CONFIG))
|
|
41
|
-
await git.configSet({ dir: req.dir, path, value });
|
|
42
|
-
await git.remoteAdd({
|
|
43
|
-
dir: req.dir,
|
|
44
|
-
name: "origin",
|
|
45
|
-
url: req.url,
|
|
46
|
-
force: true
|
|
47
|
-
});
|
|
48
|
-
const fetched = await git.fetch({
|
|
49
|
-
url: req.url,
|
|
50
|
-
dir: req.dir,
|
|
51
|
-
onAuth,
|
|
52
|
-
// Omission is passed through as omission, because the tool the model
|
|
53
|
-
// reads says so: `repo_clone` describes `depth` as "omit for full
|
|
54
|
-
// history; needed if you must rebase", and `/repo` deliberately leaves
|
|
55
|
-
// the key off when it is unset. Defaulting to 1 here made every such
|
|
56
|
-
// clone shallow, so the one thing the model was told to omit it for —
|
|
57
|
-
// rebasing — failed on a repository it had asked for in full.
|
|
58
|
-
...(req.depth === undefined ? {} : { depth: req.depth }),
|
|
59
|
-
singleBranch: true,
|
|
60
|
-
tags: false,
|
|
61
|
-
...(req.branch ? { ref: req.branch } : {})
|
|
62
|
-
});
|
|
63
|
-
const landed = req.branch ?? fetched.defaultBranch?.replace(/^refs\/heads\//, "");
|
|
64
|
-
if (!landed)
|
|
65
|
-
throw new Error(`cloned ${req.url} but the remote named no default branch to check out`);
|
|
66
|
-
await git.checkout({ dir: req.dir, ref: landed });
|
|
67
|
-
// What a real `git clone` writes and the container's git will look for:
|
|
68
|
-
// without it the branch tracks nothing, and an agent reaching for a bare
|
|
69
|
-
// `git status` in the shell sees a branch with no upstream.
|
|
70
|
-
await git.configSet({
|
|
71
|
-
dir: req.dir,
|
|
72
|
-
path: `branch.${landed}.remote`,
|
|
73
|
-
value: "origin"
|
|
74
|
-
});
|
|
75
|
-
await git.configSet({
|
|
76
|
-
dir: req.dir,
|
|
77
|
-
path: `branch.${landed}.merge`,
|
|
78
|
-
value: `refs/heads/${landed}`
|
|
79
|
-
});
|
|
80
|
-
return landed;
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
async fetch(req) {
|
|
84
|
-
return this.#run(req.allowedHosts, async (git, onAuth) => {
|
|
85
|
-
const result = await git.fetch({
|
|
86
|
-
url: req.url,
|
|
87
|
-
dir: req.dir,
|
|
88
|
-
onAuth,
|
|
89
|
-
prune: true,
|
|
90
|
-
tags: false,
|
|
91
|
-
singleBranch: false,
|
|
92
|
-
// Every branch is fetched, but isomorphic-git still resolves one ref
|
|
93
|
-
// against the remote's list, and left to itself it picks the current
|
|
94
|
-
// branch's upstream. A checkout left on a branch whose pull request
|
|
95
|
-
// merged — and whose remote branch went with it — then fails every
|
|
96
|
-
// fetch with `Could not find refs/heads/<branch>`. The remote's HEAD
|
|
97
|
-
// is always listed.
|
|
98
|
-
remoteRef: "HEAD",
|
|
99
|
-
...(req.depth ? { depth: req.depth } : {})
|
|
100
|
-
});
|
|
101
|
-
return `fetched ${req.url} (default branch ${result.defaultBranch ?? "unknown"})`;
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* `remote.origin.url` from the local store, or `undefined` where there is no
|
|
106
|
-
* checkout or no origin. Reads only, and needs no credential.
|
|
107
|
-
*/
|
|
108
|
-
async origin(dir) {
|
|
109
|
-
try {
|
|
110
|
-
const url = await this.deps
|
|
111
|
-
.git()
|
|
112
|
-
.configGet({ dir, path: "remote.origin.url" });
|
|
113
|
-
return typeof url === "string" && url ? url : undefined;
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
return undefined;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
async push(req) {
|
|
120
|
-
return this.#run(req.allowedHosts, async (git, onAuth) => {
|
|
121
|
-
const result = await git.push({
|
|
122
|
-
url: req.url,
|
|
123
|
-
dir: req.dir,
|
|
124
|
-
ref: req.branch,
|
|
125
|
-
remoteRef: req.branch,
|
|
126
|
-
onAuth
|
|
127
|
-
// No `force`, ever, and not a knob: `/repo` refuses anything but a plain
|
|
128
|
-
// branch name precisely so that a push cannot be turned into a force
|
|
129
|
-
// push, and this is the other end of that promise.
|
|
130
|
-
});
|
|
131
|
-
// isomorphic-git reports a rejected push in the *result* rather than by
|
|
132
|
-
// throwing — a non-fast-forward comes back `ok: false` with the reason on
|
|
133
|
-
// the ref. Reading only the absence of an exception would report every
|
|
134
|
-
// rejected push as a success, in the one plugin whose entire theme is that
|
|
135
|
-
// a failed command is not a completed operation.
|
|
136
|
-
if (!result.ok) {
|
|
137
|
-
const perRef = Object.entries(result.refs)
|
|
138
|
-
.filter(([, status]) => !status.ok)
|
|
139
|
-
.map(([ref, status]) => `${ref}: ${status.error ?? "rejected"}`)
|
|
140
|
-
.join("; ");
|
|
141
|
-
// `||`, not `??`, on the second rung: `perRef` is a joined string, so
|
|
142
|
-
// "no failing ref carried a reason" is `""` rather than nullish — and
|
|
143
|
-
// `??` would hand that through as the error message. A rejected push
|
|
144
|
-
// reporting a blank reason is the one outcome worse than a generic one.
|
|
145
|
-
throw new Error(result.error ?? (perRef || "the remote rejected the push"));
|
|
146
|
-
}
|
|
147
|
-
return `pushed ${req.branch} to ${req.url}`;
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* The shared body: entry-point bookkeeping, the credential, and the translation
|
|
152
|
-
* back into something that survives RPC.
|
|
153
|
-
*
|
|
154
|
-
* A thrown `GitError` loses its prototype crossing a Durable Object boundary,
|
|
155
|
-
* so `instanceof` on the far side is not available and the caller would be left
|
|
156
|
-
* pattern-matching a string. The `code` is lifted here, while the error is
|
|
157
|
-
* still itself, and travels as data.
|
|
158
|
-
*/
|
|
159
|
-
async #run(allowedHosts, body) {
|
|
160
|
-
// Bound to the credential rather than checked before the call, which is
|
|
161
|
-
// strictly stronger: this is the moment the token would be handed over, and
|
|
162
|
-
// it sees the URL git actually authenticated against — including one it
|
|
163
|
-
// reached by redirect. A host nobody allowed gets no credential and the
|
|
164
|
-
// request fails unauthenticated, rather than the token being offered to it
|
|
165
|
-
// and *then* the mistake being noticed.
|
|
166
|
-
const onAuth = (url) => {
|
|
167
|
-
let host;
|
|
168
|
-
try {
|
|
169
|
-
const parsed = new URL(url);
|
|
170
|
-
if (parsed.protocol !== "https:")
|
|
171
|
-
return {};
|
|
172
|
-
host = parsed.hostname;
|
|
173
|
-
}
|
|
174
|
-
catch {
|
|
175
|
-
return {};
|
|
176
|
-
}
|
|
177
|
-
if (!allowedHosts.includes(host)) {
|
|
178
|
-
console.warn(`[${this.deps.tag()}] refused to authenticate to a host`, {
|
|
179
|
-
host
|
|
180
|
-
});
|
|
181
|
-
return {};
|
|
182
|
-
}
|
|
183
|
-
return { username: "x-access-token", password: this.deps.token() };
|
|
184
|
-
};
|
|
185
|
-
try {
|
|
186
|
-
return { ok: true, detail: await body(this.deps.git(), onAuth) };
|
|
187
|
-
}
|
|
188
|
-
catch (err) {
|
|
189
|
-
const code = err?.code;
|
|
190
|
-
const message = describeGitError(err);
|
|
191
|
-
console.error(`[${this.deps.tag()}] git failed`, { code, message });
|
|
192
|
-
return {
|
|
193
|
-
ok: false,
|
|
194
|
-
...(typeof code === "string" ? { code } : {}),
|
|
195
|
-
message
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* A git error's message with the chain of causes behind it.
|
|
202
|
-
*
|
|
203
|
-
* `@cloudflare/computer/git` wraps isomorphic-git's errors, and classifies some
|
|
204
|
-
* by a heuristic: any failure whose message mentions `.git` beside a missing
|
|
205
|
-
* path or a ref it could not find is reported as "not a git repository" — a
|
|
206
|
-
* sentence that is wrong about a checkout that plainly is one, and names
|
|
207
|
-
* neither the file nor the ref. What actually failed is only on `cause`, which
|
|
208
|
-
* a Durable Object boundary does not carry, so it is folded into the message
|
|
209
|
-
* here while the error is still itself.
|
|
210
|
-
*/
|
|
211
|
-
export function describeGitError(err) {
|
|
212
|
-
const parts = [];
|
|
213
|
-
const seen = new Set();
|
|
214
|
-
let at = err;
|
|
215
|
-
for (let depth = 0; at !== undefined && at !== null && depth < 4; depth++) {
|
|
216
|
-
const e = at;
|
|
217
|
-
const text = typeof e.message === "string" ? e.message : String(at);
|
|
218
|
-
if (!seen.has(text)) {
|
|
219
|
-
seen.add(text);
|
|
220
|
-
const name = typeof e.name === "string" ? e.name : "";
|
|
221
|
-
parts.push(name && depth > 0 ? `${name}: ${text}` : text);
|
|
222
|
-
}
|
|
223
|
-
at = e.cause;
|
|
224
|
-
}
|
|
225
|
-
return parts.join(" ← ");
|
|
226
|
-
}
|