@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/README.md
CHANGED
|
@@ -26,20 +26,20 @@ An agent installs plugins by listing them in `getPlugins()`, on a `StepAgent` fr
|
|
|
26
26
|
import { StepAgent } from "@dynamicagents/core/agent";
|
|
27
27
|
import { browser } from "@dynamicagents/plugins/browser";
|
|
28
28
|
import { computer, computerWorkspace } from "@dynamicagents/plugins/computer";
|
|
29
|
-
// `
|
|
30
|
-
import { repo } from "@dynamicagents/plugins/repo";
|
|
29
|
+
// `sandboxConfig` names the `/sandbox` object this agent works in.
|
|
30
|
+
import { repo } from "@dynamicagents/plugins/repo-github";
|
|
31
31
|
|
|
32
32
|
export class Coder extends StepAgent<Env> {
|
|
33
|
-
//
|
|
33
|
+
// The tools run commands in a container, so the agent's own workspace —
|
|
34
34
|
// what Think's `read` and `write` work on — has to be that container's.
|
|
35
|
-
override workspace = computerWorkspace(
|
|
35
|
+
override workspace = computerWorkspace(sandboxConfig(this.env), () =>
|
|
36
36
|
this.pluginContext().runtime()
|
|
37
37
|
);
|
|
38
38
|
|
|
39
39
|
override getPlugins() {
|
|
40
40
|
return [
|
|
41
41
|
browser({ binding: this.env.BROWSER }),
|
|
42
|
-
computer(
|
|
42
|
+
computer({ shell: "bash" }),
|
|
43
43
|
repo(repoConfig(this.env))
|
|
44
44
|
];
|
|
45
45
|
}
|
|
@@ -64,14 +64,14 @@ plugin to the tools an install names.
|
|
|
64
64
|
|
|
65
65
|
## The plugins
|
|
66
66
|
|
|
67
|
-
| Subpath | What it adds
|
|
68
|
-
| ---------------------------------- |
|
|
69
|
-
| [`/browser`](src/browser/) | Read web pages via Browser Rendering Quick Actions
|
|
70
|
-
| [`/claude-code`](src/claude-code/) | Sub-agents whose model is a Claude Code session in
|
|
71
|
-
| [`/computer`](src/computer/) | An agent's tools over a
|
|
72
|
-
| [`/repo`](src/repo/)
|
|
73
|
-
| [`/
|
|
74
|
-
| [`/
|
|
67
|
+
| Subpath | What it adds | Needs |
|
|
68
|
+
| ---------------------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
|
|
69
|
+
| [`/browser`](src/browser/) | Read web pages via Browser Rendering Quick Actions | `BROWSER` (paid plan) |
|
|
70
|
+
| [`/claude-code`](src/claude-code/) | Sub-agents whose model is a Claude Code session in a sandbox | one or more `claude setup-token` credentials |
|
|
71
|
+
| [`/computer`](src/computer/) | An agent's tools over a sandbox — `bash`, `grep`, `edit` — and the workspace Think's file tools use | a `/sandbox` object |
|
|
72
|
+
| [`/repo-github`](src/repo-github/) | Clone, commit, push a branch, open a pull request on GitHub — over any container | `GITHUB_TOKEN` |
|
|
73
|
+
| [`/sandbox`](src/sandbox/) | A container whose disk is the checkout, driven from a Durable Object through `agents`' `ContainerHarness` | `@cloudflare/sandbox`, an image running its daemon |
|
|
74
|
+
| [`/scratch`](src/scratch/) | A throwaway git repository with no remote, for work that needs a container but no checkout | — |
|
|
75
75
|
|
|
76
76
|
Each directory has its own README with the config shape and a paste-ready `wrangler.jsonc`
|
|
77
77
|
snippet — a plugin cannot add its own binding, which is why it declares what it needs.
|
|
@@ -111,7 +111,7 @@ The rules the whole design rests on:
|
|
|
111
111
|
|
|
112
112
|
A write that must not happen twice — a comment, a reply — is a Think `action()` in
|
|
113
113
|
`actions(ctx)`, with an idempotency key, so a recovered turn replays it rather than
|
|
114
|
-
repeating it. [`/repo`](src/repo/) has two.
|
|
114
|
+
repeating it. [`/repo-github`](src/repo-github/) has two.
|
|
115
115
|
|
|
116
116
|
A plugin that owns a domain some work is delegated into exports a `SubAgentSpec` as data,
|
|
117
117
|
and the agent binds it to a `SubAgent` class — see [`/claude-code`](src/claude-code/).
|
|
@@ -52,4 +52,6 @@ export declare function initCard(event: InitEvent): Card;
|
|
|
52
52
|
export declare function resultCard(result: ClaudeCodeResult): Card;
|
|
53
53
|
/** A session its run gave up on before reading its end, and why. */
|
|
54
54
|
export declare function lostCard(why: string): Card;
|
|
55
|
+
/** A session's container lost mid-turn, and the session resuming in a new one. */
|
|
56
|
+
export declare function containerLostCard(reason: string): Card;
|
|
55
57
|
export {};
|
|
@@ -272,6 +272,17 @@ export function lostCard(why) {
|
|
|
272
272
|
}
|
|
273
273
|
};
|
|
274
274
|
}
|
|
275
|
+
/** A session's container lost mid-turn, and the session resuming in a new one. */
|
|
276
|
+
export function containerLostCard(reason) {
|
|
277
|
+
return {
|
|
278
|
+
text: "Container lost — the session resumes in a new one",
|
|
279
|
+
detail: {
|
|
280
|
+
title: "Session",
|
|
281
|
+
status: "error",
|
|
282
|
+
sections: [section("Why", reason, "text")]
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
275
286
|
function duration(ms) {
|
|
276
287
|
const s = Math.round(ms / 1000);
|
|
277
288
|
if (s < 60)
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Config at instantiation rather than an `env` argument, for the reason the
|
|
5
|
-
* whole package works this way: `Env` is the ambient interface `wrangler types`
|
|
6
|
-
* generates into a *consumer's* app and does not exist here, and on Workers
|
|
7
|
-
* there is no module-scope `env` to read anyway.
|
|
2
|
+
* What one deployment tunes about its sessions, in one shape: the daemon reads
|
|
3
|
+
* the CLI's half, and the sub-agent's model the ceiling.
|
|
8
4
|
*/
|
|
9
5
|
/**
|
|
10
6
|
* The permission modes `claude --permission-mode` accepts.
|
|
@@ -49,26 +45,6 @@ export declare const DEFAULT_PERMISSION_MODE: PermissionMode;
|
|
|
49
45
|
*/
|
|
50
46
|
export type EffortLevel = "low" | "medium" | "high" | "xhigh" | "max";
|
|
51
47
|
export interface ClaudeCodeConfig {
|
|
52
|
-
/**
|
|
53
|
-
* The credential pool, in priority order — index 0 is tried first.
|
|
54
|
-
*
|
|
55
|
-
* A thunk so a rotated secret is picked up without rebuilding the plugin list.
|
|
56
|
-
* **These values never enter the container** — one is read on the Worker side
|
|
57
|
-
* by the egress gateway and swapped into the outbound request. The container
|
|
58
|
-
* gets {@link file://./run.ts CREDENTIAL_PLACEHOLDER}.
|
|
59
|
-
*
|
|
60
|
-
* `claude setup-token` OAuth credentials. There is deliberately no API-key
|
|
61
|
-
* path: the subscription credential is the one that reaches frontier models
|
|
62
|
-
* through this client, and a second path would be a second thing to get wrong.
|
|
63
|
-
*
|
|
64
|
-
* **Why a pool.** A subscription has a rolling 5-hour bucket and a weekly one,
|
|
65
|
-
* neither readable. Rather than estimate spend against them, the gateway uses
|
|
66
|
-
* the first usable entry and moves on when Anthropic says that one is done —
|
|
67
|
-
* see {@link file://./credentials.ts}. An array of one is valid and behaves
|
|
68
|
-
* exactly as a single credential did: used until its bucket empties, then
|
|
69
|
-
* refused with the reset time.
|
|
70
|
-
*/
|
|
71
|
-
credentials: () => readonly string[];
|
|
72
48
|
/** Which model the session runs. Unset, Claude Code picks its own default. */
|
|
73
49
|
model?: string;
|
|
74
50
|
/**
|
|
@@ -91,8 +67,7 @@ export interface ClaudeCodeConfig {
|
|
|
91
67
|
* Advisory, and the distinction is the reason there is no turn ceiling beside
|
|
92
68
|
* them — see the README on what this package deliberately does not do. That
|
|
93
69
|
* tree is invisible to the agent running the session, so it multiplies
|
|
94
|
-
* whatever these say; {@link timeoutMs} is the ceiling that actually holds
|
|
95
|
-
* container runtime enforces it.
|
|
70
|
+
* whatever these say; {@link timeoutMs} is the ceiling that actually holds.
|
|
96
71
|
*/
|
|
97
72
|
maxSubagentDepth?: number;
|
|
98
73
|
maxConcurrentSubagents?: number;
|
|
@@ -116,67 +91,19 @@ export interface ClaudeCodeConfig {
|
|
|
116
91
|
*
|
|
117
92
|
* What makes bypassing acceptable is not this flag being careful. It is that
|
|
118
93
|
* the container has nothing to protect: it holds no credential — the real one
|
|
119
|
-
* is swapped in by the egress
|
|
120
|
-
*
|
|
94
|
+
* is swapped in by the egress on the Worker side — and it already runs a
|
|
95
|
+
* cloned repository's `postinstall` and its test suite, which is arbitrary
|
|
121
96
|
* code execution by design. Gating the agent's own edits while that stands
|
|
122
97
|
* open buys nothing. Containment is the credential swap.
|
|
123
98
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* A run may set its own, over this one: a planning run's is `plan` — see
|
|
128
|
-
* {@link file://./model.ts ClaudeCodeModelOptions.permissionMode}.
|
|
99
|
+
* A run may set its own, over this one: a planning run's is `plan`, as its run
|
|
100
|
+
* header says — see `../sandbox/header.ts`.
|
|
129
101
|
*/
|
|
130
102
|
permissionMode?: PermissionMode;
|
|
131
103
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* is not the same thing — it means Anthropic only. See
|
|
136
|
-
* {@link file://./egress.ts EgressConfig.restrictToHosts} for the full table
|
|
137
|
-
* and for why open is the default.
|
|
138
|
-
*
|
|
139
|
-
* A restriction that leaves out `github.com` and `api.github.com` leaves out
|
|
140
|
-
* {@link githubToken} with them.
|
|
141
|
-
*/
|
|
142
|
-
restrictToHosts?: readonly string[];
|
|
143
|
-
/**
|
|
144
|
-
* GitHub as the deployment's account, for a session that pushes its branch,
|
|
145
|
-
* opens its pull request and answers its review. Read per request, on the
|
|
146
|
-
* Worker side.
|
|
147
|
-
*
|
|
148
|
-
* The session gets a placeholder as `GH_TOKEN` and as git's credential, and
|
|
149
|
-
* the egress gateway swaps in this token for GitHub's hosts only — see
|
|
150
|
-
* {@link file://./github.ts}. Unset, `gh` and git reach GitHub anonymously.
|
|
151
|
-
*/
|
|
152
|
-
githubToken?: () => string | undefined;
|
|
153
|
-
/**
|
|
154
|
-
* Ceiling on the whole session, enforced by the container runtime.
|
|
155
|
-
*
|
|
156
|
-
* **Must stay below the workspace's container-idle timer**, which is the
|
|
157
|
-
* invariant that has already caused one outage in this repository: two timers
|
|
158
|
-
* of similar length started moments apart, and whichever fired first destroyed
|
|
159
|
-
* the container the other depended on.
|
|
104
|
+
* How long a session may run before its sub-agent stops it — a ceiling on a
|
|
105
|
+
* session that has stopped making progress. See
|
|
106
|
+
* {@link file://./model.ts DEFAULT_TIMEOUT_MS}.
|
|
160
107
|
*/
|
|
161
108
|
timeoutMs?: number;
|
|
162
|
-
/** Extra environment for the session. Never secrets — see the README. */
|
|
163
|
-
env?: Record<string, string>;
|
|
164
|
-
/**
|
|
165
|
-
* Who the session's own git commits are attributed to.
|
|
166
|
-
*
|
|
167
|
-
* A session has a shell and a checkout, so it commits — and it amends,
|
|
168
|
-
* rebases and cherry-picks, none of which a host can intercept the way it
|
|
169
|
-
* names an identity on a commit it makes itself. Left unset, every one of
|
|
170
|
-
* those falls through to whatever config the checkout happens to carry, which
|
|
171
|
-
* is a value frozen whenever that checkout was created.
|
|
172
|
-
*
|
|
173
|
-
* See {@link file://./run.ts buildLaunch} for why this becomes an environment
|
|
174
|
-
* rather than a `git config`.
|
|
175
|
-
*/
|
|
176
|
-
author?: {
|
|
177
|
-
name: string;
|
|
178
|
-
email: string;
|
|
179
|
-
};
|
|
180
109
|
}
|
|
181
|
-
/** Fifteen minutes, well under the coder workspace's 20-minute idle timer. */
|
|
182
|
-
export declare const DEFAULT_TIMEOUT_MS: number;
|
|
@@ -1,39 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `@dynamicagents/plugins/claude-code` — the credential pool, and how exhaustion is
|
|
3
|
-
* detected.
|
|
4
|
-
*
|
|
5
|
-
* ## Why a pool rather than a budget
|
|
6
|
-
*
|
|
7
|
-
* A Claude subscription has two limits that matter here: a rolling 5-hour
|
|
8
|
-
* session bucket and a weekly one. Neither is readable, so staying under them by
|
|
9
|
-
* *estimating* spend is the wrong instrument twice over — the estimate is a
|
|
10
|
-
* guess about a bucket nobody can see, and it only moves when a run *ends*,
|
|
11
|
-
* since usage is learned from the terminal `result` event. It is never a cap on
|
|
12
|
-
* spend, only a gate on starting. The bucket itself says so precisely the moment
|
|
13
|
-
* it is empty: Anthropic answers `429`.
|
|
14
|
-
*
|
|
15
|
-
* So this module does not predict. It **detects and routes around**: the
|
|
16
|
-
* credential becomes an ordered pool, the gateway uses the first usable entry,
|
|
17
|
-
* and a refusal moves the lead on. One mechanism covers both the 5-hour and the
|
|
18
|
-
* weekly limit, because to this code they differ only in how far out the reset
|
|
19
|
-
* is.
|
|
20
|
-
*
|
|
21
|
-
* ## Everything here is pure
|
|
22
|
-
*
|
|
23
|
-
* No storage, no `fetch`, no Durable Object — the host passes a
|
|
24
|
-
* {@link CredentialStore} and the clock. That is the discipline `JobLifecycle`
|
|
25
|
-
* and `InstallProbe` already set in this repository, and it is what lets the
|
|
26
|
-
* rotation rules be specified without a container: a pool with a fake store is
|
|
27
|
-
* two object literals.
|
|
28
|
-
*/
|
|
29
1
|
/**
|
|
30
2
|
* One entry's state, as the host persists it.
|
|
31
3
|
*
|
|
32
4
|
* `resetAt` is a wall-clock ms deadline; `0` means usable now. `dead` is a
|
|
33
5
|
* different claim from an exhausted bucket and is kept separate for that reason:
|
|
34
6
|
* a spent credential recovers by itself at `resetAt`, a rejected one never does
|
|
35
|
-
*
|
|
36
|
-
*
|
|
7
|
+
* on a timer. Collapsing them would resurrect a revoked token at some distant
|
|
8
|
+
* `resetAt`. Both are cleared by {@link CredentialPool.reopen}, which is not a
|
|
9
|
+
* timer: a revoked token costs one `401` per session and is rotated away.
|
|
37
10
|
*/
|
|
38
11
|
export interface CredentialState {
|
|
39
12
|
/**
|
|
@@ -55,6 +28,11 @@ export interface CredentialState {
|
|
|
55
28
|
id: string;
|
|
56
29
|
resetAt: number;
|
|
57
30
|
dead?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Which {@link CredentialPool.reopen} this state belongs to; absent is `0`.
|
|
33
|
+
* A later epoch replaces an earlier one outright — see {@link merge}.
|
|
34
|
+
*/
|
|
35
|
+
epoch?: number;
|
|
58
36
|
}
|
|
59
37
|
/**
|
|
60
38
|
* A stable, non-secret identifier for a credential.
|
|
@@ -113,6 +91,12 @@ export interface CredentialPool {
|
|
|
113
91
|
spend(id: string, resetAt: number): Promise<Lead>;
|
|
114
92
|
/** This credential is not valid — revoked, not spent. See {@link spend}. */
|
|
115
93
|
reject(id: string): Promise<Lead>;
|
|
94
|
+
/**
|
|
95
|
+
* Forget every mark, so the next request asks Anthropic again — see "A mark
|
|
96
|
+
* lasts one session" above. Writes only when something was marked. The lead
|
|
97
|
+
* it returns is usable unless no credential is configured.
|
|
98
|
+
*/
|
|
99
|
+
reopen(): Promise<Lead>;
|
|
116
100
|
}
|
|
117
101
|
export interface CredentialPoolConfig {
|
|
118
102
|
/**
|
|
@@ -135,6 +119,17 @@ export interface CredentialPoolConfig {
|
|
|
135
119
|
now?: () => number;
|
|
136
120
|
}
|
|
137
121
|
export declare function credentialPool(config: CredentialPoolConfig): CredentialPool;
|
|
122
|
+
/**
|
|
123
|
+
* The pool over `config.store`, one per store for the isolate's life.
|
|
124
|
+
*
|
|
125
|
+
* The egress gateway and a session's admission hold the same store, and they
|
|
126
|
+
* have to be one pool: a pool's mirror is what carries a mark or a clear past a
|
|
127
|
+
* failed write, and its writes are ordered only among themselves. Two pools
|
|
128
|
+
* over one store each hold up alone and disagree once storage fails. The first
|
|
129
|
+
* caller's `credentials` and `now` are the pool's, which is safe because every
|
|
130
|
+
* holder of a store is configured from one deployment.
|
|
131
|
+
*/
|
|
132
|
+
export declare function sharedPool(config: CredentialPoolConfig): CredentialPool;
|
|
138
133
|
/**
|
|
139
134
|
* What an Anthropic refusal means for the credential that sent it.
|
|
140
135
|
*
|
|
@@ -151,29 +146,16 @@ export type Refusal = {
|
|
|
151
146
|
kind: "transient";
|
|
152
147
|
retryAfterMs: number;
|
|
153
148
|
};
|
|
154
|
-
/**
|
|
155
|
-
* Read the client's own bucket reading as a verdict on the credential in use.
|
|
156
|
-
*
|
|
157
|
-
* `undefined` means "nothing to act on", which today is every reading — see
|
|
158
|
-
* {@link RATE_LIMIT_SPENT}. Otherwise the epoch-milliseconds the bucket refills,
|
|
159
|
-
* ready for {@link CredentialPool.spend}.
|
|
160
|
-
*
|
|
161
|
-
* Here rather than beside the parser because this is the same question
|
|
162
|
-
* {@link readRefusal} answers about a response: is this credential still worth
|
|
163
|
-
* using? The parser's job is to say what the line contained.
|
|
164
|
-
*/
|
|
165
|
-
export declare function readRateLimitEvent(info: {
|
|
166
|
-
status: string;
|
|
167
|
-
resetsAt?: number;
|
|
168
|
-
}): number | undefined;
|
|
169
149
|
/**
|
|
170
150
|
* Read a response as a verdict on the credential that produced it.
|
|
171
151
|
*
|
|
152
|
+
* A spent subscription bucket arrives as a `429` with
|
|
153
|
+
* `anthropic-ratelimit-unified-status: rejected`, the reset in
|
|
154
|
+
* `anthropic-ratelimit-unified-reset` (unix seconds) and the same wait in
|
|
155
|
+
* `retry-after` (seconds); `-representative-claim` names the bucket.
|
|
156
|
+
*
|
|
172
157
|
* Returns `undefined` for anything it does not recognise — which the caller
|
|
173
|
-
* treats as "forward it unchanged, and log the whole thing"
|
|
174
|
-
*
|
|
175
|
-
* has ever been observed through this path**, so the rules below are inferences
|
|
176
|
-
* from the documented API rate limits, and the first real one is what will
|
|
177
|
-
* settle them.
|
|
158
|
+
* treats as "forward it unchanged, and log the whole thing", so a refusal of
|
|
159
|
+
* another shape arrives named.
|
|
178
160
|
*/
|
|
179
161
|
export declare function readRefusal(response: Response, at: number): Refusal | undefined;
|
|
@@ -18,6 +18,18 @@
|
|
|
18
18
|
* weekly limit, because to this code they differ only in how far out the reset
|
|
19
19
|
* is.
|
|
20
20
|
*
|
|
21
|
+
* ## A mark lasts one session
|
|
22
|
+
*
|
|
23
|
+
* A spent or rejected mark is Anthropic's answer at the time, not a fact until
|
|
24
|
+
* `resetAt`: a plan upgrade, extra usage switched on, or a reset Anthropic
|
|
25
|
+
* grants early all make it wrong, and a deployment that trusts it refuses work
|
|
26
|
+
* Anthropic would take, for hours. Asking again is nearly free — a refusal bills
|
|
27
|
+
* nothing and comes back in milliseconds, and rotating through a pool of three
|
|
28
|
+
* spent credentials has taken about a second. So a host calls
|
|
29
|
+
* {@link CredentialPool.reopen} when it admits a session, and that session's
|
|
30
|
+
* first model call marks again whatever is still spent. Within the session the
|
|
31
|
+
* marks route as above.
|
|
32
|
+
*
|
|
21
33
|
* ## Everything here is pure
|
|
22
34
|
*
|
|
23
35
|
* No storage, no `fetch`, no Durable Object — the host passes a
|
|
@@ -26,6 +38,7 @@
|
|
|
26
38
|
* rotation rules be specified without a container: a pool with a fake store is
|
|
27
39
|
* two object literals.
|
|
28
40
|
*/
|
|
41
|
+
import { sha256Hex } from "@dynamicagents/core";
|
|
29
42
|
/**
|
|
30
43
|
* A stable, non-secret identifier for a credential.
|
|
31
44
|
*
|
|
@@ -38,26 +51,33 @@
|
|
|
38
51
|
* resisting a preimage attack.
|
|
39
52
|
*/
|
|
40
53
|
export async function fingerprint(token) {
|
|
41
|
-
|
|
42
|
-
return
|
|
43
|
-
.map((b) => b.toString(16).padStart(2, "0"))
|
|
44
|
-
.join("");
|
|
54
|
+
// Eight bytes, so sixteen hex characters.
|
|
55
|
+
return (await sha256Hex(token)).slice(0, 16);
|
|
45
56
|
}
|
|
46
57
|
/**
|
|
47
58
|
* Fold two views of one credential's state together.
|
|
48
59
|
*
|
|
49
|
-
* Safe to apply in any order and any number of times,
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
60
|
+
* Safe to apply in any order and any number of times, which is what lets
|
|
61
|
+
* storage and the in-memory mirror below be merged rather than one of them
|
|
62
|
+
* being chosen. The later epoch wins outright — that is how a reopen clears a
|
|
63
|
+
* mark. Within an epoch both fields are **monotonic**: `resetAt` only moves
|
|
64
|
+
* later and `dead` only turns on, so two refusals racing cannot make a spent
|
|
65
|
+
* credential look usable early.
|
|
66
|
+
*
|
|
67
|
+
* An epoch rather than a timestamp, so a reopen and a refusal in the same
|
|
68
|
+
* millisecond cannot tie.
|
|
54
69
|
*/
|
|
55
70
|
function merge(a, b) {
|
|
71
|
+
const ea = a.epoch ?? 0;
|
|
72
|
+
const eb = b.epoch ?? 0;
|
|
73
|
+
if (ea !== eb)
|
|
74
|
+
return ea > eb ? a : b;
|
|
56
75
|
const dead = a.dead === true || b.dead === true;
|
|
57
76
|
return {
|
|
58
77
|
id: a.id,
|
|
59
78
|
resetAt: Math.max(a.resetAt, b.resetAt),
|
|
60
|
-
...(dead ? { dead: true } : {})
|
|
79
|
+
...(dead ? { dead: true } : {}),
|
|
80
|
+
...(ea ? { epoch: ea } : {})
|
|
61
81
|
};
|
|
62
82
|
}
|
|
63
83
|
export function credentialPool(config) {
|
|
@@ -135,27 +155,43 @@ export function credentialPool(config) {
|
|
|
135
155
|
? { ok: false }
|
|
136
156
|
: { ok: false, retryAt: earliest };
|
|
137
157
|
};
|
|
138
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Every change runs after the one before it has written. Each is a read, a
|
|
160
|
+
* change and a whole-snapshot write, so two interleaved would let the earlier
|
|
161
|
+
* snapshot land last and put back what the later one cleared.
|
|
162
|
+
*/
|
|
163
|
+
let tail = Promise.resolve();
|
|
164
|
+
const serial = (change) => {
|
|
165
|
+
const run = tail.then(change);
|
|
166
|
+
tail = run.catch(() => { });
|
|
167
|
+
return run;
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* The mirror is updated **before** the write is attempted, so a write that
|
|
171
|
+
* fails still changes what this isolate does next.
|
|
172
|
+
*/
|
|
173
|
+
const persist = async (states, detail) => {
|
|
174
|
+
mirror = states;
|
|
175
|
+
await config.store.write(states).catch((err) => {
|
|
176
|
+
console.error("[claude-code] could not persist the credential pool", {
|
|
177
|
+
...detail,
|
|
178
|
+
err: String(err)
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
const update = (id, change) => serial(async () => {
|
|
139
183
|
const { tokens, keys, states } = await load();
|
|
140
184
|
const index = keys.indexOf(id);
|
|
141
|
-
// An id that is no longer configured: the operator replaced that
|
|
142
|
-
// between the request going out and its refusal coming back.
|
|
143
|
-
// nothing to mark, and marking the slot instead is the bug `id`
|
|
144
|
-
// prevent — so this returns the current lead and moves on.
|
|
185
|
+
// An id that is no longer configured: the operator replaced that
|
|
186
|
+
// credential between the request going out and its refusal coming back.
|
|
187
|
+
// There is nothing to mark, and marking the slot instead is the bug `id`
|
|
188
|
+
// exists to prevent — so this returns the current lead and moves on.
|
|
145
189
|
if (id && index >= 0) {
|
|
146
190
|
states[index] = change(states[index] ?? { id, resetAt: 0 });
|
|
147
|
-
|
|
148
|
-
// fails still changes what this isolate does next.
|
|
149
|
-
mirror = states;
|
|
150
|
-
await config.store.write(states).catch((err) => {
|
|
151
|
-
console.error("[claude-code] could not persist the credential pool", {
|
|
152
|
-
id,
|
|
153
|
-
err: String(err)
|
|
154
|
-
});
|
|
155
|
-
});
|
|
191
|
+
await persist(states, { id });
|
|
156
192
|
}
|
|
157
193
|
return pick(tokens, keys, states);
|
|
158
|
-
};
|
|
194
|
+
});
|
|
159
195
|
return {
|
|
160
196
|
async lead() {
|
|
161
197
|
const { tokens, keys, states } = await load();
|
|
@@ -172,9 +208,50 @@ export function credentialPool(config) {
|
|
|
172
208
|
},
|
|
173
209
|
reject(id) {
|
|
174
210
|
return update(id, (state) => ({ ...state, dead: true }));
|
|
175
|
-
}
|
|
211
|
+
},
|
|
212
|
+
reopen: () => serial(async () => {
|
|
213
|
+
const { tokens, keys, states } = await load();
|
|
214
|
+
const at = now();
|
|
215
|
+
const slots = [];
|
|
216
|
+
states.forEach((state, i) => {
|
|
217
|
+
if (!state.dead && state.resetAt <= at)
|
|
218
|
+
return;
|
|
219
|
+
states[i] = {
|
|
220
|
+
id: state.id,
|
|
221
|
+
resetAt: 0,
|
|
222
|
+
epoch: (state.epoch ?? 0) + 1
|
|
223
|
+
};
|
|
224
|
+
slots.push(i);
|
|
225
|
+
});
|
|
226
|
+
if (slots.length) {
|
|
227
|
+
console.info("[claude-code] asking Anthropic again about credentials", {
|
|
228
|
+
slots
|
|
229
|
+
});
|
|
230
|
+
await persist(states, { slots });
|
|
231
|
+
}
|
|
232
|
+
return pick(tokens, keys, states);
|
|
233
|
+
})
|
|
176
234
|
};
|
|
177
235
|
}
|
|
236
|
+
const pools = new WeakMap();
|
|
237
|
+
/**
|
|
238
|
+
* The pool over `config.store`, one per store for the isolate's life.
|
|
239
|
+
*
|
|
240
|
+
* The egress gateway and a session's admission hold the same store, and they
|
|
241
|
+
* have to be one pool: a pool's mirror is what carries a mark or a clear past a
|
|
242
|
+
* failed write, and its writes are ordered only among themselves. Two pools
|
|
243
|
+
* over one store each hold up alone and disagree once storage fails. The first
|
|
244
|
+
* caller's `credentials` and `now` are the pool's, which is safe because every
|
|
245
|
+
* holder of a store is configured from one deployment.
|
|
246
|
+
*/
|
|
247
|
+
export function sharedPool(config) {
|
|
248
|
+
let pool = pools.get(config.store);
|
|
249
|
+
if (!pool) {
|
|
250
|
+
pool = credentialPool(config);
|
|
251
|
+
pools.set(config.store, pool);
|
|
252
|
+
}
|
|
253
|
+
return pool;
|
|
254
|
+
}
|
|
178
255
|
/**
|
|
179
256
|
* The only status that retires a credential.
|
|
180
257
|
*
|
|
@@ -183,9 +260,9 @@ export function credentialPool(config) {
|
|
|
183
260
|
* permission_error` for a credential that is fine but may not have the thing
|
|
184
261
|
* being asked for. Reading the second as "this credential is bad" turns a
|
|
185
262
|
* *configuration* mistake — a model the subscription cannot reach, say — into a
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
263
|
+
* pool-wide outage: every credential 403s in turn, every one is retired, and
|
|
264
|
+
* each session ends telling an operator to mint a new credential — the wrong
|
|
265
|
+
* instruction for a typo.
|
|
189
266
|
*
|
|
190
267
|
* A 403 is therefore left unclassified: forwarded to the client untouched, and
|
|
191
268
|
* logged whole by the gateway. That is the same treatment every other
|
|
@@ -224,9 +301,8 @@ function retryAfterMs(value, at) {
|
|
|
224
301
|
* Parse a reset header, which may be unix seconds or an RFC 3339 timestamp.
|
|
225
302
|
*
|
|
226
303
|
* Anthropic documents the `anthropic-ratelimit-*-reset` family as RFC 3339, and
|
|
227
|
-
* the unified
|
|
228
|
-
*
|
|
229
|
-
* unverified — and a parser that handles both cannot be wrong about which.
|
|
304
|
+
* sends the unified one as unix seconds. Both are accepted, so neither family
|
|
305
|
+
* changing form can read as "no reset".
|
|
230
306
|
*/
|
|
231
307
|
function resetAtFrom(value) {
|
|
232
308
|
if (!value)
|
|
@@ -240,52 +316,17 @@ function resetAtFrom(value) {
|
|
|
240
316
|
const parsed = Date.parse(trimmed);
|
|
241
317
|
return Number.isNaN(parsed) ? undefined : parsed;
|
|
242
318
|
}
|
|
243
|
-
/**
|
|
244
|
-
* The `rate_limit_event` statuses that mean the bucket is empty.
|
|
245
|
-
*
|
|
246
|
-
* **Empty, and that is the finding rather than a placeholder.** The client
|
|
247
|
-
* announces its bucket on every session and the only status ever observed is
|
|
248
|
-
* {@link file://./events.ts RATE_LIMIT_OK} — so there is no known value here to
|
|
249
|
-
* put in, and putting in a guess is the expensive half of the asymmetry
|
|
250
|
-
* {@link readRefusal} is built around: reading a healthy bucket as exhausted
|
|
251
|
-
* retires a working credential for hours, and with a pool of one that is an
|
|
252
|
-
* outage. The same reasoning that leaves a 403 unclassified leaves this set
|
|
253
|
-
* empty.
|
|
254
|
-
*
|
|
255
|
-
* What fills it is the log. `run.ts` reports every change of reading, and a
|
|
256
|
-
* status that is not `allowed` is also surfaced as a progress note, so the first
|
|
257
|
-
* real one arrives named — at which point it goes in here and the pool starts
|
|
258
|
-
* rotating *before* Anthropic refuses a request rather than because it did.
|
|
259
|
-
*/
|
|
260
|
-
const RATE_LIMIT_SPENT = new Set();
|
|
261
|
-
/**
|
|
262
|
-
* Read the client's own bucket reading as a verdict on the credential in use.
|
|
263
|
-
*
|
|
264
|
-
* `undefined` means "nothing to act on", which today is every reading — see
|
|
265
|
-
* {@link RATE_LIMIT_SPENT}. Otherwise the epoch-milliseconds the bucket refills,
|
|
266
|
-
* ready for {@link CredentialPool.spend}.
|
|
267
|
-
*
|
|
268
|
-
* Here rather than beside the parser because this is the same question
|
|
269
|
-
* {@link readRefusal} answers about a response: is this credential still worth
|
|
270
|
-
* using? The parser's job is to say what the line contained.
|
|
271
|
-
*/
|
|
272
|
-
export function readRateLimitEvent(info) {
|
|
273
|
-
if (!RATE_LIMIT_SPENT.has(info.status))
|
|
274
|
-
return undefined;
|
|
275
|
-
// Seconds on the wire. A value passed through unconverted lands in 1970, and a
|
|
276
|
-
// credential marked spent until then reads as usable — the failure would be
|
|
277
|
-
// this function doing nothing, silently.
|
|
278
|
-
return info.resetsAt === undefined ? undefined : info.resetsAt * 1000;
|
|
279
|
-
}
|
|
280
319
|
/**
|
|
281
320
|
* Read a response as a verdict on the credential that produced it.
|
|
282
321
|
*
|
|
322
|
+
* A spent subscription bucket arrives as a `429` with
|
|
323
|
+
* `anthropic-ratelimit-unified-status: rejected`, the reset in
|
|
324
|
+
* `anthropic-ratelimit-unified-reset` (unix seconds) and the same wait in
|
|
325
|
+
* `retry-after` (seconds); `-representative-claim` names the bucket.
|
|
326
|
+
*
|
|
283
327
|
* Returns `undefined` for anything it does not recognise — which the caller
|
|
284
|
-
* treats as "forward it unchanged, and log the whole thing"
|
|
285
|
-
*
|
|
286
|
-
* has ever been observed through this path**, so the rules below are inferences
|
|
287
|
-
* from the documented API rate limits, and the first real one is what will
|
|
288
|
-
* settle them.
|
|
328
|
+
* treats as "forward it unchanged, and log the whole thing", so a refusal of
|
|
329
|
+
* another shape arrives named.
|
|
289
330
|
*/
|
|
290
331
|
export function readRefusal(response, at) {
|
|
291
332
|
if (response.status === INVALID_STATUS)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SandboxDaemonOptions } from "../../sandbox/daemon/index.js";
|
|
2
|
+
import type { EffortLevel, PermissionMode } from "../config.js";
|
|
3
|
+
export interface ClaudeCodeDaemonOptions {
|
|
4
|
+
/** Reported in the daemon's `hello`, and what the sandbox object names as its adapter. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** The JSON schemas a run header may name, by name. */
|
|
7
|
+
schemas: Readonly<Record<string, Record<string, unknown>>>;
|
|
8
|
+
/** For a run whose header names none. */
|
|
9
|
+
model?: string;
|
|
10
|
+
effort?: EffortLevel;
|
|
11
|
+
/** A `build` run's mode. A `plan` run is always `plan`. */
|
|
12
|
+
permissionMode?: PermissionMode;
|
|
13
|
+
/** Who the session's own commits are attributed to — every git it starts, a submodule's and an amend's included. */
|
|
14
|
+
author?: {
|
|
15
|
+
name: string;
|
|
16
|
+
email: string;
|
|
17
|
+
};
|
|
18
|
+
maxSubagentDepth?: number;
|
|
19
|
+
maxConcurrentSubagents?: number;
|
|
20
|
+
/** More environment for the session. Never a credential: it is the container's. */
|
|
21
|
+
env?: Readonly<Record<string, string>>;
|
|
22
|
+
}
|
|
23
|
+
/** Claude Code, as `serveSandboxDaemon`'s `cli`. */
|
|
24
|
+
export declare function claudeCodeDaemonCli(options: ClaudeCodeDaemonOptions): SandboxDaemonOptions["cli"];
|