@dynamicagents/plugins 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.md +14 -14
  2. package/dist/claude-code/cards.d.ts +2 -0
  3. package/dist/claude-code/cards.js +11 -0
  4. package/dist/claude-code/config.d.ts +10 -83
  5. package/dist/claude-code/config.js +0 -2
  6. package/dist/claude-code/credentials.d.ts +32 -50
  7. package/dist/claude-code/credentials.js +115 -74
  8. package/dist/claude-code/daemon/index.d.ts +24 -0
  9. package/dist/claude-code/daemon/index.js +203 -0
  10. package/dist/claude-code/egress.d.ts +35 -117
  11. package/dist/claude-code/egress.js +80 -325
  12. package/dist/claude-code/events.d.ts +14 -17
  13. package/dist/claude-code/events.js +19 -15
  14. package/dist/claude-code/harness-wire.d.ts +45 -0
  15. package/dist/claude-code/harness-wire.js +47 -0
  16. package/dist/claude-code/index.d.ts +19 -29
  17. package/dist/claude-code/index.js +17 -25
  18. package/dist/claude-code/language-model.d.ts +19 -0
  19. package/dist/claude-code/language-model.js +71 -0
  20. package/dist/claude-code/model.d.ts +77 -70
  21. package/dist/claude-code/model.js +327 -327
  22. package/dist/claude-code/placeholder.d.ts +12 -0
  23. package/dist/claude-code/placeholder.js +12 -0
  24. package/dist/claude-code/spec.d.ts +4 -4
  25. package/dist/claude-code/spec.js +4 -4
  26. package/dist/computer/client.d.ts +88 -0
  27. package/dist/computer/client.js +6 -0
  28. package/dist/computer/context.d.ts +7 -7
  29. package/dist/computer/context.js +14 -18
  30. package/dist/computer/gate.d.ts +4 -23
  31. package/dist/computer/gate.js +1 -37
  32. package/dist/computer/index.d.ts +44 -43
  33. package/dist/computer/index.js +29 -50
  34. package/dist/computer/paths.d.ts +5 -27
  35. package/dist/computer/paths.js +7 -30
  36. package/dist/computer/proxy.d.ts +17 -31
  37. package/dist/computer/proxy.js +9 -44
  38. package/dist/computer/render.d.ts +3 -40
  39. package/dist/computer/render.js +3 -36
  40. package/dist/computer/sandbox.d.ts +55 -0
  41. package/dist/computer/sandbox.js +236 -0
  42. package/dist/computer/shell.d.ts +36 -0
  43. package/dist/computer/shell.js +41 -0
  44. package/dist/computer/tools-exec.js +3 -9
  45. package/dist/computer/tools-file.d.ts +1 -1
  46. package/dist/computer/tools-file.js +1 -1
  47. package/dist/{repo → repo-github}/checkout.d.ts +3 -4
  48. package/dist/{repo → repo-github}/checkout.js +3 -4
  49. package/dist/{repo → repo-github}/context.d.ts +14 -32
  50. package/dist/{repo → repo-github}/context.js +46 -81
  51. package/dist/{repo → repo-github}/index.d.ts +30 -13
  52. package/dist/{repo → repo-github}/index.js +25 -6
  53. package/dist/repo-github/mcp.d.ts +95 -0
  54. package/dist/repo-github/mcp.js +107 -0
  55. package/dist/repo-github/sandbox-egress.d.ts +68 -0
  56. package/dist/repo-github/sandbox-egress.js +272 -0
  57. package/dist/repo-github/sandbox-git.d.ts +30 -0
  58. package/dist/repo-github/sandbox-git.js +113 -0
  59. package/dist/{repo → repo-github}/tools-checks.js +43 -4
  60. package/dist/{repo → repo-github}/tools-clone.js +2 -21
  61. package/dist/{repo → repo-github}/tools-pr.d.ts +0 -1
  62. package/dist/{repo → repo-github}/tools-pr.js +85 -20
  63. package/dist/{repo → repo-github}/tools-review.d.ts +6 -0
  64. package/dist/{repo → repo-github}/tools-review.js +62 -176
  65. package/dist/{repo → repo-github}/url.d.ts +11 -0
  66. package/dist/{repo → repo-github}/url.js +12 -1
  67. package/dist/{workspace → sandbox}/advisory.d.ts +16 -35
  68. package/dist/{workspace → sandbox}/advisory.js +2 -29
  69. package/dist/sandbox/agent-exec.d.ts +31 -0
  70. package/dist/sandbox/agent-exec.js +62 -0
  71. package/dist/sandbox/ca-trust.d.ts +9 -0
  72. package/dist/sandbox/ca-trust.js +21 -0
  73. package/dist/sandbox/constants.d.ts +22 -0
  74. package/dist/sandbox/constants.js +22 -0
  75. package/dist/sandbox/daemon/composite.d.ts +9 -0
  76. package/dist/sandbox/daemon/composite.js +35 -0
  77. package/dist/sandbox/daemon/index.d.ts +47 -0
  78. package/dist/sandbox/daemon/index.js +116 -0
  79. package/dist/sandbox/egress.d.ts +81 -0
  80. package/dist/sandbox/egress.js +109 -0
  81. package/dist/sandbox/exec.d.ts +57 -0
  82. package/dist/sandbox/exec.js +52 -0
  83. package/dist/sandbox/format.d.ts +2 -0
  84. package/dist/sandbox/format.js +7 -0
  85. package/dist/sandbox/header.d.ts +50 -0
  86. package/dist/sandbox/header.js +94 -0
  87. package/dist/sandbox/index.d.ts +17 -0
  88. package/dist/sandbox/index.js +17 -0
  89. package/dist/{workspace/install.d.ts → sandbox/install-plan.d.ts} +2 -2
  90. package/dist/{workspace/install.js → sandbox/install-plan.js} +2 -2
  91. package/dist/sandbox/name.d.ts +27 -0
  92. package/dist/sandbox/name.js +32 -0
  93. package/dist/sandbox/object.d.ts +244 -0
  94. package/dist/sandbox/object.js +1139 -0
  95. package/dist/scratch/index.d.ts +1 -9
  96. package/dist/scratch/index.js +2 -18
  97. package/package.json +24 -20
  98. package/dist/claude-code/github.d.ts +0 -39
  99. package/dist/claude-code/github.js +0 -82
  100. package/dist/claude-code/run.d.ts +0 -491
  101. package/dist/claude-code/run.js +0 -913
  102. package/dist/claude-code/session.d.ts +0 -131
  103. package/dist/claude-code/session.js +0 -288
  104. package/dist/workspace/ca-trust.d.ts +0 -95
  105. package/dist/workspace/ca-trust.js +0 -312
  106. package/dist/workspace/container-deps.d.ts +0 -58
  107. package/dist/workspace/container-deps.js +0 -249
  108. package/dist/workspace/container-fault.d.ts +0 -38
  109. package/dist/workspace/container-fault.js +0 -73
  110. package/dist/workspace/deps-snapshot.d.ts +0 -110
  111. package/dist/workspace/deps-snapshot.js +0 -424
  112. package/dist/workspace/exec.d.ts +0 -94
  113. package/dist/workspace/exec.js +0 -153
  114. package/dist/workspace/format.d.ts +0 -36
  115. package/dist/workspace/format.js +0 -57
  116. package/dist/workspace/git-host.d.ts +0 -79
  117. package/dist/workspace/git-host.js +0 -226
  118. package/dist/workspace/git-identity.d.ts +0 -57
  119. package/dist/workspace/git-identity.js +0 -147
  120. package/dist/workspace/index.d.ts +0 -50
  121. package/dist/workspace/index.js +0 -58
  122. package/dist/workspace/install-job.d.ts +0 -170
  123. package/dist/workspace/install-job.js +0 -934
  124. package/dist/workspace/object.d.ts +0 -522
  125. package/dist/workspace/object.js +0 -1541
  126. package/dist/workspace/open.d.ts +0 -95
  127. package/dist/workspace/open.js +0 -63
  128. package/dist/workspace/read.d.ts +0 -16
  129. package/dist/workspace/read.js +0 -22
  130. package/dist/workspace/shell.d.ts +0 -45
  131. package/dist/workspace/shell.js +0 -80
  132. package/dist/workspace/sync.d.ts +0 -111
  133. package/dist/workspace/sync.js +0 -124
  134. package/dist/workspace/wake.d.ts +0 -21
  135. package/dist/workspace/wake.js +0 -10
  136. /package/dist/{repo → repo-github}/tools-checks.d.ts +0 -0
  137. /package/dist/{repo → repo-github}/tools-clone.d.ts +0 -0
  138. /package/dist/{repo → repo-github}/tools-worktree.d.ts +0 -0
  139. /package/dist/{repo → repo-github}/tools-worktree.js +0 -0
  140. /package/dist/{repo → repo-github}/tools-worktrees.d.ts +0 -0
  141. /package/dist/{repo → repo-github}/tools-worktrees.js +0 -0
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
- // `workspaceConfig` names the `/workspace` object this agent works in.
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
- // `computer` runs commands in a container, so the agent's own workspace —
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(workspaceConfig(this.env), () =>
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(workspaceConfig(this.env)),
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 | 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 the workspace container | one or more `claude setup-token` credentials |
71
- | [`/computer`](src/computer/) | An agent's tools over a workspace — `bash`, `grep`, `edit` — and the workspace Think's file tools use | a `/workspace` object |
72
- | [`/repo`](src/repo/) | Clone, commit, push a branch, open a pull request — over any container | `GITHUB_TOKEN` |
73
- | [`/scratch`](src/scratch/) | A throwaway git repository with no remote, for work that needs a container but no checkout | — |
74
- | [`/workspace`](src/workspace/) | The Durable Object a container workspace lives in — its container, credentialed git, dependency install and sync | `@cloudflare/computer`, `@platformatic/vfs` (paid) |
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
- * Everything one deployment of this plugin tunes, in one shape.
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, because the
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 gateway on the Worker side — and it already runs
120
- * a cloned repository's `postinstall` and its test suite, which is arbitrary
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
- * See {@link file://./run.ts buildLaunch} for the root guard this mode trips,
125
- * which is the other half of making it work.
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
- * Restrict the container's egress to these hosts, plus `api.anthropic.com`.
133
- *
134
- * **Omit it and egress is unrestricted, which is the default.** An empty array
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;
@@ -7,5 +7,3 @@
7
7
  * {@link ClaudeCodeConfig.permissionMode}.
8
8
  */
9
9
  export const DEFAULT_PERMISSION_MODE = "bypassPermissions";
10
- /** Fifteen minutes, well under the coder workspace's 20-minute idle timer. */
11
- export const DEFAULT_TIMEOUT_MS = 15 * 60_000;
@@ -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
- * and needs an operator. Collapsing them would either resurrect a revoked token
36
- * on a timer or retire a good one permanently.
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". That branch is not
174
- * a fallback, it is the instrument: **no genuine subscription-exhaustion `429`
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
- const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token));
42
- return [...new Uint8Array(digest).slice(0, 8)]
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, because both fields are
50
- * **monotonic**: `resetAt` only moves later (a bucket does not un-empty early)
51
- * and `dead` only turns on. That is what lets storage and the in-memory mirror
52
- * below be merged rather than one of them being chosen — there is no ordering in
53
- * which the merge loses information.
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
- const update = async (id, change) => {
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 credential
142
- // between the request going out and its refusal coming back. There is
143
- // nothing to mark, and marking the slot instead is the bug `id` exists to
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
- // The mirror is updated **before** the write is attempted, so a write that
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
- * permanent, pool-wide outage: every credential 403s in turn, every one is
187
- * retired, and `dead` is the one state nothing clears on its own. An operator
188
- * would have to delete Durable Object storage to recover from a typo.
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 variant has been reported as an epoch. Both are accepted rather
228
- * than guessed at, because this is exactly the field the box in the plan says is
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". That branch is not
285
- * a fallback, it is the instrument: **no genuine subscription-exhaustion `429`
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"];