@agentconnect.md/daemon 1.0.0-rc.201 → 1.0.0-rc.203
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/{agent-list-CZbaXHPF.js → agent-list-D-RwgYN7.js} +2 -2
- package/dist/{agent-list-CZbaXHPF.js.map → agent-list-D-RwgYN7.js.map} +1 -1
- package/dist/{bridge-NDvTO1zB.js → bridge-C8-TYXsj.js} +2 -2
- package/dist/{bridge-NDvTO1zB.js.map → bridge-C8-TYXsj.js.map} +1 -1
- package/dist/git-credential-DWH0EUV6.js +98 -0
- package/dist/git-credential-DWH0EUV6.js.map +1 -0
- package/dist/index.js +512 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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)
|
|
14000
|
-
|
|
14001
|
-
|
|
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
|
|
14031
|
-
|
|
14032
|
-
|
|
14033
|
-
|
|
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
|
-
|
|
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 >=
|
|
84147
|
+
if (entry.tries >= entry.maxTries) {
|
|
83728
84148
|
this.pending.delete(entry.id);
|
|
83729
|
-
entry.reject(new WireError("INTERNAL", `no ack after ${
|
|
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
|
-
|
|
83735
|
-
|
|
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) =>
|
|
102197
|
-
|
|
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-
|
|
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-
|
|
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 {
|
|
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
|