@dynamicagents/plugins 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +59 -79
  2. package/dist/browser/index.d.ts +0 -2
  3. package/dist/browser/index.js +21 -27
  4. package/dist/claude-code/cards.d.ts +53 -0
  5. package/dist/claude-code/cards.js +272 -0
  6. package/dist/claude-code/config.d.ts +73 -31
  7. package/dist/claude-code/config.js +0 -2
  8. package/dist/claude-code/credentials.d.ts +21 -9
  9. package/dist/claude-code/credentials.js +43 -9
  10. package/dist/claude-code/egress.d.ts +15 -14
  11. package/dist/claude-code/egress.js +51 -14
  12. package/dist/claude-code/events.d.ts +153 -9
  13. package/dist/claude-code/events.js +289 -58
  14. package/dist/claude-code/github.d.ts +39 -0
  15. package/dist/claude-code/github.js +82 -0
  16. package/dist/claude-code/index.d.ts +24 -80
  17. package/dist/claude-code/index.js +19 -234
  18. package/dist/claude-code/model.d.ts +152 -0
  19. package/dist/claude-code/model.js +342 -0
  20. package/dist/claude-code/run.d.ts +283 -92
  21. package/dist/claude-code/run.js +620 -114
  22. package/dist/claude-code/session.d.ts +131 -0
  23. package/dist/claude-code/session.js +285 -0
  24. package/dist/claude-code/spec.d.ts +46 -0
  25. package/dist/claude-code/spec.js +83 -0
  26. package/dist/claude-code/verified.d.ts +15 -0
  27. package/dist/claude-code/verified.js +15 -0
  28. package/dist/computer/context.d.ts +19 -0
  29. package/dist/computer/context.js +222 -0
  30. package/dist/computer/file-lock.d.ts +5 -0
  31. package/dist/computer/file-lock.js +40 -0
  32. package/dist/computer/gate.d.ts +12 -12
  33. package/dist/computer/gate.js +12 -12
  34. package/dist/computer/index.d.ts +57 -255
  35. package/dist/computer/index.js +98 -1039
  36. package/dist/computer/paths.d.ts +47 -22
  37. package/dist/computer/paths.js +64 -49
  38. package/dist/computer/proxy.d.ts +45 -0
  39. package/dist/computer/proxy.js +234 -0
  40. package/dist/computer/render.d.ts +27 -38
  41. package/dist/computer/render.js +32 -65
  42. package/dist/computer/tools-exec.d.ts +3 -0
  43. package/dist/computer/tools-exec.js +268 -0
  44. package/dist/computer/tools-file.d.ts +12 -0
  45. package/dist/computer/tools-file.js +64 -0
  46. package/dist/computer/tools-grep.d.ts +3 -0
  47. package/dist/computer/tools-grep.js +129 -0
  48. package/dist/repo/checkout.d.ts +35 -6
  49. package/dist/repo/checkout.js +64 -6
  50. package/dist/repo/context.d.ts +135 -0
  51. package/dist/repo/context.js +582 -0
  52. package/dist/repo/index.d.ts +79 -57
  53. package/dist/repo/index.js +118 -1112
  54. package/dist/repo/tools-checks.d.ts +3 -0
  55. package/dist/repo/tools-checks.js +112 -0
  56. package/dist/repo/tools-clone.d.ts +4 -0
  57. package/dist/repo/tools-clone.js +192 -0
  58. package/dist/repo/tools-pr.d.ts +14 -0
  59. package/dist/repo/tools-pr.js +196 -0
  60. package/dist/repo/tools-review.d.ts +12 -0
  61. package/dist/repo/tools-review.js +393 -0
  62. package/dist/repo/tools-worktree.d.ts +4 -0
  63. package/dist/repo/tools-worktree.js +305 -0
  64. package/dist/repo/tools-worktrees.d.ts +12 -0
  65. package/dist/repo/tools-worktrees.js +48 -0
  66. package/dist/repo/url.d.ts +2 -2
  67. package/dist/repo/url.js +7 -7
  68. package/dist/scratch/index.d.ts +4 -4
  69. package/dist/scratch/index.js +12 -11
  70. package/dist/{computer → workspace}/advisory.d.ts +33 -20
  71. package/dist/{computer → workspace}/advisory.js +32 -7
  72. package/dist/workspace/ca-trust.d.ts +95 -0
  73. package/dist/workspace/ca-trust.js +312 -0
  74. package/dist/workspace/container-deps.d.ts +54 -0
  75. package/dist/workspace/container-deps.js +224 -0
  76. package/dist/workspace/container-fault.d.ts +38 -0
  77. package/dist/workspace/container-fault.js +73 -0
  78. package/dist/workspace/exec.d.ts +94 -0
  79. package/dist/workspace/exec.js +153 -0
  80. package/dist/workspace/format.d.ts +36 -0
  81. package/dist/workspace/format.js +57 -0
  82. package/dist/workspace/git-host.d.ts +79 -0
  83. package/dist/workspace/git-host.js +226 -0
  84. package/dist/workspace/git-identity.d.ts +57 -0
  85. package/dist/workspace/git-identity.js +147 -0
  86. package/dist/workspace/index.d.ts +43 -47
  87. package/dist/workspace/index.js +52 -112
  88. package/dist/workspace/install-job.d.ts +121 -0
  89. package/dist/workspace/install-job.js +866 -0
  90. package/dist/{computer → workspace}/install.d.ts +15 -19
  91. package/dist/{computer → workspace}/install.js +8 -11
  92. package/dist/workspace/object.d.ts +502 -0
  93. package/dist/workspace/object.js +1427 -0
  94. package/dist/workspace/open.d.ts +95 -0
  95. package/dist/workspace/open.js +63 -0
  96. package/dist/workspace/read.d.ts +16 -0
  97. package/dist/workspace/read.js +22 -0
  98. package/dist/workspace/shell.d.ts +45 -0
  99. package/dist/workspace/shell.js +80 -0
  100. package/dist/workspace/sync.d.ts +111 -0
  101. package/dist/workspace/sync.js +124 -0
  102. package/dist/workspace/wake.d.ts +19 -0
  103. package/dist/workspace/wake.js +10 -0
  104. package/package.json +28 -42
  105. package/dist/arc-agi/analysis.d.ts +0 -336
  106. package/dist/arc-agi/analysis.js +0 -829
  107. package/dist/arc-agi/client.d.ts +0 -82
  108. package/dist/arc-agi/client.js +0 -113
  109. package/dist/arc-agi/game-tools.d.ts +0 -25
  110. package/dist/arc-agi/game-tools.js +0 -26
  111. package/dist/arc-agi/index.d.ts +0 -42
  112. package/dist/arc-agi/index.js +0 -175
  113. package/dist/arc-agi/main-agent.d.ts +0 -27
  114. package/dist/arc-agi/main-agent.js +0 -65
  115. package/dist/arc-agi/recipe.d.ts +0 -51
  116. package/dist/arc-agi/recipe.js +0 -100
  117. package/dist/arc-agi/scorecard.d.ts +0 -118
  118. package/dist/arc-agi/scorecard.js +0 -163
  119. package/dist/arc-agi/soul.d.ts +0 -40
  120. package/dist/arc-agi/soul.js +0 -90
  121. package/dist/arc-agi/store.d.ts +0 -154
  122. package/dist/arc-agi/store.js +0 -216
  123. package/dist/arc-agi/tools.d.ts +0 -36
  124. package/dist/arc-agi/tools.js +0 -576
  125. package/dist/arc-agi/types.d.ts +0 -212
  126. package/dist/arc-agi/types.js +0 -19
  127. package/dist/claude-code/recipe.d.ts +0 -53
  128. package/dist/claude-code/recipe.js +0 -100
  129. package/dist/computer/read.d.ts +0 -137
  130. package/dist/computer/read.js +0 -203
  131. package/dist/recall/index.d.ts +0 -99
  132. package/dist/recall/index.js +0 -171
  133. package/dist/triage/index.d.ts +0 -355
  134. package/dist/triage/index.js +0 -302
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Everything one `claude-coder` deployment tunes, in one shape.
2
+ * Everything one deployment of this plugin tunes, in one shape.
3
3
  *
4
4
  * Config at instantiation rather than an `env` argument, for the reason the
5
5
  * whole package works this way: `Env` is the ambient interface `wrangler types`
@@ -14,8 +14,16 @@
14
14
  * the exact failure this option exists to end. The CLI validates the value too,
15
15
  * but it does so by exiting 1 with a message on stderr, and stderr reaches an
16
16
  * operator far less reliably than a type error reaches a developer.
17
+ *
18
+ * Two of these are worth knowing about before picking one. `default` is not
19
+ * among the choices the pinned CLI documents, and is accepted and echoed back as
20
+ * `permissionMode: "default"` anyway — so it stays. `manual` is the reverse: the
21
+ * CLI documents it and no session here has been run under it, so it is spellable
22
+ * rather than recommended, and whatever it prompts for the headless path denies.
23
+ * See {@link ClaudeCodeConfig.permissionMode} for why the default is the
24
+ * permissive one.
17
25
  */
18
- export type PermissionMode = "default" | "acceptEdits" | "bypassPermissions" | "plan" | "dontAsk" | "auto";
26
+ export type PermissionMode = "default" | "acceptEdits" | "bypassPermissions" | "plan" | "dontAsk" | "manual" | "auto";
19
27
  /**
20
28
  * What a session runs under when a deployment says nothing.
21
29
  *
@@ -25,6 +33,21 @@ export type PermissionMode = "default" | "acceptEdits" | "bypassPermissions" | "
25
33
  * {@link ClaudeCodeConfig.permissionMode}.
26
34
  */
27
35
  export declare const DEFAULT_PERMISSION_MODE: PermissionMode;
36
+ /**
37
+ * The effort levels `claude --effort` accepts.
38
+ *
39
+ * Spelled out for a sharper reason than {@link PermissionMode}: an unrecognised
40
+ * permission mode exits 1, but an unrecognised effort is **warned about on
41
+ * stderr and then ignored**, leaving the session at the model's default. A typo
42
+ * here does not fail, it quietly buys nothing — and stderr from a headless
43
+ * session reaches an operator far less reliably than a type error reaches a
44
+ * developer.
45
+ *
46
+ * A model that does not carry a level is the same silence: the CLI clamps down
47
+ * to `high` rather than refusing, so asking for more than a model offers is
48
+ * indistinguishable from asking for `high`.
49
+ */
50
+ export type EffortLevel = "low" | "medium" | "high" | "xhigh" | "max";
28
51
  export interface ClaudeCodeConfig {
29
52
  /**
30
53
  * The credential pool, in priority order — index 0 is tried first.
@@ -46,29 +69,31 @@ export interface ClaudeCodeConfig {
46
69
  * refused with the reset time.
47
70
  */
48
71
  credentials: () => readonly string[];
49
- /**
50
- * Which workspace this agent's sessions run in.
51
- *
52
- * Resolved on the **parent**, where the verified caller is known: core
53
- * dispatches `resolveRuntime` to the plugin that declared the subtask type,
54
- * which is this one, and the value it writes is how the subagent facet learns
55
- * which Durable Object holds its checkout. A facet cannot work this out for
56
- * itself — `callerKey()` throws there by design — and it is deliberately not a
57
- * subtask param, because a model-authored workspace name would let a model
58
- * name somebody else's.
59
- */
60
- workspaceName: () => string;
61
72
  /** Which model the session runs. Unset, Claude Code picks its own default. */
62
73
  model?: string;
63
74
  /**
64
- * Ceiling on the *outer* session's turns.
75
+ * How hard the session's model thinks, per turn.
76
+ *
77
+ * Unset, the model's own default applies, which is `high` for the frontier
78
+ * models this plugin exists to reach. Raising it buys depth at a multiple of
79
+ * the spend, per turn — so the level decides how many sessions a credential
80
+ * holds, not just how well one thinks. The README's Costs section carries the
81
+ * multiples and the sizing they imply.
82
+ *
83
+ * Not to be confused with the agent's own reasoning effort, which belongs to
84
+ * a different model on the Worker side. This one reaches only the CLI in the
85
+ * container.
86
+ */
87
+ effort?: EffortLevel;
88
+ /**
89
+ * Caps on Claude Code's own subagent tree.
65
90
  *
66
- * Advisory. Claude Code's own subagent tree multiplies whatever this says, and
67
- * the tree is invisible to Dynamic Agents' scheduler. {@link timeoutMs} is
68
- * the ceiling that actually holds, because the container runtime enforces it.
91
+ * Advisory, and the distinction is the reason there is no turn ceiling beside
92
+ * them — see the README on what this package deliberately does not do. That
93
+ * 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.
69
96
  */
70
- maxTurns?: number;
71
- /** Caps on Claude Code's own subagent tree. Advisory, like `maxTurns`. */
72
97
  maxSubagentDepth?: number;
73
98
  maxConcurrentSubagents?: number;
74
99
  /**
@@ -98,6 +123,9 @@ export interface ClaudeCodeConfig {
98
123
  *
99
124
  * See {@link file://./run.ts buildLaunch} for the root guard this mode trips,
100
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}.
101
129
  */
102
130
  permissionMode?: PermissionMode;
103
131
  /**
@@ -108,20 +136,20 @@ export interface ClaudeCodeConfig {
108
136
  * {@link file://./egress.ts EgressConfig.restrictToHosts} for the full table
109
137
  * and for why open is the default.
110
138
  *
111
- * Whatever this says, the container never needs forge access: `/repo` runs
112
- * clone, fetch and push as isomorphic-git inside the workspace object, so the
113
- * forge token stays on the Worker side.
139
+ * A restriction that leaves out `github.com` and `api.github.com` leaves out
140
+ * {@link githubToken} with them.
114
141
  */
115
142
  restrictToHosts?: readonly string[];
116
143
  /**
117
- * How long one chunk blocks before checkpointing and yielding.
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.
118
147
  *
119
- * Must stay under the Workflow step timeout with room to spare, and must be
120
- * long enough that a session does not exhaust its branch's chunk allowance
121
- * while it is still thinking. Defaults to eight minutes, against a 30-minute
122
- * step timeout and a 15-minute soft chunk budget.
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.
123
151
  */
124
- windowMs?: number;
152
+ githubToken?: () => string | undefined;
125
153
  /**
126
154
  * Ceiling on the whole session, enforced by the container runtime.
127
155
  *
@@ -133,8 +161,22 @@ export interface ClaudeCodeConfig {
133
161
  timeoutMs?: number;
134
162
  /** Extra environment for the session. Never secrets — see the README. */
135
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
+ };
136
180
  }
137
- /** Eight minutes: comfortably inside `CHUNK_SOFT_MS`, long enough to be useful. */
138
- export declare const DEFAULT_WINDOW_MS: number;
139
181
  /** Fifteen minutes, well under the coder workspace's 20-minute idle timer. */
140
182
  export declare const DEFAULT_TIMEOUT_MS: number;
@@ -7,7 +7,5 @@
7
7
  * {@link ClaudeCodeConfig.permissionMode}.
8
8
  */
9
9
  export const DEFAULT_PERMISSION_MODE = "bypassPermissions";
10
- /** Eight minutes: comfortably inside `CHUNK_SOFT_MS`, long enough to be useful. */
11
- export const DEFAULT_WINDOW_MS = 8 * 60_000;
12
10
  /** Fifteen minutes, well under the coder workspace's 20-minute idle timer. */
13
11
  export const DEFAULT_TIMEOUT_MS = 15 * 60_000;
@@ -5,15 +5,12 @@
5
5
  * ## Why a pool rather than a budget
6
6
  *
7
7
  * A Claude subscription has two limits that matter here: a rolling 5-hour
8
- * session bucket and a weekly one. Neither is readable, and 0.5.0 tried to stay
9
- * under them by *estimating* spend — a counter in dollars, a cap, and a gate
10
- * that refused to start work when the counter got high.
11
- *
12
- * That was the wrong instrument. The estimate is a guess about a bucket nobody
13
- * can see, and it only moved when a run *ended* (usage is learned from the
14
- * terminal `result` event), so it was never a cap on spend at all — only a gate
15
- * on starting. Meanwhile the bucket itself says so, precisely, the moment it is
16
- * empty: Anthropic answers `429`.
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`.
17
14
  *
18
15
  * So this module does not predict. It **detects and routes around**: the
19
16
  * credential becomes an ordered pool, the gateway uses the first usable entry,
@@ -154,6 +151,21 @@ export type Refusal = {
154
151
  kind: "transient";
155
152
  retryAfterMs: number;
156
153
  };
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;
157
169
  /**
158
170
  * Read a response as a verdict on the credential that produced it.
159
171
  *
@@ -5,15 +5,12 @@
5
5
  * ## Why a pool rather than a budget
6
6
  *
7
7
  * A Claude subscription has two limits that matter here: a rolling 5-hour
8
- * session bucket and a weekly one. Neither is readable, and 0.5.0 tried to stay
9
- * under them by *estimating* spend — a counter in dollars, a cap, and a gate
10
- * that refused to start work when the counter got high.
11
- *
12
- * That was the wrong instrument. The estimate is a guess about a bucket nobody
13
- * can see, and it only moved when a run *ended* (usage is learned from the
14
- * terminal `result` event), so it was never a cap on spend at all — only a gate
15
- * on starting. Meanwhile the bucket itself says so, precisely, the moment it is
16
- * empty: Anthropic answers `429`.
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`.
17
14
  *
18
15
  * So this module does not predict. It **detects and routes around**: the
19
16
  * credential becomes an ordered pool, the gateway uses the first usable entry,
@@ -243,6 +240,43 @@ function resetAtFrom(value) {
243
240
  const parsed = Date.parse(trimmed);
244
241
  return Number.isNaN(parsed) ? undefined : parsed;
245
242
  }
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
+ }
246
280
  /**
247
281
  * Read a response as a verdict on the credential that produced it.
248
282
  *
@@ -22,7 +22,8 @@ import { type CredentialStore } from "./credentials.js";
22
22
  * with a placeholder; the swap happens here. A `postinstall` script in a
23
23
  * cloned repository can read every environment variable the container has and
24
24
  * still learn nothing — which is the repository's standing rule ("hand it the
25
- * action, not the credential") applied to a process nobody can constrain.
25
+ * action, not the credential") applied to a process nobody can constrain. A
26
+ * GitHub token, when configured, is held the same way — see `./github.ts`.
26
27
  * 2. **Any restriction that is applied is total.** Not a policy the container
27
28
  * cooperates with; the only route out. Restriction is **off by default** —
28
29
  * see {@link EgressConfig.restrictToHosts} for why.
@@ -33,10 +34,9 @@ import { type CredentialStore } from "./credentials.js";
33
34
  *
34
35
  * ## What this deliberately does not do
35
36
  *
36
- * It does not meter. 0.5.0 had a budget gate that estimated spend in dollars and
37
- * refused to start work over a cap; 0.6.0 deleted it. An estimate is a guess
38
- * about a bucket nobody can read, it only moved once per run, and the bucket
39
- * itself announces the answer exactly — see the docblock in `credentials.ts`.
37
+ * It does not meter, and a budget gate must not come back: an estimate of spend
38
+ * is a guess about a bucket nobody can read, moves only once per run, and the
39
+ * bucket announces the answer exactly — see the docblock in `credentials.ts`.
40
40
  *
41
41
  * And it does not, by default, **bound exfiltration**. The container holds the
42
42
  * checkout, and with no restriction configured it can send it anywhere. That is
@@ -49,16 +49,11 @@ import { type CredentialStore } from "./credentials.js";
49
49
  export declare const ANTHROPIC_HOST = "api.anthropic.com";
50
50
  export interface EgressConfig {
51
51
  /**
52
- * The credential pool, in priority order — index 0 is tried first.
52
+ * The credential pool — see {@link file://./config.ts ClaudeCodeConfig.credentials}.
53
53
  *
54
- * A thunk so a rotated secret is picked up without rebuilding the plugin list.
55
- * Called per request; it must be cheap. **None of these values enters the
56
- * container**: the session is launched with `CREDENTIAL_PLACEHOLDER` and the
57
- * swap happens here.
58
- *
59
- * An array of one is entirely valid and behaves as 0.5.0's single credential
60
- * did — used until its bucket empties, after which requests are refused with
61
- * the reset time.
54
+ * Called per request, so it must be cheap. This is the one place a real
55
+ * credential is read: the session is launched with `CREDENTIAL_PLACEHOLDER`
56
+ * and the swap happens here.
62
57
  */
63
58
  credentials: () => readonly string[];
64
59
  /**
@@ -109,6 +104,12 @@ export interface EgressConfig {
109
104
  * restriction, the first time a host serves user-controlled subdomains.
110
105
  */
111
106
  restrictToHosts?: readonly string[];
107
+ /**
108
+ * The GitHub token to swap for its placeholder, read per request. It reaches
109
+ * {@link GITHUB_HOSTS} and nowhere else. Unset, GitHub is reached
110
+ * anonymously like any other host. See `./github.ts`.
111
+ */
112
+ githubToken?: () => string | undefined;
112
113
  /** Named in log lines so one Worker's several gateways stay tellable apart. */
113
114
  label?: string;
114
115
  now?: () => number;
@@ -1,4 +1,5 @@
1
1
  import { credentialPool, readRefusal } from "./credentials.js";
2
+ import { GITHUB_HOSTS, githubAuthorization, githubError, presentsPlaceholder } from "./github.js";
2
3
  /**
3
4
  * `@dynamicagents/plugins/claude-code` — the container's only way out.
4
5
  *
@@ -22,7 +23,8 @@ import { credentialPool, readRefusal } from "./credentials.js";
22
23
  * with a placeholder; the swap happens here. A `postinstall` script in a
23
24
  * cloned repository can read every environment variable the container has and
24
25
  * still learn nothing — which is the repository's standing rule ("hand it the
25
- * action, not the credential") applied to a process nobody can constrain.
26
+ * action, not the credential") applied to a process nobody can constrain. A
27
+ * GitHub token, when configured, is held the same way — see `./github.ts`.
26
28
  * 2. **Any restriction that is applied is total.** Not a policy the container
27
29
  * cooperates with; the only route out. Restriction is **off by default** —
28
30
  * see {@link EgressConfig.restrictToHosts} for why.
@@ -33,10 +35,9 @@ import { credentialPool, readRefusal } from "./credentials.js";
33
35
  *
34
36
  * ## What this deliberately does not do
35
37
  *
36
- * It does not meter. 0.5.0 had a budget gate that estimated spend in dollars and
37
- * refused to start work over a cap; 0.6.0 deleted it. An estimate is a guess
38
- * about a bucket nobody can read, it only moved once per run, and the bucket
39
- * itself announces the answer exactly — see the docblock in `credentials.ts`.
38
+ * It does not meter, and a budget gate must not come back: an estimate of spend
39
+ * is a guess about a bucket nobody can read, moves only once per run, and the
40
+ * bucket announces the answer exactly — see the docblock in `credentials.ts`.
40
41
  *
41
42
  * And it does not, by default, **bound exfiltration**. The container holds the
42
43
  * checkout, and with no restriction configured it can send it anywhere. That is
@@ -58,9 +59,9 @@ export const ANTHROPIC_HOST = "api.anthropic.com";
58
59
  * Bouncing a request back into it is never a legitimate fetch, so it is refused
59
60
  * before any policy is consulted.
60
61
  *
61
- * A workspace that overrides `egressHost` on its `CloudflareContainerBackend`
62
- * should add that name here; the default is what the backend uses when nothing
63
- * says otherwise.
62
+ * A workspace that overrides `egressHost` on its `ContainerBackend` should add
63
+ * that name here; the default is what the backend uses when nothing says
64
+ * otherwise.
64
65
  */
65
66
  const NEVER_ALLOWED = new Set([
66
67
  "computer.internal",
@@ -207,6 +208,9 @@ export function claudeCodeEgress(config) {
207
208
  });
208
209
  return apiError("permission_error", `${host} is outside this workspace's egress host restriction`, 403);
209
210
  }
211
+ if (config.githubToken && GITHUB_HOSTS.has(host)) {
212
+ return await toGitHub(request, url, config.githubToken);
213
+ }
210
214
  const headers = new Headers(request.headers);
211
215
  if (host !== ANTHROPIC_HOST) {
212
216
  // The placeholder must not leave the boundary either. It is worthless to
@@ -241,8 +245,8 @@ export function claudeCodeEgress(config) {
241
245
  * The swap, and it is the whole point of the file.
242
246
  *
243
247
  * `authorization` is set and `x-api-key` removed because that is the shape
244
- * the sanctioned client sends — the Phase 0 capture showed `authorization:
245
- * Bearer …` with no `x-api-key` at all. Everything else is forwarded
248
+ * the sanctioned client sends: `authorization: Bearer …` with no
249
+ * `x-api-key` at all. Everything else is forwarded
246
250
  * untouched: the `anthropic-beta` list (which carries
247
251
  * `claude-code-20250219` and `oauth-2025-04-20`, almost certainly part of
248
252
  * what marks the request as coming from the client) and the `user-agent`
@@ -264,6 +268,39 @@ export function claudeCodeEgress(config) {
264
268
  }
265
269
  return await rotate(request, response, lead.id, lead.index);
266
270
  };
271
+ /**
272
+ * A request to GitHub: anonymous unless it presents the placeholder,
273
+ * which is swapped for the real token over https only.
274
+ *
275
+ * `redirect: "manual"`, so the token rides only the request it was swapped
276
+ * into: a redirect goes back to the client, whose next request crosses this
277
+ * gateway again and is judged on its own host.
278
+ */
279
+ const toGitHub = async (request, url, githubToken) => {
280
+ const headers = new Headers(request.headers);
281
+ const presented = presentsPlaceholder(headers.get("authorization"));
282
+ for (const name of CREDENTIAL_HEADERS)
283
+ headers.delete(name);
284
+ if (!presented) {
285
+ return await fetch(new Request(url, new Request(request, { headers })));
286
+ }
287
+ if (url.protocol !== "https:") {
288
+ console.warn(`[${tag}] refused a plaintext request to GitHub`, {
289
+ host: url.hostname,
290
+ method: request.method
291
+ });
292
+ return githubError(`${url.hostname} is reachable over https only; a credential is never ` +
293
+ "attached to a plaintext request", 403);
294
+ }
295
+ const token = githubToken();
296
+ if (!token) {
297
+ console.error(`[${tag}] a session asked for GitHub with no token set`);
298
+ return githubError("no GitHub token is configured for this deployment, so nothing can " +
299
+ "be done as its account", 401);
300
+ }
301
+ headers.set("authorization", githubAuthorization(presented, token));
302
+ return await fetch(new Request(url, new Request(request, { headers, redirect: "manual" })));
303
+ };
267
304
  /**
268
305
  * Read a refusal, move the lead if it earned one, and shape what the client
269
306
  * sees so its own retry lands on the new credential.
@@ -278,10 +315,10 @@ export function claudeCodeEgress(config) {
278
315
  /**
279
316
  * Capture the whole thing, every time.
280
317
  *
281
- * **No genuine subscription-exhaustion `429` has ever been observed through
282
- * this path** — the Phase 0 spike's 429s were the raw-API refusal, which is
283
- * a different response — so the rules in `readRefusal` are inferences from
284
- * the documented API rate limits. This log is the instrument that replaces
318
+ * **No genuine subscription-exhaustion `429` has been observed through this
319
+ * path** — a raw-API refusal is a different response — so the rules in
320
+ * `readRefusal` are inferences from the documented API rate limits. This log
321
+ * is the instrument that replaces
285
322
  * them with a fact, and it costs nothing: these statuses are rare, and the
286
323
  * body of one is a few hundred bytes.
287
324
  */