@agentconnect.md/daemon 1.0.0-rc.201 → 1.0.0-rc.202

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import { randomBytes, randomUUID } from "node:crypto";
15
15
  import { Readable, Writable } from "node:stream";
16
16
  import { lstat, open, readdir, realpath, stat as stat$1 } from "node:fs/promises";
17
17
  import { DatabaseSync } from "node:sqlite";
18
- import net from "node:net";
18
+ import net, { createServer } from "node:net";
19
19
  //#region \0rolldown/runtime.js
20
20
  var __create = Object.create;
21
21
  var __defProp$1 = Object.defineProperty;
@@ -7709,6 +7709,7 @@ const AgentSchema = object({
7709
7709
  path: string(),
7710
7710
  gitRepo: string().optional(),
7711
7711
  gitBranch: string().default("main"),
7712
+ gitCredential: _enum(["github-app"]).optional(),
7712
7713
  pullOnNewSession: boolean().default(true),
7713
7714
  skills: array(string()).default([])
7714
7715
  }),
@@ -13047,7 +13048,8 @@ const AgentWorkspace = discriminatedUnion("mode", [object({ mode: literal("scrat
13047
13048
  mode: literal("github"),
13048
13049
  gitRepo: string(),
13049
13050
  branch: string().default("main"),
13050
- agentDir: string().optional()
13051
+ agentDir: string().optional(),
13052
+ gitCredential: _enum(["github-app"]).optional()
13051
13053
  })]);
13052
13054
  /**
13053
13055
  * The editable agent definition the CP owns and the daemon needs to run it:
@@ -13226,6 +13228,45 @@ const RegisterOk = object({
13226
13228
  })
13227
13229
  });
13228
13230
  //#endregion
13231
+ //#region ../protocol/dist/frames/gitcred.js
13232
+ /**
13233
+ * Git credentials (github-app workspaces) — daemon-pulled, CP-minted.
13234
+ *
13235
+ * The CP holds the GitHub App private key and mints short-lived (1h,
13236
+ * non-renewable) installation access tokens scoped to a single repository;
13237
+ * the daemon pulls one on demand right before a remote git operation and
13238
+ * holds it in memory only. Unlike `secrets/*` (lease + reference semantics,
13239
+ * still unwired), the grant here carries the TOKEN MATERIAL itself — same
13240
+ * plaintext-over-TLS-WS posture as `integration/upsert`, and the same
13241
+ * discipline: **never log the payload**.
13242
+ *
13243
+ * The daemon may only name an agentId — the CP resolves agent → workspace →
13244
+ * repo → installation itself, so a daemon can never pick a repo it wasn't
13245
+ * assigned. Failures come back as correlated `error` REPs: `SCOPE_DENIED`
13246
+ * (not a github-app workspace, or agent not placed on this daemon — stop
13247
+ * asking), `LEASE_DENIED` (installation uninstalled/suspended or the repo
13248
+ * left its grant set — recoverable only by an operator), `RATE_LIMITED`,
13249
+ * `INTERNAL`.
13250
+ */
13251
+ const GitCredRequest = object({
13252
+ agentId: string().uuid(),
13253
+ reason: _enum([
13254
+ "clone",
13255
+ "fetch",
13256
+ "pull",
13257
+ "push",
13258
+ "helper"
13259
+ ]).optional()
13260
+ });
13261
+ const GitCredGrant = object({
13262
+ username: literal("x-access-token"),
13263
+ token: string(),
13264
+ ttlSec: number().int(),
13265
+ expiresAt: string().datetime(),
13266
+ repoFullName: string(),
13267
+ access: _enum(["read", "write"])
13268
+ });
13269
+ //#endregion
13229
13270
  //#region ../protocol/dist/frames/session.js
13230
13271
  /**
13231
13272
  * Session read-back (C→D REQ → REP) — the console's on-demand pulls.
@@ -13750,6 +13791,8 @@ const FRAME_SCHEMAS = {
13750
13791
  "integration/upsert": IntegrationUpsert,
13751
13792
  "integration/remove": IntegrationRemove,
13752
13793
  "integration/channels": IntegrationChannels,
13794
+ "gitcred/request": GitCredRequest,
13795
+ "gitcred/grant": GitCredGrant,
13753
13796
  "secrets/request": SecretsRequest,
13754
13797
  "secrets/grant": SecretsGrant,
13755
13798
  "secrets/renew": SecretsRenew,
@@ -13832,6 +13875,8 @@ discriminatedUnion("type", [
13832
13875
  frame("integration/upsert", FRAME_SCHEMAS["integration/upsert"]),
13833
13876
  frame("integration/remove", FRAME_SCHEMAS["integration/remove"]),
13834
13877
  frame("integration/channels", FRAME_SCHEMAS["integration/channels"]),
13878
+ frame("gitcred/request", FRAME_SCHEMAS["gitcred/request"]),
13879
+ frame("gitcred/grant", FRAME_SCHEMAS["gitcred/grant"]),
13835
13880
  frame("secrets/request", FRAME_SCHEMAS["secrets/request"]),
13836
13881
  frame("secrets/grant", FRAME_SCHEMAS["secrets/grant"]),
13837
13882
  frame("secrets/renew", FRAME_SCHEMAS["secrets/renew"]),
@@ -13981,9 +14026,133 @@ function isFrame(type) {
13981
14026
  return (frame) => frame.type === type;
13982
14027
  }
13983
14028
  //#endregion
14029
+ //#region src/workspace/git-injection.ts
14030
+ /**
14031
+ * Git credential injection (docs/designs/github-app-git-credentials.md
14032
+ * §git 操作注入 / §子仓库) — the TWO secret-free channels that point every git
14033
+ * process at `agentconnect git-credential`:
14034
+ *
14035
+ * 1. Repo-local `.git/config` (written once post-clone): covers agent-run git
14036
+ * in the main checkout. Multiple agents can share one checkout — the file
14037
+ * carries the LAST writer's agentId, so channel 2 is authoritative there.
14038
+ * 2. Session-env `GIT_CONFIG_GLOBAL` pointing at a generated per-agent
14039
+ * gitconfig (includes the user's real global config first). Inherited by
14040
+ * the WHOLE agent process tree — submodules and nested clones hit OUR
14041
+ * helper (and get a clean denial) instead of leaking to a machine-global
14042
+ * osxkeychain credential. NOT GIT_CONFIG_COUNT: the indexed env vars don't
14043
+ * merge across processes, so any child that sets its own COUNT would
14044
+ * silently drop the reset and reopen the leak.
14045
+ *
14046
+ * All config values single-quote the shim path (git runs `!`-helpers via
14047
+ * `sh -c` — a space in $HOME must not word-split), reset the helper list first
14048
+ * (empty entry) and set useHttpPath=true (git strips `path` from credential
14049
+ * lookups by default; the reset-list semantics for URL-scoped keys are
14050
+ * implementation behavior verified on git ≥2.32 — see the design doc).
14051
+ *
14052
+ * Everything written here is a POINTER to the daemon — never a secret.
14053
+ */
14054
+ /** Module-level init (workspace-manager is functional; mirrors cloneInFlight). */
14055
+ let shimPath;
14056
+ let runDir;
14057
+ let preWarm;
14058
+ function initGitInjection(opts) {
14059
+ shimPath = opts.shimPath;
14060
+ runDir = opts.runDir;
14061
+ preWarm = opts.preWarm;
14062
+ }
14063
+ function quotedHelper(agentId) {
14064
+ if (!shimPath) throw new Error("git credential injection is not initialized");
14065
+ return `!'${shimPath.replaceAll("'", "'\\''")}' --agent ${agentId}`;
14066
+ }
14067
+ /** The three github.com-scoped config pairs both channels share. */
14068
+ function configPairs(agentId) {
14069
+ return [
14070
+ ["credential.https://github.com.helper", ""],
14071
+ ["credential.https://github.com.helper", quotedHelper(agentId)],
14072
+ ["credential.https://github.com.useHttpPath", "true"]
14073
+ ];
14074
+ }
14075
+ /**
14076
+ * Env for a DAEMON-RUN git process (clone — no repo config exists yet).
14077
+ * Spread over `process.env` by the caller: simple-git's `.env()` REPLACES the
14078
+ * child environment (v3.36 verified), a bare object would strip PATH/HOME.
14079
+ */
14080
+ function cloneGitEnv(agentId) {
14081
+ const pairs = configPairs(agentId);
14082
+ const env = {
14083
+ GIT_TERMINAL_PROMPT: "0",
14084
+ GIT_ASKPASS: "",
14085
+ SSH_ASKPASS: "",
14086
+ GIT_CONFIG_COUNT: String(pairs.length)
14087
+ };
14088
+ pairs.forEach(([k, v], i) => {
14089
+ env[`GIT_CONFIG_KEY_${i}`] = k;
14090
+ env[`GIT_CONFIG_VALUE_${i}`] = v;
14091
+ });
14092
+ return env;
14093
+ }
14094
+ /**
14095
+ * Session-env channel: (re)write the per-agent gitconfig and return the env to
14096
+ * inject into the agent's ACP runtime process. Regenerated on every host spawn
14097
+ * (#251 retries rebuild the host ⇒ this re-runs).
14098
+ */
14099
+ function sessionGitEnv(agentId) {
14100
+ if (!runDir) throw new Error("git credential injection is not initialized");
14101
+ const dir = join(runDir, "gitcred");
14102
+ mkdirSync(dir, {
14103
+ recursive: true,
14104
+ mode: 448
14105
+ });
14106
+ const file = join(dir, `${agentId}.gitconfig`);
14107
+ writeFileSync(file, [
14108
+ "# agentconnect session git config — regenerated on agent start; NO secrets.",
14109
+ "# Keeps the user global config, then pins github.com credentials to the daemon helper.",
14110
+ "[include]",
14111
+ `\tpath = ${join(homedir(), ".gitconfig")}`,
14112
+ "[credential \"https://github.com\"]",
14113
+ " helper = ",
14114
+ `\thelper = ${quotedHelper(agentId)}`,
14115
+ " useHttpPath = true",
14116
+ ""
14117
+ ].join("\n"), { mode: 420 });
14118
+ return {
14119
+ GIT_CONFIG_GLOBAL: file,
14120
+ GIT_TERMINAL_PROMPT: "0"
14121
+ };
14122
+ }
14123
+ /** Post-clone: pin the repo-local helper so agent-run git in the checkout works. */
14124
+ async function writeRepoHelperConfig(cwd, agentId) {
14125
+ const git = simpleGit(cwd);
14126
+ await git.raw([
14127
+ "config",
14128
+ "--replace-all",
14129
+ "credential.https://github.com.helper",
14130
+ ""
14131
+ ]);
14132
+ await git.raw([
14133
+ "config",
14134
+ "--add",
14135
+ "credential.https://github.com.helper",
14136
+ quotedHelper(agentId)
14137
+ ]);
14138
+ await git.raw([
14139
+ "config",
14140
+ "credential.https://github.com.useHttpPath",
14141
+ "true"
14142
+ ]);
14143
+ }
14144
+ /** Pre-warm hook for workspace-manager (no-op until initialized). */
14145
+ async function preWarmGitCred(agentId, reason) {
14146
+ if (!preWarm) return;
14147
+ await preWarm(agentId, reason);
14148
+ }
14149
+ //#endregion
13984
14150
  //#region src/workspace/workspace-manager.ts
13985
14151
  const PULL_TIMEOUT_MS$1 = 4500;
13986
14152
  const cloneInFlight = /* @__PURE__ */ new Map();
14153
+ function usesGithubApp(agent) {
14154
+ return agent.workspace.mode === "git-repo" && agent.workspace.gitCredential === "github-app";
14155
+ }
13987
14156
  async function prepareWorkspace(agent) {
13988
14157
  const cwd = agent.workspace.path;
13989
14158
  mkdirSync(cwd, { recursive: true });
@@ -13996,9 +14165,15 @@ async function prepareWorkspace(agent) {
13996
14165
  await cloneRepo(agent);
13997
14166
  return cwd;
13998
14167
  }
13999
- if (agent.workspace.pullOnNewSession) try {
14000
- await Promise.race([simpleGit(cwd).pull(["--ff-only"]), new Promise((_, rej) => setTimeout(() => rej(/* @__PURE__ */ new Error("pull timeout")), PULL_TIMEOUT_MS$1))]);
14001
- } catch {}
14168
+ if (agent.workspace.pullOnNewSession) {
14169
+ if (usesGithubApp(agent)) await preWarmGitCred(agent.id, "pull").catch(() => void 0);
14170
+ try {
14171
+ await Promise.race([simpleGit(cwd).env({
14172
+ ...process.env,
14173
+ GIT_TERMINAL_PROMPT: "0"
14174
+ }).pull(["--ff-only"]), new Promise((_, rej) => setTimeout(() => rej(/* @__PURE__ */ new Error("pull timeout")), PULL_TIMEOUT_MS$1))]);
14175
+ } catch {}
14176
+ }
14002
14177
  return cwd;
14003
14178
  }
14004
14179
  /**
@@ -14026,12 +14201,30 @@ async function cloneRepo(agent) {
14026
14201
  const gitRepo = agent.workspace.gitRepo && normalizeGitUrl(agent.workspace.gitRepo);
14027
14202
  if (!gitRepo) throw new Error(`workspace clone: agent "${agent.id}" has git-repo mode but no gitRepo configured`);
14028
14203
  const branch = agent.workspace.gitBranch;
14204
+ const githubApp = usesGithubApp(agent);
14029
14205
  const p = (async () => {
14030
- await simpleGit().clone(gitRepo, cwd, [
14031
- "--branch",
14032
- branch,
14033
- "--single-branch"
14034
- ]);
14206
+ if (githubApp) await preWarmGitCred(agent.id, "clone");
14207
+ const git = githubApp ? simpleGit().env({
14208
+ ...process.env,
14209
+ ...cloneGitEnv(agent.id)
14210
+ }) : simpleGit().env({
14211
+ ...process.env,
14212
+ GIT_TERMINAL_PROMPT: "0"
14213
+ });
14214
+ try {
14215
+ await git.clone(gitRepo, cwd, [
14216
+ "--branch",
14217
+ branch,
14218
+ "--single-branch"
14219
+ ]);
14220
+ } catch (e) {
14221
+ rmSync(join(cwd, ".git"), {
14222
+ recursive: true,
14223
+ force: true
14224
+ });
14225
+ throw e;
14226
+ }
14227
+ if (githubApp) await writeRepoHelperConfig(cwd, agent.id);
14035
14228
  })().finally(() => {
14036
14229
  cloneInFlight.delete(cwd);
14037
14230
  });
@@ -25259,6 +25452,228 @@ var McpControlServer = class {
25259
25452
  }
25260
25453
  };
25261
25454
  //#endregion
25455
+ //#region src/cp/git-credential.ts
25456
+ /** Hand out only while more than this remains (nests under the CP's 15min floor). */
25457
+ const HANDOUT_MIN_MS = 600 * 1e3;
25458
+ var GitCredUnavailableError = class extends Error {
25459
+ terminal;
25460
+ constructor(message, terminal) {
25461
+ super(message);
25462
+ this.terminal = terminal;
25463
+ this.name = "GitCredUnavailableError";
25464
+ }
25465
+ };
25466
+ var GitCredentialCache = class {
25467
+ deps;
25468
+ entries = /* @__PURE__ */ new Map();
25469
+ inflight = /* @__PURE__ */ new Map();
25470
+ /** Agents the CP told us to stop asking for (SCOPE_DENIED — moved/mode-off). */
25471
+ denied = /* @__PURE__ */ new Set();
25472
+ monoNow;
25473
+ constructor(deps) {
25474
+ this.deps = deps;
25475
+ this.monoNow = deps.monoNow ?? (() => performance.now());
25476
+ }
25477
+ /** A live credential for the agent's workspace repo — cached, coalesced, or freshly pulled. */
25478
+ async get(agentId, reason) {
25479
+ if (this.denied.has(agentId)) throw new GitCredUnavailableError("control plane denied git credentials for this agent", true);
25480
+ const cached = this.entries.get(agentId);
25481
+ if (cached && cached.expiresAtMono - this.monoNow() > HANDOUT_MIN_MS) return cached;
25482
+ let pending = this.inflight.get(agentId);
25483
+ if (!pending) {
25484
+ pending = this.fetch(agentId, reason).finally(() => this.inflight.delete(agentId));
25485
+ this.inflight.set(agentId, pending);
25486
+ }
25487
+ try {
25488
+ return await pending;
25489
+ } catch (e) {
25490
+ if (cached && cached.expiresAtMono > this.monoNow()) {
25491
+ this.deps.log.warn(`gitcred: refresh failed for agent ${agentId} — serving cached token`);
25492
+ return cached;
25493
+ }
25494
+ throw e;
25495
+ }
25496
+ }
25497
+ /**
25498
+ * Invalidate on rejection. Git calls the helper's `erase` with the failing
25499
+ * credential when the remote refuses it (GitHub revokes instantly on
25500
+ * uninstall/suspend) — matching on the presented password keeps a racing
25501
+ * fresh grant from being wiped by a stale erase.
25502
+ */
25503
+ invalidate(agentId, presentedPassword) {
25504
+ const entry = this.entries.get(agentId);
25505
+ if (!entry) return;
25506
+ if (presentedPassword !== void 0 && entry.token !== presentedPassword) return;
25507
+ this.entries.delete(agentId);
25508
+ }
25509
+ /** agent/remove: drop the orphaned bearer token with the agent dir. */
25510
+ remove(agentId) {
25511
+ this.entries.delete(agentId);
25512
+ this.denied.delete(agentId);
25513
+ }
25514
+ async fetch(agentId, reason) {
25515
+ let grant;
25516
+ try {
25517
+ grant = await this.deps.request({
25518
+ agentId,
25519
+ reason
25520
+ });
25521
+ } catch (e) {
25522
+ if (e.code === "SCOPE_DENIED") {
25523
+ this.denied.add(agentId);
25524
+ this.entries.delete(agentId);
25525
+ throw new GitCredUnavailableError(e.message, true);
25526
+ }
25527
+ throw new GitCredUnavailableError(e.message, false);
25528
+ }
25529
+ const entry = {
25530
+ username: grant.username,
25531
+ token: grant.token,
25532
+ repoFullName: grant.repoFullName,
25533
+ access: grant.access,
25534
+ expiresAtMono: this.monoNow() + grant.ttlSec * 1e3
25535
+ };
25536
+ this.entries.set(agentId, entry);
25537
+ return entry;
25538
+ }
25539
+ /** A replicated spec change may re-enable a previously denied agent. */
25540
+ clearDenied(agentId) {
25541
+ this.denied.delete(agentId);
25542
+ }
25543
+ };
25544
+ //#endregion
25545
+ //#region src/cp/gitcred-server.ts
25546
+ /**
25547
+ * gitcred.sock — the local credential channel for agent-run git
25548
+ * (docs/designs/github-app-git-credentials.md §本地 helper 通道).
25549
+ *
25550
+ * A tiny newline-delimited-JSON server over a unix socket (0700 dir + 0600
25551
+ * socket, stale-socket cleanup — the `mcp/control-server.ts` pattern). The
25552
+ * `agentconnect git-credential` CLI subcommand connects per git invocation:
25553
+ * { op: 'get', agentId } → { ok, username, password } | { ok:false, error }
25554
+ * { op: 'erase', agentId, password? } → { ok: true }
25555
+ *
25556
+ * Trust model (design §信任模型 #1): same host + same OS user = ONE trust
25557
+ * domain. `agentId` here is routing, not authorization — any same-user process
25558
+ * can already read every workspace on this daemon. Tokens transit the socket
25559
+ * and helper stdout only; nothing lands on disk.
25560
+ *
25561
+ * Alongside the socket the daemon (re)writes two SECRET-FREE files per boot:
25562
+ * the shim `run/git-credential-helper.sh` (pins the current node + CLI path so
25563
+ * `.git/config` survives daemon upgrades) and per-agent gitconfig includes for
25564
+ * the session-env channel (see workspace/git-injection.ts).
25565
+ */
25566
+ function gitcredSocketPath(root) {
25567
+ return join(root, "run", "gitcred.sock");
25568
+ }
25569
+ function gitcredShimPath(root) {
25570
+ return join(root, "run", "git-credential-helper.sh");
25571
+ }
25572
+ var GitCredServer = class {
25573
+ cache;
25574
+ path;
25575
+ log;
25576
+ server;
25577
+ constructor(cache, path, log) {
25578
+ this.cache = cache;
25579
+ this.path = path;
25580
+ this.log = log;
25581
+ }
25582
+ start() {
25583
+ const dir = dirname(this.path);
25584
+ mkdirSync(dir, {
25585
+ recursive: true,
25586
+ mode: 448
25587
+ });
25588
+ try {
25589
+ chmodSync(dir, 448);
25590
+ } catch {}
25591
+ rmSync(this.path, { force: true });
25592
+ const server = createServer((sock) => this.serve(sock));
25593
+ server.listen(this.path, () => {
25594
+ try {
25595
+ chmodSync(this.path, 384);
25596
+ } catch {}
25597
+ this.log.info(`gitcred: helper socket at ${this.path}`);
25598
+ });
25599
+ server.on("error", (e) => this.log.warn(`gitcred: socket error: ${e.message}`));
25600
+ this.server = server;
25601
+ }
25602
+ stop() {
25603
+ this.server?.close();
25604
+ rmSync(this.path, { force: true });
25605
+ }
25606
+ serve(sock) {
25607
+ let buf = "";
25608
+ sock.on("data", (chunk) => {
25609
+ buf += chunk.toString("utf8");
25610
+ const nl = buf.indexOf("\n");
25611
+ if (nl === -1) return;
25612
+ const line = buf.slice(0, nl);
25613
+ buf = buf.slice(nl + 1);
25614
+ this.handle(line, sock);
25615
+ });
25616
+ sock.on("error", () => sock.destroy());
25617
+ }
25618
+ async handle(line, sock) {
25619
+ const reply = (msg) => {
25620
+ sock.write(JSON.stringify(msg) + "\n");
25621
+ sock.end();
25622
+ };
25623
+ let req;
25624
+ try {
25625
+ req = JSON.parse(line);
25626
+ } catch {
25627
+ return reply({
25628
+ ok: false,
25629
+ error: "malformed request"
25630
+ });
25631
+ }
25632
+ if (req.op === "erase") {
25633
+ this.cache.invalidate(req.agentId, req.password);
25634
+ return reply({ ok: true });
25635
+ }
25636
+ if (req.op !== "get" || typeof req.agentId !== "string") return reply({
25637
+ ok: false,
25638
+ error: "unsupported op"
25639
+ });
25640
+ try {
25641
+ const cred = await this.cache.get(req.agentId, "helper");
25642
+ return reply({
25643
+ ok: true,
25644
+ username: cred.username,
25645
+ password: cred.token
25646
+ });
25647
+ } catch (e) {
25648
+ return reply({
25649
+ ok: false,
25650
+ error: e instanceof GitCredUnavailableError ? e.message : `git credentials unavailable: ${e.message}`
25651
+ });
25652
+ }
25653
+ }
25654
+ };
25655
+ /**
25656
+ * (Re)write the secret-free shim `.git/config` helper lines exec through. The
25657
+ * absolute node + CLI paths are re-pinned every daemon boot, so repo configs
25658
+ * keep working across upgrades/relocations. Quoted throughout — a home dir
25659
+ * with a space (macOS "/Users/John Smith/…") must not word-split.
25660
+ */
25661
+ function writeGitcredShim(root, cliEntry) {
25662
+ const shim = gitcredShimPath(root);
25663
+ mkdirSync(dirname(shim), {
25664
+ recursive: true,
25665
+ mode: 448
25666
+ });
25667
+ writeFileSync(shim, [
25668
+ "#!/bin/sh",
25669
+ "# agentconnect git credential helper shim — regenerated on daemon start; NO secrets.",
25670
+ `AGENTCONNECT_ROOT='${root.replaceAll("'", "'\\''")}' \\`,
25671
+ ` exec '${process.execPath.replaceAll("'", "'\\''")}' '${cliEntry.replaceAll("'", "'\\''")}' git-credential "$@"`,
25672
+ ""
25673
+ ].join("\n"), { mode: 493 });
25674
+ return shim;
25675
+ }
25676
+ //#endregion
25262
25677
  //#region src/mcp/inject.ts
25263
25678
  /**
25264
25679
  * Build the `mcpServers` entry that makes the agent harness spawn our stdio
@@ -83706,7 +84121,7 @@ var ReqRep = class {
83706
84121
  this.ackTimeoutMs = ackTimeoutMs;
83707
84122
  this.maxTries = maxTries;
83708
84123
  }
83709
- request(frame, write) {
84124
+ request(frame, write, opts = {}) {
83710
84125
  const encoded = JSON.stringify(frame);
83711
84126
  return new Promise((resolve, reject) => {
83712
84127
  const entry = {
@@ -83714,25 +84129,40 @@ var ReqRep = class {
83714
84129
  encoded,
83715
84130
  resolve,
83716
84131
  reject,
83717
- tries: 1
84132
+ tries: 1,
84133
+ maxTries: opts.maxTries ?? this.maxTries
83718
84134
  };
83719
84135
  this.pending.set(frame.id, entry);
83720
- this.arm(entry, write);
83721
- write(encoded);
84136
+ this.arm(entry, write, opts.ackTimeoutMs ?? this.ackTimeoutMs);
84137
+ try {
84138
+ write(encoded);
84139
+ } catch (e) {
84140
+ this.fail(entry, new WireError("INTERNAL", `send failed: ${e.message}`, true));
84141
+ }
83722
84142
  });
83723
84143
  }
83724
- arm(entry, write) {
84144
+ arm(entry, write, ackTimeoutMs) {
83725
84145
  entry.timer = this.clock.setTimeout(() => {
83726
84146
  if (!this.pending.has(entry.id)) return;
83727
- if (entry.tries >= this.maxTries) {
84147
+ if (entry.tries >= entry.maxTries) {
83728
84148
  this.pending.delete(entry.id);
83729
- entry.reject(new WireError("INTERNAL", `no ack after ${this.maxTries} tries for ${entry.id}`, true));
84149
+ entry.reject(new WireError("INTERNAL", `no ack after ${entry.maxTries} tries for ${entry.id}`, true));
83730
84150
  return;
83731
84151
  }
83732
84152
  entry.tries += 1;
83733
- this.arm(entry, write);
83734
- write(entry.encoded);
83735
- }, this.ackTimeoutMs);
84153
+ this.arm(entry, write, ackTimeoutMs);
84154
+ try {
84155
+ write(entry.encoded);
84156
+ } catch (e) {
84157
+ this.fail(entry, new WireError("INTERNAL", `send failed: ${e.message}`, true));
84158
+ }
84159
+ }, ackTimeoutMs);
84160
+ }
84161
+ fail(entry, err) {
84162
+ if (!this.pending.has(entry.id)) return;
84163
+ this.pending.delete(entry.id);
84164
+ if (entry.timer !== void 0) this.clock.clearTimeout(entry.timer);
84165
+ entry.reject(err);
83736
84166
  }
83737
84167
  settle(frame) {
83738
84168
  const corr = frame.corr;
@@ -83939,6 +84369,25 @@ var CpClient = class {
83939
84369
  const frame = buildEnvelope(type, payload);
83940
84370
  return this.correlator.request(frame, (e) => this.transport.send(e));
83941
84371
  }
84372
+ /**
84373
+ * `gitcred/request` (D→C REQ) → grant payload. The FIRST post-handshake
84374
+ * daemon-issued REQ, so unlike the handshake `request()` it is state-GATED:
84375
+ * outside READY/DRAINING it fails immediately (the credential cache degrades
84376
+ * to its unexpired copy or errors) — never queued, never a bare
84377
+ * `transport!.send` on a dead socket. Single send + 10s budget: the default
84378
+ * 5s×5 retransmit would blow every git-side timeout, and CP-side mint
84379
+ * single-flight makes a later fresh request cheap anyway.
84380
+ */
84381
+ async requestGitCred(payload) {
84382
+ if (this.state !== "READY" && this.state !== "DRAINING" || !this.transport) throw new WireError("INTERNAL", `control plane unreachable (client ${this.state})`, true);
84383
+ const frame = buildEnvelope("gitcred/request", payload);
84384
+ const rep = await this.correlator.request(frame, (e) => this.transport.send(e), {
84385
+ maxTries: 1,
84386
+ ackTimeoutMs: 1e4
84387
+ });
84388
+ if (rep.type !== "gitcred/grant") throw new WireError("INTERNAL", `expected gitcred/grant, got ${rep.type}`, false);
84389
+ return rep.payload;
84390
+ }
83942
84391
  async onText(text) {
83943
84392
  const decoded = decodeEnvelope(text);
83944
84393
  if (!decoded.ok) {
@@ -83972,6 +84421,7 @@ var CpClient = class {
83972
84421
  }
83973
84422
  onClose(code, _reason) {
83974
84423
  this.stopHeartbeat();
84424
+ this.transport = void 0;
83975
84425
  this.correlator.rejectAll(new WireError("INTERNAL", "connection closed", true));
83976
84426
  if (code === 4401) {
83977
84427
  this.fatal = true;
@@ -84623,6 +85073,8 @@ function applySpecFields(raw, spec, opts) {
84623
85073
  if (ws.mode === "github") {
84624
85074
  existing.gitRepo = normalizeGitUrl(ws.gitRepo);
84625
85075
  existing.gitBranch = ws.branch;
85076
+ if (ws.gitCredential !== void 0) existing.gitCredential = ws.gitCredential;
85077
+ else delete existing.gitCredential;
84626
85078
  }
84627
85079
  if (opts.creating && existing.path === void 0) existing.path = join(opts.agentDir, "workspace");
84628
85080
  raw.workspace = existing;
@@ -100698,6 +101150,8 @@ var Daemon = class Daemon {
100698
101150
  opts;
100699
101151
  store;
100700
101152
  mcp;
101153
+ gitCreds;
101154
+ gitCredServer;
100701
101155
  agents = /* @__PURE__ */ new Map();
100702
101156
  fileAgents = /* @__PURE__ */ new Map();
100703
101157
  hosts = /* @__PURE__ */ new Map();
@@ -100812,6 +101266,26 @@ var Daemon = class Daemon {
100812
101266
  });
100813
101267
  await this.mcp.start();
100814
101268
  const cliEntry = process.argv[1] ?? "";
101269
+ this.gitCreds = new GitCredentialCache({
101270
+ request: (payload) => {
101271
+ const client = this.cpClient;
101272
+ if (!client) throw Object.assign(/* @__PURE__ */ new Error("control plane is not connected"), { code: "INTERNAL" });
101273
+ return client.requestGitCred(payload);
101274
+ },
101275
+ log: { warn: (m) => this.log.warn(m) }
101276
+ });
101277
+ initGitInjection({
101278
+ shimPath: writeGitcredShim(root, cliEntry),
101279
+ runDir: join(root, "run"),
101280
+ preWarm: async (agentId, reason) => {
101281
+ await this.gitCreds.get(agentId, reason);
101282
+ }
101283
+ });
101284
+ this.gitCredServer = new GitCredServer(this.gitCreds, gitcredSocketPath(root), {
101285
+ info: (m) => this.log.info(m),
101286
+ warn: (m) => this.log.warn(m)
101287
+ });
101288
+ this.gitCredServer.start();
100815
101289
  this.sessions = new SessionManager({
100816
101290
  store: this.store,
100817
101291
  hostFor: (agentId) => this.ensureHostAsync(agentId),
@@ -101092,7 +101566,8 @@ var Daemon = class Daemon {
101092
101566
  onUpdate,
101093
101567
  env: {
101094
101568
  ...agentChildEnv(agent),
101095
- ...cpRuntimeEnv(agent)
101569
+ ...cpRuntimeEnv(agent),
101570
+ ...agent.workspace.mode === "git-repo" && agent.workspace.gitCredential === "github-app" ? sessionGitEnv(agent.id) : {}
101096
101571
  },
101097
101572
  configPrefs: {
101098
101573
  model: agent.runtimeOverrides?.model,
@@ -102193,8 +102668,14 @@ var Daemon = class Daemon {
102193
102668
  if (snap.agents.length) this.log.debug(`cp: converged ${snap.agents.length} agent spec(s)`);
102194
102669
  if (snap.integrations?.length) this.log.debug(`cp: converged ${snap.integrations.length} integration(s)`);
102195
102670
  },
102196
- applyAgentUpsert: (agentId, spec) => this.cpAgents?.upsert(agentId, spec),
102197
- applyAgentRemove: (agentId) => this.cpAgents?.remove(agentId),
102671
+ applyAgentUpsert: (agentId, spec) => {
102672
+ this.gitCreds?.clearDenied(agentId);
102673
+ this.cpAgents?.upsert(agentId, spec);
102674
+ },
102675
+ applyAgentRemove: (agentId) => {
102676
+ this.gitCreds?.remove(agentId);
102677
+ this.cpAgents?.remove(agentId);
102678
+ },
102198
102679
  applyIntegrationUpsert: (spec) => this.cpIntegrations?.upsert(spec),
102199
102680
  applyIntegrationRemove: (integrationId) => this.cpIntegrations?.remove(integrationId),
102200
102681
  upsertCron: (cron) => this.cpCrons.upsert(cron),
@@ -102451,6 +102932,7 @@ var Daemon = class Daemon {
102451
102932
  for (const c of this.connections) await Promise.resolve(c.stop()).catch((e) => errors.push(e));
102452
102933
  for (const h of this.hosts.values()) await Promise.resolve(h.stop()).catch((e) => errors.push(e));
102453
102934
  await Promise.resolve(this.mcp?.stop()).catch((e) => errors.push(e));
102935
+ this.gitCredServer?.stop();
102454
102936
  this.store?.close();
102455
102937
  if (errors.length) throw new AggregateError(errors, "stop: partial failure");
102456
102938
  }
@@ -102720,7 +103202,7 @@ program.command("run").description("Run the daemon in the foreground").action(as
102720
103202
  }
102721
103203
  });
102722
103204
  program.command("mcp-bridge", { hidden: true }).description("internal: stdio MCP bridge to the running daemon").action(async () => {
102723
- const { runBridge } = await import("./bridge-NDvTO1zB.js");
103205
+ const { runBridge } = await import("./bridge-C8-TYXsj.js");
102724
103206
  await runBridge();
102725
103207
  });
102726
103208
  const controller = () => resolveController({ root: program.opts().root });
@@ -102822,10 +103304,14 @@ program.command("login").description("Interactive onboarding: test the Control P
102822
103304
  process.exit(1);
102823
103305
  }
102824
103306
  });
103307
+ program.command("git-credential").description("Git credential helper backed by the local daemon (github-app workspaces)").requiredOption("--agent <id>", "agent whose workspace credentials to serve").argument("<action>", "git credential action: get | store | erase").action(async (action, opts) => {
103308
+ const { runGitCredential } = await import("./git-credential-DWH0EUV6.js");
103309
+ await runGitCredential(action, opts.agent);
103310
+ });
102825
103311
  program.command("agent").description("Inspect local agent directories").command("list").description("List agents discovered under --agents-dir (id, status, runtime, name, dir)").action(async () => {
102826
103312
  const opts = program.opts();
102827
103313
  try {
102828
- const { runAgentList } = await import("./agent-list-CZbaXHPF.js");
103314
+ const { runAgentList } = await import("./agent-list-D-RwgYN7.js");
102829
103315
  await runAgentList({
102830
103316
  agentsDir: opts.agentsDir,
102831
103317
  configPath: opts.config,
@@ -102854,6 +103340,6 @@ program.command("chat [message]").description("Discover an agent under --agents-
102854
103340
  });
102855
103341
  program.parse();
102856
103342
  //#endregion
102857
- export { datetime as C, __toESM as E, unknown as S, __commonJSMin as T, optional as _, loadConfig as a, string as b, array as c, discriminatedUnion as d, intersection as f, object as g, number as h, discoverAgents as i, boolean as l, looseObject as m, encodeFrame as n, _enum as o, literal as p, DAEMON_VERSION as r, _null as s, decodeFrames as t, custom as u, preprocess as v, safeParse$1 as w, union as x, record as y };
103343
+ export { union as C, __commonJSMin as D, safeParse$1 as E, __toESM as O, string as S, datetime as T, number as _, discoverAgents as a, preprocess as b, _enum as c, boolean as d, custom as f, looseObject as g, literal as h, DAEMON_VERSION as i, _null as l, intersection as m, decodeFrames as n, loadConfig as o, discriminatedUnion as p, encodeFrame as r, resolveRoot as s, gitcredSocketPath as t, array as u, object as v, unknown as w, record as x, optional as y };
102858
103344
 
102859
103345
  //# sourceMappingURL=index.js.map