@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.
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 +22 -18
  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
@@ -1,10 +1,18 @@
1
- import { DEFAULT_ALLOWED_HOSTS, parseRepo, repoLocation } from "./url.js";
1
+ import { truncateOutput } from "@dynamicagents/core";
2
+ import { githubMcp } from "./mcp.js";
3
+ import { DEFAULT_ALLOWED_HOSTS, GITHUB_HOST, parseRepo, repoLocation } from "./url.js";
2
4
  /**
3
5
  * The machinery every repo tool closes over: the resolved config, the git
4
6
  * runners, and the GitHub client.
5
7
  */
6
8
  const DEFAULT_WORKDIR = "/workspace";
7
- const DEFAULT_API_BASE = "https://api.github.com";
9
+ /**
10
+ * GitHub's API. Not a host's to configure: the hosted MCP server these tools
11
+ * read through is github.com's alone, so an Enterprise base here would point
12
+ * half of this plugin at an install the other half cannot reach. That is
13
+ * {@link GITHUB_HOST}, which `githubRepo` holds every checkout to.
14
+ */
15
+ const API_BASE = `https://api.${GITHUB_HOST}`;
8
16
  const DEFAULT_AUTHOR = {
9
17
  name: "da-coder",
10
18
  email: "coder@dynamicagents.invalid"
@@ -30,70 +38,25 @@ const GITHUB_TIMEOUT_MS = 30_000;
30
38
  */
31
39
  export const GITHUB_PAGE_SIZE = 100;
32
40
  /**
33
- * Where GraphQL lives, given where REST does.
34
- *
35
- * On github.com they share an origin and `/graphql` hangs off it. On GitHub
36
- * Enterprise — which {@link RepoConfig.apiBase} exists for — REST is under
37
- * `/api/v3` and GraphQL is its **sibling** at `/api/graphql`, not a path beneath
38
- * it. Appending blindly yields `/api/v3/graphql`, which is a 404 on every
39
- * Enterprise install and no failure at all on the public API, so the mistake
40
- * survives every test written against github.com.
41
+ * Where GraphQL lives. A constant, because github.com is the only host these
42
+ * tools talk to — see `./mcp.ts` for why Enterprise is not supported.
41
43
  */
42
- export function graphqlEndpoint(apiBase) {
43
- const trimmed = apiBase.replace(/\/+$/, "");
44
- const enterprise = trimmed.replace(/\/api\/v3$/, "/api/graphql");
45
- return enterprise === trimmed ? `${trimmed}/graphql` : enterprise;
46
- }
47
- /**
48
- * Middle-out truncation, so both the head of a diff and its tail survive.
49
- *
50
- * Deliberately a **copy** of `/workspace`'s function of the same name, not an
51
- * import. `npm run verify:exports` fails any subpath whose module graph
52
- * reaches a sibling's directory, and its own advice for a shared helper is to
53
- * duplicate it — installing `repo` must not drag `/workspace` (and therefore
54
- * `@cloudflare/computer`) into a consumer's bundle. Fifteen lines is a cheaper
55
- * price than that coupling, and it is the same reason `exec` is injected here
56
- * rather than imported.
57
- *
58
- * The `half < 1` guard is not padding: without it a small `max` makes `half`
59
- * zero or negative, and `slice(-0)` is `slice(0)` — the whole string — so the
60
- * function would return *more* than it was given.
61
- */
62
- export function truncateOutput(text, max) {
63
- if (text.length <= max)
64
- return text;
65
- const marker = (dropped) => `\n\n… [${dropped} characters omitted from the middle] …\n\n`;
66
- const half = Math.floor((max - marker(text.length).length) / 2);
67
- if (half < 1)
68
- return text.slice(0, Math.max(0, max));
69
- return (text.slice(0, half) + marker(text.length - half * 2) + text.slice(-half));
70
- }
44
+ const GRAPHQL_URL = `${API_BASE}/graphql`;
71
45
  /**
72
46
  * What to tell the model when the plumbing, not the command, is the problem.
73
- *
74
- * A sibling of the computer plugin's `execLostNote` rather than an import, for
75
- * the same reason {@link truncateOutput} is a copy: `verify:exports` fails any
76
- * subpath whose module graph reaches a sibling's, and installing `repo` must
77
- * not drag `/computer` in behind it. The wording differs anyway, and the
78
- * difference is the point — that plugin cannot know the lost command was git,
79
- * and this one does. What a model needs after a lost `git push` is not "re-run
80
- * it" but whether re-running it is *safe*.
47
+ * What a model needs after a lost `git push` is not "re-run it" but whether
48
+ * re-running it is *safe*, which only this plugin knows: the lost command was
49
+ * git.
81
50
  */
82
51
  function unreachableNote(err) {
83
- if (err?.code === "EEXEC_LOST") {
84
- return ("the container was replaced while this command was running, so nothing here " +
85
- "is known to have finished. This is infrastructure — not git, and not what " +
86
- "you asked for. The checkout is durable and is exactly as you left it. " +
87
- "Retry, and read what the retry says rather than assuming it starts from " +
88
- "nothing: a command that had already reached GitHub may have taken " +
89
- "effect. Retrying is safe either way — these commands push one branch to " +
90
- "the branch of the same name, and never force.");
91
- }
92
52
  return (`the command could not be run: ${String(err)}. This is the plumbing — the ` +
93
53
  `container, the transport to it, or the host's own configuration — rather ` +
94
54
  `than git or anything you passed, and nothing is known to have completed. ` +
95
- `Retry once; if it happens again, say so in your result rather than working ` +
96
- `around it.`);
55
+ `Retry once, and read what the retry says rather than assuming it starts ` +
56
+ `from nothing: a command that had already reached GitHub may have taken ` +
57
+ `effect. Retrying is safe either way — these commands push one branch to ` +
58
+ `the branch of the same name, and never force. If it happens again, say so ` +
59
+ `in your result rather than working around it.`);
97
60
  }
98
61
  /**
99
62
  * One git command at a time, per plugin instance.
@@ -185,13 +148,6 @@ export function actionTask(ctx) {
185
148
  "the repo plugin's actions run only in turns core started for a task");
186
149
  return task;
187
150
  }
188
- /** A hex SHA-256, so a key names a body without carrying it. */
189
- export async function sha256(text) {
190
- const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
191
- return [...new Uint8Array(digest)]
192
- .map((byte) => byte.toString(16).padStart(2, "0"))
193
- .join("");
194
- }
195
151
  export function repoContext(config,
196
152
  /**
197
153
  * The running sub-agent's `runtime()`, read per command. Forwarded to every
@@ -199,8 +155,6 @@ export function repoContext(config,
199
155
  */
200
156
  runtime = () => undefined) {
201
157
  const workdir = config.workdir ?? DEFAULT_WORKDIR;
202
- const apiBase = config.apiBase ?? DEFAULT_API_BASE;
203
- const graphqlUrl = graphqlEndpoint(apiBase);
204
158
  const author = config.author ?? DEFAULT_AUTHOR;
205
159
  const allowedHosts = config.allowedHosts ?? DEFAULT_ALLOWED_HOSTS;
206
160
  const maxChars = config.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
@@ -412,13 +366,8 @@ runtime = () => undefined) {
412
366
  * its own sentence to add: a POST whose answer never arrived may have been
413
367
  * received, which a GET has no reason to warn about.
414
368
  */
415
- const github = (tool, path, init) => githubAt(tool, `${apiBase}${path}`, init);
416
- /**
417
- * The same call, against a URL rather than a path under {@link apiBase}.
418
- *
419
- * GraphQL needs it: on Enterprise the REST base carries `/api/v3` and GraphQL
420
- * does not live under it — see {@link graphqlEndpoint}.
421
- */
369
+ const github = (tool, path, init) => githubAt(tool, `${API_BASE}${path}`, init);
370
+ /** The same call, against a URL rather than a path under {@link API_BASE}. */
422
371
  const githubAt = async (tool, url, init) => {
423
372
  const secret = credential();
424
373
  if ("failure" in secret) {
@@ -464,13 +413,12 @@ runtime = () => undefined) {
464
413
  * nothing there" and is acted on accordingly. That is the whole reason this
465
414
  * wrapper exists rather than each caller posting to `/graphql` itself.
466
415
  *
467
- * What needs it is what REST cannot express: resolving a review thread has no
468
- * REST equivalent at all, the threads themselves are not on the issue timeline
469
- * `repo_issue_view` reads, and a review *request* naming an app is invisible to
470
- * REST — see {@link file://./tools-review.ts REVIEW_REQUESTS_QUERY}.
416
+ * What still needs it is answering a review thread: replying to one and
417
+ * resolving it have no REST equivalent at all. Reading them does not — that
418
+ * goes through `./mcp.ts` with everything else this plugin reads.
471
419
  */
472
420
  const githubGraphql = async (tool, query, variables) => {
473
- const answered = await githubAt(tool, graphqlUrl, {
421
+ const answered = await githubAt(tool, GRAPHQL_URL, {
474
422
  method: "POST",
475
423
  body: { query, variables }
476
424
  });
@@ -484,7 +432,7 @@ runtime = () => undefined) {
484
432
  logFailure(tool, { stderr: detail });
485
433
  return {
486
434
  ok: false,
487
- message: `${graphqlUrl} refused the query: ${detail.slice(0, 500)}`
435
+ message: `${GRAPHQL_URL} refused the query: ${detail.slice(0, 500)}`
488
436
  };
489
437
  }
490
438
  return { ok: true, data: body.data };
@@ -499,6 +447,13 @@ runtime = () => undefined) {
499
447
  * the reach this plugin does not want to widen; and an agent asking about
500
448
  * "issue 42" almost always means the repository it is working in, so the extra
501
449
  * parameter would mostly be an extra way to be wrong.
450
+ *
451
+ * **The host allowlist is not narrow enough on its own.** It bounds cloning,
452
+ * which is generic git and works on any host; every API call here goes to
453
+ * github.com — see {@link GITHUB_HOST}. A widened `allowedHosts` therefore
454
+ * admits an origin whose `owner/repo` names a *different* repository on
455
+ * github.com, and resolving it would read that one and let a write reach it.
456
+ * So the host is checked here rather than discarded.
502
457
  */
503
458
  const githubRepo = async (dir) => {
504
459
  const { remote, unreachable } = await origin(dir);
@@ -508,7 +463,13 @@ runtime = () => undefined) {
508
463
  return {
509
464
  refusal: `${dir} has no origin on an allowed host — clone it with repo_clone first`
510
465
  };
511
- const parsed = parseRepo(remote.url, allowedHosts);
466
+ if (remote.host !== GITHUB_HOST)
467
+ return {
468
+ refusal: `${dir}'s origin is on ${remote.host}, and these tools read and write ` +
469
+ `${GITHUB_HOST} alone — an owner/repo from another host would name a ` +
470
+ `different repository there`
471
+ };
472
+ const parsed = parseRepo(remote.url, [GITHUB_HOST]);
512
473
  if (!parsed)
513
474
  return {
514
475
  refusal: `could not parse an owner/repo out of ${dir}'s origin`
@@ -575,6 +536,10 @@ runtime = () => undefined) {
575
536
  fetchOrigin,
576
537
  pushBranch,
577
538
  origin,
539
+ // Keyed on the config, so one plugin instance keeps one MCP connection
540
+ // however many times its tools are rebuilt — the reason the git queue
541
+ // above is keyed the same way.
542
+ mcp: githubMcp(config, credential, logFailure),
578
543
  github,
579
544
  githubGraphql,
580
545
  githubRepo
@@ -7,13 +7,29 @@ import type { AgentPlugin } from "@dynamicagents/core";
7
7
  * though it is implemented in `url.ts`.
8
8
  */
9
9
  export { parseRepo } from "./url.js";
10
- export { graphqlEndpoint, truncateOutput } from "./context.js";
11
10
  /**
12
- * `@dynamicagents/plugins/repo` — clone, commit, push, open a pull request.
11
+ * The hosted MCP server this plugin reads GitHub through, and the allowlist it
12
+ * is held to. Exported so a deployment can state in its own docs what the
13
+ * token is able to reach, and so a spec can assert the list has not grown.
14
+ *
15
+ * Named for the provider rather than for the protocol: the protocol's own
16
+ * surface is `@dynamicagents/core/mcp`'s, and a bare `MCP_TOOLS` here would not
17
+ * say whose policy it is.
18
+ */
19
+ export { GITHUB_MCP_URL, GITHUB_MCP_TOOLS } from "./mcp.js";
20
+ /**
21
+ * Over a `/sandbox` object: the sandbox's egress rule for GitHub, and the
22
+ * `RepoGit` that runs the container's own git behind it.
23
+ */
24
+ export { GITHUB_TOKEN_PLACEHOLDER, githubSandboxEgress, type GitHubSandboxEgressOptions, type GitPushPolicy } from "./sandbox-egress.js";
25
+ export { sandboxRepoGit, type SandboxGitHost } from "./sandbox-git.js";
26
+ /**
27
+ * `@dynamicagents/plugins/repo-github` — clone, commit, push, open a pull
28
+ * request, on GitHub.
13
29
  *
14
30
  * Layered over a container rather than owning one: it needs a shell with `git`
15
- * on it, and `@dynamicagents/plugins/workspace` provides exactly that through
16
- * `workspaceExec`. Passing `exec` in rather than importing that plugin keeps the
31
+ * on it, and `@dynamicagents/plugins/sandbox` provides exactly that through
32
+ * `sandboxExec`. Passing `exec` in rather than importing that plugin keeps the
17
33
  * two independent — a host with its own container can use this against that
18
34
  * instead, and the tests here need no container at all.
19
35
  *
@@ -102,10 +118,10 @@ export type RepoExec = (command: string, options?: {
102
118
  * `commit` and `checkout` are local, need nothing to authenticate with, and stay
103
119
  * on `exec` where they are cheap and where the model can see them work.
104
120
  *
105
- * A host implements this against whatever git it has on its own side of the
106
- * boundary. The coder in `starter` runs isomorphic-git inside the
107
- * Durable Object that owns the workspace filesystem, so a push reads the token
108
- * from that object's environment and the container never holds one.
121
+ * A host implements this against whatever git it has. Over `/sandbox`,
122
+ * `sandboxRepoGit` runs the container's own git with a placeholder, and the
123
+ * egress swaps the token in on the Worker side, so the container never holds
124
+ * one.
109
125
  *
110
126
  * ## Failures arrive as values
111
127
  *
@@ -186,8 +202,6 @@ export interface RepoConfig {
186
202
  token: () => string;
187
203
  /** Where clones land. Defaults to `/workspace`. */
188
204
  workdir?: string;
189
- /** API origin, for GitHub Enterprise. Defaults to the public API. */
190
- apiBase?: string;
191
205
  /** Committer identity. Defaults to a generic agent identity. */
192
206
  author?: {
193
207
  name: string;
@@ -197,8 +211,11 @@ export interface RepoConfig {
197
211
  * Hosts a clone may target, lowercase and exact. Defaults to `github.com`.
198
212
  *
199
213
  * A fail-closed allowlist rather than a blocklist, because the failure it
200
- * prevents is credential exfiltration and the input is model-chosen. Widen it
201
- * for GitHub Enterprise (alongside `apiBase`); an empty array clones nothing.
214
+ * prevents is credential exfiltration and the input is model-chosen. An empty
215
+ * array clones nothing.
216
+ *
217
+ * This bounds *cloning*, which is generic git and works against any host. The
218
+ * GitHub tools are github.com's alone — see `./mcp.ts`.
202
219
  */
203
220
  allowedHosts?: string[];
204
221
  /**
@@ -287,7 +304,7 @@ export interface RepoConfig {
287
304
  tools?: readonly RepoToolName[];
288
305
  }
289
306
  /** Every tool and action this plugin can offer, by the name the model sees. */
290
- export declare const REPO_TOOL_NAMES: readonly ["repo_clone", "repo_fetch", "repo_status", "repo_diff", "repo_commit", "repo_push", "repo_open_pr", "repo_issue_view", "repo_pr_view", "repo_pr_comment", "repo_pr_review_status", "repo_pr_threads", "repo_pr_thread_reply", "repo_pr_checks", "repo_worktrees", "repo_worktree"];
307
+ export declare const REPO_TOOL_NAMES: readonly ["repo_clone", "repo_fetch", "repo_status", "repo_diff", "repo_commit", "repo_push", "repo_open_pr", "repo_issue_view", "repo_pr_view", "repo_pr_comment", "repo_pr_threads", "repo_pr_thread_reply", "repo_pr_checks", "repo_worktrees", "repo_worktree"];
291
308
  export type RepoToolName = (typeof REPO_TOOL_NAMES)[number];
292
309
  /**
293
310
  * Checkouts a host keeps apart from the main one — each holding a branch its
@@ -14,7 +14,22 @@ import { worktreesTools } from "./tools-worktrees.js";
14
14
  * though it is implemented in `url.ts`.
15
15
  */
16
16
  export { parseRepo } from "./url.js";
17
- export { graphqlEndpoint, truncateOutput } from "./context.js";
17
+ /**
18
+ * The hosted MCP server this plugin reads GitHub through, and the allowlist it
19
+ * is held to. Exported so a deployment can state in its own docs what the
20
+ * token is able to reach, and so a spec can assert the list has not grown.
21
+ *
22
+ * Named for the provider rather than for the protocol: the protocol's own
23
+ * surface is `@dynamicagents/core/mcp`'s, and a bare `MCP_TOOLS` here would not
24
+ * say whose policy it is.
25
+ */
26
+ export { GITHUB_MCP_URL, GITHUB_MCP_TOOLS } from "./mcp.js";
27
+ /**
28
+ * Over a `/sandbox` object: the sandbox's egress rule for GitHub, and the
29
+ * `RepoGit` that runs the container's own git behind it.
30
+ */
31
+ export { GITHUB_TOKEN_PLACEHOLDER, githubSandboxEgress } from "./sandbox-egress.js";
32
+ export { sandboxRepoGit } from "./sandbox-git.js";
18
33
  /** Every tool and action this plugin can offer, by the name the model sees. */
19
34
  export const REPO_TOOL_NAMES = [
20
35
  "repo_clone",
@@ -27,7 +42,6 @@ export const REPO_TOOL_NAMES = [
27
42
  "repo_issue_view",
28
43
  "repo_pr_view",
29
44
  "repo_pr_comment",
30
- "repo_pr_review_status",
31
45
  "repo_pr_threads",
32
46
  "repo_pr_thread_reply",
33
47
  "repo_pr_checks",
@@ -46,7 +60,7 @@ export function repo(config) {
46
60
  has("repo_issue_view") &&
47
61
  "`repo_issue_view` reads an issue or pull request with its comments",
48
62
  has("repo_pr_view") &&
49
- "`repo_pr_view` shows a pull request's state and the files it touches",
63
+ "`repo_pr_view` shows a pull request's state, the reviews that have landed on it and the files it touches",
50
64
  has("repo_pr_comment") && "`repo_pr_comment` leaves a comment"
51
65
  ].filter((line) => typeof line === "string");
52
66
  const worktrees = config.worktrees && [
@@ -82,8 +96,9 @@ export function repo(config) {
82
96
  "`repo_open_pr` opens the pull request and returns its URL.",
83
97
  views.length > 0 &&
84
98
  `${views.join(", ")}. ${views.length > 1 ? "Each acts" : "It acts"} on the repository you have checked out — read the issue a task refers to before guessing what it asks for.`,
85
- has("repo_pr_review_status") &&
86
- "`repo_pr_review_status` says whether a reviewer has finished. Ask it before reading a review: one that has not landed has left nothing, so an empty list of threads means nothing yet rather than nothing to do.",
99
+ has("repo_pr_threads") &&
100
+ has("repo_pr_view") &&
101
+ "Read `repo_pr_view` before reading a review: a review that has not landed has left nothing, so an empty list of threads means nothing yet rather than nothing to do.",
87
102
  has("repo_pr_threads") &&
88
103
  "`repo_pr_threads` reads the review threads — the comments left on particular lines, which are not on the " +
89
104
  (has("repo_issue_view")
@@ -110,7 +125,11 @@ export function repo(config) {
110
125
  ...(rules.length > 0 ? [rules.join(" ")] : [])
111
126
  ].join("\n");
112
127
  return definePlugin({
113
- name: "repo",
128
+ // The installed capability's identity, and so the label core namespaces
129
+ // this plugin's context block under. The provider is in it for the same
130
+ // reason it is in the subpath: an agent installs one forge plugin, and
131
+ // which one is a thing the model may as well be able to read.
132
+ name: "repo-github",
114
133
  // `runtime` goes through to `exec` untouched and is read per command, so a
115
134
  // sub-agent's git commands run in the same container its parent cloned
116
135
  // into.
@@ -0,0 +1,95 @@
1
+ /**
2
+ * GitHub's hosted MCP server, called from the Worker.
3
+ *
4
+ * The reads this plugin makes — a pull request, its files, its reviews, its
5
+ * review threads, its checks, an issue and its comments — are one
6
+ * vendor-maintained surface. What stays ours is everything above the transport:
7
+ * the tool names, their schemas, the repository binding and the renders. The
8
+ * model never sees an MCP tool; see `./index.ts` for why the tools are wrapped
9
+ * rather than surfaced.
10
+ *
11
+ * The transport itself is not here and not ours. `@dynamicagents/core/mcp`
12
+ * holds it, over the official `@modelcontextprotocol/client`, and its module
13
+ * comment is where the protocol decisions live — why no `authProvider` is
14
+ * configured, which headers a caller may not set, how a connection is kept per
15
+ * plugin instance. What this file is, is the policy that is GitHub's.
16
+ *
17
+ * ## The credential, and what bounds it
18
+ *
19
+ * The token is the host's, read through {@link RepoConfig.token} at the moment
20
+ * of use and sent as `Authorization: Bearer` from here — the Worker. It reaches
21
+ * no container: this host is deliberately absent from the egress gateway's
22
+ * `GITHUB_HOSTS`, so nothing inside a session's container can offer the
23
+ * placeholder to it and be given the real one.
24
+ *
25
+ * Two caps ride on every request, and they are policy rather than tuning:
26
+ *
27
+ * 1. {@link GITHUB_MCP_TOOLS} goes out as `X-MCP-Tools`, an **enable** list.
28
+ * The server offers nothing else, so a bug here — or an argument a prompt
29
+ * injection talked the model into — cannot reach a tool that writes.
30
+ * 2. {@link GITHUB_MCP_URL} is the server's read-only variant, which refuses a
31
+ * write whatever the list says.
32
+ *
33
+ * Both, not either. The list is the narrow bound and the path is the one that
34
+ * still holds if the list is ever edited carelessly. Neither is redundant with
35
+ * the wrapping: wrapping keeps GitHub's schemas out of the model's context, and
36
+ * these two bound what this Worker can do with the token at all.
37
+ *
38
+ * GitHub Enterprise Server is not supported, which is why the URL is a constant
39
+ * rather than something a host configures.
40
+ */
41
+ import { type McpAnswer } from "@dynamicagents/core/mcp";
42
+ import type { RepoConfig } from "./index.js";
43
+ /**
44
+ * The server, read-only. A single toolset or a modifier is a path segment on
45
+ * this server — see the `X-MCP-Tools` note above for why the list does the
46
+ * narrow work and this does the coarse.
47
+ */
48
+ export declare const GITHUB_MCP_URL = "https://api.githubcopilot.com/mcp/readonly";
49
+ /**
50
+ * Every tool this plugin may call, as `X-MCP-Tools`.
51
+ *
52
+ * Deliberately not configurable. A host that could widen this could hand the
53
+ * deployment's token to a tool nobody reviewed, and the point of an allowlist
54
+ * is that adding to it is a code change somebody reads.
55
+ *
56
+ * Both are read tools. `pull_request_read` is one name over nine methods —
57
+ * `get`, `get_files`, `get_reviews`, `get_review_comments`, `get_check_runs`,
58
+ * `get_status` are the ones used here — and `issue_read` covers an issue and
59
+ * its comments.
60
+ *
61
+ * **`pull_request_review_write` is the one deliberate absence worth naming.**
62
+ * It is the only tool that resolves a review thread, and the same name submits
63
+ * a review — `APPROVE` included — so taking thread resolution would mean taking
64
+ * self-approval. Nothing needs it: a revising Claude Code session answers and
65
+ * resolves threads itself, with `gh` inside its container. Merging, commenting
66
+ * and replying are absent for the same reason.
67
+ */
68
+ export declare const GITHUB_MCP_TOOLS: readonly ["pull_request_read", "issue_read"];
69
+ /**
70
+ * One call to GitHub's MCP server, as the rest of this plugin asks for it.
71
+ *
72
+ * `tool` is this plugin's own tool — `repo_pr_view` — and is what the failure
73
+ * log names, because that is the name an operator reading the log has in front
74
+ * of them. `name` is the MCP tool, which must be one of
75
+ * {@link GITHUB_MCP_TOOLS}.
76
+ */
77
+ export type GithubMcpCall = (tool: string, name: string, args: Record<string, unknown>) => Promise<McpAnswer>;
78
+ /**
79
+ * The caller for one plugin instance.
80
+ *
81
+ * `config` is the key a connection is kept against, the way the git queue
82
+ * beside it is keyed: the tools are rebuilt every turn, so anything held in
83
+ * that closure would renegotiate on each one.
84
+ *
85
+ * No turn signal is threaded through. Core's client takes one, and a caller
86
+ * that has one should pass it; these tools do not hold the turn's signal today,
87
+ * and the call is bounded by core's own deadline either way.
88
+ */
89
+ export declare function githubMcp(config: RepoConfig, credential: () => {
90
+ token: string;
91
+ } | {
92
+ failure: string;
93
+ }, logFailure: (tool: string, detail: {
94
+ stderr?: string;
95
+ }) => void): GithubMcpCall;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * GitHub's hosted MCP server, called from the Worker.
3
+ *
4
+ * The reads this plugin makes — a pull request, its files, its reviews, its
5
+ * review threads, its checks, an issue and its comments — are one
6
+ * vendor-maintained surface. What stays ours is everything above the transport:
7
+ * the tool names, their schemas, the repository binding and the renders. The
8
+ * model never sees an MCP tool; see `./index.ts` for why the tools are wrapped
9
+ * rather than surfaced.
10
+ *
11
+ * The transport itself is not here and not ours. `@dynamicagents/core/mcp`
12
+ * holds it, over the official `@modelcontextprotocol/client`, and its module
13
+ * comment is where the protocol decisions live — why no `authProvider` is
14
+ * configured, which headers a caller may not set, how a connection is kept per
15
+ * plugin instance. What this file is, is the policy that is GitHub's.
16
+ *
17
+ * ## The credential, and what bounds it
18
+ *
19
+ * The token is the host's, read through {@link RepoConfig.token} at the moment
20
+ * of use and sent as `Authorization: Bearer` from here — the Worker. It reaches
21
+ * no container: this host is deliberately absent from the egress gateway's
22
+ * `GITHUB_HOSTS`, so nothing inside a session's container can offer the
23
+ * placeholder to it and be given the real one.
24
+ *
25
+ * Two caps ride on every request, and they are policy rather than tuning:
26
+ *
27
+ * 1. {@link GITHUB_MCP_TOOLS} goes out as `X-MCP-Tools`, an **enable** list.
28
+ * The server offers nothing else, so a bug here — or an argument a prompt
29
+ * injection talked the model into — cannot reach a tool that writes.
30
+ * 2. {@link GITHUB_MCP_URL} is the server's read-only variant, which refuses a
31
+ * write whatever the list says.
32
+ *
33
+ * Both, not either. The list is the narrow bound and the path is the one that
34
+ * still holds if the list is ever edited carelessly. Neither is redundant with
35
+ * the wrapping: wrapping keeps GitHub's schemas out of the model's context, and
36
+ * these two bound what this Worker can do with the token at all.
37
+ *
38
+ * GitHub Enterprise Server is not supported, which is why the URL is a constant
39
+ * rather than something a host configures.
40
+ */
41
+ import { mcpClient } from "@dynamicagents/core/mcp";
42
+ /**
43
+ * The server, read-only. A single toolset or a modifier is a path segment on
44
+ * this server — see the `X-MCP-Tools` note above for why the list does the
45
+ * narrow work and this does the coarse.
46
+ */
47
+ export const GITHUB_MCP_URL = "https://api.githubcopilot.com/mcp/readonly";
48
+ /**
49
+ * Every tool this plugin may call, as `X-MCP-Tools`.
50
+ *
51
+ * Deliberately not configurable. A host that could widen this could hand the
52
+ * deployment's token to a tool nobody reviewed, and the point of an allowlist
53
+ * is that adding to it is a code change somebody reads.
54
+ *
55
+ * Both are read tools. `pull_request_read` is one name over nine methods —
56
+ * `get`, `get_files`, `get_reviews`, `get_review_comments`, `get_check_runs`,
57
+ * `get_status` are the ones used here — and `issue_read` covers an issue and
58
+ * its comments.
59
+ *
60
+ * **`pull_request_review_write` is the one deliberate absence worth naming.**
61
+ * It is the only tool that resolves a review thread, and the same name submits
62
+ * a review — `APPROVE` included — so taking thread resolution would mean taking
63
+ * self-approval. Nothing needs it: a revising Claude Code session answers and
64
+ * resolves threads itself, with `gh` inside its container. Merging, commenting
65
+ * and replying are absent for the same reason.
66
+ */
67
+ export const GITHUB_MCP_TOOLS = ["pull_request_read", "issue_read"];
68
+ /**
69
+ * The caller for one plugin instance.
70
+ *
71
+ * `config` is the key a connection is kept against, the way the git queue
72
+ * beside it is keyed: the tools are rebuilt every turn, so anything held in
73
+ * that closure would renegotiate on each one.
74
+ *
75
+ * No turn signal is threaded through. Core's client takes one, and a caller
76
+ * that has one should pass it; these tools do not hold the turn's signal today,
77
+ * and the call is bounded by core's own deadline either way.
78
+ */
79
+ export function githubMcp(config, credential, logFailure) {
80
+ const call = mcpClient({
81
+ url: GITHUB_MCP_URL,
82
+ keyedOn: config,
83
+ // Thrown rather than returned, because core keeps an `Error`'s own message
84
+ // verbatim: the sentence a host's unreadable secret deserves is written
85
+ // once, in `repoContext`, and reaches the model unframed.
86
+ token: () => {
87
+ const secret = credential();
88
+ if ("failure" in secret)
89
+ throw new Error(secret.failure);
90
+ return secret.token;
91
+ },
92
+ headers: () => ({
93
+ "x-mcp-tools": GITHUB_MCP_TOOLS.join(","),
94
+ "user-agent": "da-coder"
95
+ }),
96
+ clientInfo: { name: "dynamicagents-repo-github", version: "1" }
97
+ });
98
+ return async (tool, name, args) => {
99
+ const answered = await call(name, args);
100
+ // Said out loud as well as returned. A failure that only reaches the model
101
+ // leaves nothing behind for an operator, and an unreadable credential is a
102
+ // likely reason to be on this path at all.
103
+ if (!answered.ok)
104
+ logFailure(tool, { stderr: answered.message });
105
+ return answered;
106
+ };
107
+ }
@@ -0,0 +1,68 @@
1
+ import { type SandboxEgressRule } from "../sandbox/egress.js";
2
+ /**
3
+ * GitHub from inside a sandbox, with the token on the Worker side: the
4
+ * sandbox's egress rule for it.
5
+ *
6
+ * The harness intercepts `github.com` over HTTPS, and git's credential helper
7
+ * answers with {@link GITHUB_TOKEN_PLACEHOLDER}. **Never an `insteadOf` onto a
8
+ * placeholder host:** git reports the rewritten URL to whatever reads a remote
9
+ * (`git remote get-url`, `git remote -v`), so `/repo` would take a checkout's
10
+ * origin for a host it does not allow, and `gh` would find no GitHub remote. A
11
+ * sandbox allowed the API intercepts `api.github.com` too, and gives `gh` the
12
+ * placeholder as `GH_TOKEN`.
13
+ *
14
+ * The egress swaps the placeholder for the real token, so a process can *use*
15
+ * GitHub as the deployment's account and never *read* the token. A request
16
+ * that presents no placeholder goes out anonymous, as it would anywhere else.
17
+ *
18
+ * Any process in the container can present the placeholder, a model's own
19
+ * shell included, so what `repo_push` refuses is refused here too, for every
20
+ * credentialed push: a ref that is not a branch, a protected name, and the
21
+ * repository's own default branch, which work reaches through a pull request.
22
+ */
23
+ /** What `GH_TOKEN` and git's credential helper hold. Shaped like a classic token, so nothing refuses it on shape. */
24
+ export declare const GITHUB_TOKEN_PLACEHOLDER: string;
25
+ /**
26
+ * Where a credentialed git request may go, beyond what every push is held to.
27
+ *
28
+ * Checked on git's own requests only: anonymous traffic to GitHub is whatever
29
+ * GitHub allows anyone. `repos` are `owner/repo`; `branches` are branch names,
30
+ * a trailing `*` matching any suffix. A deletion is an update like any other,
31
+ * so it is held to the same names.
32
+ */
33
+ export interface GitPushPolicy {
34
+ readonly repos: readonly string[];
35
+ readonly branches: readonly string[];
36
+ }
37
+ export interface GitHubSandboxEgressOptions {
38
+ /** The `env` binding holding the token, read at the moment it is used and never carried. */
39
+ tokenBinding: string;
40
+ /** Absent: any repository and any work branch the token reaches. */
41
+ push?: GitPushPolicy;
42
+ /** Intercept `api.github.com` as well, so `gh` works as the deployment's account. */
43
+ api?: boolean;
44
+ }
45
+ /** The egress rule of a sandbox whose processes use GitHub. */
46
+ export declare function githubSandboxEgress(options: GitHubSandboxEgressOptions): SandboxEgressRule;
47
+ /** `owner/repo` of a smart-HTTP git path, and which service it asks for. */
48
+ export declare function gitTarget(pathname: string): {
49
+ repo: string;
50
+ service: "upload-pack" | "receive-pack" | undefined;
51
+ } | undefined;
52
+ /** Whether `branch` is one the policy allows. */
53
+ export declare function branchAllowed(policy: GitPushPolicy, branch: string): boolean;
54
+ /**
55
+ * The refs a `git-receive-pack` request updates, from its pkt-line commands.
56
+ *
57
+ * Each command is `<old> <new> <ref>`, the first carrying `\0<capabilities>`,
58
+ * and the list ends at a flush packet; the pack follows. Only the list is read,
59
+ * so a caller hands this one branch of a `tee()` and forwards the other: the
60
+ * pack is never held in the Worker. git compresses the request body when it is
61
+ * large, so a gzip body is inflated as it is read.
62
+ *
63
+ * Read as strictly as git reads it, since the refs this returns are the ones a
64
+ * policy allows: a length is four hex digits, `0000` ends the list, and any
65
+ * other packet shorter than its own length or past the ceiling is refused
66
+ * rather than skipped. A ref is UTF-8, as git writes it.
67
+ */
68
+ export declare function receivePackRefs(body: ReadableStream<Uint8Array>, encoding: string | null): Promise<string[]>;