@agentconnect.md/daemon 1.0.0-rc.297 → 1.0.0-rc.299
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/{acp-host-03mQVw2Q.js → acp-host-DezWSmEG.js} +2 -2
- package/dist/{acp-host-03mQVw2Q.js.map → acp-host-DezWSmEG.js.map} +1 -1
- package/dist/{agent-list-DHFLd9ux.js → agent-list-Da9Osn4O.js} +2 -2
- package/dist/{agent-list-DHFLd9ux.js.map → agent-list-Da9Osn4O.js.map} +1 -1
- package/dist/{chat-IRJd4_lj.js → chat-D8QIIHor.js} +3 -3
- package/dist/{chat-IRJd4_lj.js.map → chat-D8QIIHor.js.map} +1 -1
- package/dist/{load-agents-8AzZXdVx.js → dist-Cq1Kimg_.js} +18 -399
- package/dist/dist-Cq1Kimg_.js.map +1 -0
- package/dist/gh-token-BfPlYQav.js +97 -0
- package/dist/gh-token-BfPlYQav.js.map +1 -0
- package/dist/{git-credential-DLRF9bOx.js → git-credential-DhWn1IBJ.js} +26 -8
- package/dist/git-credential-DhWn1IBJ.js.map +1 -0
- package/dist/{gitcred-server-Dhp-tyEI.js → gitcred-server-KXxLyYxc.js} +107 -55
- package/dist/gitcred-server-KXxLyYxc.js.map +1 -0
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/load-agents-gTqVkOwl.js +401 -0
- package/dist/load-agents-gTqVkOwl.js.map +1 -0
- package/dist/{login-xtaBgstv.js → login-BP4bFP-d.js} +4 -3
- package/dist/{login-xtaBgstv.js.map → login-BP4bFP-d.js.map} +1 -1
- package/dist/{run-foreground-BHU49ZSi.js → run-foreground-B8sw7WKS.js} +216 -143
- package/dist/{run-foreground-BHU49ZSi.js.map → run-foreground-B8sw7WKS.js.map} +1 -1
- package/dist/run-foreground-jOExu3YR.js +2 -0
- package/package.json +1 -1
- package/dist/git-credential-DLRF9bOx.js.map +0 -1
- package/dist/gitcred-server-Dhp-tyEI.js.map +0 -1
- package/dist/load-agents-8AzZXdVx.js.map +0 -1
- package/dist/run-foreground-BiYUl931.js +0 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { l as resolveRoot } from "./paths-DEcUO8lC.js";
|
|
2
|
-
import { n as gitcredSocketPath } from "./gitcred-server-
|
|
2
|
+
import { n as gitcredSocketPath } from "./gitcred-server-KXxLyYxc.js";
|
|
3
3
|
import { createConnection } from "node:net";
|
|
4
4
|
//#region src/cli/git-credential.ts
|
|
5
5
|
/**
|
|
@@ -12,6 +12,13 @@ import { createConnection } from "node:net";
|
|
|
12
12
|
* erase → forward to the daemon (invalidate the cached token git just had rejected)
|
|
13
13
|
* store → no-op
|
|
14
14
|
*
|
|
15
|
+
* The `path` git sends (useHttpPath=true is part of the injection) ROUTES the
|
|
16
|
+
* request since issue #457: it is parsed to "owner/repo" and forwarded, so an
|
|
17
|
+
* authorized non-workspace repo gets ITS token (multi-repo design decision 5).
|
|
18
|
+
* Path absent/unparseable ⇒ the workspace token, the pre-#457 behavior. This
|
|
19
|
+
* is routing, not authorization — the CP gate decides; token scope enforces
|
|
20
|
+
* the real boundary at GitHub either way.
|
|
21
|
+
*
|
|
15
22
|
* Exit style: on ANY failure print a human-actionable line to stderr and exit 1
|
|
16
23
|
* — since #251 surfaces turn failures to end users, this text is user-facing.
|
|
17
24
|
* NEVER print or log the token outside the protocol response.
|
|
@@ -19,11 +26,13 @@ import { createConnection } from "node:net";
|
|
|
19
26
|
async function runGitCredential(action, agentId) {
|
|
20
27
|
if (action === "store") return;
|
|
21
28
|
const input = parseStdin(await readStdin());
|
|
29
|
+
const repo = input.path !== void 0 ? repoFromPath(input.path) : void 0;
|
|
22
30
|
if (action === "erase") {
|
|
23
31
|
await ipc({
|
|
24
32
|
op: "erase",
|
|
25
33
|
agentId,
|
|
26
|
-
password: input.password
|
|
34
|
+
password: input.password,
|
|
35
|
+
repoFullName: repo
|
|
27
36
|
}).catch(() => void 0);
|
|
28
37
|
return;
|
|
29
38
|
}
|
|
@@ -31,23 +40,32 @@ async function runGitCredential(action, agentId) {
|
|
|
31
40
|
if (input.host !== void 0 && input.host.toLowerCase() !== "github.com") return;
|
|
32
41
|
const res = await ipc({
|
|
33
42
|
op: "get",
|
|
34
|
-
agentId
|
|
43
|
+
agentId,
|
|
44
|
+
repoFullName: repo
|
|
35
45
|
});
|
|
36
46
|
if (!res.ok || !res.username || !res.password) {
|
|
37
|
-
process.stderr.write(`agentconnect: no git credentials for agent ${agentId}: ${res.error ?? "unknown error"}\n(the daemon must be running and connected to the control plane)\n`);
|
|
47
|
+
process.stderr.write(`agentconnect: no git credentials for agent ${agentId}${repo ? ` on ${repo}` : ""}: ${res.error ?? "unknown error"}\n(the daemon must be running and connected to the control plane)\n`);
|
|
38
48
|
process.exitCode = 1;
|
|
39
49
|
return;
|
|
40
50
|
}
|
|
41
|
-
if (
|
|
51
|
+
if (repo !== void 0) {
|
|
42
52
|
const want = normalizeRepoPath(res.repoFullName ?? "");
|
|
43
|
-
|
|
44
|
-
if (want && got && got !== want && !got.startsWith(`${want}/`)) process.stderr.write(`agentconnect: note — git asked for ${got} but this agent's repo is ${want} (repo renamed? recreate the agent to re-point it)\n`);
|
|
53
|
+
if (want && want !== repo) process.stderr.write(`agentconnect: note — git asked for ${repo} but the daemon answered for ${want} (daemon predates per-repo credentials?)\n`);
|
|
45
54
|
}
|
|
46
55
|
process.stdout.write(`username=${res.username}\npassword=${res.password}\n`);
|
|
47
56
|
}
|
|
48
57
|
function normalizeRepoPath(p) {
|
|
49
58
|
return p.replace(/^\/+/, "").replace(/\.git$/i, "").toLowerCase();
|
|
50
59
|
}
|
|
60
|
+
/** "owner/repo" from git's credential `path` — tolerates a leading slash, a
|
|
61
|
+
* `.git` suffix, and LFS-ish subpaths (`owner/repo.git/info/lfs`). */
|
|
62
|
+
function repoFromPath(p) {
|
|
63
|
+
const segs = p.replace(/^\/+/, "").split("/");
|
|
64
|
+
const owner = segs[0];
|
|
65
|
+
const repo = segs[1]?.replace(/\.git$/i, "");
|
|
66
|
+
if (!owner || !repo) return void 0;
|
|
67
|
+
return `${owner}/${repo}`.toLowerCase();
|
|
68
|
+
}
|
|
51
69
|
function parseStdin(text) {
|
|
52
70
|
const out = {};
|
|
53
71
|
for (const line of text.split("\n")) {
|
|
@@ -96,4 +114,4 @@ function ipc(msg) {
|
|
|
96
114
|
//#endregion
|
|
97
115
|
export { runGitCredential };
|
|
98
116
|
|
|
99
|
-
//# sourceMappingURL=git-credential-
|
|
117
|
+
//# sourceMappingURL=git-credential-DhWn1IBJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-credential-DhWn1IBJ.js","names":[],"sources":["../src/cli/git-credential.ts"],"sourcesContent":["/**\n * `agentconnect git-credential --agent <id>` — the git credential helper\n * (docs/designs/github-app-git-credentials.md §本地 helper 通道).\n *\n * Speaks git's credential-helper protocol on stdin/stdout and proxies to the\n * daemon's gitcred.sock; the token exists only on the reply pipe. Actions:\n * get → username=x-access-token / password=<token> (host must be github.com)\n * erase → forward to the daemon (invalidate the cached token git just had rejected)\n * store → no-op\n *\n * The `path` git sends (useHttpPath=true is part of the injection) ROUTES the\n * request since issue #457: it is parsed to \"owner/repo\" and forwarded, so an\n * authorized non-workspace repo gets ITS token (multi-repo design decision 5).\n * Path absent/unparseable ⇒ the workspace token, the pre-#457 behavior. This\n * is routing, not authorization — the CP gate decides; token scope enforces\n * the real boundary at GitHub either way.\n *\n * Exit style: on ANY failure print a human-actionable line to stderr and exit 1\n * — since #251 surfaces turn failures to end users, this text is user-facing.\n * NEVER print or log the token outside the protocol response.\n */\nimport { createConnection } from 'node:net'\nimport { resolveRoot } from '../paths.js'\nimport { gitcredSocketPath } from '../cp/gitcred-server.js'\n\ninterface HelperInput {\n protocol?: string\n host?: string\n path?: string\n password?: string\n}\n\nexport async function runGitCredential(action: string, agentId: string): Promise<void> {\n if (action === 'store') return // git offers the accepted credential back — nothing to do\n\n const input = parseStdin(await readStdin())\n const repo = input.path !== undefined ? repoFromPath(input.path) : undefined\n\n if (action === 'erase') {\n // Route the invalidation to the same (agent, repo) key the get used.\n await ipc({ op: 'erase', agentId, password: input.password, repoFullName: repo }).catch(() => undefined) // best-effort\n return\n }\n if (action !== 'get') return // unknown actions are ignored per the helper contract\n\n if (input.host !== undefined && input.host.toLowerCase() !== 'github.com') {\n // Not ours — stay silent so git can try other helpers / fail cleanly.\n return\n }\n\n const res = await ipc({ op: 'get', agentId, repoFullName: repo })\n if (!res.ok || !res.username || !res.password) {\n process.stderr.write(\n `agentconnect: no git credentials for agent ${agentId}${repo ? ` on ${repo}` : ''}: ${res.error ?? 'unknown error'}\\n` +\n `(the daemon must be running and connected to the control plane)\\n`\n )\n process.exitCode = 1\n return\n }\n // Diagnostics-only sanity (the daemon guard already refuses cross-repo\n // grants): a mismatch here means an old daemon answered with the workspace\n // token — GitHub's single-repo token scope still enforces the boundary.\n if (repo !== undefined) {\n const want = normalizeRepoPath(res.repoFullName ?? '')\n if (want && want !== repo) {\n process.stderr.write(\n `agentconnect: note — git asked for ${repo} but the daemon answered for ${want} ` +\n `(daemon predates per-repo credentials?)\\n`\n )\n }\n }\n process.stdout.write(`username=${res.username}\\npassword=${res.password}\\n`)\n}\n\nfunction normalizeRepoPath(p: string): string {\n return p\n .replace(/^\\/+/, '')\n .replace(/\\.git$/i, '')\n .toLowerCase()\n}\n\n/** \"owner/repo\" from git's credential `path` — tolerates a leading slash, a\n * `.git` suffix, and LFS-ish subpaths (`owner/repo.git/info/lfs`). */\nexport function repoFromPath(p: string): string | undefined {\n const segs = p.replace(/^\\/+/, '').split('/')\n const owner = segs[0]\n const repo = segs[1]?.replace(/\\.git$/i, '')\n if (!owner || !repo) return undefined\n return `${owner}/${repo}`.toLowerCase()\n}\n\nfunction parseStdin(text: string): HelperInput {\n const out: Record<string, string> = {}\n for (const line of text.split('\\n')) {\n const eq = line.indexOf('=')\n if (eq > 0) out[line.slice(0, eq)] = line.slice(eq + 1)\n }\n return out\n}\n\nfunction readStdin(): Promise<string> {\n return new Promise((resolve) => {\n let buf = ''\n process.stdin.setEncoding('utf8')\n process.stdin.on('data', (c) => (buf += c))\n process.stdin.on('end', () => resolve(buf))\n process.stdin.on('error', () => resolve(buf))\n })\n}\n\ninterface IpcReply {\n ok: boolean\n username?: string\n password?: string\n repoFullName?: string\n error?: string\n}\n\nfunction ipc(msg: unknown): Promise<IpcReply> {\n const path = gitcredSocketPath(resolveRoot())\n return new Promise((resolve) => {\n const sock = createConnection(path)\n let buf = ''\n const fail = (error: string) => resolve({ ok: false, error })\n sock.setTimeout(15_000, () => {\n sock.destroy()\n fail('daemon did not answer in time')\n })\n sock.on('connect', () => sock.write(JSON.stringify(msg) + '\\n'))\n sock.on('data', (c) => {\n buf += c.toString('utf8')\n const nl = buf.indexOf('\\n')\n if (nl === -1) return\n sock.destroy()\n try {\n resolve(JSON.parse(buf.slice(0, nl)) as IpcReply)\n } catch {\n fail('malformed daemon reply')\n }\n })\n sock.on('error', (e) => fail(`cannot reach the daemon socket at ${path}: ${e.message}`))\n })\n}\n"],"mappings":"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"}
|
|
@@ -5,20 +5,24 @@ import { createServer } from "node:net";
|
|
|
5
5
|
//#region src/cp/git-credential.ts
|
|
6
6
|
/** Hand out only while more than this remains (nests under the CP's 15min floor). */
|
|
7
7
|
const HANDOUT_MIN_MS = 600 * 1e3;
|
|
8
|
+
/** Repo-level denials are retried after this — the console-authorization lag. */
|
|
9
|
+
const REPO_DENIAL_TTL_MS = 60 * 1e3;
|
|
8
10
|
/** The widened scope set behind `GH_TOKEN` (P2.5 回写): git contents plus the
|
|
9
11
|
* issue/PR scopes `gh issue comment` / `gh pr comment` need. The CP clamps
|
|
10
|
-
* every capability to the agent's gitAccess
|
|
11
|
-
*
|
|
12
|
+
* every capability — the workspace repo to the agent's gitAccess, an
|
|
13
|
+
* additional repo to its authorization tier (comment ⇒ contents:read +
|
|
14
|
+
* issues/PR:write). */
|
|
12
15
|
const GH_TOKEN_CAPABILITIES = [
|
|
13
16
|
"contents",
|
|
14
17
|
"issues",
|
|
15
18
|
"pull_requests"
|
|
16
19
|
];
|
|
17
|
-
/** Cache
|
|
18
|
-
*
|
|
19
|
-
const
|
|
20
|
+
/** Cache key — NUL-joined so no owner/repo string can collide across fields
|
|
21
|
+
* (the GH_KEY precedent). */
|
|
22
|
+
const keyOf = (agentId, plane, repo) => `${plane}\u0000${agentId}\u0000${repo?.toLowerCase() ?? ""}`;
|
|
20
23
|
/** Cache key for a repo-targeted COMMENT token (the GithubPoster's — issues/PR
|
|
21
|
-
*
|
|
24
|
+
* only, no contents requested; its capability set differs from both planes so
|
|
25
|
+
* it gets its own keyspace). One per (agent, repo). */
|
|
22
26
|
const POST_KEY = (agentId, repo) => `post\u0000${agentId}\u0000${repo.toLowerCase()}`;
|
|
23
27
|
var GitCredUnavailableError = class extends Error {
|
|
24
28
|
terminal;
|
|
@@ -32,50 +36,65 @@ var GitCredentialCache = class {
|
|
|
32
36
|
deps;
|
|
33
37
|
entries = /* @__PURE__ */ new Map();
|
|
34
38
|
inflight = /* @__PURE__ */ new Map();
|
|
35
|
-
/**
|
|
39
|
+
/** Workspace keys the CP told us to stop asking for (agent-level
|
|
40
|
+
* SCOPE_DENIED — moved/mode-off). Per KEY, so a gh-plane denial never
|
|
41
|
+
* silences the git plane. */
|
|
36
42
|
denied = /* @__PURE__ */ new Set();
|
|
43
|
+
/** Repo-keyed denials → monotonic retry-after deadline (60s negative cache). */
|
|
44
|
+
deniedRepos = /* @__PURE__ */ new Map();
|
|
37
45
|
monoNow;
|
|
38
46
|
constructor(deps) {
|
|
39
47
|
this.deps = deps;
|
|
40
48
|
this.monoNow = deps.monoNow ?? (() => performance.now());
|
|
41
49
|
}
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
/**
|
|
51
|
+
* A live credential for one of the agent's authorized repos — cached,
|
|
52
|
+
* coalesced, or freshly pulled. `repo` ("owner/repo") absent ⇒ the workspace
|
|
53
|
+
* repo (the pre-#457 behavior, byte-identical on the wire). `plane` picks the
|
|
54
|
+
* capability set: 'git' (contents, the credential helper) or 'gh' (the
|
|
55
|
+
* widened GH_TOKEN set for the gh wrapper).
|
|
56
|
+
*/
|
|
57
|
+
async get(agentId, reason, opts = {}) {
|
|
58
|
+
const plane = opts.plane ?? "git";
|
|
59
|
+
return this.getKeyed(keyOf(agentId, plane, opts.repo), agentId, {
|
|
45
60
|
agentId,
|
|
46
|
-
reason
|
|
61
|
+
reason,
|
|
62
|
+
...plane === "gh" ? { capabilities: [...GH_TOKEN_CAPABILITIES] } : {},
|
|
63
|
+
...opts.repo !== void 0 ? { repoFullName: opts.repo } : {}
|
|
47
64
|
});
|
|
48
65
|
}
|
|
49
66
|
/**
|
|
50
|
-
* The widened GH_TOKEN grant (P2.5 回写) —
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* expiry (≤1h, non-renewable) means write-back goes stale until the next
|
|
54
|
-
* host spawn — the documented MVP limitation.
|
|
67
|
+
* The widened GH_TOKEN grant (P2.5 回写) for the workspace repo — kept as a
|
|
68
|
+
* named entry point for the spawn-time env injection. Per-invocation gh calls
|
|
69
|
+
* go through {@link get} with `plane: 'gh'` (and a repo) instead.
|
|
55
70
|
*/
|
|
56
71
|
async getGhToken(agentId) {
|
|
57
|
-
return this.
|
|
58
|
-
agentId,
|
|
59
|
-
reason: "helper",
|
|
60
|
-
capabilities: [...GH_TOKEN_CAPABILITIES]
|
|
61
|
-
});
|
|
72
|
+
return this.get(agentId, "helper", { plane: "gh" });
|
|
62
73
|
}
|
|
63
74
|
/**
|
|
64
|
-
* The GithubPoster's comment token
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
75
|
+
* The GithubPoster's comment token (P3 出向): scoped to `repo`, requesting
|
|
76
|
+
* issues/pull_requests only (no contents — the token can talk on threads,
|
|
77
|
+
* not read code). The CP admits it through the agent's AgentRepoAuthorization
|
|
78
|
+
* rows like every repo-targeted mint and clamps to the row's tier (a `read`
|
|
79
|
+
* tier yields read-only scopes and the poster's writes 403 — authorize the
|
|
80
|
+
* repo at `comment` or above). Same cache/coalesce/degradation discipline as
|
|
81
|
+
* {@link get}; the token never enters the agent's environment.
|
|
68
82
|
*/
|
|
69
83
|
async getPostToken(agentId, repo) {
|
|
70
84
|
return this.getKeyed(POST_KEY(agentId, repo), agentId, {
|
|
71
85
|
agentId,
|
|
72
86
|
reason: "helper",
|
|
73
87
|
capabilities: ["issues", "pull_requests"],
|
|
74
|
-
repo
|
|
88
|
+
repoFullName: repo
|
|
75
89
|
});
|
|
76
90
|
}
|
|
77
91
|
async getKeyed(key, agentId, payload) {
|
|
78
92
|
if (this.denied.has(key)) throw new GitCredUnavailableError("control plane denied git credentials for this agent", true);
|
|
93
|
+
const retryAfter = this.deniedRepos.get(key);
|
|
94
|
+
if (retryAfter !== void 0) {
|
|
95
|
+
if (retryAfter > this.monoNow()) throw new GitCredUnavailableError(`${payload.repoFullName ?? "repo"} is not authorized for this agent — authorize it under the agent's Repositories settings`, false);
|
|
96
|
+
this.deniedRepos.delete(key);
|
|
97
|
+
}
|
|
79
98
|
const cached = this.entries.get(key);
|
|
80
99
|
if (cached && cached.expiresAtMono - this.monoNow() > HANDOUT_MIN_MS) return cached;
|
|
81
100
|
let pending = this.inflight.get(key);
|
|
@@ -86,7 +105,7 @@ var GitCredentialCache = class {
|
|
|
86
105
|
try {
|
|
87
106
|
return await pending;
|
|
88
107
|
} catch (e) {
|
|
89
|
-
if (cached && cached.expiresAtMono > this.monoNow()) {
|
|
108
|
+
if (cached && this.entries.get(key) === cached && cached.expiresAtMono > this.monoNow()) {
|
|
90
109
|
this.deps.log.warn(`gitcred: refresh failed for agent ${agentId} — serving cached token`);
|
|
91
110
|
return cached;
|
|
92
111
|
}
|
|
@@ -97,20 +116,22 @@ var GitCredentialCache = class {
|
|
|
97
116
|
* Invalidate on rejection. Git calls the helper's `erase` with the failing
|
|
98
117
|
* credential when the remote refuses it (GitHub revokes instantly on
|
|
99
118
|
* uninstall/suspend) — matching on the presented password keeps a racing
|
|
100
|
-
* fresh grant from being wiped by a stale erase.
|
|
119
|
+
* fresh grant from being wiped by a stale erase. `repo` routes the erase to
|
|
120
|
+
* the same key the `get` used; absent ⇒ the workspace git-plane entry.
|
|
101
121
|
*/
|
|
102
|
-
invalidate(agentId, presentedPassword) {
|
|
103
|
-
const
|
|
122
|
+
invalidate(agentId, presentedPassword, opts = {}) {
|
|
123
|
+
const key = keyOf(agentId, opts.plane ?? "git", opts.repo);
|
|
124
|
+
const entry = this.entries.get(key);
|
|
104
125
|
if (!entry) return;
|
|
105
126
|
if (presentedPassword !== void 0 && entry.token !== presentedPassword) return;
|
|
106
|
-
this.entries.delete(
|
|
127
|
+
this.entries.delete(key);
|
|
107
128
|
}
|
|
108
129
|
/** agent/remove: drop the orphaned bearer tokens with the agent dir. */
|
|
109
130
|
remove(agentId) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
131
|
+
const marker = `\u0000${agentId}\u0000`;
|
|
132
|
+
for (const key of [...this.entries.keys()]) if (key.includes(marker)) this.entries.delete(key);
|
|
133
|
+
for (const key of [...this.deniedRepos.keys()]) if (key.includes(marker)) this.deniedRepos.delete(key);
|
|
134
|
+
for (const key of [...this.denied]) if (key.includes(marker)) this.denied.delete(key);
|
|
114
135
|
}
|
|
115
136
|
async fetch(key, payload) {
|
|
116
137
|
let grant;
|
|
@@ -118,12 +139,18 @@ var GitCredentialCache = class {
|
|
|
118
139
|
grant = await this.deps.request(payload);
|
|
119
140
|
} catch (e) {
|
|
120
141
|
if (e.code === "SCOPE_DENIED") {
|
|
142
|
+
if (payload.repoFullName !== void 0) {
|
|
143
|
+
this.deniedRepos.set(key, this.monoNow() + REPO_DENIAL_TTL_MS);
|
|
144
|
+
this.entries.delete(key);
|
|
145
|
+
throw new GitCredUnavailableError(e.message, false);
|
|
146
|
+
}
|
|
121
147
|
this.denied.add(key);
|
|
122
148
|
this.entries.delete(key);
|
|
123
149
|
throw new GitCredUnavailableError(e.message, true);
|
|
124
150
|
}
|
|
125
151
|
throw new GitCredUnavailableError(e.message, false);
|
|
126
152
|
}
|
|
153
|
+
if (payload.repoFullName !== void 0 && grant.repoFullName.toLowerCase() !== payload.repoFullName.toLowerCase()) throw new GitCredUnavailableError(`control plane is too old for per-repo credentials (asked ${payload.repoFullName}, got ${grant.repoFullName})`, false);
|
|
127
154
|
const entry = {
|
|
128
155
|
username: grant.username,
|
|
129
156
|
token: grant.token,
|
|
@@ -136,31 +163,41 @@ var GitCredentialCache = class {
|
|
|
136
163
|
}
|
|
137
164
|
/** A replicated spec change may re-enable a previously denied agent. */
|
|
138
165
|
clearDenied(agentId) {
|
|
139
|
-
|
|
140
|
-
this.denied.delete(
|
|
166
|
+
const marker = `\u0000${agentId}\u0000`;
|
|
167
|
+
for (const key of [...this.denied]) if (key.includes(marker)) this.denied.delete(key);
|
|
168
|
+
for (const key of [...this.deniedRepos.keys()]) if (key.includes(marker)) this.deniedRepos.delete(key);
|
|
141
169
|
}
|
|
142
170
|
};
|
|
143
171
|
//#endregion
|
|
144
172
|
//#region src/cp/gitcred-server.ts
|
|
145
173
|
/**
|
|
146
|
-
* gitcred.sock — the local credential channel for agent-run git
|
|
147
|
-
* (docs/designs/github-app-git-credentials.md §本地 helper
|
|
174
|
+
* gitcred.sock — the local credential channel for agent-run git AND gh
|
|
175
|
+
* (docs/designs/github-app-git-credentials.md §本地 helper 通道;
|
|
176
|
+
* agent-multi-repo-authorization.md §daemon 侧 for per-repo routing, #457).
|
|
148
177
|
*
|
|
149
178
|
* A tiny newline-delimited-JSON server over a unix socket (0700 dir + 0600
|
|
150
179
|
* socket, stale-socket cleanup — the `mcp/control-server.ts` pattern). The
|
|
151
|
-
* `agentconnect git-credential` CLI
|
|
152
|
-
*
|
|
153
|
-
* { op: '
|
|
180
|
+
* `agentconnect git-credential` / `gh-token` CLI subcommands connect per
|
|
181
|
+
* invocation:
|
|
182
|
+
* { op: 'get', agentId, repoFullName?, plane? } → { ok, username, password } | { ok:false, error }
|
|
183
|
+
* { op: 'erase', agentId, password?, repoFullName?, plane? } → { ok: true }
|
|
184
|
+
*
|
|
185
|
+
* `repoFullName` ("owner/repo") routes to that repo's token; absent — or equal
|
|
186
|
+
* to the agent's workspace repo, which is NORMALIZED onto the repo-less key so
|
|
187
|
+
* the helper path and the pre-warm/spawn paths share one cache entry — ⇒ the
|
|
188
|
+
* workspace token. `plane: 'gh'` picks the widened GH_TOKEN capability set.
|
|
154
189
|
*
|
|
155
190
|
* Trust model (design §信任模型 #1): same host + same OS user = ONE trust
|
|
156
|
-
* domain. `agentId` here
|
|
157
|
-
* can already read every workspace on
|
|
158
|
-
* and helper stdout only; nothing lands
|
|
191
|
+
* domain. `agentId`/`repoFullName` here are routing, not authorization (the CP
|
|
192
|
+
* gate decides) — any same-user process can already read every workspace on
|
|
193
|
+
* this daemon. Tokens transit the socket and helper stdout only; nothing lands
|
|
194
|
+
* on disk.
|
|
159
195
|
*
|
|
160
|
-
* Alongside the socket the daemon (re)writes
|
|
196
|
+
* Alongside the socket the daemon (re)writes SECRET-FREE files per boot:
|
|
161
197
|
* the shim `run/git-credential-helper.sh` (pins the current node + CLI path so
|
|
162
|
-
* `.git/config` survives daemon upgrades)
|
|
163
|
-
* the session-env channel
|
|
198
|
+
* `.git/config` survives daemon upgrades), the `run/bin/gh` wrapper (see
|
|
199
|
+
* cp/gh-shim.ts) and per-agent gitconfig includes for the session-env channel
|
|
200
|
+
* (see workspace/git-injection.ts).
|
|
164
201
|
*/
|
|
165
202
|
function gitcredSocketPath(root) {
|
|
166
203
|
return join(root, "run", "gitcred.sock");
|
|
@@ -171,12 +208,14 @@ function gitcredShimPath(root) {
|
|
|
171
208
|
var GitCredServer = class {
|
|
172
209
|
cache;
|
|
173
210
|
path;
|
|
174
|
-
log;
|
|
175
211
|
server;
|
|
176
|
-
|
|
212
|
+
log;
|
|
213
|
+
workspaceRepoOf;
|
|
214
|
+
constructor(cache, path, deps) {
|
|
177
215
|
this.cache = cache;
|
|
178
216
|
this.path = path;
|
|
179
|
-
this.log = log;
|
|
217
|
+
this.log = deps.log;
|
|
218
|
+
if (deps.workspaceRepoOf) this.workspaceRepoOf = deps.workspaceRepoOf;
|
|
180
219
|
}
|
|
181
220
|
start() {
|
|
182
221
|
const dir = dirname(this.path);
|
|
@@ -228,8 +267,17 @@ var GitCredServer = class {
|
|
|
228
267
|
error: "malformed request"
|
|
229
268
|
});
|
|
230
269
|
}
|
|
270
|
+
const plane = req.plane === "gh" ? "gh" : "git";
|
|
271
|
+
let repo = typeof req.repoFullName === "string" && req.repoFullName.includes("/") ? req.repoFullName : void 0;
|
|
272
|
+
if (repo !== void 0) {
|
|
273
|
+
const workspace = this.workspaceRepoOf?.(req.agentId);
|
|
274
|
+
if (workspace && workspace.toLowerCase() === repo.toLowerCase()) repo = void 0;
|
|
275
|
+
}
|
|
231
276
|
if (req.op === "erase") {
|
|
232
|
-
this.cache.invalidate(req.agentId, req.password
|
|
277
|
+
this.cache.invalidate(req.agentId, req.password, {
|
|
278
|
+
plane,
|
|
279
|
+
...repo !== void 0 ? { repo } : {}
|
|
280
|
+
});
|
|
233
281
|
return reply({ ok: true });
|
|
234
282
|
}
|
|
235
283
|
if (req.op !== "get" || typeof req.agentId !== "string") return reply({
|
|
@@ -237,11 +285,15 @@ var GitCredServer = class {
|
|
|
237
285
|
error: "unsupported op"
|
|
238
286
|
});
|
|
239
287
|
try {
|
|
240
|
-
const cred = await this.cache.get(req.agentId, "helper"
|
|
288
|
+
const cred = await this.cache.get(req.agentId, "helper", {
|
|
289
|
+
plane,
|
|
290
|
+
...repo !== void 0 ? { repo } : {}
|
|
291
|
+
});
|
|
241
292
|
return reply({
|
|
242
293
|
ok: true,
|
|
243
294
|
username: cred.username,
|
|
244
|
-
password: cred.token
|
|
295
|
+
password: cred.token,
|
|
296
|
+
repoFullName: cred.repoFullName
|
|
245
297
|
});
|
|
246
298
|
} catch (e) {
|
|
247
299
|
return reply({
|
|
@@ -282,4 +334,4 @@ function writeGitcredShim(root, cliEntry) {
|
|
|
282
334
|
//#endregion
|
|
283
335
|
export { GitCredentialCache as i, gitcredSocketPath as n, writeGitcredShim as r, GitCredServer as t };
|
|
284
336
|
|
|
285
|
-
//# sourceMappingURL=gitcred-server-
|
|
337
|
+
//# sourceMappingURL=gitcred-server-KXxLyYxc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitcred-server-KXxLyYxc.js","names":[],"sources":["../src/cp/git-credential.ts","../src/cp/gitcred-server.ts"],"sourcesContent":["/**\n * GitCredentialCache — daemon-side memory for CP-minted git credentials\n * (docs/designs/github-app-git-credentials.md §daemon 侧;\n * docs/designs/agent-multi-repo-authorization.md §daemon 侧 for the\n * per-repo keying, issue #457).\n *\n * Tokens live ONLY here (and transiently on helper stdout pipes): never disk,\n * never argv, never a log line. Expiry is tracked on the MONOTONIC clock as\n * `receivedAt + ttlSec` — a skewed wall clock must never resurrect a dead\n * token (`expiresAt` in the grant is observability only).\n *\n * Keying: `(agentId, plane, repo?)` — the git plane ('contents' capability)\n * and the gh plane (contents+issues+pull_requests) are distinct tokens with\n * separate lifecycles, and every non-workspace repo is its own token (single\n * repo scope). `repo` absent ⇒ the agent's workspace repo, the pre-#457 keys.\n *\n * Threshold nesting: entries are handed out only while >10min remain (the CP\n * serves grants with ≥15min, so a fresh grant always clears this). Per-key\n * in-flight coalescing (the `cloneInFlight` pattern) keeps a cold cache from\n * fanning N concurrent helper/pull/prefetch calls into N WS REQs.\n *\n * Denial semantics split (multi-repo design §安全边界): an AGENT-level\n * SCOPE_DENIED (workspace request) is terminal — stop asking until the spec\n * replicates. A REPO-level SCOPE_DENIED (request that named a repo) is a\n * 60s NEGATIVE CACHE only: the operator authorizing the repo in the console\n * must take effect on the next gh/git call within a minute, without a spec\n * push or daemon restart.\n *\n * Degradation: outside READY the fetch fails fast (see CpClient.requestGitCred)\n * and an UNEXPIRED cached token keeps serving — CP downtime only breaks remote\n * git once the hour runs out.\n */\nimport type { GitCredCapability, GitCredGrant } from '@agentconnect.md/protocol'\n\n/** Hand out only while more than this remains (nests under the CP's 15min floor). */\nconst HANDOUT_MIN_MS = 10 * 60 * 1000\n\n/** Repo-level denials are retried after this — the console-authorization lag. */\nconst REPO_DENIAL_TTL_MS = 60 * 1000\n\n/** The widened scope set behind `GH_TOKEN` (P2.5 回写): git contents plus the\n * issue/PR scopes `gh issue comment` / `gh pr comment` need. The CP clamps\n * every capability — the workspace repo to the agent's gitAccess, an\n * additional repo to its authorization tier (comment ⇒ contents:read +\n * issues/PR:write). */\nconst GH_TOKEN_CAPABILITIES: readonly GitCredCapability[] = ['contents', 'issues', 'pull_requests']\n\n/** The credential plane: 'git' = contents-only (the credential helper), 'gh' =\n * the widened GH_TOKEN set. Distinct capability sets are distinct tokens. */\nexport type CredPlane = 'git' | 'gh'\n\n/** Cache key — NUL-joined so no owner/repo string can collide across fields\n * (the GH_KEY precedent). */\nconst keyOf = (agentId: string, plane: CredPlane, repo?: string): string =>\n `${plane}\\u0000${agentId}\\u0000${repo?.toLowerCase() ?? ''}`\n\n/** Cache key for a repo-targeted COMMENT token (the GithubPoster's — issues/PR\n * only, no contents requested; its capability set differs from both planes so\n * it gets its own keyspace). One per (agent, repo). */\nconst POST_KEY = (agentId: string, repo: string): string => `post\\u0000${agentId}\\u0000${repo.toLowerCase()}`\n\nexport class GitCredUnavailableError extends Error {\n constructor(\n message: string,\n /** Terminal (agent-level SCOPE_DENIED): stop asking for this agent until config changes. */\n readonly terminal: boolean\n ) {\n super(message)\n this.name = 'GitCredUnavailableError'\n }\n}\n\ninterface Entry {\n username: string\n token: string\n repoFullName: string\n access: 'read' | 'write'\n /** Monotonic deadline (ms on the injected monotonic clock). */\n expiresAtMono: number\n}\n\nexport interface GitCredentialCacheDeps {\n /** D→C `gitcred/request` — throws WireError-shaped errors (`code`, `retryable`). */\n request: (payload: {\n agentId: string\n reason?: 'clone' | 'fetch' | 'pull' | 'push' | 'helper'\n capabilities?: GitCredCapability[]\n repoFullName?: string\n }) => Promise<GitCredGrant>\n log: { warn: (msg: string) => void }\n /** Monotonic ms; injectable for tests. */\n monoNow?: () => number\n}\n\nexport class GitCredentialCache {\n private readonly entries = new Map<string, Entry>()\n private readonly inflight = new Map<string, Promise<Entry>>()\n /** Workspace keys the CP told us to stop asking for (agent-level\n * SCOPE_DENIED — moved/mode-off). Per KEY, so a gh-plane denial never\n * silences the git plane. */\n private readonly denied = new Set<string>()\n /** Repo-keyed denials → monotonic retry-after deadline (60s negative cache). */\n private readonly deniedRepos = new Map<string, number>()\n private readonly monoNow: () => number\n\n constructor(private readonly deps: GitCredentialCacheDeps) {\n this.monoNow = deps.monoNow ?? (() => performance.now())\n }\n\n /**\n * A live credential for one of the agent's authorized repos — cached,\n * coalesced, or freshly pulled. `repo` (\"owner/repo\") absent ⇒ the workspace\n * repo (the pre-#457 behavior, byte-identical on the wire). `plane` picks the\n * capability set: 'git' (contents, the credential helper) or 'gh' (the\n * widened GH_TOKEN set for the gh wrapper).\n */\n async get(\n agentId: string,\n reason: 'clone' | 'fetch' | 'pull' | 'push' | 'helper',\n opts: { plane?: CredPlane; repo?: string } = {}\n ): Promise<Entry> {\n const plane = opts.plane ?? 'git'\n return this.getKeyed(keyOf(agentId, plane, opts.repo), agentId, {\n agentId,\n reason,\n ...(plane === 'gh' ? { capabilities: [...GH_TOKEN_CAPABILITIES] } : {}),\n ...(opts.repo !== undefined ? { repoFullName: opts.repo } : {})\n })\n }\n\n /**\n * The widened GH_TOKEN grant (P2.5 回写) for the workspace repo — kept as a\n * named entry point for the spawn-time env injection. Per-invocation gh calls\n * go through {@link get} with `plane: 'gh'` (and a repo) instead.\n */\n async getGhToken(agentId: string): Promise<Entry> {\n return this.get(agentId, 'helper', { plane: 'gh' })\n }\n\n /**\n * The GithubPoster's comment token (P3 出向): scoped to `repo`, requesting\n * issues/pull_requests only (no contents — the token can talk on threads,\n * not read code). The CP admits it through the agent's AgentRepoAuthorization\n * rows like every repo-targeted mint and clamps to the row's tier (a `read`\n * tier yields read-only scopes and the poster's writes 403 — authorize the\n * repo at `comment` or above). Same cache/coalesce/degradation discipline as\n * {@link get}; the token never enters the agent's environment.\n */\n async getPostToken(agentId: string, repo: string): Promise<Entry> {\n return this.getKeyed(POST_KEY(agentId, repo), agentId, {\n agentId,\n reason: 'helper',\n capabilities: ['issues', 'pull_requests'],\n repoFullName: repo\n })\n }\n\n private async getKeyed(\n key: string,\n agentId: string,\n payload: Parameters<GitCredentialCacheDeps['request']>[0]\n ): Promise<Entry> {\n if (this.denied.has(key)) {\n throw new GitCredUnavailableError('control plane denied git credentials for this agent', true)\n }\n const retryAfter = this.deniedRepos.get(key)\n if (retryAfter !== undefined) {\n if (retryAfter > this.monoNow()) {\n throw new GitCredUnavailableError(\n `${payload.repoFullName ?? 'repo'} is not authorized for this agent — authorize it under the agent's Repositories settings`,\n false\n )\n }\n this.deniedRepos.delete(key)\n }\n const cached = this.entries.get(key)\n if (cached && cached.expiresAtMono - this.monoNow() > HANDOUT_MIN_MS) return cached\n\n let pending = this.inflight.get(key)\n if (!pending) {\n pending = this.fetch(key, payload).finally(() => this.inflight.delete(key))\n this.inflight.set(key, pending)\n }\n try {\n return await pending\n } catch (e) {\n // CP unreachable / transient: an UNEXPIRED cached token still serves.\n // A DENIAL is authoritative, not transient — fetch() deletes the entry on\n // SCOPE_DENIED, so the identity check (`=== cached`) fails and we do NOT\n // resurrect the token the denial just purged (a revoked repo must break on\n // the discovering call, not one call later).\n if (cached && this.entries.get(key) === cached && cached.expiresAtMono > this.monoNow()) {\n this.deps.log.warn(`gitcred: refresh failed for agent ${agentId} — serving cached token`)\n return cached\n }\n throw e\n }\n }\n\n /**\n * Invalidate on rejection. Git calls the helper's `erase` with the failing\n * credential when the remote refuses it (GitHub revokes instantly on\n * uninstall/suspend) — matching on the presented password keeps a racing\n * fresh grant from being wiped by a stale erase. `repo` routes the erase to\n * the same key the `get` used; absent ⇒ the workspace git-plane entry.\n */\n invalidate(agentId: string, presentedPassword?: string, opts: { plane?: CredPlane; repo?: string } = {}): void {\n const key = keyOf(agentId, opts.plane ?? 'git', opts.repo)\n const entry = this.entries.get(key)\n if (!entry) return\n if (presentedPassword !== undefined && entry.token !== presentedPassword) return\n this.entries.delete(key)\n }\n\n /** agent/remove: drop the orphaned bearer tokens with the agent dir. */\n remove(agentId: string): void {\n const marker = `\\u0000${agentId}\\u0000`\n for (const key of [...this.entries.keys()]) if (key.includes(marker)) this.entries.delete(key)\n for (const key of [...this.deniedRepos.keys()]) if (key.includes(marker)) this.deniedRepos.delete(key)\n for (const key of [...this.denied]) if (key.includes(marker)) this.denied.delete(key)\n }\n\n private async fetch(key: string, payload: Parameters<GitCredentialCacheDeps['request']>[0]): Promise<Entry> {\n let grant: GitCredGrant\n try {\n grant = await this.deps.request(payload)\n } catch (e) {\n const code = (e as { code?: string }).code\n if (code === 'SCOPE_DENIED') {\n if (payload.repoFullName !== undefined) {\n // Repo-keyed: negative-cache and retry in a minute — the operator may\n // be authorizing the repo in the console right now.\n this.deniedRepos.set(key, this.monoNow() + REPO_DENIAL_TTL_MS)\n this.entries.delete(key)\n throw new GitCredUnavailableError((e as Error).message, false)\n }\n // Workspace-keyed terminal: the agent moved daemons or left github-app\n // mode. Stop asking on THIS key until the CP replicates a new spec\n // (which clears it).\n this.denied.add(key)\n this.entries.delete(key)\n throw new GitCredUnavailableError((e as Error).message, true)\n }\n throw new GitCredUnavailableError((e as Error).message, false)\n }\n // Old-CP mismatch guard (multi-repo design §协议改动): a CP that predates\n // `repoFullName` strips the field and answers with the WORKSPACE grant —\n // serving that under a repo key would hand the wrong repo's token to gh/git.\n // A NEW CP ECHOES the requested name back verbatim (even after a GitHub\n // canonical rename — it mints by numeric id but reports what we asked), so\n // this only ever fires on the genuine old-CP case, never on a rename.\n if (payload.repoFullName !== undefined && grant.repoFullName.toLowerCase() !== payload.repoFullName.toLowerCase()) {\n throw new GitCredUnavailableError(\n `control plane is too old for per-repo credentials (asked ${payload.repoFullName}, got ${grant.repoFullName})`,\n false\n )\n }\n const entry: Entry = {\n username: grant.username,\n token: grant.token,\n repoFullName: grant.repoFullName,\n access: grant.access,\n expiresAtMono: this.monoNow() + grant.ttlSec * 1000\n }\n this.entries.set(key, entry)\n return entry\n }\n\n /** A replicated spec change may re-enable a previously denied agent. */\n clearDenied(agentId: string): void {\n const marker = `\\u0000${agentId}\\u0000`\n for (const key of [...this.denied]) if (key.includes(marker)) this.denied.delete(key)\n for (const key of [...this.deniedRepos.keys()]) if (key.includes(marker)) this.deniedRepos.delete(key)\n }\n}\n","/**\n * gitcred.sock — the local credential channel for agent-run git AND gh\n * (docs/designs/github-app-git-credentials.md §本地 helper 通道;\n * agent-multi-repo-authorization.md §daemon 侧 for per-repo routing, #457).\n *\n * A tiny newline-delimited-JSON server over a unix socket (0700 dir + 0600\n * socket, stale-socket cleanup — the `mcp/control-server.ts` pattern). The\n * `agentconnect git-credential` / `gh-token` CLI subcommands connect per\n * invocation:\n * { op: 'get', agentId, repoFullName?, plane? } → { ok, username, password } | { ok:false, error }\n * { op: 'erase', agentId, password?, repoFullName?, plane? } → { ok: true }\n *\n * `repoFullName` (\"owner/repo\") routes to that repo's token; absent — or equal\n * to the agent's workspace repo, which is NORMALIZED onto the repo-less key so\n * the helper path and the pre-warm/spawn paths share one cache entry — ⇒ the\n * workspace token. `plane: 'gh'` picks the widened GH_TOKEN capability set.\n *\n * Trust model (design §信任模型 #1): same host + same OS user = ONE trust\n * domain. `agentId`/`repoFullName` here are routing, not authorization (the CP\n * gate decides) — any same-user process can already read every workspace on\n * this daemon. Tokens transit the socket and helper stdout only; nothing lands\n * on disk.\n *\n * Alongside the socket the daemon (re)writes SECRET-FREE files per boot:\n * the shim `run/git-credential-helper.sh` (pins the current node + CLI path so\n * `.git/config` survives daemon upgrades), the `run/bin/gh` wrapper (see\n * cp/gh-shim.ts) and per-agent gitconfig includes for the session-env channel\n * (see workspace/git-injection.ts).\n */\nimport { createServer, type Server, type Socket } from 'node:net'\nimport { chmodSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { dirname, join } from 'node:path'\nimport { GitCredentialCache, GitCredUnavailableError, type CredPlane } from './git-credential.js'\n\nexport function gitcredSocketPath(root: string): string {\n return join(root, 'run', 'gitcred.sock')\n}\n\nexport function gitcredShimPath(root: string): string {\n return join(root, 'run', 'git-credential-helper.sh')\n}\n\ninterface GitCredIpcRequest {\n op: 'get' | 'erase'\n agentId: string\n password?: string\n /** \"owner/repo\" to route to (multi-repo, #457); absent ⇒ workspace. */\n repoFullName?: string\n /** 'gh' ⇒ the widened GH_TOKEN capability set; absent/'git' ⇒ contents-only. */\n plane?: string\n}\n\nexport interface GitCredServerDeps {\n log: { info: (m: string) => void; warn: (m: string) => void }\n /** The agent's workspace \"owner/repo\" label (lowercase-insensitive compare) —\n * lets a helper request that names the workspace repo share the repo-less\n * cache key with pre-warm/spawn instead of splitting the cache. */\n workspaceRepoOf?: (agentId: string) => string | undefined\n}\n\nexport class GitCredServer {\n private server?: Server\n private readonly log: GitCredServerDeps['log']\n private readonly workspaceRepoOf?: (agentId: string) => string | undefined\n\n constructor(\n private readonly cache: GitCredentialCache,\n private readonly path: string,\n deps: GitCredServerDeps\n ) {\n this.log = deps.log\n if (deps.workspaceRepoOf) this.workspaceRepoOf = deps.workspaceRepoOf\n }\n\n start(): void {\n const dir = dirname(this.path)\n mkdirSync(dir, { recursive: true, mode: 0o700 })\n try {\n chmodSync(dir, 0o700) // defeat a loose umask; best-effort on non-POSIX\n } catch {\n /* best-effort */\n }\n rmSync(this.path, { force: true }) // a stale socket from a crash makes listen() throw\n\n const server = createServer((sock) => this.serve(sock))\n server.listen(this.path, () => {\n try {\n chmodSync(this.path, 0o600)\n } catch {\n /* best-effort */\n }\n this.log.info(`gitcred: helper socket at ${this.path}`)\n })\n server.on('error', (e) => this.log.warn(`gitcred: socket error: ${e.message}`))\n this.server = server\n }\n\n stop(): void {\n this.server?.close()\n rmSync(this.path, { force: true })\n }\n\n private serve(sock: Socket): void {\n let buf = ''\n sock.on('data', (chunk) => {\n buf += chunk.toString('utf8')\n const nl = buf.indexOf('\\n')\n if (nl === -1) return\n const line = buf.slice(0, nl)\n buf = buf.slice(nl + 1)\n void this.handle(line, sock)\n })\n sock.on('error', () => sock.destroy())\n }\n\n private async handle(line: string, sock: Socket): Promise<void> {\n const reply = (msg: unknown) => {\n sock.write(JSON.stringify(msg) + '\\n')\n sock.end()\n }\n let req: GitCredIpcRequest\n try {\n req = JSON.parse(line) as GitCredIpcRequest\n } catch {\n return reply({ ok: false, error: 'malformed request' })\n }\n const plane: CredPlane = req.plane === 'gh' ? 'gh' : 'git'\n // Workspace normalization: a request naming the workspace repo folds onto\n // the repo-less key (one cache entry with pre-warm/spawn; and old CPs that\n // strip the wire field keep serving the workspace unchanged).\n let repo = typeof req.repoFullName === 'string' && req.repoFullName.includes('/') ? req.repoFullName : undefined\n if (repo !== undefined) {\n const workspace = this.workspaceRepoOf?.(req.agentId)\n if (workspace && workspace.toLowerCase() === repo.toLowerCase()) repo = undefined\n }\n if (req.op === 'erase') {\n // Git presents the rejected credential — GitHub revokes instantly on\n // uninstall/suspend, and this is how the daemon cache learns.\n this.cache.invalidate(req.agentId, req.password, { plane, ...(repo !== undefined ? { repo } : {}) })\n return reply({ ok: true })\n }\n if (req.op !== 'get' || typeof req.agentId !== 'string') {\n return reply({ ok: false, error: 'unsupported op' })\n }\n try {\n const cred = await this.cache.get(req.agentId, 'helper', { plane, ...(repo !== undefined ? { repo } : {}) })\n return reply({ ok: true, username: cred.username, password: cred.token, repoFullName: cred.repoFullName })\n } catch (e) {\n const msg =\n e instanceof GitCredUnavailableError ? e.message : `git credentials unavailable: ${(e as Error).message}`\n return reply({ ok: false, error: msg })\n }\n }\n}\n\n/**\n * (Re)write the secret-free shim `.git/config` helper lines exec through. The\n * absolute node + CLI paths are re-pinned every daemon boot, so repo configs\n * keep working across upgrades/relocations. Quoted throughout — a home dir\n * with a space (macOS \"/Users/John Smith/…\") must not word-split.\n */\nexport function writeGitcredShim(root: string, cliEntry: string): string {\n const shim = gitcredShimPath(root)\n mkdirSync(dirname(shim), { recursive: true, mode: 0o700 })\n const q = (v: string) => `'${v.replaceAll(\"'\", \"'\\\\''\")}'`\n // Production runs the built dist (a .js entry node executes directly). A dev\n // daemon runs under tsx with a .ts argv[1] — route the shim through the tsx\n // CLI then, or plain `node entry.ts` would die resolving .js-suffixed imports.\n const argv = [q(process.execPath)]\n if (cliEntry.endsWith('.ts')) {\n const req = createRequire(import.meta.url)\n argv.push(q(req.resolve('tsx/cli')))\n }\n argv.push(q(cliEntry))\n const body = [\n '#!/bin/sh',\n '# agentconnect git credential helper shim — regenerated on daemon start; NO secrets.',\n `AGENTCONNECT_ROOT=${q(root)} \\\\`,\n ` exec ${argv.join(' ')} git-credential \"$@\"`,\n ''\n ].join('\\n')\n writeFileSync(shim, body, { mode: 0o755 })\n return shim\n}\n"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -68492,9 +68492,9 @@ const telemetry = startDaemonOpenTelemetry({ serviceVersion: DAEMON_VERSION });
|
|
|
68492
68492
|
const [{ Command }, { fileURLToPath }, { runChat }, { runLogin }, { runForeground }, { resolveController }, { acquireSingletonLock }, { resolveRoot }] = await Promise.all([
|
|
68493
68493
|
import("./commander-B5QVEFzH.js"),
|
|
68494
68494
|
import("node:url"),
|
|
68495
|
-
import("./chat-
|
|
68496
|
-
import("./login-
|
|
68497
|
-
import("./run-foreground-
|
|
68495
|
+
import("./chat-D8QIIHor.js"),
|
|
68496
|
+
import("./login-BP4bFP-d.js"),
|
|
68497
|
+
import("./run-foreground-jOExu3YR.js"),
|
|
68498
68498
|
import("./service-6w0WKhdx.js"),
|
|
68499
68499
|
import("./lock-CA9Bm8_s.js"),
|
|
68500
68500
|
import("./paths-CcRUjFjy.js")
|
|
@@ -68646,13 +68646,17 @@ program.command("login").description("Interactive onboarding: test the Control P
|
|
|
68646
68646
|
}
|
|
68647
68647
|
});
|
|
68648
68648
|
program.command("git-credential").description("Git credential helper backed by the local daemon (github-app workspaces)").argument("<agentId>", "agent whose workspace credentials to serve").argument("<action>", "git credential action: get | store | erase").action(async (agentId, action) => {
|
|
68649
|
-
const { runGitCredential } = await import("./git-credential-
|
|
68649
|
+
const { runGitCredential } = await import("./git-credential-DhWn1IBJ.js");
|
|
68650
68650
|
await runGitCredential(action, agentId);
|
|
68651
68651
|
});
|
|
68652
|
+
program.command("gh-token").description("Fetch a per-repo GH_TOKEN from the local daemon (gh wrapper backend)").argument("<agentId>", "agent whose credentials to serve").argument("[repo]", "target repo (owner/repo, host/owner/repo, or URL); absent = workspace").action(async (agentId, repo) => {
|
|
68653
|
+
const { runGhToken } = await import("./gh-token-BfPlYQav.js");
|
|
68654
|
+
await runGhToken(agentId, repo);
|
|
68655
|
+
});
|
|
68652
68656
|
program.command("agent").description("Inspect local agent directories").command("list").description("List agents discovered under --agents-dir (id, status, runtime, name, dir)").action(async () => {
|
|
68653
68657
|
const opts = program.opts();
|
|
68654
68658
|
try {
|
|
68655
|
-
const { runAgentList } = await import("./agent-list-
|
|
68659
|
+
const { runAgentList } = await import("./agent-list-Da9Osn4O.js");
|
|
68656
68660
|
await runAgentList({
|
|
68657
68661
|
agentsDir: opts.agentsDir,
|
|
68658
68662
|
configPath: opts.config,
|