@snowyroad/braid 0.96.8 → 0.98.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -4
- package/dist/{chunk-QHWKKDPN.js → chunk-3SHZYLWH.js} +205 -8
- package/dist/cli.js +1259 -132
- package/dist/{http-3WYFRJZR.js → http-7QNDIXBN.js} +1 -1
- package/dist/mcp/server.js +120 -2
- package/package.json +1 -1
- package/service-shrinkwrap.json +19 -19
package/README.md
CHANGED
|
@@ -107,6 +107,7 @@ with `npx @snowyroad/braid` if you did not install globally.
|
|
|
107
107
|
| `braid tools <readonly\|full> [name]` | Set what the agent may do when channel members ask (readonly = read + reply; full = run commands + edit files) |
|
|
108
108
|
| `braid ipc <compat\|strict> [name]` | Set the IPC confinement profile for a saved agent |
|
|
109
109
|
| `braid scope [name]` | Show the effective OS sandbox scope; `braid scope allow <agent> <host>` approves a custom model-endpoint host |
|
|
110
|
+
| `braid git-auth <set\|show\|clear> <agent> ...` | Give a saved agent git access to one repository with a local fine-grained token (`set <agent> <owner/repo> --mode <autonomous\|pr-only> [--allow-workflows] [--allow-risk-paths] [--allow-public] --token-stdin`), or show or remove that grant. See [Giving an agent git access](#giving-an-agent-git-access-without-your-credentials) |
|
|
110
111
|
| `braid service <install\|uninstall\|start\|stop\|restart\|status\|logs> [name]` | Run a saved agent as a background service (starts at login, restarts on crash) — see [docs/SERVICE-MODE.md](docs/SERVICE-MODE.md) |
|
|
111
112
|
| `braid update` | Update the pinned service runtime and restart all installed services atomically |
|
|
112
113
|
| `braid attach [name]` | Watch a running service's live activity over the local control socket (same machine) |
|
|
@@ -231,7 +232,7 @@ descendant process and cannot be shed.
|
|
|
231
232
|
dirs** — `~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.kube/config`, and the bridge's own
|
|
232
233
|
credential store (`~/.braid`).
|
|
233
234
|
- **Network:** a curated allow-list (provider API hosts, package registries, and
|
|
234
|
-
|
|
235
|
+
read-only source-content hosts such as codeload.github.com) so normal work keeps working; everything else is
|
|
235
236
|
blocked. Operator-extensible.
|
|
236
237
|
- **Fail-closed:** in **full** tool mode the bridge REFUSES to start if the OS
|
|
237
238
|
sandbox facility is unavailable, and tells you how to fix it. In read-and-reply
|
|
@@ -243,9 +244,11 @@ descendant process and cannot be shed.
|
|
|
243
244
|
or the `BRAID_SCOPE_ALLOW_WRITE` / `BRAID_SCOPE_ALLOW_READ` / `BRAID_SCOPE_ALLOW_DOMAINS`
|
|
244
245
|
env vars (see below). Credential directories (`~/.ssh`, `~/.aws`, `~/.config/gh`,
|
|
245
246
|
and the rest of the protected inventory) are reserved and CANNOT be granted: a
|
|
246
|
-
grant that lands inside one is refused at startup with a SCOPE ERROR.
|
|
247
|
-
|
|
248
|
-
|
|
247
|
+
grant that lands inside one is refused at startup with a SCOPE ERROR. Code hosts
|
|
248
|
+
(github.com, gitlab.com, bitbucket.org) are NOT reachable from inside the jail:
|
|
249
|
+
authenticated git goes through the bridge's git tools. Grant a project data directory, never a credential store.
|
|
250
|
+
|
|
251
|
+
**Code hosts are not reachable from the jail.** `github.com`, `api.github.com`, `gitlab.com` and `bitbucket.org` are a reserved set: an `--allow-domain` or `scope.network.allowedDomains` entry that reaches them is refused at startup. To accept that risk for one agent (for example, `git+https` dependencies during installs) run `braid scope allow-code-hosts <agent>`; the startup banner then carries a warning. The agent's own `git` runs with credential helpers disabled, so it cannot read your `gh` or keychain credentials. To let an agent push, fetch and open pull requests without those credentials, see [Giving an agent git access](#giving-an-agent-git-access-without-your-credentials).
|
|
249
252
|
|
|
250
253
|
- **Unix socket access (Linux):** on Linux, seccomp-bpf cannot filter Unix sockets
|
|
251
254
|
by path. The default `compat` IPC profile allows all pathname sockets. The `strict`
|
|
@@ -253,6 +256,27 @@ descendant process and cannot be shed.
|
|
|
253
256
|
env-var hygiene to block common IPC paths. See [docs/ipc-profiles.md](docs/ipc-profiles.md)
|
|
254
257
|
for the full profile reference, support matrix, and selection commands.
|
|
255
258
|
|
|
259
|
+
### Giving an agent git access (without your credentials)
|
|
260
|
+
|
|
261
|
+
Code hosts are not reachable from inside the sandbox, and the agent's own `git`
|
|
262
|
+
cannot use your `gh` login or keychain. To let an agent push, fetch and open pull
|
|
263
|
+
requests, give it a **grant** for one repository:
|
|
264
|
+
|
|
265
|
+
- **Braid GitHub App (recommended):** in the web app, Settings > Integrations installs
|
|
266
|
+
the Braid App on the repositories you choose; then Agents > your agent > Git access
|
|
267
|
+
adds a repository and picks the mode. The Braid server mints a 1-hour token narrowed
|
|
268
|
+
to that repository each time the agent needs one; the agent never sees it.
|
|
269
|
+
- **Local token (fallback):** `braid git-auth set <agent> <owner/repo> --mode <autonomous|pr-only> --token-stdin`
|
|
270
|
+
with a fine-grained token scoped to that repository (Contents and Pull requests
|
|
271
|
+
read/write, no Workflows). Stored under `~/.braid`, which the jail cannot read.
|
|
272
|
+
|
|
273
|
+
Modes: **autonomous** pushes to any branch, opens and merges pull requests, and never
|
|
274
|
+
waits for a human. **pr-only** pushes only to `braid/<agent>/*` branches and opens draft
|
|
275
|
+
pull requests for a human to merge. In both modes pushes are fast-forward only, never
|
|
276
|
+
delete or tag, and `.github/workflows` changes need the "allow workflow changes" toggle.
|
|
277
|
+
The agent uses the `braid_git_status`, `braid_git_fetch`, `braid_git_push`, `braid_open_pr`
|
|
278
|
+
and `braid_merge_pr` tools; Braid runs the git commands outside the sandbox.
|
|
279
|
+
|
|
256
280
|
**Requirements:** macOS needs `ripgrep` (`brew install ripgrep`). Linux needs
|
|
257
281
|
`bubblewrap`, `socat`, and `ripgrep`. On an unsupported platform (e.g. Windows) the
|
|
258
282
|
bridge fails closed to read-and-reply.
|
|
@@ -400,7 +400,7 @@ var InviteExpiredError = class extends Error {
|
|
|
400
400
|
}
|
|
401
401
|
};
|
|
402
402
|
async function redeemInvite(relayHttpUrl, code) {
|
|
403
|
-
const { boundedJsonFetch: boundedJsonFetch2 } = await import("./http-
|
|
403
|
+
const { boundedJsonFetch: boundedJsonFetch2 } = await import("./http-7QNDIXBN.js");
|
|
404
404
|
const r = await boundedJsonFetch2(`${relayHttpUrl.replace(/\/$/, "")}/invites/redeem`, {
|
|
405
405
|
method: "POST",
|
|
406
406
|
headers: { "content-type": "application/json" },
|
|
@@ -492,7 +492,7 @@ function validateReplacementToken(newToken, currentToken, nowMs = Date.now()) {
|
|
|
492
492
|
return { ok: true };
|
|
493
493
|
}
|
|
494
494
|
async function mintAccessToken(relayHttpUrl, agentKey, fetchFn = fetch, dpopProof) {
|
|
495
|
-
const { boundedJsonFetch: boundedJsonFetch2 } = await import("./http-
|
|
495
|
+
const { boundedJsonFetch: boundedJsonFetch2 } = await import("./http-7QNDIXBN.js");
|
|
496
496
|
const r = await boundedJsonFetch2(`${relayHttpUrl.replace(/\/$/, "")}/agents/token`, {
|
|
497
497
|
method: "POST",
|
|
498
498
|
headers: {
|
|
@@ -672,7 +672,7 @@ function verifyWin32Acl(target, sid, deps, userName = "") {
|
|
|
672
672
|
}
|
|
673
673
|
return unexpected;
|
|
674
674
|
}
|
|
675
|
-
function assertKeystorePerms(dir, agentFile, deps) {
|
|
675
|
+
function assertKeystorePerms(dir, agentFile, deps, extraSecretFiles = []) {
|
|
676
676
|
const platform = deps?.platform ?? process.platform;
|
|
677
677
|
if (platform === "win32") {
|
|
678
678
|
const d = deps ?? productionWin32Deps();
|
|
@@ -688,6 +688,11 @@ function assertKeystorePerms(dir, agentFile, deps) {
|
|
|
688
688
|
if (filePresent) {
|
|
689
689
|
verifyWin32Acl(agentFile, sid, d, userName);
|
|
690
690
|
}
|
|
691
|
+
for (const extra of extraSecretFiles) {
|
|
692
|
+
if (!extra || !fsExists(extra)) continue;
|
|
693
|
+
applyWin32Acl(extra, sid, "file", d);
|
|
694
|
+
verifyWin32Acl(extra, sid, d, userName);
|
|
695
|
+
}
|
|
691
696
|
return;
|
|
692
697
|
}
|
|
693
698
|
try {
|
|
@@ -716,6 +721,21 @@ function assertKeystorePerms(dir, agentFile, deps) {
|
|
|
716
721
|
} catch (err) {
|
|
717
722
|
if (err instanceof KeystorePermsError) throw err;
|
|
718
723
|
}
|
|
724
|
+
for (const extra of extraSecretFiles) {
|
|
725
|
+
try {
|
|
726
|
+
const emode = statSync(extra).mode & 511;
|
|
727
|
+
if ((emode & 63) !== 0) {
|
|
728
|
+
throw new KeystorePermsError(
|
|
729
|
+
`[braid] refusing to start: a stored secret file is readable by other users on this machine.
|
|
730
|
+
path : ${extra}
|
|
731
|
+
mode : ${emode.toString(8).padStart(3, "0")} (expected 600)
|
|
732
|
+
fix : chmod 600 ${extra}`
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
} catch (err) {
|
|
736
|
+
if (err instanceof KeystorePermsError) throw err;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
719
739
|
}
|
|
720
740
|
function configDir(env = process.env) {
|
|
721
741
|
const override = env.BRAID_CONFIG_DIR?.trim();
|
|
@@ -812,6 +832,7 @@ function parseScope(v) {
|
|
|
812
832
|
if (allowedDomains) net.allowedDomains = allowedDomains;
|
|
813
833
|
const deniedDomains = strArr(n.deniedDomains);
|
|
814
834
|
if (deniedDomains) net.deniedDomains = deniedDomains;
|
|
835
|
+
if (typeof n.allowCodeHosts === "boolean") net.allowCodeHosts = n.allowCodeHosts;
|
|
815
836
|
if (Object.keys(net).length > 0) out.network = net;
|
|
816
837
|
}
|
|
817
838
|
return Object.keys(out).length > 0 ? out : void 0;
|
|
@@ -1049,6 +1070,13 @@ function pinEndpoint(dir, agentName, pin) {
|
|
|
1049
1070
|
agent.pinnedEndpoints = next;
|
|
1050
1071
|
saveAgent(dir, agent);
|
|
1051
1072
|
}
|
|
1073
|
+
function setScopeAllowCodeHosts(dir, agentName, value) {
|
|
1074
|
+
const entry = loadAgent(dir, agentName);
|
|
1075
|
+
const agent = entry.agent;
|
|
1076
|
+
const network = { ...agent.scope?.network ?? {}, allowCodeHosts: value };
|
|
1077
|
+
agent.scope = { ...agent.scope ?? {}, network };
|
|
1078
|
+
saveAgent(dir, agent);
|
|
1079
|
+
}
|
|
1052
1080
|
function updateAgentWorkspace(dir, agentName, info) {
|
|
1053
1081
|
try {
|
|
1054
1082
|
const entry = loadAgent(dir, agentName);
|
|
@@ -2832,13 +2860,44 @@ var DEFAULT_ALLOWED_DOMAINS = [
|
|
|
2832
2860
|
// Crates.io
|
|
2833
2861
|
"crates.io",
|
|
2834
2862
|
"static.crates.io",
|
|
2835
|
-
// Source hosts
|
|
2863
|
+
// Source hosts: READ-ONLY content delivery only. The code-host write paths
|
|
2864
|
+
// (github.com, api.github.com, gitlab.com, bitbucket.org) left this list on
|
|
2865
|
+
// 2026-10-06 (git gateway D12/G2): with them present, an injected agent could
|
|
2866
|
+
// push private files to an attacker's repo using an attacker-supplied token,
|
|
2867
|
+
// no operator credential needed. Authenticated git goes through the bridge's
|
|
2868
|
+
// git gateway tools; see RESERVED_CODE_HOST_DOMAINS for the enforced floor.
|
|
2869
|
+
"codeload.github.com",
|
|
2870
|
+
"*.githubusercontent.com"
|
|
2871
|
+
];
|
|
2872
|
+
var RESERVED_CODE_HOST_DOMAINS = [
|
|
2836
2873
|
"github.com",
|
|
2837
2874
|
"*.github.com",
|
|
2838
|
-
"*.githubusercontent.com",
|
|
2839
2875
|
"gitlab.com",
|
|
2840
|
-
"
|
|
2876
|
+
"*.gitlab.com",
|
|
2877
|
+
"bitbucket.org",
|
|
2878
|
+
"*.bitbucket.org"
|
|
2841
2879
|
];
|
|
2880
|
+
var RESERVED_CODE_HOST_READ_ONLY = /* @__PURE__ */ new Set(["codeload.github.com"]);
|
|
2881
|
+
function codeHostConflicts(allowed) {
|
|
2882
|
+
const out = [];
|
|
2883
|
+
for (const a of allowed) {
|
|
2884
|
+
const d = a.domain.trim().toLowerCase().replace(/\.$/, "");
|
|
2885
|
+
if (RESERVED_CODE_HOST_READ_ONLY.has(d)) continue;
|
|
2886
|
+
for (const r of RESERVED_CODE_HOST_DOMAINS) {
|
|
2887
|
+
const base = r.startsWith("*.") ? r.slice(2) : r;
|
|
2888
|
+
const isAllMatch = d === "*";
|
|
2889
|
+
const isReservedItself = d === r || d === base;
|
|
2890
|
+
const isSubdomainOfReservedWildcard = r.startsWith("*.") && d.endsWith("." + base);
|
|
2891
|
+
const isBroaderWildcard = d.startsWith("*.") && (base === d.slice(2) || base.endsWith("." + d.slice(2)));
|
|
2892
|
+
const hit = isAllMatch || isReservedItself || isSubdomainOfReservedWildcard || isBroaderWildcard;
|
|
2893
|
+
if (hit) {
|
|
2894
|
+
out.push({ domain: a.domain, source: a.source, reservation: r });
|
|
2895
|
+
break;
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
return out;
|
|
2900
|
+
}
|
|
2842
2901
|
var PROVIDER_PATHS = {
|
|
2843
2902
|
"claude-code": ["~/.claude", "~/.claude.json"],
|
|
2844
2903
|
codex: ["~/.codex"],
|
|
@@ -3022,6 +3081,24 @@ Narrow the grant to a path outside the protected set. (Ancestor grants like "~"
|
|
|
3022
3081
|
...scope.network?.deniedDomains ?? [],
|
|
3023
3082
|
...parseEnvList(env["BRAID_SCOPE_DENY_DOMAINS"])
|
|
3024
3083
|
]);
|
|
3084
|
+
const allowCodeHosts = scope.network?.allowCodeHosts === true;
|
|
3085
|
+
if (!disabled && !allowCodeHosts) {
|
|
3086
|
+
const labelledDomains = [
|
|
3087
|
+
...(scope.network?.allowedDomains ?? []).map((domain) => ({ domain, source: "scope.network.allowedDomains" })),
|
|
3088
|
+
...(cfg.derivedAllowedDomains ?? []).map((domain) => ({ domain, source: "derived model endpoint" })),
|
|
3089
|
+
...parseEnvList(env["BRAID_SCOPE_ALLOW_DOMAINS"]).map((domain) => ({ domain, source: "BRAID_SCOPE_ALLOW_DOMAINS" })),
|
|
3090
|
+
...(cfg.scopeFlags?.allowDomains ?? []).map((domain) => ({ domain, source: "--allow-domain" }))
|
|
3091
|
+
];
|
|
3092
|
+
const hits = codeHostConflicts(labelledDomains);
|
|
3093
|
+
if (hits.length > 0) {
|
|
3094
|
+
const detail = hits.map((h) => ` - ${h.source}: "${h.domain}" is a code host (reserved: ${h.reservation})`).join("\n");
|
|
3095
|
+
throw new Error(
|
|
3096
|
+
`[braid] SCOPE ERROR: a network grant reaches a code host and was refused:
|
|
3097
|
+
${detail}
|
|
3098
|
+
The sandboxed agent must not reach code hosts directly (an injected agent could push private files anywhere on that host). Use the Braid git tools for authenticated git, or run \`braid scope allow-code-hosts <agent>\` to accept the exfiltration risk for this agent.`
|
|
3099
|
+
);
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3025
3102
|
function describe() {
|
|
3026
3103
|
const lines = [];
|
|
3027
3104
|
if (disabled) {
|
|
@@ -3042,6 +3119,9 @@ Narrow the grant to a path outside the protected set. (Ancestor grants like "~"
|
|
|
3042
3119
|
lines.push(`[scope] provider paths (writable): ${providerPaths.join(", ")}`);
|
|
3043
3120
|
}
|
|
3044
3121
|
lines.push(`[scope] allowed domains (${allowedDomains.length}): ${allowedDomains.slice(0, 5).join(", ")}${allowedDomains.length > 5 ? " ..." : ""}`);
|
|
3122
|
+
if (allowCodeHosts) {
|
|
3123
|
+
lines.push("[scope] WARNING: direct code-host egress is ON for this agent (exfiltration path accepted by the owner via `braid scope allow-code-hosts`)");
|
|
3124
|
+
}
|
|
3045
3125
|
if ((cfg.derivedAllowedDomains ?? []).length > 0) {
|
|
3046
3126
|
lines.push(`[scope] model endpoints (derived): ${(cfg.derivedAllowedDomains ?? []).join(", ")}`);
|
|
3047
3127
|
}
|
|
@@ -5460,6 +5540,63 @@ var RelayClient = class _RelayClient {
|
|
|
5460
5540
|
const body = r.json;
|
|
5461
5541
|
return Array.isArray(body?.emojis) ? body.emojis.filter((e) => typeof e === "string") : [];
|
|
5462
5542
|
}
|
|
5543
|
+
/** Git gateway (D4): the agent's active app grants, never tokens. Malformed entries are
|
|
5544
|
+
* skipped, unknown fields dropped. Returns null on a non-2xx or malformed body (never throws),
|
|
5545
|
+
* [] only for a genuinely empty list. */
|
|
5546
|
+
async fetchGitGrants() {
|
|
5547
|
+
const r = await this.httpJson("/agents/me/git-grants", { requestClass: "relay.git.grants" });
|
|
5548
|
+
if (!r.ok) return null;
|
|
5549
|
+
const body = r.json;
|
|
5550
|
+
if (!Array.isArray(body?.grants)) return null;
|
|
5551
|
+
const out = [];
|
|
5552
|
+
for (const g of body.grants) {
|
|
5553
|
+
if (!g || typeof g.id !== "string" || typeof g.repo !== "string") continue;
|
|
5554
|
+
if (g.mode !== "autonomous" && g.mode !== "pr-only") continue;
|
|
5555
|
+
const slash = g.repo.indexOf("/");
|
|
5556
|
+
if (slash <= 0 || slash === g.repo.length - 1) continue;
|
|
5557
|
+
out.push({
|
|
5558
|
+
id: g.id,
|
|
5559
|
+
source: "app",
|
|
5560
|
+
repoOwner: g.repo.slice(0, slash),
|
|
5561
|
+
repoName: g.repo.slice(slash + 1),
|
|
5562
|
+
mode: g.mode,
|
|
5563
|
+
allowWorkflows: g.allowWorkflows === true,
|
|
5564
|
+
allowRiskPaths: g.allowRiskPaths === true,
|
|
5565
|
+
allowPublic: g.allowPublic === true
|
|
5566
|
+
});
|
|
5567
|
+
}
|
|
5568
|
+
return out;
|
|
5569
|
+
}
|
|
5570
|
+
/** Git gateway (D4): mint the repo-narrowed 1h token for ONE grant. The response body
|
|
5571
|
+
* carries the token: never log it, never put it in an error message. Null on any non-2xx
|
|
5572
|
+
* (409 grant_changed, 404 grant_not_found) or malformed body. */
|
|
5573
|
+
async mintGitToken(grantId) {
|
|
5574
|
+
const id = this.pathId(grantId, "grantId");
|
|
5575
|
+
if (id === null) return null;
|
|
5576
|
+
const r = await this.httpJson(`/agents/me/git-grants/${id}/token`, {
|
|
5577
|
+
method: "POST",
|
|
5578
|
+
body: JSON.stringify({}),
|
|
5579
|
+
requestClass: "relay.git.mint",
|
|
5580
|
+
timeoutMs: 2e4
|
|
5581
|
+
});
|
|
5582
|
+
if (!r.ok) return null;
|
|
5583
|
+
const b = r.json;
|
|
5584
|
+
if (typeof b?.token !== "string" || !b.token || typeof b.repo !== "string") return null;
|
|
5585
|
+
if (b.mode !== "autonomous" && b.mode !== "pr-only") return null;
|
|
5586
|
+
const expiresAt = typeof b.expiresAt === "string" ? Date.parse(b.expiresAt) : NaN;
|
|
5587
|
+
if (!Number.isFinite(expiresAt)) return null;
|
|
5588
|
+
return {
|
|
5589
|
+
token: b.token,
|
|
5590
|
+
expiresAt,
|
|
5591
|
+
private: typeof b.private === "boolean" ? b.private : null,
|
|
5592
|
+
defaultBranch: typeof b.defaultBranch === "string" ? b.defaultBranch : null,
|
|
5593
|
+
repo: b.repo,
|
|
5594
|
+
mode: b.mode,
|
|
5595
|
+
allowWorkflows: b.allowWorkflows === true,
|
|
5596
|
+
allowRiskPaths: b.allowRiskPaths === true,
|
|
5597
|
+
allowPublic: b.allowPublic === true
|
|
5598
|
+
};
|
|
5599
|
+
}
|
|
5463
5600
|
/** Post a bounded-flow reply (turn or synthesis) to the flow-scoped endpoint.
|
|
5464
5601
|
* agentId MUST be the agent NAME — the relay's flow gate resolves turn ownership and
|
|
5465
5602
|
* synthesis role via resolveAgentUUID (a name lookup); a UUID resolves to uuid.Nil -> 403. */
|
|
@@ -5701,17 +5838,26 @@ function brokerSocketPath(uuid, platform = process.platform) {
|
|
|
5701
5838
|
var SourceBroker = class {
|
|
5702
5839
|
/** `platform` injected so the win32 named-pipe branch is unit-testable on a POSIX host
|
|
5703
5840
|
* (same seam as startControlSocket / controlSocketPath). */
|
|
5704
|
-
constructor(channelId, reader, platform = process.platform) {
|
|
5841
|
+
constructor(channelId, reader, platform = process.platform, gateway) {
|
|
5705
5842
|
this.channelId = channelId;
|
|
5706
5843
|
this.reader = reader;
|
|
5707
5844
|
this.platform = platform;
|
|
5845
|
+
this.gateway = gateway;
|
|
5708
5846
|
}
|
|
5709
5847
|
channelId;
|
|
5710
5848
|
reader;
|
|
5711
5849
|
platform;
|
|
5850
|
+
gateway;
|
|
5712
5851
|
server = null;
|
|
5713
5852
|
token = randomBytes(24).toString("hex");
|
|
5714
5853
|
socketPath = "";
|
|
5854
|
+
/** D15: git routes are disabled while a lisa fresh turn runs on this channel. A depth
|
|
5855
|
+
* counter, so overlapping fresh turns keep git off until the LAST one finishes:
|
|
5856
|
+
* false increments, true decrements (never below 0), enabled only at depth 0. */
|
|
5857
|
+
gitDisabledDepth = 0;
|
|
5858
|
+
setGitEnabled(enabled) {
|
|
5859
|
+
this.gitDisabledDepth = Math.max(0, this.gitDisabledDepth + (enabled ? -1 : 1));
|
|
5860
|
+
}
|
|
5715
5861
|
async start() {
|
|
5716
5862
|
if (this.server) throw new Error("SourceBroker already started");
|
|
5717
5863
|
const isWin32 = this.platform === "win32";
|
|
@@ -5751,6 +5897,9 @@ var SourceBroker = class {
|
|
|
5751
5897
|
// blind critic, it would silently leak the transcript the relay's read-filter is
|
|
5752
5898
|
// deliberately withholding (D9/G2b) and defeat critic blindness. Do not add a
|
|
5753
5899
|
// channel/flow message-read route here without addressing this.
|
|
5900
|
+
// The /git/ routes (git gateway, D6/D7) are write and egress routes; they ARE gated
|
|
5901
|
+
// off lisa fresh turns: ChannelSession.onLisaFreshTurn calls setGitEnabled(false)
|
|
5902
|
+
// for the turn's duration, so a critic or builder round gets 403 git_disabled_for_turn (D15).
|
|
5754
5903
|
async handle(req, res) {
|
|
5755
5904
|
const send = (status, body2) => {
|
|
5756
5905
|
const s = JSON.stringify(body2);
|
|
@@ -6002,6 +6151,22 @@ var SourceBroker = class {
|
|
|
6002
6151
|
const r = await this.reader.appendCanvasSection(this.channelId, heading, content);
|
|
6003
6152
|
return send(r.status, r.json ?? { ok: r.ok });
|
|
6004
6153
|
}
|
|
6154
|
+
if (req.url?.startsWith("/git/")) {
|
|
6155
|
+
if (!this.gateway) return send(404, { error: "unknown route" });
|
|
6156
|
+
if (this.gitDisabledDepth > 0) return send(403, { error: "git_disabled_for_turn", text: "Git tools are unavailable during this turn." });
|
|
6157
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
6158
|
+
const pathArg = str(body.path);
|
|
6159
|
+
if (req.url === "/git/pr/merge") {
|
|
6160
|
+
if (!(typeof body.number === "number" && Number.isInteger(body.number) && body.number > 0)) return send(400, { error: "invalid_number", text: "number must be a whole pull request number." });
|
|
6161
|
+
if (body.method !== void 0 && !(typeof body.method === "string" && ["merge", "squash", "rebase"].includes(body.method))) {
|
|
6162
|
+
return send(400, { error: "invalid_method", text: "method must be merge, squash or rebase." });
|
|
6163
|
+
}
|
|
6164
|
+
}
|
|
6165
|
+
const result = req.url === "/git/status" ? await this.gateway.status(this.channelId, { path: pathArg }) : req.url === "/git/fetch" ? await this.gateway.fetch(this.channelId, { path: pathArg }) : req.url === "/git/push" ? await this.gateway.push(this.channelId, { path: pathArg, branch: str(body.branch) ?? "", sha: str(body.sha) }) : req.url === "/git/pr/open" ? await this.gateway.openPr(this.channelId, { path: pathArg, branch: str(body.branch) ?? "", title: str(body.title) ?? "", body: str(body.body), base: str(body.base) }) : req.url === "/git/pr/merge" ? await this.gateway.mergePr(this.channelId, { number: Number(body.number), method: str(body.method) }) : null;
|
|
6166
|
+
if (!result) return send(404, { error: "unknown route" });
|
|
6167
|
+
if (result.ok) return send(200, { ...result.data ?? {}, ok: true, text: result.text });
|
|
6168
|
+
return send(result.status, { error: result.code, text: result.text });
|
|
6169
|
+
}
|
|
6005
6170
|
return send(404, { error: "unknown route" });
|
|
6006
6171
|
} catch (err) {
|
|
6007
6172
|
return send(500, { error: String(err) });
|
|
@@ -6225,6 +6390,29 @@ function formatRateLimitNotice(agentName, provider, message) {
|
|
|
6225
6390
|
];
|
|
6226
6391
|
}
|
|
6227
6392
|
|
|
6393
|
+
// src/gitEnv.ts
|
|
6394
|
+
var AGENT_GIT_ENV = {
|
|
6395
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
6396
|
+
GCM_INTERACTIVE: "never",
|
|
6397
|
+
GIT_CONFIG_NOSYSTEM: "1"
|
|
6398
|
+
};
|
|
6399
|
+
var AGENT_GIT_CONFIG = [
|
|
6400
|
+
["credential.helper", ""],
|
|
6401
|
+
["core.askPass", ""]
|
|
6402
|
+
];
|
|
6403
|
+
function withAgentGitEnv(extra, baseGitConfigCount = process.env.GIT_CONFIG_COUNT) {
|
|
6404
|
+
const out = { ...extra, ...AGENT_GIT_ENV };
|
|
6405
|
+
let n = Number.parseInt(extra.GIT_CONFIG_COUNT ?? baseGitConfigCount ?? "0", 10);
|
|
6406
|
+
if (!Number.isFinite(n) || n < 0) n = 0;
|
|
6407
|
+
for (const [key, value] of AGENT_GIT_CONFIG) {
|
|
6408
|
+
out[`GIT_CONFIG_KEY_${n}`] = key;
|
|
6409
|
+
out[`GIT_CONFIG_VALUE_${n}`] = value;
|
|
6410
|
+
n++;
|
|
6411
|
+
}
|
|
6412
|
+
out.GIT_CONFIG_COUNT = String(n);
|
|
6413
|
+
return out;
|
|
6414
|
+
}
|
|
6415
|
+
|
|
6228
6416
|
// src/adapter.ts
|
|
6229
6417
|
function defaultToolPolicy() {
|
|
6230
6418
|
return { mode: "readonly", configDirAbs: resolve2(configDir(process.env)), allowReadAbs: [] };
|
|
@@ -6324,6 +6512,10 @@ function resolvePathBinary(binary, hint) {
|
|
|
6324
6512
|
return found;
|
|
6325
6513
|
}
|
|
6326
6514
|
function launchSpecFor(agent) {
|
|
6515
|
+
const spec = launchSpecForProvider(agent);
|
|
6516
|
+
return { ...spec, env: withAgentGitEnv(spec.env ?? {}) };
|
|
6517
|
+
}
|
|
6518
|
+
function launchSpecForProvider(agent) {
|
|
6327
6519
|
const cwd = process.cwd();
|
|
6328
6520
|
switch (agent) {
|
|
6329
6521
|
case "claude-code": {
|
|
@@ -6468,7 +6660,7 @@ function interactiveHandoffSupported(agent) {
|
|
|
6468
6660
|
}
|
|
6469
6661
|
function interactiveResumeSpecFor(agent, sessionId, cwd) {
|
|
6470
6662
|
const build = HANDOFF_RESUME_SPECS[agent];
|
|
6471
|
-
return build ? build(sessionId, cwd) : null;
|
|
6663
|
+
return build ? { ...build(sessionId, cwd), env: withAgentGitEnv({}) } : null;
|
|
6472
6664
|
}
|
|
6473
6665
|
var defaultAcpClientFactory = (launch) => new AcpClient(launch);
|
|
6474
6666
|
var MAX_CONSECUTIVE_RESTARTS = 3;
|
|
@@ -7911,6 +8103,9 @@ export {
|
|
|
7911
8103
|
sanitizeForTty,
|
|
7912
8104
|
restoreTerminal,
|
|
7913
8105
|
isShellSafeName,
|
|
8106
|
+
productionWin32Deps,
|
|
8107
|
+
resolveCurrentUser,
|
|
8108
|
+
applyWin32Acl,
|
|
7914
8109
|
assertKeystorePerms,
|
|
7915
8110
|
configDir,
|
|
7916
8111
|
readPathWithFallback,
|
|
@@ -7921,6 +8116,7 @@ export {
|
|
|
7921
8116
|
loadAgent,
|
|
7922
8117
|
acquireAgentLock,
|
|
7923
8118
|
pinEndpoint,
|
|
8119
|
+
setScopeAllowCodeHosts,
|
|
7924
8120
|
updateAgentWorkspace,
|
|
7925
8121
|
isFilesystemRoot,
|
|
7926
8122
|
narrowestMode,
|
|
@@ -7950,6 +8146,7 @@ export {
|
|
|
7950
8146
|
scopedLaunch,
|
|
7951
8147
|
resetScope,
|
|
7952
8148
|
sandboxInstaller,
|
|
8149
|
+
withAgentGitEnv,
|
|
7953
8150
|
resolveClaudeBinary,
|
|
7954
8151
|
describeClaudeBinary,
|
|
7955
8152
|
interactiveHandoffSupported,
|