@arbiterforge/ca-pi 0.6.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/LICENSE +661 -0
- package/README.md +558 -0
- package/package.json +35 -0
- package/plugins/ca-pi/CHANGELOG.md +1030 -0
- package/plugins/ca-pi/COMMANDS.md +90 -0
- package/plugins/ca-pi/ORCHESTRATOR.md +159 -0
- package/plugins/ca-pi/SKILLS.md +47 -0
- package/plugins/ca-pi/SPRINT.md +142 -0
- package/plugins/ca-pi/agents/INDEX.md +31 -0
- package/plugins/ca-pi/agents/architecture-drift-reviewer.md +86 -0
- package/plugins/ca-pi/agents/auth-crypto-reviewer.md +60 -0
- package/plugins/ca-pi/agents/backend-author.md +60 -0
- package/plugins/ca-pi/agents/checkpoint-aggregator.md +111 -0
- package/plugins/ca-pi/agents/coverage-auditor.md +71 -0
- package/plugins/ca-pi/agents/decision-challenger.md +116 -0
- package/plugins/ca-pi/agents/dependency-reviewer.md +79 -0
- package/plugins/ca-pi/agents/design-quality-reviewer.md +80 -0
- package/plugins/ca-pi/agents/finding-triage.md +86 -0
- package/plugins/ca-pi/agents/frontend-author.md +64 -0
- package/plugins/ca-pi/agents/grader.md +173 -0
- package/plugins/ca-pi/agents/infra-author.md +64 -0
- package/plugins/ca-pi/agents/map-deps.md +35 -0
- package/plugins/ca-pi/agents/map-structure.md +37 -0
- package/plugins/ca-pi/agents/migration-reviewer.md +65 -0
- package/plugins/ca-pi/agents/scout.md +127 -0
- package/plugins/ca-pi/agents/security-reviewer.md +72 -0
- package/plugins/ca-pi/agents/tribunal-lens-reviewer.md +65 -0
- package/plugins/ca-pi/extensions/codearbiter-child.js +1885 -0
- package/plugins/ca-pi/extensions/codearbiter.js +9802 -0
- package/plugins/ca-pi/generated/command-catalog.json +197 -0
- package/plugins/ca-pi/generated/roles.json +213 -0
- package/plugins/ca-pi/helpers/windows-supervisor.js +205 -0
- package/plugins/ca-pi/hooks/_activationlib.py +196 -0
- package/plugins/ca-pi/hooks/_arbiterstatelib.py +208 -0
- package/plugins/ca-pi/hooks/_babysitlib.py +76 -0
- package/plugins/ca-pi/hooks/_bashguardlib.py +1667 -0
- package/plugins/ca-pi/hooks/_boxlib.py +131 -0
- package/plugins/ca-pi/hooks/_colorlib.py +304 -0
- package/plugins/ca-pi/hooks/_durabilitylib.py +186 -0
- package/plugins/ca-pi/hooks/_entrylib.py +41 -0
- package/plugins/ca-pi/hooks/_fmtlib.py +161 -0
- package/plugins/ca-pi/hooks/_gitexec.py +45 -0
- package/plugins/ca-pi/hooks/_githooks.py +920 -0
- package/plugins/ca-pi/hooks/_gitlib.py +110 -0
- package/plugins/ca-pi/hooks/_hooklib.py +595 -0
- package/plugins/ca-pi/hooks/_host.py +115 -0
- package/plugins/ca-pi/hooks/_intentlib.py +242 -0
- package/plugins/ca-pi/hooks/_ledgerlib.py +1035 -0
- package/plugins/ca-pi/hooks/_metricslib.py +709 -0
- package/plugins/ca-pi/hooks/_pathnorm.py +74 -0
- package/plugins/ca-pi/hooks/_planfilelib.py +664 -0
- package/plugins/ca-pi/hooks/_previewlib.py +193 -0
- package/plugins/ca-pi/hooks/_protectedlib.py +312 -0
- package/plugins/ca-pi/hooks/_protectedstatelib.py +411 -0
- package/plugins/ca-pi/hooks/_provenancelib.py +971 -0
- package/plugins/ca-pi/hooks/_prunelib.py +1398 -0
- package/plugins/ca-pi/hooks/_prunepolicy.py +235 -0
- package/plugins/ca-pi/hooks/_readinjectlib.py +1080 -0
- package/plugins/ca-pi/hooks/_releaselib.py +2657 -0
- package/plugins/ca-pi/hooks/_scopelib.py +262 -0
- package/plugins/ca-pi/hooks/_segmentslib.py +278 -0
- package/plugins/ca-pi/hooks/_sensitivelib.py +270 -0
- package/plugins/ca-pi/hooks/_sessionlib.py +78 -0
- package/plugins/ca-pi/hooks/_sloplib.py +244 -0
- package/plugins/ca-pi/hooks/_standuplib.py +214 -0
- package/plugins/ca-pi/hooks/_subagentslib.py +219 -0
- package/plugins/ca-pi/hooks/_taskboardlib.py +1088 -0
- package/plugins/ca-pi/hooks/_updatelib.py +278 -0
- package/plugins/ca-pi/hooks/babysit.py +47 -0
- package/plugins/ca-pi/hooks/boardsync.py +129 -0
- package/plugins/ca-pi/hooks/doctor.py +420 -0
- package/plugins/ca-pi/hooks/git-enforce.py +325 -0
- package/plugins/ca-pi/hooks/hostapi.py +460 -0
- package/plugins/ca-pi/hooks/init-codearbiter.py +225 -0
- package/plugins/ca-pi/hooks/metrics.py +62 -0
- package/plugins/ca-pi/hooks/migration-pass.py +129 -0
- package/plugins/ca-pi/hooks/pi-bridge.py +543 -0
- package/plugins/ca-pi/hooks/post-write-edit.py +231 -0
- package/plugins/ca-pi/hooks/pre-bash.py +90 -0
- package/plugins/ca-pi/hooks/pre-edit.py +284 -0
- package/plugins/ca-pi/hooks/pre-read.py +81 -0
- package/plugins/ca-pi/hooks/pre-write.py +217 -0
- package/plugins/ca-pi/hooks/preview.py +69 -0
- package/plugins/ca-pi/hooks/prune-transcript.py +232 -0
- package/plugins/ca-pi/hooks/releasehash.py +216 -0
- package/plugins/ca-pi/hooks/security-pass.py +139 -0
- package/plugins/ca-pi/hooks/session-start.py +1218 -0
- package/plugins/ca-pi/hooks/statusline.py +736 -0
- package/plugins/ca-pi/hooks/taskwrite.py +351 -0
- package/plugins/ca-pi/hooks/update-refresh.py +51 -0
- package/plugins/ca-pi/hooks/wire-statusline.py +435 -0
- package/plugins/ca-pi/includes/anti-slop-design/INDEX.md +55 -0
- package/plugins/ca-pi/includes/anti-slop-design/color.md +43 -0
- package/plugins/ca-pi/includes/anti-slop-design/core.md +244 -0
- package/plugins/ca-pi/includes/anti-slop-design/images.md +32 -0
- package/plugins/ca-pi/includes/anti-slop-design/layout.md +45 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-cli.md +39 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-dataviz.md +43 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-diagram.md +35 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-documents.md +70 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-slides.md +30 -0
- package/plugins/ca-pi/includes/anti-slop-design/medium-web.md +39 -0
- package/plugins/ca-pi/includes/anti-slop-design/typography.md +51 -0
- package/plugins/ca-pi/includes/author-tdd-workflow.md +14 -0
- package/plugins/ca-pi/includes/compaction-charter.md +16 -0
- package/plugins/ca-pi/includes/cut-docs.md +16 -0
- package/plugins/ca-pi/includes/dev-mode.md +30 -0
- package/plugins/ca-pi/includes/farm.md +237 -0
- package/plugins/ca-pi/includes/fresh-verification.md +14 -0
- package/plugins/ca-pi/includes/harvest.md +69 -0
- package/plugins/ca-pi/includes/maturity-coverage.md +102 -0
- package/plugins/ca-pi/includes/pi-host-notes.md +69 -0
- package/plugins/ca-pi/includes/redirect.md +69 -0
- package/plugins/ca-pi/includes/reference-map.md +22 -0
- package/plugins/ca-pi/includes/review-matrix.md +14 -0
- package/plugins/ca-pi/includes/reviewer-contract.md +53 -0
- package/plugins/ca-pi/includes/routing-table.md +47 -0
- package/plugins/ca-pi/includes/security-gate-record.md +22 -0
- package/plugins/ca-pi/includes/smarts/core.md +90 -0
- package/plugins/ca-pi/includes/smarts/decision-log-format.md +56 -0
- package/plugins/ca-pi/routines/INDEX.md +32 -0
- package/plugins/ca-pi/routines/brainstorming/SKILL.md +122 -0
- package/plugins/ca-pi/routines/commit-gate/SKILL.md +151 -0
- package/plugins/ca-pi/routines/context-check/SKILL.md +85 -0
- package/plugins/ca-pi/routines/context-creation/SKILL.md +171 -0
- package/plugins/ca-pi/routines/crypto-compliance/SKILL.md +41 -0
- package/plugins/ca-pi/routines/debug/SKILL.md +99 -0
- package/plugins/ca-pi/routines/decision-lifecycle/SKILL.md +104 -0
- package/plugins/ca-pi/routines/decision-lifecycle/references/adr-template.md +74 -0
- package/plugins/ca-pi/routines/decision-variance/SKILL.md +147 -0
- package/plugins/ca-pi/routines/decompose/SKILL.md +168 -0
- package/plugins/ca-pi/routines/dispatching-parallel-agents/SKILL.md +76 -0
- package/plugins/ca-pi/routines/executing-plans/SKILL.md +83 -0
- package/plugins/ca-pi/routines/finishing-a-development-branch/SKILL.md +91 -0
- package/plugins/ca-pi/routines/post-merge-cleanup/SKILL.md +233 -0
- package/plugins/ca-pi/routines/refactor/SKILL.md +91 -0
- package/plugins/ca-pi/routines/release/SKILL.md +315 -0
- package/plugins/ca-pi/routines/secret-handling/SKILL.md +67 -0
- package/plugins/ca-pi/routines/security-architecture/SKILL.md +63 -0
- package/plugins/ca-pi/routines/skill-author/SKILL.md +108 -0
- package/plugins/ca-pi/routines/skill-author/references/skill-template.md +58 -0
- package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +149 -0
- package/plugins/ca-pi/routines/subagent-driven-development/references/farm-dispatch.md +145 -0
- package/plugins/ca-pi/routines/tdd/SKILL.md +139 -0
- package/plugins/ca-pi/routines/tribunal/SKILL.md +109 -0
- package/plugins/ca-pi/routines/tribunal/references/ai-markers.md +29 -0
- package/plugins/ca-pi/routines/tribunal/references/cost-and-models.md +64 -0
- package/plugins/ca-pi/routines/tribunal/references/finding-record.md +27 -0
- package/plugins/ca-pi/routines/tribunal/references/issue-filing.md +47 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/appsec.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/architecture.md +23 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/coverage.md +20 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/infra.md +24 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/migration.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/observability.md +21 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/performance.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/reliability.md +23 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/secrets-supply.md +22 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/test-fidelity.md +24 -0
- package/plugins/ca-pi/routines/tribunal/references/lenses/typesafety.md +21 -0
- package/plugins/ca-pi/routines/tribunal/references/report.md +19 -0
- package/plugins/ca-pi/routines/tribunal/references/schemas.md +58 -0
- package/plugins/ca-pi/routines/tribunal/references/telemetry.md +28 -0
- package/plugins/ca-pi/routines/tribunal/references/triage.md +53 -0
- package/plugins/ca-pi/routines/using-git-worktrees/SKILL.md +85 -0
- package/plugins/ca-pi/routines/writing-plans/SKILL.md +129 -0
- package/plugins/ca-pi/routines/writing-plans/references/farm-plan.md +50 -0
- package/plugins/ca-pi/skills/ca-add-dep/SKILL.md +88 -0
- package/plugins/ca-pi/skills/ca-adr/SKILL.md +30 -0
- package/plugins/ca-pi/skills/ca-adr-status/SKILL.md +30 -0
- package/plugins/ca-pi/skills/ca-arbiter/SKILL.md +36 -0
- package/plugins/ca-pi/skills/ca-audit/SKILL.md +51 -0
- package/plugins/ca-pi/skills/ca-btw/SKILL.md +23 -0
- package/plugins/ca-pi/skills/ca-checkpoint/SKILL.md +50 -0
- package/plugins/ca-pi/skills/ca-chore/SKILL.md +58 -0
- package/plugins/ca-pi/skills/ca-cleanup/SKILL.md +55 -0
- package/plugins/ca-pi/skills/ca-commands/SKILL.md +21 -0
- package/plugins/ca-pi/skills/ca-commit/SKILL.md +27 -0
- package/plugins/ca-pi/skills/ca-conflict/SKILL.md +61 -0
- package/plugins/ca-pi/skills/ca-context-check/SKILL.md +32 -0
- package/plugins/ca-pi/skills/ca-create-context/SKILL.md +32 -0
- package/plugins/ca-pi/skills/ca-debug/SKILL.md +42 -0
- package/plugins/ca-pi/skills/ca-decompose/SKILL.md +30 -0
- package/plugins/ca-pi/skills/ca-dev/SKILL.md +42 -0
- package/plugins/ca-pi/skills/ca-doctor/SKILL.md +44 -0
- package/plugins/ca-pi/skills/ca-feature/SKILL.md +105 -0
- package/plugins/ca-pi/skills/ca-fix/SKILL.md +42 -0
- package/plugins/ca-pi/skills/ca-init/SKILL.md +56 -0
- package/plugins/ca-pi/skills/ca-metrics/SKILL.md +80 -0
- package/plugins/ca-pi/skills/ca-new-skill/SKILL.md +34 -0
- package/plugins/ca-pi/skills/ca-override/SKILL.md +72 -0
- package/plugins/ca-pi/skills/ca-pr/SKILL.md +61 -0
- package/plugins/ca-pi/skills/ca-preview/SKILL.md +86 -0
- package/plugins/ca-pi/skills/ca-prune/SKILL.md +100 -0
- package/plugins/ca-pi/skills/ca-reconcile/SKILL.md +43 -0
- package/plugins/ca-pi/skills/ca-refactor/SKILL.md +43 -0
- package/plugins/ca-pi/skills/ca-release/SKILL.md +57 -0
- package/plugins/ca-pi/skills/ca-review/SKILL.md +63 -0
- package/plugins/ca-pi/skills/ca-spike/SKILL.md +41 -0
- package/plugins/ca-pi/skills/ca-sprint/SKILL.md +44 -0
- package/plugins/ca-pi/skills/ca-standup/SKILL.md +112 -0
- package/plugins/ca-pi/skills/ca-status/SKILL.md +56 -0
- package/plugins/ca-pi/skills/ca-task/SKILL.md +61 -0
- package/plugins/ca-pi/skills/ca-threat-model/SKILL.md +45 -0
- package/plugins/ca-pi/skills/ca-tribunal/SKILL.md +44 -0
- package/plugins/ca-pi/skills/ca-watch/SKILL.md +78 -0
|
@@ -0,0 +1,1885 @@
|
|
|
1
|
+
// <define:__CODEARBITER_PI_TOOL_CLASSES__>
|
|
2
|
+
var define_CODEARBITER_PI_TOOL_CLASSES_default = { bash: "EXEC", write: "WRITE", edit: "EDIT", read: "READ" };
|
|
3
|
+
|
|
4
|
+
// src/child-extension.ts
|
|
5
|
+
import { createReadStream } from "node:fs";
|
|
6
|
+
import { readFile as readFile2, realpath as realpath4 } from "node:fs/promises";
|
|
7
|
+
import { dirname as dirname3, resolve as resolve5 } from "node:path";
|
|
8
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
9
|
+
|
|
10
|
+
// src/attestation.ts
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
var CHILD_ATTESTATION_DOMAIN = "ca-pi-child-attestation-v1";
|
|
13
|
+
var CHILD_ATTESTATION_TITLE = "codeArbiter isolated child readiness";
|
|
14
|
+
var CHILD_ATTESTATION_TIMEOUT_MS = 5e3;
|
|
15
|
+
function childAttestationDigest(input) {
|
|
16
|
+
return createHash("sha256").update(JSON.stringify([
|
|
17
|
+
CHILD_ATTESTATION_DOMAIN,
|
|
18
|
+
input.nonce,
|
|
19
|
+
input.challenge,
|
|
20
|
+
input.cwd,
|
|
21
|
+
input.provider,
|
|
22
|
+
input.model,
|
|
23
|
+
[...input.tools].sort(),
|
|
24
|
+
input.projectTrusted,
|
|
25
|
+
input.mode
|
|
26
|
+
]), "utf8").digest("hex");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/bridge.ts
|
|
30
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
31
|
+
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
32
|
+
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
33
|
+
import { realpath as realpath2 } from "node:fs/promises";
|
|
34
|
+
import { isAbsolute, posix as posix2, win32 as win322 } from "node:path";
|
|
35
|
+
|
|
36
|
+
// src/audit-sink.ts
|
|
37
|
+
import { constants as fsConstants } from "node:fs";
|
|
38
|
+
import { lstat, open, realpath } from "node:fs/promises";
|
|
39
|
+
import { relative, resolve as resolve2 } from "node:path";
|
|
40
|
+
|
|
41
|
+
// src/path-boundary.ts
|
|
42
|
+
import { posix, resolve, win32 } from "node:path";
|
|
43
|
+
function flavorForPlatform(platform) {
|
|
44
|
+
return platform === "win32" ? "win32" : "posix";
|
|
45
|
+
}
|
|
46
|
+
function pathApiFor(flavor) {
|
|
47
|
+
return flavor === "win32" ? win32 : posix;
|
|
48
|
+
}
|
|
49
|
+
function lexicallyInside(path, root, flavor = flavorForPlatform(process.platform)) {
|
|
50
|
+
const pathApi = pathApiFor(flavor);
|
|
51
|
+
const suffix = pathApi.relative(root, path);
|
|
52
|
+
return suffix === "" || !suffix.startsWith("..") && !pathApi.isAbsolute(suffix);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/audit-sink.ts
|
|
56
|
+
var AUDIT_LOG_NAME = "gate-events.log";
|
|
57
|
+
var AUDIT_STATE_DIRECTORY = ".codearbiter";
|
|
58
|
+
var MAX_AUDIT_LINE_BYTES = 2048;
|
|
59
|
+
var NODE_AUDIT_SINK_IO = Object.freeze({ realpath, lstat, open });
|
|
60
|
+
function sameAuditFile(left, right) {
|
|
61
|
+
return left.isFile() && right.isFile() && !left.isSymbolicLink() && !right.isSymbolicLink() && left.nlink === 1 && right.nlink === 1 && left.dev === right.dev && left.ino === right.ino;
|
|
62
|
+
}
|
|
63
|
+
function sameAuditDirectory(left, right) {
|
|
64
|
+
return left.isDirectory() && right.isDirectory() && !left.isSymbolicLink() && !right.isSymbolicLink() && left.dev === right.dev && left.ino === right.ino;
|
|
65
|
+
}
|
|
66
|
+
async function openedAuditTarget(target, io) {
|
|
67
|
+
const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
|
|
68
|
+
const existingFlags = fsConstants.O_WRONLY | fsConstants.O_APPEND | noFollow;
|
|
69
|
+
const createFlags = existingFlags | fsConstants.O_CREAT | fsConstants.O_EXCL;
|
|
70
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
71
|
+
let expected;
|
|
72
|
+
try {
|
|
73
|
+
expected = await io.lstat(target);
|
|
74
|
+
if (!expected.isFile() || expected.isSymbolicLink() || expected.nlink !== 1) return void 0;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error.code !== "ENOENT") return void 0;
|
|
77
|
+
}
|
|
78
|
+
let handle;
|
|
79
|
+
try {
|
|
80
|
+
handle = await io.open(target, expected === void 0 ? createFlags : existingFlags, 384);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (expected === void 0 && error.code === "EEXIST" && attempt === 0) continue;
|
|
83
|
+
return void 0;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const opened = await handle.stat();
|
|
87
|
+
const pathname = await io.lstat(target);
|
|
88
|
+
if (!sameAuditFile(opened, pathname) || expected !== void 0 && !sameAuditFile(opened, expected)) {
|
|
89
|
+
await handle.close();
|
|
90
|
+
return void 0;
|
|
91
|
+
}
|
|
92
|
+
return Object.freeze({ handle, identity: opened });
|
|
93
|
+
} catch {
|
|
94
|
+
try {
|
|
95
|
+
await handle.close();
|
|
96
|
+
} catch {
|
|
97
|
+
}
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return void 0;
|
|
102
|
+
}
|
|
103
|
+
async function appendAuditLineWithIo(cwd, line, io) {
|
|
104
|
+
try {
|
|
105
|
+
if (Buffer.byteLength(line, "utf8") > MAX_AUDIT_LINE_BYTES || !line.endsWith("\n") || line.slice(0, -1).includes("\n")) return false;
|
|
106
|
+
const root = await io.realpath(cwd);
|
|
107
|
+
const statePath = resolve2(root, AUDIT_STATE_DIRECTORY);
|
|
108
|
+
const stateInfo = await io.lstat(statePath);
|
|
109
|
+
if (!stateInfo.isDirectory() || stateInfo.isSymbolicLink()) return false;
|
|
110
|
+
const state = await io.realpath(statePath);
|
|
111
|
+
const stateRelative = relative(root, state);
|
|
112
|
+
if (stateRelative === "" || !lexicallyInside(state, root) || resolve2(root, stateRelative) !== state) return false;
|
|
113
|
+
const stateIdentity = await io.lstat(state);
|
|
114
|
+
if (!sameAuditDirectory(stateInfo, stateIdentity)) return false;
|
|
115
|
+
const stateIsCurrent = async () => {
|
|
116
|
+
try {
|
|
117
|
+
return await io.realpath(statePath) === state && sameAuditDirectory(stateIdentity, await io.lstat(statePath));
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
if (!await stateIsCurrent()) return false;
|
|
123
|
+
const target = resolve2(state, AUDIT_LOG_NAME);
|
|
124
|
+
const opened = await openedAuditTarget(target, io);
|
|
125
|
+
if (opened === void 0) return false;
|
|
126
|
+
const { handle, identity: identity2 } = opened;
|
|
127
|
+
try {
|
|
128
|
+
const before = await handle.stat();
|
|
129
|
+
const beforePath = await io.lstat(target);
|
|
130
|
+
if (!sameAuditFile(identity2, before) || !sameAuditFile(before, beforePath) || !await stateIsCurrent()) return false;
|
|
131
|
+
await handle.appendFile(line, { encoding: "utf8" });
|
|
132
|
+
await handle.sync();
|
|
133
|
+
const after = await handle.stat();
|
|
134
|
+
const afterPath = await io.lstat(target);
|
|
135
|
+
if (!sameAuditFile(before, after) || !sameAuditFile(after, afterPath) || after.size < before.size + Buffer.byteLength(line, "utf8") || !await stateIsCurrent()) return false;
|
|
136
|
+
} finally {
|
|
137
|
+
await handle.close();
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function appendAuditLine(cwd, line) {
|
|
145
|
+
return await appendAuditLineWithIo(cwd, line, NODE_AUDIT_SINK_IO);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/redaction.ts
|
|
149
|
+
import { randomBytes } from "node:crypto";
|
|
150
|
+
|
|
151
|
+
// ../../ca/tools/redactor.ts
|
|
152
|
+
var SECRET_LINE = new RegExp(
|
|
153
|
+
[
|
|
154
|
+
"api[_-]?key",
|
|
155
|
+
"token",
|
|
156
|
+
"secret",
|
|
157
|
+
"password",
|
|
158
|
+
"BEGIN.*PRIVATE",
|
|
159
|
+
"sk-ant",
|
|
160
|
+
"sk-proj-[A-Za-z0-9_-]{8}",
|
|
161
|
+
"AKIA[0-9A-Z]{16}",
|
|
162
|
+
"ghp_[A-Za-z0-9]{36}",
|
|
163
|
+
"glpat-[A-Za-z0-9_-]{8}",
|
|
164
|
+
// basic auth in a URL: scheme://user:secret@host - the colon-and-at shape,
|
|
165
|
+
// not any "@", so `https://example.com/@scope/pkg` is untouched.
|
|
166
|
+
// NOTE the doubled backslashes: these are JS STRING literals, and "\\s" in
|
|
167
|
+
// a string is a literal "s". The first cut of this shipped `[^/\s:@]`,
|
|
168
|
+
// which silently became `[^/s:@]` and only matched by luck.
|
|
169
|
+
"https?://[^/\\s:@]+:[^/\\s@]+@",
|
|
170
|
+
// a bearer credential, which is dot-segmented base64url in practice
|
|
171
|
+
"Bearer\\s+[A-Za-z0-9_-]{10,}"
|
|
172
|
+
// DELIBERATELY NOT ADDED: a `-u user:pass` rule for curl. `-u 1000:1000`
|
|
173
|
+
// is an everyday docker/podman uid:gid pair, so the shape collides with
|
|
174
|
+
// benign input. Broad is the safe direction for this redactor, but not at
|
|
175
|
+
// the cost of firing on ordinary container arguments.
|
|
176
|
+
].join("|"),
|
|
177
|
+
"i"
|
|
178
|
+
);
|
|
179
|
+
var PEM_BEGIN = /^-----BEGIN .*-----\s*$/;
|
|
180
|
+
var PEM_END = /^-----END .*-----\s*$/;
|
|
181
|
+
var REDACTION_MARKER = "[REDACTED \u2014 secret-pattern match removed before transmission]";
|
|
182
|
+
function redactSecrets(contents) {
|
|
183
|
+
const lines = contents.split("\n");
|
|
184
|
+
const out = [];
|
|
185
|
+
for (let i = 0; i < lines.length; i++) {
|
|
186
|
+
const line = lines[i];
|
|
187
|
+
if (PEM_BEGIN.test(line.trim())) {
|
|
188
|
+
out.push(REDACTION_MARKER);
|
|
189
|
+
i++;
|
|
190
|
+
while (i < lines.length && !PEM_END.test(lines[i].trim())) i++;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
out.push(SECRET_LINE.test(line) ? REDACTION_MARKER : line);
|
|
194
|
+
}
|
|
195
|
+
return out.join("\n");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/redaction.ts
|
|
199
|
+
function redactSecrets2(value) {
|
|
200
|
+
return redactSecrets(value);
|
|
201
|
+
}
|
|
202
|
+
var MIN_BENIGN_LITERAL_LENGTH = 8;
|
|
203
|
+
function maskBenign(value, benign) {
|
|
204
|
+
const literals = [...new Set(benign)].filter((literal) => literal.length >= MIN_BENIGN_LITERAL_LENGTH).sort((a, b) => b.length - a.length);
|
|
205
|
+
const identity2 = { masked: value, restore: (text) => text };
|
|
206
|
+
if (literals.length === 0) return identity2;
|
|
207
|
+
const nonce = randomBytes(8).toString("hex");
|
|
208
|
+
const captured = [];
|
|
209
|
+
let masked = value;
|
|
210
|
+
for (const literal of literals) {
|
|
211
|
+
const pattern = new RegExp(literal.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"), "gu");
|
|
212
|
+
masked = masked.replace(pattern, (match) => `${nonce}-${captured.push(match) - 1}`);
|
|
213
|
+
}
|
|
214
|
+
if (captured.length === 0) return identity2;
|
|
215
|
+
const placeholder = new RegExp(`${nonce}-(\\d+)`, "gu");
|
|
216
|
+
return {
|
|
217
|
+
masked,
|
|
218
|
+
restore: (text) => text.replace(placeholder, (whole, index) => captured[Number(index)] ?? whole)
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function safeDiagnostic(value, maxChars = 2e3, benignPaths = []) {
|
|
222
|
+
const { masked, restore } = maskBenign(value, benignPaths);
|
|
223
|
+
const normalized2 = restore(redactSecrets2(masked)).replace(/\r\n?/gu, "\n").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, "\uFFFD").trim();
|
|
224
|
+
return normalized2.length <= maxChars ? normalized2 : `${normalized2.slice(0, maxChars)}\u2026`;
|
|
225
|
+
}
|
|
226
|
+
function redactJson(value, depth = 0) {
|
|
227
|
+
if (depth > 32) return "[REDACTED OVERSIZE VALUE]";
|
|
228
|
+
if (typeof value === "string") return safeDiagnostic(value, 16e3);
|
|
229
|
+
if (Array.isArray(value)) return value.map((item) => redactJson(item, depth + 1));
|
|
230
|
+
if (value !== null && typeof value === "object") {
|
|
231
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactJson(item, depth + 1)]));
|
|
232
|
+
}
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/bridge.ts
|
|
237
|
+
var RESPONSE_KEYS = /* @__PURE__ */ new Set(["version", "outcome", "ruleId", "message", "context", "resultPatch", "auditCode"]);
|
|
238
|
+
var OUTCOMES = /* @__PURE__ */ new Set(["allow", "block", "warn", "notice"]);
|
|
239
|
+
var PI_MAX_HOME_CHARS = 32768;
|
|
240
|
+
var CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/u;
|
|
241
|
+
function minimalEnvironment(identities, userHome) {
|
|
242
|
+
const allowed = ["SystemRoot", "WINDIR", "TEMP", "TMP"];
|
|
243
|
+
const env = { PYTHONIOENCODING: "utf-8", PYTHONUTF8: "1" };
|
|
244
|
+
for (const key of allowed) if (process.env[key] !== void 0) env[key] = process.env[key];
|
|
245
|
+
if (identities !== void 0) {
|
|
246
|
+
const pathApi = process.platform === "win32" ? win322 : posix2;
|
|
247
|
+
const searchDirectories = /* @__PURE__ */ new Set([
|
|
248
|
+
pathApi.dirname(identities.git),
|
|
249
|
+
pathApi.dirname(identities.python)
|
|
250
|
+
]);
|
|
251
|
+
const systemRoot = process.env.SystemRoot ?? process.env.WINDIR;
|
|
252
|
+
if (process.platform === "win32" && systemRoot !== void 0 && win322.isAbsolute(systemRoot)) {
|
|
253
|
+
searchDirectories.add(win322.join(systemRoot, "System32"));
|
|
254
|
+
}
|
|
255
|
+
env.PATH = [...searchDirectories].join(process.platform === "win32" ? ";" : ":");
|
|
256
|
+
env.CODEARBITER_GIT_EXECUTABLE = identities.git;
|
|
257
|
+
env.CODEARBITER_PYTHON_EXECUTABLE = identities.python;
|
|
258
|
+
}
|
|
259
|
+
if (userHome !== void 0) {
|
|
260
|
+
if (process.platform === "win32") env.USERPROFILE = userHome;
|
|
261
|
+
else env.HOME = userHome;
|
|
262
|
+
}
|
|
263
|
+
return env;
|
|
264
|
+
}
|
|
265
|
+
function canonicalExecutable(candidate, platform) {
|
|
266
|
+
const pathApi = platform === "win32" ? win322 : posix2;
|
|
267
|
+
if (!pathApi.isAbsolute(candidate)) return void 0;
|
|
268
|
+
try {
|
|
269
|
+
const canonical = realpathSync(candidate);
|
|
270
|
+
if (!statSync(canonical).isFile()) return void 0;
|
|
271
|
+
if (platform !== "win32") accessSync(canonical, constants.X_OK);
|
|
272
|
+
return canonical;
|
|
273
|
+
} catch {
|
|
274
|
+
return void 0;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async function canonicalUserHome(projectRoot, packageRoot, platform = process.platform) {
|
|
278
|
+
const pathApi = platform === "win32" ? win322 : posix2;
|
|
279
|
+
const candidate = platform === "win32" ? process.env.USERPROFILE : process.env.HOME;
|
|
280
|
+
if (typeof candidate !== "string" || candidate.length < 1 || candidate.length > PI_MAX_HOME_CHARS || candidate !== candidate.trim() || CONTROL_RE.test(candidate) || !pathApi.isAbsolute(candidate)) return void 0;
|
|
281
|
+
try {
|
|
282
|
+
const canonical = await realpath2(candidate);
|
|
283
|
+
if (!statSync(canonical).isDirectory() || lexicallyInside(canonical, projectRoot, flavorForPlatform(platform)) || lexicallyInside(canonical, packageRoot, flavorForPlatform(platform))) return void 0;
|
|
284
|
+
return canonical;
|
|
285
|
+
} catch {
|
|
286
|
+
return void 0;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function trustedPathCandidate(basename, projectCwd, platform, pathValue) {
|
|
290
|
+
const pathApi = platform === "win32" ? win322 : posix2;
|
|
291
|
+
const separator = platform === "win32" ? ";" : ":";
|
|
292
|
+
const canonicalProject = canonicalExecutable(projectCwd, platform) ?? (() => {
|
|
293
|
+
try {
|
|
294
|
+
return realpathSync(projectCwd);
|
|
295
|
+
} catch {
|
|
296
|
+
return pathApi.resolve(projectCwd);
|
|
297
|
+
}
|
|
298
|
+
})();
|
|
299
|
+
for (const rawEntry of pathValue.split(separator)) {
|
|
300
|
+
const entry = rawEntry.trim();
|
|
301
|
+
if (entry === "" || !pathApi.isAbsolute(entry)) continue;
|
|
302
|
+
let directory;
|
|
303
|
+
try {
|
|
304
|
+
directory = realpathSync(entry);
|
|
305
|
+
} catch {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const candidate = canonicalExecutable(pathApi.join(directory, basename), platform);
|
|
309
|
+
if (candidate !== void 0 && !lexicallyInside(candidate, canonicalProject, flavorForPlatform(platform))) return candidate;
|
|
310
|
+
}
|
|
311
|
+
return void 0;
|
|
312
|
+
}
|
|
313
|
+
function resolveGitExecutable(projectCwd, platform = process.platform, pathValue = process.env.PATH ?? "") {
|
|
314
|
+
const name = platform === "win32" ? "git.exe" : "git";
|
|
315
|
+
const executable = trustedPathCandidate(name, projectCwd, platform, pathValue);
|
|
316
|
+
if (executable === void 0) {
|
|
317
|
+
throw new Error("codeArbiter could not resolve an absolute trusted Git executable; run /ca-doctor.");
|
|
318
|
+
}
|
|
319
|
+
return executable;
|
|
320
|
+
}
|
|
321
|
+
function windowsTaskkillExecutable() {
|
|
322
|
+
if (process.platform !== "win32") return void 0;
|
|
323
|
+
const systemRoot = process.env.SystemRoot ?? process.env.WINDIR;
|
|
324
|
+
if (systemRoot === void 0 || !win322.isAbsolute(systemRoot)) return void 0;
|
|
325
|
+
const candidate = canonicalExecutable(win322.join(systemRoot, "System32", "taskkill.exe"), "win32");
|
|
326
|
+
if (candidate === void 0) return void 0;
|
|
327
|
+
let canonicalRoot;
|
|
328
|
+
try {
|
|
329
|
+
canonicalRoot = realpathSync(systemRoot);
|
|
330
|
+
} catch {
|
|
331
|
+
return void 0;
|
|
332
|
+
}
|
|
333
|
+
return lexicallyInside(candidate, canonicalRoot, "win32") ? candidate : void 0;
|
|
334
|
+
}
|
|
335
|
+
function validResponse(value) {
|
|
336
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
337
|
+
const record = value;
|
|
338
|
+
if (Object.keys(record).some((key) => !RESPONSE_KEYS.has(key))) return false;
|
|
339
|
+
if (record.version !== 1 || typeof record.outcome !== "string" || !OUTCOMES.has(record.outcome)) return false;
|
|
340
|
+
for (const key of ["ruleId", "message", "context", "auditCode"]) {
|
|
341
|
+
if (record[key] !== void 0 && typeof record[key] !== "string") return false;
|
|
342
|
+
}
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
var WINDOWS_TASKKILL_TIMEOUT_MS = 1e3;
|
|
346
|
+
var KILL_SETTLE_DEADLINE_MS = 2e3;
|
|
347
|
+
function killTree(child, taskkillExecutable) {
|
|
348
|
+
if (child.pid === void 0) return;
|
|
349
|
+
if (process.platform === "win32") {
|
|
350
|
+
if (taskkillExecutable === void 0) {
|
|
351
|
+
child.kill("SIGKILL");
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
void killWindowsTree(child, taskkillExecutable);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
process.kill(-child.pid, "SIGKILL");
|
|
359
|
+
} catch {
|
|
360
|
+
child.kill("SIGKILL");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function killWindowsTree(child, taskkillExecutable) {
|
|
364
|
+
return new Promise((resolveKill) => {
|
|
365
|
+
let settled = false;
|
|
366
|
+
const finish = () => {
|
|
367
|
+
if (settled) return;
|
|
368
|
+
settled = true;
|
|
369
|
+
resolveKill();
|
|
370
|
+
};
|
|
371
|
+
let helper;
|
|
372
|
+
try {
|
|
373
|
+
helper = spawn(taskkillExecutable, ["/pid", String(child.pid), "/t", "/f"], {
|
|
374
|
+
env: minimalEnvironment(),
|
|
375
|
+
shell: false,
|
|
376
|
+
stdio: "ignore",
|
|
377
|
+
windowsHide: true
|
|
378
|
+
});
|
|
379
|
+
} catch {
|
|
380
|
+
child.kill("SIGKILL");
|
|
381
|
+
finish();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const timer = setTimeout(() => {
|
|
385
|
+
try {
|
|
386
|
+
helper.kill("SIGKILL");
|
|
387
|
+
} catch {
|
|
388
|
+
}
|
|
389
|
+
child.kill("SIGKILL");
|
|
390
|
+
finish();
|
|
391
|
+
}, WINDOWS_TASKKILL_TIMEOUT_MS);
|
|
392
|
+
timer.unref?.();
|
|
393
|
+
helper.once("error", () => {
|
|
394
|
+
clearTimeout(timer);
|
|
395
|
+
child.kill("SIGKILL");
|
|
396
|
+
finish();
|
|
397
|
+
});
|
|
398
|
+
helper.once("close", (code) => {
|
|
399
|
+
clearTimeout(timer);
|
|
400
|
+
if (code !== 0) child.kill("SIGKILL");
|
|
401
|
+
finish();
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
var BRIDGE_CORRELATION_RE = /^[a-f0-9]{64}$/u;
|
|
406
|
+
function normalizedRequest(request) {
|
|
407
|
+
const { correlation, ...rest } = request;
|
|
408
|
+
return typeof correlation === "string" && BRIDGE_CORRELATION_RE.test(correlation) ? { ...rest, correlation } : rest;
|
|
409
|
+
}
|
|
410
|
+
function sanitizedResponse(response, request) {
|
|
411
|
+
return {
|
|
412
|
+
...response,
|
|
413
|
+
...response.ruleId === void 0 ? {} : { ruleId: safeDiagnostic(response.ruleId, 100) },
|
|
414
|
+
...response.message === void 0 ? {} : { message: safeDiagnostic(response.message) },
|
|
415
|
+
...response.context === void 0 ? {} : { context: safeDiagnostic(response.context, 16e3) },
|
|
416
|
+
...response.auditCode === void 0 ? {} : { auditCode: safeDiagnostic(response.auditCode, 100) },
|
|
417
|
+
...response.resultPatch === void 0 ? {} : {
|
|
418
|
+
resultPatch: request.event === "plan_file" ? response.resultPatch : redactJson(response.resultPatch)
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
var spawnImpl = spawn;
|
|
423
|
+
var BridgeClient = class {
|
|
424
|
+
constructor(options) {
|
|
425
|
+
this.options = options;
|
|
426
|
+
this.timeoutMs = options.timeoutMs ?? 1e4;
|
|
427
|
+
this.maxRequestBytes = options.maxRequestBytes ?? 262144;
|
|
428
|
+
this.maxStreamBytes = options.maxStreamBytes ?? 1048576;
|
|
429
|
+
this.ready = this.validatePaths();
|
|
430
|
+
this.ready.catch(() => void 0);
|
|
431
|
+
}
|
|
432
|
+
options;
|
|
433
|
+
ready;
|
|
434
|
+
timeoutMs;
|
|
435
|
+
maxRequestBytes;
|
|
436
|
+
maxStreamBytes;
|
|
437
|
+
async validatePaths() {
|
|
438
|
+
if (this.options.pythonExecutable === void 0 || this.options.gitExecutable === void 0) {
|
|
439
|
+
throw new Error("Python 3 or Git is unavailable");
|
|
440
|
+
}
|
|
441
|
+
if (!isAbsolute(this.options.pythonExecutable) || !isAbsolute(this.options.gitExecutable) || !isAbsolute(this.options.bridgeScript) || !isAbsolute(this.options.packageRoot)) {
|
|
442
|
+
throw new Error("bridge paths must be absolute");
|
|
443
|
+
}
|
|
444
|
+
const [git, python, script, root] = await Promise.all([
|
|
445
|
+
realpath2(this.options.gitExecutable),
|
|
446
|
+
realpath2(this.options.pythonExecutable),
|
|
447
|
+
realpath2(this.options.bridgeScript),
|
|
448
|
+
realpath2(this.options.packageRoot)
|
|
449
|
+
]);
|
|
450
|
+
if (canonicalExecutable(git, process.platform) === void 0 || canonicalExecutable(python, process.platform) === void 0) {
|
|
451
|
+
throw new Error("bridge executable identity is invalid");
|
|
452
|
+
}
|
|
453
|
+
if (!lexicallyInside(script, root)) throw new Error("bridge script is outside the installed package");
|
|
454
|
+
const taskkill = windowsTaskkillExecutable();
|
|
455
|
+
if (process.platform === "win32" && taskkill === void 0) throw new Error("taskkill is unavailable");
|
|
456
|
+
return { git, python, root, script, ...taskkill === void 0 ? {} : { taskkill } };
|
|
457
|
+
}
|
|
458
|
+
failure(request, detail) {
|
|
459
|
+
const category = this.options.toolClasses[request.tool ?? ""] ?? "OTHER";
|
|
460
|
+
const advisory = request.event !== "tool_call" || category === "READ";
|
|
461
|
+
return {
|
|
462
|
+
version: 1,
|
|
463
|
+
outcome: advisory ? "warn" : "block",
|
|
464
|
+
ruleId: "PI-BRIDGE",
|
|
465
|
+
message: `codeArbiter Pi bridge ${safeDiagnostic(detail)}; ${advisory ? "continuing advisory operation; " : "mutation blocked; "}run /ca-doctor.`,
|
|
466
|
+
auditCode: advisory ? "PI_BRIDGE_WARN" : "PI_BRIDGE_BLOCK"
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
async auditFailure(request, response, counts) {
|
|
470
|
+
try {
|
|
471
|
+
if (this.options.shouldAuditFailure?.(request) === false) return;
|
|
472
|
+
} catch {
|
|
473
|
+
}
|
|
474
|
+
const line = [
|
|
475
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}]`,
|
|
476
|
+
"HOST: pi",
|
|
477
|
+
`RULE: ${response.ruleId ?? "PI-BRIDGE"}`,
|
|
478
|
+
`AUDIT: ${response.auditCode ?? "PI_BRIDGE_FAILURE"}`,
|
|
479
|
+
`CORRELATION: ${request.correlation ?? randomUUID()}`,
|
|
480
|
+
`REQUEST_BYTES: ${counts.request}`,
|
|
481
|
+
`STDOUT_BYTES: ${counts.stdout}`,
|
|
482
|
+
`STDERR_BYTES: ${counts.stderr}`
|
|
483
|
+
].join(" | ") + "\n";
|
|
484
|
+
await appendAuditLine(request.cwd, line);
|
|
485
|
+
}
|
|
486
|
+
async failed(request, detail, counts = { request: 0, stdout: 0, stderr: 0 }) {
|
|
487
|
+
const response = this.failure(request, detail);
|
|
488
|
+
await this.auditFailure(request, response, counts);
|
|
489
|
+
return response;
|
|
490
|
+
}
|
|
491
|
+
async call(rawRequest, signal) {
|
|
492
|
+
const request = normalizedRequest(rawRequest);
|
|
493
|
+
let paths;
|
|
494
|
+
let userHome;
|
|
495
|
+
try {
|
|
496
|
+
paths = await this.ready;
|
|
497
|
+
} catch {
|
|
498
|
+
return await this.failed(request, "path validation failed");
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
const project = await realpath2(request.cwd);
|
|
502
|
+
if (lexicallyInside(paths.git, project) || lexicallyInside(paths.python, project)) {
|
|
503
|
+
return await this.failed(request, "path validation failed");
|
|
504
|
+
}
|
|
505
|
+
const canonicalHome = await canonicalUserHome(project, paths.root);
|
|
506
|
+
if (canonicalHome === void 0) return await this.failed(request, "path validation failed");
|
|
507
|
+
userHome = canonicalHome;
|
|
508
|
+
} catch {
|
|
509
|
+
return await this.failed(request, "path validation failed");
|
|
510
|
+
}
|
|
511
|
+
let body;
|
|
512
|
+
try {
|
|
513
|
+
body = Buffer.from(JSON.stringify(request), "utf8");
|
|
514
|
+
} catch {
|
|
515
|
+
return await this.failed(request, "request serialization failed");
|
|
516
|
+
}
|
|
517
|
+
if (body.byteLength > this.maxRequestBytes) return await this.failed(request, "request overflow", { request: body.byteLength, stdout: 0, stderr: 0 });
|
|
518
|
+
if (signal.aborted) return await this.failed(request, "cancelled", { request: body.byteLength, stdout: 0, stderr: 0 });
|
|
519
|
+
return await new Promise((resolveResponse) => {
|
|
520
|
+
let child;
|
|
521
|
+
try {
|
|
522
|
+
child = spawnImpl(paths.python, [...this.options.pythonPrefixArgs ?? [], paths.script], {
|
|
523
|
+
cwd: paths.root,
|
|
524
|
+
detached: process.platform !== "win32",
|
|
525
|
+
env: minimalEnvironment({ git: paths.git, python: paths.python }, userHome),
|
|
526
|
+
shell: false,
|
|
527
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
528
|
+
windowsHide: true
|
|
529
|
+
});
|
|
530
|
+
} catch {
|
|
531
|
+
void this.failed(
|
|
532
|
+
request,
|
|
533
|
+
"bridge launch failed",
|
|
534
|
+
{ request: body.byteLength, stdout: 0, stderr: 0 }
|
|
535
|
+
).then(resolveResponse, () => resolveResponse(this.failure(request, "bridge launch failed")));
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const stdout = [];
|
|
539
|
+
const stderr = [];
|
|
540
|
+
let stdoutBytes = 0;
|
|
541
|
+
let stderrBytes = 0;
|
|
542
|
+
let reason;
|
|
543
|
+
let settled = false;
|
|
544
|
+
let finishing = false;
|
|
545
|
+
let settleDeadline;
|
|
546
|
+
const finish = (response) => {
|
|
547
|
+
if (settled) return;
|
|
548
|
+
settled = true;
|
|
549
|
+
clearTimeout(timer);
|
|
550
|
+
if (settleDeadline !== void 0) clearTimeout(settleDeadline);
|
|
551
|
+
signal.removeEventListener("abort", abort);
|
|
552
|
+
resolveResponse(response);
|
|
553
|
+
};
|
|
554
|
+
const failAndKill = (value) => {
|
|
555
|
+
if (reason !== void 0) return;
|
|
556
|
+
reason = value;
|
|
557
|
+
killTree(child, paths.taskkill);
|
|
558
|
+
settleDeadline = setTimeout(() => finishFailure(value), KILL_SETTLE_DEADLINE_MS);
|
|
559
|
+
settleDeadline.unref?.();
|
|
560
|
+
};
|
|
561
|
+
const finishFailure = (detail) => {
|
|
562
|
+
if (settled || finishing) return;
|
|
563
|
+
finishing = true;
|
|
564
|
+
void this.failed(request, detail, { request: body.byteLength, stdout: stdoutBytes, stderr: stderrBytes }).then(finish, () => finish(this.failure(request, detail)));
|
|
565
|
+
};
|
|
566
|
+
const collect = (target, chunk, stream) => {
|
|
567
|
+
const count = stream === "stdout" ? stdoutBytes : stderrBytes;
|
|
568
|
+
const remaining = Math.max(0, this.maxStreamBytes - count);
|
|
569
|
+
if (remaining > 0) target.push(chunk.subarray(0, remaining));
|
|
570
|
+
if (stream === "stdout") stdoutBytes += chunk.byteLength;
|
|
571
|
+
else stderrBytes += chunk.byteLength;
|
|
572
|
+
if (count + chunk.byteLength > this.maxStreamBytes) failAndKill("protocol overflow");
|
|
573
|
+
};
|
|
574
|
+
const abort = () => failAndKill("cancelled");
|
|
575
|
+
const timer = setTimeout(() => failAndKill("timed out"), this.timeoutMs);
|
|
576
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
577
|
+
child.stdout.on("data", (chunk) => collect(stdout, chunk, "stdout"));
|
|
578
|
+
child.stderr.on("data", (chunk) => collect(stderr, chunk, "stderr"));
|
|
579
|
+
child.on("error", () => finishFailure("bridge launch failed"));
|
|
580
|
+
child.on("close", (code) => {
|
|
581
|
+
if (reason !== void 0) return finishFailure(reason);
|
|
582
|
+
if (code !== 0) return finishFailure("bridge process failed");
|
|
583
|
+
const stdoutText = Buffer.concat(stdout).toString("utf8");
|
|
584
|
+
let parsed;
|
|
585
|
+
try {
|
|
586
|
+
parsed = JSON.parse(stdoutText);
|
|
587
|
+
} catch {
|
|
588
|
+
return finishFailure("returned malformed protocol");
|
|
589
|
+
}
|
|
590
|
+
if (!validResponse(parsed)) return finishFailure("returned malformed protocol");
|
|
591
|
+
finish(sanitizedResponse(parsed, request));
|
|
592
|
+
});
|
|
593
|
+
child.stdin.on("error", () => void 0);
|
|
594
|
+
child.stdin.end(body);
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
function systemPythonProbe(executable, prefixArgs, cwd) {
|
|
599
|
+
const probe = spawnSync(executable, [...prefixArgs, "-c", "import sys; print(sys.version_info[0]); print(sys.executable)"], {
|
|
600
|
+
cwd,
|
|
601
|
+
encoding: "utf8",
|
|
602
|
+
env: minimalEnvironment(),
|
|
603
|
+
shell: false,
|
|
604
|
+
timeout: 2e3,
|
|
605
|
+
windowsHide: true
|
|
606
|
+
});
|
|
607
|
+
return { status: probe.status, stdout: probe.stdout ?? "", stderr: probe.stderr ?? "" };
|
|
608
|
+
}
|
|
609
|
+
function resolvePythonCommand(platform = process.platform, probe = systemPythonProbe, searchCwd, excludedProjectCwd, pathValue = process.env.PATH ?? "") {
|
|
610
|
+
const pathApi = platform === "win32" ? win322 : posix2;
|
|
611
|
+
const safeCwd = searchCwd ?? (platform === "win32" ? win322.parse(process.execPath).root : "/");
|
|
612
|
+
if (!pathApi.isAbsolute(safeCwd)) {
|
|
613
|
+
throw new Error("codeArbiter Python search cwd must be absolute; run /ca-doctor.");
|
|
614
|
+
}
|
|
615
|
+
const candidates = platform === "win32" ? [["py.exe", ["-3"]], ["python.exe", []], ["python3.exe", []]] : [["python3", []], ["python", []]];
|
|
616
|
+
for (const [candidate, prefixArgs] of candidates) {
|
|
617
|
+
const probedCandidate = probe === systemPythonProbe ? trustedPathCandidate(candidate, excludedProjectCwd ?? safeCwd, platform, pathValue) : candidate.replace(/\.exe$/u, "");
|
|
618
|
+
if (probedCandidate === void 0) continue;
|
|
619
|
+
const result = probe(probedCandidate, prefixArgs, safeCwd);
|
|
620
|
+
const lines = result.stdout.trim().split(/\r?\n/u);
|
|
621
|
+
const executable = lines[1] ?? "";
|
|
622
|
+
const absolute = platform === "win32" ? win322.isAbsolute(executable) : posix2.isAbsolute(executable);
|
|
623
|
+
const canonical = absolute ? probe === systemPythonProbe ? canonicalExecutable(executable, platform) : executable : void 0;
|
|
624
|
+
if (result.status === 0 && lines[0] === "3" && canonical !== void 0 && (probe !== systemPythonProbe || !lexicallyInside(canonical, excludedProjectCwd ?? safeCwd, flavorForPlatform(platform)))) {
|
|
625
|
+
return { executable: canonical, prefixArgs: [] };
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
throw new Error("codeArbiter could not resolve an absolute Python interpreter; run /ca-doctor.");
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// src/compatibility.ts
|
|
632
|
+
var SUPPORTED_PI_VERSIONS = /* @__PURE__ */ new Set(["0.80.5", "0.80.10"]);
|
|
633
|
+
var MINIMUM_NODE = [22, 19, 0];
|
|
634
|
+
var SEMVER_PREFIX = /^(\d+)\.(\d+)\.(\d+)(?:$|[-+])/u;
|
|
635
|
+
function atLeast(version, minimum) {
|
|
636
|
+
const match = SEMVER_PREFIX.exec(version.replace(/^v/u, ""));
|
|
637
|
+
if (match === null) return false;
|
|
638
|
+
const actual = match.slice(1).map(Number);
|
|
639
|
+
for (let index = 0; index < minimum.length; index += 1) {
|
|
640
|
+
if (actual[index] > minimum[index]) return true;
|
|
641
|
+
if (actual[index] < minimum[index]) return false;
|
|
642
|
+
}
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
function compatibilityDirection(input) {
|
|
646
|
+
if (!SUPPORTED_PI_VERSIONS.has(input.piVersion)) {
|
|
647
|
+
return "codeArbiter requires Pi 0.80.5 or 0.80.10; install a supported Pi version and run /ca-doctor.";
|
|
648
|
+
}
|
|
649
|
+
if (!atLeast(input.nodeVersion, MINIMUM_NODE)) {
|
|
650
|
+
return "codeArbiter requires Node >=22.19.0 for Pi; upgrade Node and run /ca-doctor.";
|
|
651
|
+
}
|
|
652
|
+
if (input.pythonMajor !== 3) {
|
|
653
|
+
return "codeArbiter requires Python 3; install Python 3 and run /ca-doctor.";
|
|
654
|
+
}
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/runtime-resolver.ts
|
|
659
|
+
import { lstat as lstat2, readFile, realpath as realpath3 } from "node:fs/promises";
|
|
660
|
+
import { createRequire } from "node:module";
|
|
661
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, resolve as resolve4 } from "node:path";
|
|
662
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
663
|
+
var PI_RUNTIME_DIAGNOSIS = "codeArbiter could not validate the active Pi CLI runtime; start from the Pi CLI and run /ca-doctor.";
|
|
664
|
+
var trustedIdentities = /* @__PURE__ */ new WeakSet();
|
|
665
|
+
function fail(cause) {
|
|
666
|
+
throw new Error(PI_RUNTIME_DIAGNOSIS, cause === void 0 ? void 0 : { cause });
|
|
667
|
+
}
|
|
668
|
+
async function owningPackageRoot(file, expectedName) {
|
|
669
|
+
let cursor = dirname2(file);
|
|
670
|
+
while (true) {
|
|
671
|
+
const candidate = resolve4(cursor, "package.json");
|
|
672
|
+
try {
|
|
673
|
+
const manifest = JSON.parse(await readFile(candidate, "utf8"));
|
|
674
|
+
if (manifest.name !== expectedName) return fail();
|
|
675
|
+
const canonicalRoot = await realpath3(cursor);
|
|
676
|
+
if (!lexicallyInside(file, canonicalRoot) || !lexicallyInside(await realpath3(candidate), canonicalRoot)) return fail();
|
|
677
|
+
return canonicalRoot;
|
|
678
|
+
} catch (error) {
|
|
679
|
+
if (error.code !== "ENOENT") return fail(error);
|
|
680
|
+
}
|
|
681
|
+
const parent = dirname2(cursor);
|
|
682
|
+
if (parent === cursor) return fail();
|
|
683
|
+
cursor = parent;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function binTarget(manifest) {
|
|
687
|
+
if (typeof manifest.bin === "string") return manifest.bin;
|
|
688
|
+
if (manifest.bin !== null && typeof manifest.bin === "object") {
|
|
689
|
+
const value = manifest.bin.pi;
|
|
690
|
+
if (typeof value === "string") return value;
|
|
691
|
+
}
|
|
692
|
+
return fail();
|
|
693
|
+
}
|
|
694
|
+
function importTarget(manifest) {
|
|
695
|
+
if (manifest.exports === null || typeof manifest.exports !== "object") return fail();
|
|
696
|
+
const rootExport = manifest.exports["."];
|
|
697
|
+
if (typeof rootExport === "string") return rootExport;
|
|
698
|
+
if (rootExport !== null && typeof rootExport === "object") {
|
|
699
|
+
const value = rootExport.import;
|
|
700
|
+
if (typeof value === "string") return value;
|
|
701
|
+
}
|
|
702
|
+
return fail();
|
|
703
|
+
}
|
|
704
|
+
function identitiesMatch(left, right) {
|
|
705
|
+
return left.cliEntry === right.cliEntry && left.manifestPath === right.manifestPath && left.moduleEntry === right.moduleEntry && left.packageRoot === right.packageRoot && left.version === right.version;
|
|
706
|
+
}
|
|
707
|
+
async function resolvePiRuntimeIdentity(cliCandidate) {
|
|
708
|
+
try {
|
|
709
|
+
const activeAnchor = process.argv[1];
|
|
710
|
+
if (typeof activeAnchor !== "string" || activeAnchor.length === 0 || !isAbsolute2(activeAnchor)) return fail();
|
|
711
|
+
const canonicalAnchor = await realpath3(activeAnchor);
|
|
712
|
+
if (cliCandidate !== void 0) {
|
|
713
|
+
if (!isAbsolute2(cliCandidate) || await realpath3(cliCandidate) !== canonicalAnchor) return fail();
|
|
714
|
+
}
|
|
715
|
+
const shippedModule = await realpath3(fileURLToPath(import.meta.url));
|
|
716
|
+
const extensionPackageRoot = await owningPackageRoot(shippedModule, "@arbiterforge/ca-pi");
|
|
717
|
+
let cursor = dirname2(canonicalAnchor);
|
|
718
|
+
let manifest;
|
|
719
|
+
let manifestPath = "";
|
|
720
|
+
while (true) {
|
|
721
|
+
const candidate = resolve4(cursor, "package.json");
|
|
722
|
+
try {
|
|
723
|
+
manifest = JSON.parse(await readFile(candidate, "utf8"));
|
|
724
|
+
manifestPath = candidate;
|
|
725
|
+
break;
|
|
726
|
+
} catch (error) {
|
|
727
|
+
if (error.code !== "ENOENT") return fail(error);
|
|
728
|
+
}
|
|
729
|
+
const parent = dirname2(cursor);
|
|
730
|
+
if (parent === cursor) return fail();
|
|
731
|
+
cursor = parent;
|
|
732
|
+
}
|
|
733
|
+
if (manifest.name !== "@earendil-works/pi-coding-agent" || typeof manifest.version !== "string") return fail();
|
|
734
|
+
const packageRoot = await realpath3(cursor);
|
|
735
|
+
const canonicalManifest = await realpath3(manifestPath);
|
|
736
|
+
if (!lexicallyInside(canonicalAnchor, packageRoot) || !lexicallyInside(canonicalManifest, packageRoot)) return fail();
|
|
737
|
+
if (lexicallyInside(packageRoot, extensionPackageRoot)) return fail();
|
|
738
|
+
const declaredBin = resolve4(packageRoot, binTarget(manifest));
|
|
739
|
+
if (!lexicallyInside(declaredBin, packageRoot) || await realpath3(declaredBin) !== canonicalAnchor) return fail();
|
|
740
|
+
if (!(await lstat2(canonicalAnchor)).isFile()) return fail();
|
|
741
|
+
const declaredExport = importTarget(manifest);
|
|
742
|
+
if (!declaredExport.startsWith("./")) return fail();
|
|
743
|
+
const requireFromPi = createRequire(resolve4(packageRoot, "package.json"));
|
|
744
|
+
const moduleEntry = await realpath3(requireFromPi.resolve(declaredExport));
|
|
745
|
+
if (!lexicallyInside(moduleEntry, packageRoot)) return fail();
|
|
746
|
+
if (!(await lstat2(moduleEntry)).isFile()) return fail();
|
|
747
|
+
const identity2 = Object.freeze({
|
|
748
|
+
cliEntry: canonicalAnchor,
|
|
749
|
+
manifestPath: canonicalManifest,
|
|
750
|
+
moduleEntry,
|
|
751
|
+
packageRoot,
|
|
752
|
+
version: manifest.version
|
|
753
|
+
});
|
|
754
|
+
trustedIdentities.add(identity2);
|
|
755
|
+
return identity2;
|
|
756
|
+
} catch (error) {
|
|
757
|
+
if (error instanceof Error && error.message === PI_RUNTIME_DIAGNOSIS) throw error;
|
|
758
|
+
return fail(error);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
async function loadPiRuntime(identity2) {
|
|
762
|
+
try {
|
|
763
|
+
if (!trustedIdentities.has(identity2)) return fail();
|
|
764
|
+
const beforeImport = await resolvePiRuntimeIdentity(identity2.cliEntry);
|
|
765
|
+
if (!identitiesMatch(identity2, beforeImport)) return fail();
|
|
766
|
+
const runtime = await import(pathToFileURL(identity2.moduleEntry).href);
|
|
767
|
+
const requiredFunctions = [
|
|
768
|
+
"getAgentDir",
|
|
769
|
+
"createBashToolDefinition",
|
|
770
|
+
"createWriteToolDefinition",
|
|
771
|
+
"createEditToolDefinition",
|
|
772
|
+
"createReadToolDefinition"
|
|
773
|
+
];
|
|
774
|
+
if (runtime.VERSION !== identity2.version || typeof runtime.ModelRegistry !== "function" || typeof runtime.SettingsManager !== "function" || requiredFunctions.some((name) => typeof runtime[name] !== "function")) return fail();
|
|
775
|
+
const afterImport = await resolvePiRuntimeIdentity(identity2.cliEntry);
|
|
776
|
+
if (!identitiesMatch(identity2, afterImport)) return fail();
|
|
777
|
+
return {
|
|
778
|
+
cliEntry: identity2.cliEntry,
|
|
779
|
+
moduleEntry: identity2.moduleEntry,
|
|
780
|
+
packageRoot: identity2.packageRoot,
|
|
781
|
+
version: identity2.version,
|
|
782
|
+
ModelRegistry: runtime.ModelRegistry,
|
|
783
|
+
SettingsManager: runtime.SettingsManager,
|
|
784
|
+
getAgentDir: runtime.getAgentDir,
|
|
785
|
+
createBashToolDefinition: runtime.createBashToolDefinition,
|
|
786
|
+
createWriteToolDefinition: runtime.createWriteToolDefinition,
|
|
787
|
+
createEditToolDefinition: runtime.createEditToolDefinition,
|
|
788
|
+
createReadToolDefinition: runtime.createReadToolDefinition
|
|
789
|
+
};
|
|
790
|
+
} catch (error) {
|
|
791
|
+
if (error instanceof Error && error.message === PI_RUNTIME_DIAGNOSIS) throw error;
|
|
792
|
+
return fail(error);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/tool-guard.ts
|
|
797
|
+
import { createHash as createHash4, randomUUID as randomUUID2 } from "node:crypto";
|
|
798
|
+
import { realpathSync as realpathSync2 } from "node:fs";
|
|
799
|
+
import { types as utilTypes2 } from "node:util";
|
|
800
|
+
|
|
801
|
+
// src/notices.ts
|
|
802
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
803
|
+
var MAX_NOTICE_BYTES = 16e3;
|
|
804
|
+
var TRUNCATED = "\n[codeArbiter notice truncated]";
|
|
805
|
+
function normalized(value) {
|
|
806
|
+
return redactSecrets2(value).replace(/\r\n?/gu, "\n").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, "\uFFFD").trim();
|
|
807
|
+
}
|
|
808
|
+
function identity(ruleId, value) {
|
|
809
|
+
const normalizedRule = normalized(ruleId ?? "context");
|
|
810
|
+
return createHash3("sha256").update(`${normalizedRule}\0${value}`, "utf8").digest("hex");
|
|
811
|
+
}
|
|
812
|
+
function owned(block, id) {
|
|
813
|
+
if (block === null || typeof block !== "object" || Array.isArray(block)) return false;
|
|
814
|
+
const value = block;
|
|
815
|
+
return value.type === "text" && value.codearbiter?.kind === "codearbiter-notice" && value.codearbiter.version === 1 && value.codearbiter.id === id && typeof value.text === "string" && value.text.includes(`:${id} -->`);
|
|
816
|
+
}
|
|
817
|
+
function truncateBody(prefix, body) {
|
|
818
|
+
if (Buffer.byteLength(prefix + body, "utf8") <= MAX_NOTICE_BYTES) return prefix + body;
|
|
819
|
+
const budget = MAX_NOTICE_BYTES - Buffer.byteLength(prefix + TRUNCATED, "utf8");
|
|
820
|
+
let kept = "";
|
|
821
|
+
let used = 0;
|
|
822
|
+
for (const character of body) {
|
|
823
|
+
const size = Buffer.byteLength(character, "utf8");
|
|
824
|
+
if (used + size > budget) break;
|
|
825
|
+
kept += character;
|
|
826
|
+
used += size;
|
|
827
|
+
}
|
|
828
|
+
return prefix + kept + TRUNCATED;
|
|
829
|
+
}
|
|
830
|
+
function applyToolResultNotice(event, response) {
|
|
831
|
+
if (response.outcome !== "notice" && response.outcome !== "warn") return void 0;
|
|
832
|
+
const raw = response.message ?? response.context;
|
|
833
|
+
if (typeof raw !== "string" || raw.length === 0) return void 0;
|
|
834
|
+
const body = normalized(raw);
|
|
835
|
+
if (body.length === 0) return void 0;
|
|
836
|
+
const id = identity(response.ruleId, body);
|
|
837
|
+
const original = Array.isArray(event.content) ? event.content : [];
|
|
838
|
+
if (original.some((block2) => owned(block2, id))) return void 0;
|
|
839
|
+
const prefix = `<!-- codearbiter:pi-tool-result:${id} -->
|
|
840
|
+
`;
|
|
841
|
+
const block = {
|
|
842
|
+
type: "text",
|
|
843
|
+
text: truncateBody(prefix, body),
|
|
844
|
+
codearbiter: { kind: "codearbiter-notice", version: 1, id }
|
|
845
|
+
};
|
|
846
|
+
return { content: [...original, block] };
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/policy.ts
|
|
850
|
+
import { types as utilTypes } from "node:util";
|
|
851
|
+
var POLICY_MODES = Object.freeze(["plan", "execute"]);
|
|
852
|
+
var POLICY_DECISIONS = Object.freeze(["allow", "ask", "deny"]);
|
|
853
|
+
var POLICY_ACTION_CLASSES = Object.freeze([
|
|
854
|
+
"read",
|
|
855
|
+
"inspection",
|
|
856
|
+
"source-write",
|
|
857
|
+
"source-edit",
|
|
858
|
+
"config-write",
|
|
859
|
+
"config-edit",
|
|
860
|
+
"planning-write",
|
|
861
|
+
"shell-mutation",
|
|
862
|
+
"dependency-change",
|
|
863
|
+
"network-side-effect",
|
|
864
|
+
"external-side-effect",
|
|
865
|
+
"background-launch",
|
|
866
|
+
"push",
|
|
867
|
+
"release"
|
|
868
|
+
]);
|
|
869
|
+
var POLICY_TABLE = Object.freeze({
|
|
870
|
+
plan: Object.freeze({
|
|
871
|
+
read: "allow",
|
|
872
|
+
inspection: "allow",
|
|
873
|
+
"source-write": "deny",
|
|
874
|
+
"source-edit": "deny",
|
|
875
|
+
"config-write": "deny",
|
|
876
|
+
"config-edit": "deny",
|
|
877
|
+
"planning-write": "allow",
|
|
878
|
+
"shell-mutation": "deny",
|
|
879
|
+
"dependency-change": "deny",
|
|
880
|
+
"network-side-effect": "deny",
|
|
881
|
+
"external-side-effect": "deny",
|
|
882
|
+
"background-launch": "deny",
|
|
883
|
+
push: "deny",
|
|
884
|
+
release: "deny"
|
|
885
|
+
}),
|
|
886
|
+
execute: Object.freeze({
|
|
887
|
+
read: "allow",
|
|
888
|
+
inspection: "allow",
|
|
889
|
+
"source-write": "ask",
|
|
890
|
+
"source-edit": "ask",
|
|
891
|
+
"config-write": "ask",
|
|
892
|
+
"config-edit": "ask",
|
|
893
|
+
"planning-write": "ask",
|
|
894
|
+
"shell-mutation": "ask",
|
|
895
|
+
"dependency-change": "ask",
|
|
896
|
+
"network-side-effect": "ask",
|
|
897
|
+
"external-side-effect": "ask",
|
|
898
|
+
"background-launch": "ask",
|
|
899
|
+
push: "ask",
|
|
900
|
+
release: "ask"
|
|
901
|
+
})
|
|
902
|
+
});
|
|
903
|
+
var POLICY_CONSEQUENCES = Object.freeze({
|
|
904
|
+
read: "Read project or session data.",
|
|
905
|
+
inspection: "Inspect operational state without mutation.",
|
|
906
|
+
"source-write": "Write source files.",
|
|
907
|
+
"source-edit": "Edit source files.",
|
|
908
|
+
"config-write": "Write configuration files.",
|
|
909
|
+
"config-edit": "Edit configuration files.",
|
|
910
|
+
"planning-write": "Write the active plan's governed planning files.",
|
|
911
|
+
"shell-mutation": "Run a mutating shell operation.",
|
|
912
|
+
"dependency-change": "Change project dependencies.",
|
|
913
|
+
"network-side-effect": "Perform a network side effect.",
|
|
914
|
+
"external-side-effect": "Perform an external side effect.",
|
|
915
|
+
"background-launch": "Launch a session-scoped background process.",
|
|
916
|
+
push: "Push repository state.",
|
|
917
|
+
release: "Create or publish a release."
|
|
918
|
+
});
|
|
919
|
+
var MODES = new Set(POLICY_MODES);
|
|
920
|
+
var ACTIONS = new Set(POLICY_ACTION_CLASSES);
|
|
921
|
+
var CATEGORIES = /* @__PURE__ */ new Set(["EXEC", "WRITE", "EDIT", "READ", "OTHER"]);
|
|
922
|
+
var SURFACE_NAME = /^[a-z][a-z0-9_-]{0,127}$/u;
|
|
923
|
+
var CONTROL = /[\u0000-\u001f\u007f-\u009f\u061c\u200b-\u200f\u202a-\u202e\u2060-\u206f\ufeff]/gu;
|
|
924
|
+
var DESCRIPTOR_ENTRY_LIMIT = 128;
|
|
925
|
+
var REQUEST_ACTION_LIMIT = 32;
|
|
926
|
+
var CWD_CODE_POINT_LIMIT = 256;
|
|
927
|
+
var CWD_BYTE_LIMIT = 512;
|
|
928
|
+
var actions = (...values) => Object.freeze(values);
|
|
929
|
+
var TOOL_ACTIONS = Object.freeze({
|
|
930
|
+
READ: actions("read"),
|
|
931
|
+
WRITE: actions("source-write", "config-write"),
|
|
932
|
+
EDIT: actions("source-edit", "config-edit"),
|
|
933
|
+
EXEC: actions(
|
|
934
|
+
"inspection",
|
|
935
|
+
"shell-mutation",
|
|
936
|
+
"dependency-change",
|
|
937
|
+
"network-side-effect",
|
|
938
|
+
"external-side-effect",
|
|
939
|
+
"background-launch",
|
|
940
|
+
"push",
|
|
941
|
+
"release"
|
|
942
|
+
),
|
|
943
|
+
OTHER: actions()
|
|
944
|
+
});
|
|
945
|
+
var ALLOW = Object.freeze({ decision: "allow" });
|
|
946
|
+
var DENY = Object.freeze({ decision: "deny" });
|
|
947
|
+
var COMPILED = /* @__PURE__ */ new WeakSet();
|
|
948
|
+
function plainDataRecord(value) {
|
|
949
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || utilTypes.isProxy(value)) return void 0;
|
|
950
|
+
if (Object.getPrototypeOf(value) !== Object.prototype) return void 0;
|
|
951
|
+
const keys = Reflect.ownKeys(value);
|
|
952
|
+
if (keys.some((key) => typeof key !== "string")) return void 0;
|
|
953
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
954
|
+
for (const key of keys) {
|
|
955
|
+
const descriptor = descriptors[key];
|
|
956
|
+
if (descriptor === void 0 || !("value" in descriptor) || descriptor.enumerable !== true) return void 0;
|
|
957
|
+
}
|
|
958
|
+
return Object.freeze({ descriptors, keys: Object.freeze(keys) });
|
|
959
|
+
}
|
|
960
|
+
function exactKeys(record, expected) {
|
|
961
|
+
return record.keys.length === expected.length && expected.every((key) => record.keys.includes(key));
|
|
962
|
+
}
|
|
963
|
+
function compileMapping(value, validValue) {
|
|
964
|
+
const record = plainDataRecord(value);
|
|
965
|
+
if (record === void 0 || record.keys.length > DESCRIPTOR_ENTRY_LIMIT) return void 0;
|
|
966
|
+
const output = /* @__PURE__ */ Object.create(null);
|
|
967
|
+
for (const name of record.keys) {
|
|
968
|
+
const item = record.descriptors[name].value;
|
|
969
|
+
if (!SURFACE_NAME.test(name) || !validValue(item)) return void 0;
|
|
970
|
+
output[name] = item;
|
|
971
|
+
}
|
|
972
|
+
return Object.freeze(output);
|
|
973
|
+
}
|
|
974
|
+
function compilePermissionPolicyDescriptor(raw) {
|
|
975
|
+
try {
|
|
976
|
+
const record = plainDataRecord(raw);
|
|
977
|
+
if (record === void 0 || !exactKeys(record, ["toolClasses", "actionClasses"])) return void 0;
|
|
978
|
+
const toolClasses = compileMapping(
|
|
979
|
+
record.descriptors.toolClasses.value,
|
|
980
|
+
(candidate) => CATEGORIES.has(candidate)
|
|
981
|
+
);
|
|
982
|
+
const actionClasses = compileMapping(
|
|
983
|
+
record.descriptors.actionClasses.value,
|
|
984
|
+
(candidate) => ACTIONS.has(candidate)
|
|
985
|
+
);
|
|
986
|
+
if (toolClasses === void 0 || actionClasses === void 0) return void 0;
|
|
987
|
+
for (const [tool, exactAction] of Object.entries(actionClasses)) {
|
|
988
|
+
if (!hasOwn(toolClasses, tool)) continue;
|
|
989
|
+
const category = toolClasses[tool];
|
|
990
|
+
if (category === void 0 || !TOOL_ACTIONS[category].includes(exactAction)) return void 0;
|
|
991
|
+
}
|
|
992
|
+
const compiled = Object.freeze({ toolClasses, actionClasses });
|
|
993
|
+
COMPILED.add(compiled);
|
|
994
|
+
return compiled;
|
|
995
|
+
} catch {
|
|
996
|
+
return void 0;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
function hasOwn(value, key) {
|
|
1000
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
1001
|
+
}
|
|
1002
|
+
function boundedCwd(value) {
|
|
1003
|
+
const normalized2 = value.replace(CONTROL, " ").replace(/\s+/gu, " ").trim();
|
|
1004
|
+
const source = normalized2 === "" ? "(unknown working directory)" : normalized2;
|
|
1005
|
+
const points = Array.from(source);
|
|
1006
|
+
if (points.length <= CWD_CODE_POINT_LIMIT && Buffer.byteLength(source, "utf8") <= CWD_BYTE_LIMIT) return source;
|
|
1007
|
+
const kept = [];
|
|
1008
|
+
let bytes = 0;
|
|
1009
|
+
const ellipsisBytes = Buffer.byteLength("\u2026", "utf8");
|
|
1010
|
+
for (const point of points) {
|
|
1011
|
+
const pointBytes = Buffer.byteLength(point, "utf8");
|
|
1012
|
+
if (kept.length >= CWD_CODE_POINT_LIMIT - 1 || bytes + pointBytes + ellipsisBytes > CWD_BYTE_LIMIT) break;
|
|
1013
|
+
kept.push(point);
|
|
1014
|
+
bytes += pointBytes;
|
|
1015
|
+
}
|
|
1016
|
+
return `${kept.join("")}\u2026`;
|
|
1017
|
+
}
|
|
1018
|
+
function normalizeActions(value) {
|
|
1019
|
+
if (!Array.isArray(value) || utilTypes.isProxy(value)) return void 0;
|
|
1020
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1021
|
+
const lengthDescriptor = descriptors.length;
|
|
1022
|
+
const rawLength = lengthDescriptor !== void 0 && "value" in lengthDescriptor ? lengthDescriptor.value : void 0;
|
|
1023
|
+
if (lengthDescriptor === void 0 || !("value" in lengthDescriptor) || typeof rawLength !== "number" || !Number.isInteger(rawLength) || rawLength < 1 || rawLength > REQUEST_ACTION_LIMIT) return void 0;
|
|
1024
|
+
const length = rawLength;
|
|
1025
|
+
const keys = Reflect.ownKeys(value);
|
|
1026
|
+
if (keys.some((key) => typeof key === "symbol") || keys.length !== length + 1) return void 0;
|
|
1027
|
+
const present = /* @__PURE__ */ new Set();
|
|
1028
|
+
for (let index = 0; index < length; index += 1) {
|
|
1029
|
+
const descriptor = descriptors[String(index)];
|
|
1030
|
+
if (descriptor === void 0 || !("value" in descriptor) || descriptor.enumerable !== true || typeof descriptor.value !== "string" || !ACTIONS.has(descriptor.value)) return void 0;
|
|
1031
|
+
present.add(descriptor.value);
|
|
1032
|
+
}
|
|
1033
|
+
return Object.freeze(POLICY_ACTION_CLASSES.filter((action) => present.has(action)));
|
|
1034
|
+
}
|
|
1035
|
+
function normalizeRequest(raw) {
|
|
1036
|
+
const record = plainDataRecord(raw);
|
|
1037
|
+
if (record === void 0 || !exactKeys(record, ["mode", "tool", "actions", "cwd"])) return void 0;
|
|
1038
|
+
const mode = record.descriptors.mode.value;
|
|
1039
|
+
const tool = record.descriptors.tool.value;
|
|
1040
|
+
const cwd = record.descriptors.cwd.value;
|
|
1041
|
+
const actions2 = normalizeActions(record.descriptors.actions.value);
|
|
1042
|
+
if (typeof mode !== "string" || !MODES.has(mode) || typeof tool !== "string" || !SURFACE_NAME.test(tool) || typeof cwd !== "string" || actions2 === void 0) return void 0;
|
|
1043
|
+
return Object.freeze({ mode, tool, actions: actions2, cwd: boundedCwd(cwd) });
|
|
1044
|
+
}
|
|
1045
|
+
function descriptorAllows(descriptor, tool, action) {
|
|
1046
|
+
const exact = hasOwn(descriptor.actionClasses, tool) ? descriptor.actionClasses[tool] : void 0;
|
|
1047
|
+
if (action === "planning-write") return tool === "ca-plan" && exact === "planning-write";
|
|
1048
|
+
if (Object.values(descriptor.actionClasses).includes(action)) return exact === action;
|
|
1049
|
+
if (exact === action) return true;
|
|
1050
|
+
if (!hasOwn(descriptor.toolClasses, tool)) return false;
|
|
1051
|
+
const category = descriptor.toolClasses[tool];
|
|
1052
|
+
return category !== void 0 && TOOL_ACTIONS[category].includes(action);
|
|
1053
|
+
}
|
|
1054
|
+
function evaluatePolicy(descriptor, rawRequest) {
|
|
1055
|
+
try {
|
|
1056
|
+
if (descriptor === null || typeof descriptor !== "object" || utilTypes.isProxy(descriptor) || !COMPILED.has(descriptor)) return DENY;
|
|
1057
|
+
const request = normalizeRequest(rawRequest);
|
|
1058
|
+
if (request === void 0) return DENY;
|
|
1059
|
+
const ownedAction = hasOwn(descriptor.actionClasses, request.tool) ? descriptor.actionClasses[request.tool] : void 0;
|
|
1060
|
+
if (ownedAction !== void 0 && !request.actions.includes(ownedAction)) return DENY;
|
|
1061
|
+
let decision = "allow";
|
|
1062
|
+
let consequenceAction;
|
|
1063
|
+
for (const action of request.actions) {
|
|
1064
|
+
if (!descriptorAllows(descriptor, request.tool, action)) return DENY;
|
|
1065
|
+
const actionDecision = POLICY_TABLE[request.mode][action];
|
|
1066
|
+
if (actionDecision === "deny") return DENY;
|
|
1067
|
+
if (actionDecision === "ask") {
|
|
1068
|
+
decision = "ask";
|
|
1069
|
+
consequenceAction = action;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
if (decision === "allow") return ALLOW;
|
|
1073
|
+
if (consequenceAction === void 0) return DENY;
|
|
1074
|
+
return Object.freeze({
|
|
1075
|
+
decision: "ask",
|
|
1076
|
+
confirmation: Object.freeze({
|
|
1077
|
+
actionClasses: request.actions,
|
|
1078
|
+
cwd: request.cwd,
|
|
1079
|
+
consequence: POLICY_CONSEQUENCES[consequenceAction]
|
|
1080
|
+
})
|
|
1081
|
+
});
|
|
1082
|
+
} catch {
|
|
1083
|
+
return DENY;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// src/tool-guard.ts
|
|
1088
|
+
var STANDALONE_GENERATION = Object.freeze({});
|
|
1089
|
+
var CONFIRMATION_TITLE = "Allow governed operation?";
|
|
1090
|
+
var CONFIRMATION_TIMEOUT_MS = 6e4;
|
|
1091
|
+
var COMMAND_LIMIT = 8192;
|
|
1092
|
+
var SHELL_CONTROL = /[\r\n\u0000;&|<>`(){}]|\$\(/u;
|
|
1093
|
+
var DANGEROUS_INSPECTION_OPTION = /(?:^|\s)(?:--ext-diff|--textconv|--config-env|--output|--pre|--hostname-bin|--search-zip)(?:[=\s]|$)/iu;
|
|
1094
|
+
var CONFIG_PATH = /(?:^|\/)(?:\.codearbiter|\.github)(?:\/|$)|(?:^|\/)(?:package(?:-lock)?\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?|tsconfig(?:\.[^/]*)?\.json|pyproject\.toml|cargo\.toml|cargo\.lock|go\.mod|go\.sum|dockerfile|makefile|\.env(?:\.[^/]*)?|[^/]+\.(?:json|ya?ml|toml|ini|cfg|conf|lock))(?:$)/iu;
|
|
1095
|
+
var INSPECTION_COMMANDS = Object.freeze([
|
|
1096
|
+
/^(?:pwd|cd|ls|dir|Get-ChildItem|Get-Location)(?:\s+[^;&|<>`]*)?$/iu,
|
|
1097
|
+
/^(?:cat|type|Get-Content|head|tail|wc|where|where\.exe|Get-Command)(?:\s+[^;&|<>`]*)?$/iu,
|
|
1098
|
+
/^(?:rg|grep|findstr)(?![^\r\n]*(?:--pre|--hostname-bin|--search-zip))(?:\s+[^;&|<>`]*)?$/iu,
|
|
1099
|
+
/^git\s+(?:status(?:\s+[^;&|<>`]*)?|log(?=[^;&|<>`]*--no-ext-diff(?:\s|$))(?=[^;&|<>`]*--no-textconv(?:\s|$))(?:\s+[^;&|<>`]*)?|show(?=[^;&|<>`]*--no-ext-diff(?:\s|$))(?=[^;&|<>`]*--no-textconv(?:\s|$))(?:\s+[^;&|<>`]*)?|branch\s+--show-current|rev-parse(?:\s+[^;&|<>`]*)?|diff(?=[^;&|<>`]*--no-ext-diff(?:\s|$))(?=[^;&|<>`]*--no-textconv(?:\s|$))(?:\s+[^;&|<>`]*)?|add\s+[^;&|<>`]*--dry-run[^;&|<>`]*)$/iu
|
|
1100
|
+
]);
|
|
1101
|
+
var DEPENDENCY_COMMAND = /(?:^|\s)(?:npm\s+(?:i|install|ci|uninstall|update)|pnpm\s+(?:add|install|remove|update)|yarn\s+(?:add|install|remove|upgrade)|bun\s+(?:add|install|remove|update)|pip(?:3)?\s+(?:install|uninstall)|python(?:3)?\s+-m\s+pip\s+(?:install|uninstall)|cargo\s+(?:add|remove|update)|dotnet\s+(?:add|remove)\s+package)(?:\s|$)/iu;
|
|
1102
|
+
var NETWORK_COMMAND = /(?:^|\s)(?:curl|wget|Invoke-WebRequest|iwr|git\s+(?:push|pull|fetch)|gh\s+|npm\s+(?:i|install|ci|publish)|pnpm\s+(?:add|install)|yarn\s+(?:add|install)|bun\s+(?:add|install)|pip(?:3)?\s+install|python(?:3)?\s+-m\s+pip\s+install|cargo\s+(?:add|publish)|twine\s+upload)(?:\s|$)/iu;
|
|
1103
|
+
var EXTERNAL_COMMAND = /(?:^|\s)(?:curl|wget|Invoke-WebRequest|iwr|gh\s+|git\s+push|npm\s+(?:i|install|ci|publish)|pnpm\s+(?:add|install)|yarn\s+(?:add|install)|bun\s+(?:add|install)|pip(?:3)?\s+install|python(?:3)?\s+-m\s+pip\s+install|cargo\s+(?:add|publish)|twine\s+upload)(?:\s|$)/iu;
|
|
1104
|
+
var PUSH_COMMAND = /(?:^|\s)git\s+push(?:\s|$)/iu;
|
|
1105
|
+
var RELEASE_COMMAND = /(?:^|\s)(?:gh\s+release|npm\s+publish|cargo\s+publish|twine\s+upload|dotnet\s+nuget\s+push|git\s+tag)(?:\s|$)/iu;
|
|
1106
|
+
function hasOwn2(value, key) {
|
|
1107
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
1108
|
+
}
|
|
1109
|
+
function compileBuiltinPermissionPolicy(toolClasses, actionClasses) {
|
|
1110
|
+
return compilePermissionPolicyDescriptor({ toolClasses: { ...toolClasses }, actionClasses: { ...actionClasses } });
|
|
1111
|
+
}
|
|
1112
|
+
function orderedActions(values) {
|
|
1113
|
+
return Object.freeze(POLICY_ACTION_CLASSES.filter((value) => values.has(value)));
|
|
1114
|
+
}
|
|
1115
|
+
function normalizedPath(params) {
|
|
1116
|
+
const value = hasOwn2(params, "path") ? params.path : hasOwn2(params, "file_path") ? params.file_path : void 0;
|
|
1117
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 4096 || /[\u0000-\u001f\u007f]/u.test(value)) return void 0;
|
|
1118
|
+
return value.replace(/\\/gu, "/");
|
|
1119
|
+
}
|
|
1120
|
+
function classifyPermissionActions(descriptor, tool, params) {
|
|
1121
|
+
try {
|
|
1122
|
+
if (!hasOwn2(descriptor.toolClasses, tool)) return void 0;
|
|
1123
|
+
const category = descriptor.toolClasses[tool];
|
|
1124
|
+
if (category === "OTHER" || category === void 0) return void 0;
|
|
1125
|
+
const exact = hasOwn2(descriptor.actionClasses, tool) ? descriptor.actionClasses[tool] : void 0;
|
|
1126
|
+
const background = exact === "background-launch";
|
|
1127
|
+
if (exact !== void 0 && !background) return orderedActions(/* @__PURE__ */ new Set([exact]));
|
|
1128
|
+
if (category === "READ") return Object.freeze(["read"]);
|
|
1129
|
+
if (category === "WRITE" || category === "EDIT") {
|
|
1130
|
+
const path = normalizedPath(params);
|
|
1131
|
+
if (path === void 0) return void 0;
|
|
1132
|
+
const config = CONFIG_PATH.test(path);
|
|
1133
|
+
return Object.freeze([category === "WRITE" ? config ? "config-write" : "source-write" : config ? "config-edit" : "source-edit"]);
|
|
1134
|
+
}
|
|
1135
|
+
if (category !== "EXEC") return void 0;
|
|
1136
|
+
const command = hasOwn2(params, "command") ? params.command : void 0;
|
|
1137
|
+
if (typeof command !== "string" || command.length === 0 || command.length > COMMAND_LIMIT) return void 0;
|
|
1138
|
+
const normalized2 = command.trim().replace(/\s+/gu, " ");
|
|
1139
|
+
const labels = /* @__PURE__ */ new Set(["shell-mutation"]);
|
|
1140
|
+
if (background) labels.add("background-launch");
|
|
1141
|
+
if (normalized2 === "" || SHELL_CONTROL.test(command) || DANGEROUS_INSPECTION_OPTION.test(normalized2)) {
|
|
1142
|
+
return orderedActions(labels);
|
|
1143
|
+
}
|
|
1144
|
+
if (!background && INSPECTION_COMMANDS.some((pattern) => pattern.test(normalized2))) return Object.freeze(["inspection"]);
|
|
1145
|
+
if (DEPENDENCY_COMMAND.test(normalized2)) labels.add("dependency-change");
|
|
1146
|
+
if (NETWORK_COMMAND.test(normalized2)) labels.add("network-side-effect");
|
|
1147
|
+
if (EXTERNAL_COMMAND.test(normalized2)) labels.add("external-side-effect");
|
|
1148
|
+
if (PUSH_COMMAND.test(normalized2)) labels.add("push");
|
|
1149
|
+
if (RELEASE_COMMAND.test(normalized2)) labels.add("release");
|
|
1150
|
+
return orderedActions(labels);
|
|
1151
|
+
} catch {
|
|
1152
|
+
return void 0;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
function auditCorrelation(toolCallId) {
|
|
1156
|
+
return createHash4("sha256").update(toolCallId, "utf8").digest("hex");
|
|
1157
|
+
}
|
|
1158
|
+
var TRUST_WITHDRAWN_MESSAGE = "codeArbiter project trust is not affirmative; mutation blocked; run /trust in Pi, approve this project, then start a new session.";
|
|
1159
|
+
var LIFECYCLE_STALE_MESSAGE = "codeArbiter lifecycle changed while approval was pending; mutation blocked; run /ca-doctor.";
|
|
1160
|
+
function liveProjectTrust(context, source) {
|
|
1161
|
+
if (source === "not-required") return true;
|
|
1162
|
+
try {
|
|
1163
|
+
const probe = context?.isProjectTrusted;
|
|
1164
|
+
if (typeof probe === "function") return probe.call(context) === true;
|
|
1165
|
+
} catch {
|
|
1166
|
+
return false;
|
|
1167
|
+
}
|
|
1168
|
+
try {
|
|
1169
|
+
return source() === true;
|
|
1170
|
+
} catch {
|
|
1171
|
+
return false;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
function permissionAuditRow(toolCallId, toolClass, actionClasses, decision) {
|
|
1175
|
+
return Object.freeze({
|
|
1176
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1177
|
+
correlation: auditCorrelation(toolCallId),
|
|
1178
|
+
toolClass,
|
|
1179
|
+
actionClasses: Object.freeze([...actionClasses]),
|
|
1180
|
+
decision
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
function permissionAuditCodeRow(toolCallId, toolClass, auditCode) {
|
|
1184
|
+
return Object.freeze({
|
|
1185
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1186
|
+
correlation: auditCorrelation(toolCallId),
|
|
1187
|
+
toolClass,
|
|
1188
|
+
auditCode
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
function failTool(message) {
|
|
1192
|
+
throw new Error(safeDiagnostic(message));
|
|
1193
|
+
}
|
|
1194
|
+
function appendWarning(result, warning) {
|
|
1195
|
+
const content = Array.isArray(result.content) ? [...result.content] : [];
|
|
1196
|
+
if (!JSON.stringify(content).includes(warning)) content.push({ type: "text", text: warning });
|
|
1197
|
+
return { ...result, content };
|
|
1198
|
+
}
|
|
1199
|
+
function canonicalSnapshot(value, seen = /* @__PURE__ */ new Set(), depth = 0) {
|
|
1200
|
+
if (depth > 32) throw new TypeError("parameters exceed nesting limit");
|
|
1201
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
1202
|
+
if (typeof value === "number") {
|
|
1203
|
+
if (!Number.isFinite(value)) throw new TypeError("parameters contain a non-finite number");
|
|
1204
|
+
return value;
|
|
1205
|
+
}
|
|
1206
|
+
if (typeof value !== "object" || seen.has(value) || utilTypes2.isProxy(value)) throw new TypeError("parameters are not acyclic JSON");
|
|
1207
|
+
seen.add(value);
|
|
1208
|
+
try {
|
|
1209
|
+
if (Array.isArray(value)) {
|
|
1210
|
+
const descriptors2 = Object.getOwnPropertyDescriptors(value);
|
|
1211
|
+
const length = descriptors2.length?.value;
|
|
1212
|
+
if (!Number.isSafeInteger(length) || length < 0 || Reflect.ownKeys(value).length !== length + 1) {
|
|
1213
|
+
throw new TypeError("parameters contain a sparse or decorated array");
|
|
1214
|
+
}
|
|
1215
|
+
const output2 = [];
|
|
1216
|
+
for (let index = 0; index < length; index += 1) {
|
|
1217
|
+
const descriptor = descriptors2[String(index)];
|
|
1218
|
+
if (descriptor === void 0 || !("value" in descriptor) || descriptor.enumerable !== true) {
|
|
1219
|
+
throw new TypeError("parameters contain an accessor");
|
|
1220
|
+
}
|
|
1221
|
+
output2.push(canonicalSnapshot(descriptor.value, seen, depth + 1));
|
|
1222
|
+
}
|
|
1223
|
+
return Object.freeze(output2);
|
|
1224
|
+
}
|
|
1225
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1226
|
+
if (prototype !== Object.prototype && prototype !== null) throw new TypeError("parameters contain a non-plain object");
|
|
1227
|
+
const output = {};
|
|
1228
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1229
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
1230
|
+
if (typeof key !== "string") throw new TypeError("parameters contain a symbol key");
|
|
1231
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") throw new TypeError("parameters contain an unsafe key");
|
|
1232
|
+
const descriptor = descriptors[key];
|
|
1233
|
+
if (descriptor === void 0 || !("value" in descriptor) || descriptor.enumerable !== true) {
|
|
1234
|
+
throw new TypeError("parameters contain an accessor");
|
|
1235
|
+
}
|
|
1236
|
+
output[key] = canonicalSnapshot(descriptor.value, seen, depth + 1);
|
|
1237
|
+
}
|
|
1238
|
+
return Object.freeze(output);
|
|
1239
|
+
} finally {
|
|
1240
|
+
seen.delete(value);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
function matchesSnapshot(raw, snapshot, seen = /* @__PURE__ */ new Set(), depth = 0) {
|
|
1244
|
+
if (depth > 32) return false;
|
|
1245
|
+
if (snapshot === null || typeof snapshot !== "object") return Object.is(raw, snapshot);
|
|
1246
|
+
if (raw === null || typeof raw !== "object" || utilTypes2.isProxy(raw) || seen.has(raw)) return false;
|
|
1247
|
+
seen.add(raw);
|
|
1248
|
+
try {
|
|
1249
|
+
if (Array.isArray(snapshot)) {
|
|
1250
|
+
if (!Array.isArray(raw)) return false;
|
|
1251
|
+
const descriptors2 = Object.getOwnPropertyDescriptors(raw);
|
|
1252
|
+
if (descriptors2.length?.value !== snapshot.length || Reflect.ownKeys(raw).length !== snapshot.length + 1) return false;
|
|
1253
|
+
return snapshot.every((item, index) => {
|
|
1254
|
+
const descriptor = descriptors2[String(index)];
|
|
1255
|
+
return descriptor !== void 0 && "value" in descriptor && descriptor.enumerable === true && matchesSnapshot(descriptor.value, item, seen, depth + 1);
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
if (Array.isArray(raw) || Object.getPrototypeOf(raw) !== Object.prototype && Object.getPrototypeOf(raw) !== null) return false;
|
|
1259
|
+
const descriptors = Object.getOwnPropertyDescriptors(raw);
|
|
1260
|
+
const rawKeys = Reflect.ownKeys(raw);
|
|
1261
|
+
const snapshotKeys = Object.keys(snapshot);
|
|
1262
|
+
if (rawKeys.some((key) => typeof key !== "string") || rawKeys.length !== snapshotKeys.length) return false;
|
|
1263
|
+
return snapshotKeys.every((key) => {
|
|
1264
|
+
const descriptor = descriptors[key];
|
|
1265
|
+
return descriptor !== void 0 && "value" in descriptor && descriptor.enumerable === true && matchesSnapshot(descriptor.value, snapshot[key], seen, depth + 1);
|
|
1266
|
+
});
|
|
1267
|
+
} catch {
|
|
1268
|
+
return false;
|
|
1269
|
+
} finally {
|
|
1270
|
+
seen.delete(raw);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
function currentMode(getMode) {
|
|
1274
|
+
try {
|
|
1275
|
+
const mode = getMode();
|
|
1276
|
+
return mode === "execute" || mode === "plan" ? mode : void 0;
|
|
1277
|
+
} catch {
|
|
1278
|
+
return void 0;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
function registryOwns(pi, tool, wrapperSourcePath) {
|
|
1282
|
+
try {
|
|
1283
|
+
if (!pi.getActiveTools().includes(tool)) return false;
|
|
1284
|
+
const info = pi.getAllTools().find((candidate) => candidate.name === tool);
|
|
1285
|
+
return info !== void 0 && samePath(info.sourceInfo.path, wrapperSourcePath);
|
|
1286
|
+
} catch {
|
|
1287
|
+
return false;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function confirmationMessage(actions2, cwd, consequence) {
|
|
1291
|
+
return [
|
|
1292
|
+
`Action classes: ${actions2.join(", ")}`,
|
|
1293
|
+
`Working directory: ${cwd}`,
|
|
1294
|
+
`Consequence: ${consequence}`
|
|
1295
|
+
].join("\n");
|
|
1296
|
+
}
|
|
1297
|
+
function confirmationUi(context) {
|
|
1298
|
+
try {
|
|
1299
|
+
if (context?.mode !== "tui" || context.hasUI !== true) return void 0;
|
|
1300
|
+
const ui = context.ui;
|
|
1301
|
+
const confirm = ui?.confirm;
|
|
1302
|
+
return ui !== void 0 && typeof confirm === "function" ? Object.freeze({ ui, confirm }) : void 0;
|
|
1303
|
+
} catch {
|
|
1304
|
+
return void 0;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
function nativeSessionId(context) {
|
|
1308
|
+
const manager = context?.sessionManager;
|
|
1309
|
+
if (manager === void 0 || typeof manager.getSessionId !== "function") return void 0;
|
|
1310
|
+
try {
|
|
1311
|
+
const value = manager.getSessionId.call(manager);
|
|
1312
|
+
if (typeof value !== "string" || value.trim() === "" || value.length > 1024) return void 0;
|
|
1313
|
+
return value;
|
|
1314
|
+
} catch {
|
|
1315
|
+
return void 0;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
function fixedFallbackSessionId() {
|
|
1319
|
+
const fallback = randomUUID2();
|
|
1320
|
+
return (context) => nativeSessionId(context) ?? fallback;
|
|
1321
|
+
}
|
|
1322
|
+
function executionCwd(context, fallback) {
|
|
1323
|
+
return context !== null && typeof context === "object" && typeof context.cwd === "string" ? context.cwd : fallback;
|
|
1324
|
+
}
|
|
1325
|
+
function wrappedDefinition(pi, toolName, factory, nativeFactory, category, cwd, bridge, boundGeneration, activeGeneration, isReady, sessionIdFor, permissionPolicy, getMode, permissionAudit, wrapperSourcePath, projectTrust, onTrustWithdrawn, allowNativeFallback = true, bridgeToolName = toolName) {
|
|
1326
|
+
const original = factory(cwd);
|
|
1327
|
+
if (original.name !== toolName || typeof original.execute !== "function") {
|
|
1328
|
+
throw new Error(`Pi built-in ${toolName} factory identity is invalid; run /ca-doctor.`);
|
|
1329
|
+
}
|
|
1330
|
+
const originalExecute = original.execute;
|
|
1331
|
+
const executeNativeFromContext = async (toolCallId, params, signal, onUpdate, context) => {
|
|
1332
|
+
const currentCwd = executionCwd(context, cwd);
|
|
1333
|
+
const native = nativeFactory(currentCwd);
|
|
1334
|
+
return await native.execute(toolCallId, params, signal, onUpdate, context);
|
|
1335
|
+
};
|
|
1336
|
+
return {
|
|
1337
|
+
...original,
|
|
1338
|
+
execute: async (toolCallId, params, signal, onUpdate, context) => {
|
|
1339
|
+
const trustHeld = () => {
|
|
1340
|
+
if (liveProjectTrust(context, projectTrust)) return true;
|
|
1341
|
+
onTrustWithdrawn();
|
|
1342
|
+
return false;
|
|
1343
|
+
};
|
|
1344
|
+
const trusted = trustHeld();
|
|
1345
|
+
const generation = activeGeneration();
|
|
1346
|
+
if (generation === void 0 || generation !== boundGeneration || !isReady() || !trusted) {
|
|
1347
|
+
if (!allowNativeFallback || generation !== void 0 && category !== "READ") {
|
|
1348
|
+
return failTool(trusted ? "codeArbiter enforcement is not ready; mutation blocked; run /ca-doctor." : TRUST_WITHDRAWN_MESSAGE);
|
|
1349
|
+
}
|
|
1350
|
+
return await executeNativeFromContext(toolCallId, params, signal, onUpdate, context);
|
|
1351
|
+
}
|
|
1352
|
+
let approved;
|
|
1353
|
+
try {
|
|
1354
|
+
approved = canonicalSnapshot(params);
|
|
1355
|
+
} catch {
|
|
1356
|
+
return failTool("Pi tool parameters are not canonical JSON; mutation blocked; run /ca-doctor.");
|
|
1357
|
+
}
|
|
1358
|
+
let response;
|
|
1359
|
+
try {
|
|
1360
|
+
response = await bridge.call({
|
|
1361
|
+
version: 1,
|
|
1362
|
+
event: "tool_call",
|
|
1363
|
+
cwd,
|
|
1364
|
+
...category === "READ" ? { sessionId: sessionIdFor(context) } : {},
|
|
1365
|
+
tool: bridgeToolName,
|
|
1366
|
+
input: approved,
|
|
1367
|
+
correlation: auditCorrelation(toolCallId)
|
|
1368
|
+
}, signal ?? new AbortController().signal);
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
const stillTrusted = trustHeld();
|
|
1371
|
+
if (stillTrusted && activeGeneration() === generation) throw error;
|
|
1372
|
+
if (category !== "READ") {
|
|
1373
|
+
return failTool(stillTrusted ? LIFECYCLE_STALE_MESSAGE : TRUST_WITHDRAWN_MESSAGE);
|
|
1374
|
+
}
|
|
1375
|
+
return await executeNativeFromContext(toolCallId, approved, signal, onUpdate, context);
|
|
1376
|
+
}
|
|
1377
|
+
const trustedAfterBridge = trustHeld();
|
|
1378
|
+
if (!trustedAfterBridge || activeGeneration() !== generation) {
|
|
1379
|
+
if (category !== "READ") {
|
|
1380
|
+
return failTool(trustedAfterBridge ? LIFECYCLE_STALE_MESSAGE : TRUST_WITHDRAWN_MESSAGE);
|
|
1381
|
+
}
|
|
1382
|
+
return await executeNativeFromContext(toolCallId, approved, signal, onUpdate, context);
|
|
1383
|
+
}
|
|
1384
|
+
if (response.outcome === "block") return failTool(response.message ?? `Blocked by ${response.ruleId ?? "codeArbiter"}`);
|
|
1385
|
+
if (response.outcome !== "allow" && response.outcome !== "notice" && response.outcome !== "warn") {
|
|
1386
|
+
return failTool("Mutation bridge returned an unknown verdict; mutation blocked; run /ca-doctor.");
|
|
1387
|
+
}
|
|
1388
|
+
const mode = currentMode(getMode);
|
|
1389
|
+
const actions2 = classifyPermissionActions(permissionPolicy, toolName, approved);
|
|
1390
|
+
const policy = mode === void 0 || actions2 === void 0 ? { decision: "deny" } : evaluatePolicy(permissionPolicy, { mode, tool: toolName, actions: actions2, cwd });
|
|
1391
|
+
const audit = async (decision) => {
|
|
1392
|
+
if (actions2 === void 0 || permissionAudit === void 0) return false;
|
|
1393
|
+
try {
|
|
1394
|
+
return await permissionAudit(
|
|
1395
|
+
cwd,
|
|
1396
|
+
permissionAuditRow(toolCallId, category, actions2, decision)
|
|
1397
|
+
) === true;
|
|
1398
|
+
} catch {
|
|
1399
|
+
return false;
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1402
|
+
const auditFixed = async (auditCode) => {
|
|
1403
|
+
if (permissionAudit === void 0) return false;
|
|
1404
|
+
try {
|
|
1405
|
+
return await permissionAudit(cwd, permissionAuditCodeRow(toolCallId, category, auditCode)) === true;
|
|
1406
|
+
} catch {
|
|
1407
|
+
return false;
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
if (policy.decision === "deny" || actions2 === void 0 || mode === void 0) {
|
|
1411
|
+
if (actions2 === void 0) await auditFixed("PI_PERMISSION_UNCLASSIFIED");
|
|
1412
|
+
else if (mode === void 0) await auditFixed("PI_PERMISSION_INVALID_MODE");
|
|
1413
|
+
else await audit("denied");
|
|
1414
|
+
return failTool("Pi permission policy denied this operation; run /ca-doctor.");
|
|
1415
|
+
}
|
|
1416
|
+
const executeOriginal = async () => await originalExecute.call(original, toolCallId, approved, signal, onUpdate, context);
|
|
1417
|
+
const revalidate = () => {
|
|
1418
|
+
const requestStable = currentMode(getMode) === mode && registryOwns(pi, toolName, wrapperSourcePath) && original.execute === originalExecute && matchesSnapshot(params, approved) && signal?.aborted !== true;
|
|
1419
|
+
if (!requestStable) return "request-stale";
|
|
1420
|
+
if (!trustHeld()) return "lifecycle-stale";
|
|
1421
|
+
return activeGeneration() === generation && generation === boundGeneration && isReady() ? "current" : "lifecycle-stale";
|
|
1422
|
+
};
|
|
1423
|
+
if (policy.decision === "ask") {
|
|
1424
|
+
const confirmation = confirmationUi(context);
|
|
1425
|
+
if (confirmation === void 0) {
|
|
1426
|
+
await audit("denied");
|
|
1427
|
+
return failTool("Pi confirmation UI is unavailable; mutation blocked.");
|
|
1428
|
+
}
|
|
1429
|
+
if (!registryOwns(pi, toolName, wrapperSourcePath) || original.execute !== originalExecute) {
|
|
1430
|
+
await audit("denied");
|
|
1431
|
+
return failTool("Pi tool ownership changed before confirmation; mutation blocked; run /ca-doctor.");
|
|
1432
|
+
}
|
|
1433
|
+
let confirmed = false;
|
|
1434
|
+
try {
|
|
1435
|
+
confirmed = await confirmation.confirm.call(
|
|
1436
|
+
confirmation.ui,
|
|
1437
|
+
CONFIRMATION_TITLE,
|
|
1438
|
+
confirmationMessage(policy.confirmation.actionClasses, policy.confirmation.cwd, policy.confirmation.consequence),
|
|
1439
|
+
{ timeout: CONFIRMATION_TIMEOUT_MS, signal }
|
|
1440
|
+
) === true;
|
|
1441
|
+
} catch {
|
|
1442
|
+
confirmed = false;
|
|
1443
|
+
}
|
|
1444
|
+
if (!confirmed) {
|
|
1445
|
+
await audit("cancelled");
|
|
1446
|
+
return failTool("Pi permission confirmation was cancelled; mutation blocked.");
|
|
1447
|
+
}
|
|
1448
|
+
if (revalidate() !== "current") {
|
|
1449
|
+
await audit("denied");
|
|
1450
|
+
return failTool("Pi permission approval became stale; mutation blocked; run /ca-doctor.");
|
|
1451
|
+
}
|
|
1452
|
+
if (!await audit("approved")) {
|
|
1453
|
+
return failTool("Pi permission audit is unavailable; approved mutation blocked; run /ca-doctor.");
|
|
1454
|
+
}
|
|
1455
|
+
if (revalidate() !== "current") {
|
|
1456
|
+
return failTool("Pi permission approval became stale; mutation blocked; run /ca-doctor.");
|
|
1457
|
+
}
|
|
1458
|
+
} else {
|
|
1459
|
+
await audit("allow");
|
|
1460
|
+
const current = revalidate();
|
|
1461
|
+
if (current !== "current") {
|
|
1462
|
+
if (current === "lifecycle-stale" && category === "READ") {
|
|
1463
|
+
return await executeNativeFromContext(toolCallId, approved, signal, onUpdate, context);
|
|
1464
|
+
}
|
|
1465
|
+
return failTool("Pi permission allowance became stale; operation blocked; run /ca-doctor.");
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
const result = await executeOriginal();
|
|
1469
|
+
if (activeGeneration() !== generation) return result;
|
|
1470
|
+
if (category === "READ") {
|
|
1471
|
+
const patch = applyToolResultNotice(result, response);
|
|
1472
|
+
return patch === void 0 ? result : { ...result, ...patch };
|
|
1473
|
+
}
|
|
1474
|
+
if ((response.outcome === "warn" || response.outcome === "notice") && response.message !== void 0) {
|
|
1475
|
+
return appendWarning(result, response.message);
|
|
1476
|
+
}
|
|
1477
|
+
return result;
|
|
1478
|
+
}
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
function wrapMissingBuiltins(pi, bridge, options, wrapped, definitions, definitionGenerations, activeGeneration = () => STANDALONE_GENERATION, isReady = () => true, sessionIdFor = fixedFallbackSessionId(), onTrustWithdrawn = () => void 0) {
|
|
1482
|
+
const boundGeneration = activeGeneration() ?? STANDALONE_GENERATION;
|
|
1483
|
+
const permissionPolicy = options.permissionPolicy ?? compileBuiltinPermissionPolicy(options.descriptor, {});
|
|
1484
|
+
if (permissionPolicy === void 0) throw new Error("Pi permission policy descriptor is invalid; run /ca-doctor.");
|
|
1485
|
+
const getMode = options.getMode ?? (() => "execute");
|
|
1486
|
+
for (const name of ["bash", "write", "edit", "read"]) {
|
|
1487
|
+
if (wrapped.has(name) && definitionGenerations?.get(name) === boundGeneration) continue;
|
|
1488
|
+
const category = options.descriptor[name] ?? "OTHER";
|
|
1489
|
+
if (category === "OTHER") throw new Error(`Pi descriptor does not classify built-in ${name}; run /ca-doctor.`);
|
|
1490
|
+
const definition = wrappedDefinition(
|
|
1491
|
+
pi,
|
|
1492
|
+
name,
|
|
1493
|
+
options.factories[name],
|
|
1494
|
+
(options.nativeFactories ?? options.factories)[name],
|
|
1495
|
+
category,
|
|
1496
|
+
options.cwd,
|
|
1497
|
+
bridge,
|
|
1498
|
+
boundGeneration,
|
|
1499
|
+
activeGeneration,
|
|
1500
|
+
isReady,
|
|
1501
|
+
sessionIdFor,
|
|
1502
|
+
permissionPolicy,
|
|
1503
|
+
getMode,
|
|
1504
|
+
options.permissionAudit,
|
|
1505
|
+
options.wrapperSourcePath,
|
|
1506
|
+
options.projectTrust,
|
|
1507
|
+
onTrustWithdrawn
|
|
1508
|
+
);
|
|
1509
|
+
pi.registerTool(definition);
|
|
1510
|
+
definitions?.set(name, definition);
|
|
1511
|
+
definitionGenerations?.set(name, boundGeneration);
|
|
1512
|
+
wrapped.add(name);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
var EnforcementInstaller = class {
|
|
1516
|
+
bootstrapInstalled = false;
|
|
1517
|
+
bootstrapActive = false;
|
|
1518
|
+
ready = false;
|
|
1519
|
+
guardInstalled = false;
|
|
1520
|
+
resultsInstalled = false;
|
|
1521
|
+
wrapped = /* @__PURE__ */ new Set();
|
|
1522
|
+
definitions = /* @__PURE__ */ new Map();
|
|
1523
|
+
definitionGenerations = /* @__PURE__ */ new Map();
|
|
1524
|
+
fallbackSessionId;
|
|
1525
|
+
lifecycleGeneration;
|
|
1526
|
+
trustRevoked = false;
|
|
1527
|
+
sessionIdFor(context) {
|
|
1528
|
+
const native = nativeSessionId(context);
|
|
1529
|
+
if (native !== void 0) return native;
|
|
1530
|
+
this.fallbackSessionId ??= randomUUID2();
|
|
1531
|
+
return this.fallbackSessionId;
|
|
1532
|
+
}
|
|
1533
|
+
ensureBootstrap(pi, descriptor) {
|
|
1534
|
+
if (this.bootstrapInstalled) return;
|
|
1535
|
+
pi.on("tool_call", (event) => {
|
|
1536
|
+
if (!this.bootstrapActive || this.ready) return void 0;
|
|
1537
|
+
const name = typeof event.toolName === "string" ? event.toolName : "";
|
|
1538
|
+
if ((descriptor[name] ?? "OTHER") === "READ") return void 0;
|
|
1539
|
+
return {
|
|
1540
|
+
block: true,
|
|
1541
|
+
reason: "codeArbiter enforcement is not ready; this Pi tool is potentially mutating and is blocked; run /ca-doctor."
|
|
1542
|
+
};
|
|
1543
|
+
});
|
|
1544
|
+
this.bootstrapInstalled = true;
|
|
1545
|
+
}
|
|
1546
|
+
beginBlockedGeneration() {
|
|
1547
|
+
this.bootstrapActive = true;
|
|
1548
|
+
this.ready = false;
|
|
1549
|
+
this.trustRevoked = false;
|
|
1550
|
+
this.fallbackSessionId = randomUUID2();
|
|
1551
|
+
this.lifecycleGeneration = Object.freeze({});
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Retires the ready lifecycle the instant live project trust stops being
|
|
1555
|
+
* affirmative. Every wrapper stays bound to the previous generation, so its
|
|
1556
|
+
* mutators fail closed and its reads fall back to the untrusted native path.
|
|
1557
|
+
* Enforcement only returns through a new affirmatively trusted session.
|
|
1558
|
+
*/
|
|
1559
|
+
revokeForTrustWithdrawal() {
|
|
1560
|
+
if (this.trustRevoked || this.lifecycleGeneration === void 0) return;
|
|
1561
|
+
this.trustRevoked = true;
|
|
1562
|
+
this.ready = false;
|
|
1563
|
+
this.lifecycleGeneration = Object.freeze({});
|
|
1564
|
+
}
|
|
1565
|
+
beginActivation() {
|
|
1566
|
+
this.beginBlockedGeneration();
|
|
1567
|
+
}
|
|
1568
|
+
beginBootstrap() {
|
|
1569
|
+
this.beginBlockedGeneration();
|
|
1570
|
+
}
|
|
1571
|
+
markReady() {
|
|
1572
|
+
if (this.bootstrapActive && !this.trustRevoked) this.ready = true;
|
|
1573
|
+
}
|
|
1574
|
+
deactivate() {
|
|
1575
|
+
this.bootstrapActive = false;
|
|
1576
|
+
this.ready = false;
|
|
1577
|
+
this.trustRevoked = false;
|
|
1578
|
+
this.fallbackSessionId = void 0;
|
|
1579
|
+
this.lifecycleGeneration = void 0;
|
|
1580
|
+
}
|
|
1581
|
+
ensureGuard(pi, descriptor, wrapperSourcePath) {
|
|
1582
|
+
if (this.guardInstalled) return;
|
|
1583
|
+
guardUnknownTools(pi, descriptor, wrapperSourcePath, () => this.bootstrapActive);
|
|
1584
|
+
this.guardInstalled = true;
|
|
1585
|
+
}
|
|
1586
|
+
ensureResults(pi, bridge, descriptor) {
|
|
1587
|
+
if (this.resultsInstalled) return;
|
|
1588
|
+
bridgeToolResults(pi, bridge, descriptor, () => this.lifecycleGeneration);
|
|
1589
|
+
this.resultsInstalled = true;
|
|
1590
|
+
}
|
|
1591
|
+
ensureBuiltins(pi, bridge, options) {
|
|
1592
|
+
wrapMissingBuiltins(
|
|
1593
|
+
pi,
|
|
1594
|
+
bridge,
|
|
1595
|
+
options,
|
|
1596
|
+
this.wrapped,
|
|
1597
|
+
this.definitions,
|
|
1598
|
+
this.definitionGenerations,
|
|
1599
|
+
() => this.lifecycleGeneration,
|
|
1600
|
+
() => this.ready,
|
|
1601
|
+
(context) => this.sessionIdFor(context),
|
|
1602
|
+
() => this.revokeForTrustWithdrawal()
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
ensureCustomTool(pi, bridge, options) {
|
|
1606
|
+
const boundGeneration = this.lifecycleGeneration ?? STANDALONE_GENERATION;
|
|
1607
|
+
if (this.wrapped.has(options.name) && this.definitionGenerations.get(options.name) === boundGeneration) return;
|
|
1608
|
+
const category = options.descriptor[options.name] ?? "OTHER";
|
|
1609
|
+
if (category === "OTHER" || category === "READ") {
|
|
1610
|
+
throw new Error(`Pi descriptor does not classify custom mutator ${options.name}; run /ca-doctor.`);
|
|
1611
|
+
}
|
|
1612
|
+
const bridgeToolName = options.bridgeToolName ?? options.name;
|
|
1613
|
+
if ((options.descriptor[bridgeToolName] ?? "OTHER") !== category) {
|
|
1614
|
+
throw new Error("Pi custom tool bridge alias category is invalid; run /ca-doctor.");
|
|
1615
|
+
}
|
|
1616
|
+
const permissionPolicy = options.permissionPolicy ?? compileBuiltinPermissionPolicy(options.descriptor, {});
|
|
1617
|
+
if (permissionPolicy === void 0) throw new Error("Pi permission policy descriptor is invalid; run /ca-doctor.");
|
|
1618
|
+
const definition = wrappedDefinition(
|
|
1619
|
+
pi,
|
|
1620
|
+
options.name,
|
|
1621
|
+
options.factory,
|
|
1622
|
+
options.factory,
|
|
1623
|
+
category,
|
|
1624
|
+
options.cwd,
|
|
1625
|
+
bridge,
|
|
1626
|
+
boundGeneration,
|
|
1627
|
+
() => this.lifecycleGeneration,
|
|
1628
|
+
() => this.ready,
|
|
1629
|
+
(context) => this.sessionIdFor(context),
|
|
1630
|
+
permissionPolicy,
|
|
1631
|
+
options.getMode ?? (() => "execute"),
|
|
1632
|
+
options.permissionAudit,
|
|
1633
|
+
options.wrapperSourcePath,
|
|
1634
|
+
options.projectTrust,
|
|
1635
|
+
() => this.revokeForTrustWithdrawal(),
|
|
1636
|
+
false,
|
|
1637
|
+
bridgeToolName
|
|
1638
|
+
);
|
|
1639
|
+
pi.registerTool(definition);
|
|
1640
|
+
this.definitions.set(options.name, definition);
|
|
1641
|
+
this.definitionGenerations.set(options.name, boundGeneration);
|
|
1642
|
+
this.wrapped.add(options.name);
|
|
1643
|
+
}
|
|
1644
|
+
async runDoctorWrapperSelfTest(signal) {
|
|
1645
|
+
const bash = this.definitions.get("bash");
|
|
1646
|
+
if (bash === void 0) throw new Error("The active Pi bash wrapper is unavailable; run /ca-doctor.");
|
|
1647
|
+
return await bash.execute(
|
|
1648
|
+
"codearbiter-doctor-wrapper-self-test",
|
|
1649
|
+
{ command: "git add --all --dry-run" },
|
|
1650
|
+
signal ?? new AbortController().signal
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
};
|
|
1654
|
+
function samePath(left, right) {
|
|
1655
|
+
const equal = (a, b) => process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
1656
|
+
if (equal(left, right)) return true;
|
|
1657
|
+
try {
|
|
1658
|
+
return equal(realpathSync2(left), realpathSync2(right));
|
|
1659
|
+
} catch {
|
|
1660
|
+
return false;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
function guardUnknownTools(pi, descriptor, wrapperSourcePath, isActive = () => true) {
|
|
1664
|
+
pi.on("tool_call", (event) => {
|
|
1665
|
+
if (!isActive()) return void 0;
|
|
1666
|
+
const name = typeof event.toolName === "string" ? event.toolName : "";
|
|
1667
|
+
const category = descriptor[name] ?? "OTHER";
|
|
1668
|
+
if (category === "OTHER") {
|
|
1669
|
+
return { block: true, reason: "An unknown Pi tool is potentially mutating and is blocked; classify it in the generated descriptor or run /ca-doctor." };
|
|
1670
|
+
}
|
|
1671
|
+
if (category === "READ") return void 0;
|
|
1672
|
+
const active = new Set(pi.getActiveTools());
|
|
1673
|
+
const info = pi.getAllTools().find((tool) => tool.name === name);
|
|
1674
|
+
if (!active.has(name) || info === void 0 || !samePath(info.sourceInfo.path, wrapperSourcePath)) {
|
|
1675
|
+
return { block: true, reason: `Governed Pi tool ${name} has source drift or no active final-execution wrapper; mutation blocked; run /ca-doctor.` };
|
|
1676
|
+
}
|
|
1677
|
+
return void 0;
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
function bridgeToolResults(pi, bridge, descriptor, activeGeneration = () => STANDALONE_GENERATION) {
|
|
1681
|
+
pi.on("tool_result", async (event, context) => {
|
|
1682
|
+
const generation = activeGeneration();
|
|
1683
|
+
if (generation === void 0) return void 0;
|
|
1684
|
+
const name = typeof event.toolName === "string" ? event.toolName : "";
|
|
1685
|
+
const category = descriptor[name] ?? "OTHER";
|
|
1686
|
+
if (category !== "WRITE" && category !== "EDIT") return void 0;
|
|
1687
|
+
const toolCallId = typeof event.toolCallId === "string" && event.toolCallId !== "" ? event.toolCallId : void 0;
|
|
1688
|
+
let response;
|
|
1689
|
+
try {
|
|
1690
|
+
response = await bridge.call({
|
|
1691
|
+
version: 1,
|
|
1692
|
+
event: "tool_result",
|
|
1693
|
+
cwd: context.cwd,
|
|
1694
|
+
tool: name,
|
|
1695
|
+
input: event.input,
|
|
1696
|
+
result: { content: event.content, isError: event.isError === true },
|
|
1697
|
+
...toolCallId === void 0 ? {} : { correlation: auditCorrelation(toolCallId) }
|
|
1698
|
+
}, context.signal ?? new AbortController().signal);
|
|
1699
|
+
} catch (error) {
|
|
1700
|
+
if (activeGeneration() !== generation) return void 0;
|
|
1701
|
+
throw error;
|
|
1702
|
+
}
|
|
1703
|
+
if (activeGeneration() !== generation) return void 0;
|
|
1704
|
+
if (response.outcome === "warn" && response.message !== void 0) {
|
|
1705
|
+
context.ui.notify(response.message, "warning");
|
|
1706
|
+
}
|
|
1707
|
+
return applyToolResultNotice(event, response);
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// src/child-extension.ts
|
|
1712
|
+
var HANDSHAKE_COMMAND = "codearbiter-internal-child-handshake";
|
|
1713
|
+
var NONCE = /^[0-9a-f]{32}$/u;
|
|
1714
|
+
var HANDSHAKE_ARGS = /^([0-9a-f]{32}) ([0-9a-f]{32})$/u;
|
|
1715
|
+
var CHILD_TOOLS = /* @__PURE__ */ new Set(["read", "bash", "edit", "write"]);
|
|
1716
|
+
function fixedFailure(message) {
|
|
1717
|
+
return new Error(`codeArbiter child handshake ${message}; child remains blocked; run /ca-doctor.`);
|
|
1718
|
+
}
|
|
1719
|
+
function installChild(pi, dependencies) {
|
|
1720
|
+
const descriptor = dependencies.descriptor ?? { read: "READ", bash: "EXEC", edit: "EDIT", write: "WRITE" };
|
|
1721
|
+
const enforcement = new EnforcementInstaller();
|
|
1722
|
+
let consumed = false;
|
|
1723
|
+
let activeCwd = dependencies.cwd;
|
|
1724
|
+
let wrappersInstalled = false;
|
|
1725
|
+
enforcement.ensureBootstrap(pi, descriptor);
|
|
1726
|
+
enforcement.beginActivation();
|
|
1727
|
+
if (dependencies.wrapperSourcePath !== void 0) enforcement.ensureGuard(pi, descriptor, dependencies.wrapperSourcePath);
|
|
1728
|
+
if (dependencies.bridge !== void 0) enforcement.ensureResults(pi, dependencies.bridge, descriptor);
|
|
1729
|
+
const installWrappers = (cwd) => {
|
|
1730
|
+
if (dependencies.bridge === void 0 || dependencies.factories === void 0 || dependencies.wrapperSourcePath === void 0) return false;
|
|
1731
|
+
enforcement.ensureBuiltins(pi, dependencies.bridge, {
|
|
1732
|
+
cwd,
|
|
1733
|
+
descriptor,
|
|
1734
|
+
factories: dependencies.factories,
|
|
1735
|
+
nativeFactories: dependencies.nativeFactories ?? dependencies.factories,
|
|
1736
|
+
wrapperSourcePath: dependencies.wrapperSourcePath,
|
|
1737
|
+
// The child runs deliberately with Pi project trust false: its handshake
|
|
1738
|
+
// rejects any context whose isProjectTrusted is not false, and its authority
|
|
1739
|
+
// is the consumed nonce plus the operator attestation, not project trust.
|
|
1740
|
+
projectTrust: "not-required"
|
|
1741
|
+
});
|
|
1742
|
+
wrappersInstalled = true;
|
|
1743
|
+
return true;
|
|
1744
|
+
};
|
|
1745
|
+
pi.on("session_start", (_event, context) => {
|
|
1746
|
+
activeCwd = context.cwd;
|
|
1747
|
+
enforcement.beginBootstrap();
|
|
1748
|
+
installWrappers(context.cwd);
|
|
1749
|
+
});
|
|
1750
|
+
pi.on("session_shutdown", () => {
|
|
1751
|
+
enforcement.deactivate();
|
|
1752
|
+
consumed = true;
|
|
1753
|
+
});
|
|
1754
|
+
pi.registerCommand(HANDSHAKE_COMMAND, {
|
|
1755
|
+
handler: async (args, context) => {
|
|
1756
|
+
if (dependencies.marker !== "1") throw fixedFailure("has no validated subagent marker");
|
|
1757
|
+
if (consumed) throw fixedFailure("nonce was already consumed");
|
|
1758
|
+
if (dependencies.expectedNonce === void 0) throw fixedFailure("capability is missing");
|
|
1759
|
+
if (!NONCE.test(dependencies.expectedNonce)) throw fixedFailure("capability is malformed");
|
|
1760
|
+
const framing = HANDSHAKE_ARGS.exec(args.trim());
|
|
1761
|
+
if (framing === null) throw fixedFailure("nonce or challenge is malformed");
|
|
1762
|
+
const [, nonce, challenge] = framing;
|
|
1763
|
+
if (nonce === void 0 || challenge === void 0) throw fixedFailure("nonce or challenge is malformed");
|
|
1764
|
+
if (nonce !== dependencies.expectedNonce) throw fixedFailure("nonce does not match the parent capability");
|
|
1765
|
+
consumed = true;
|
|
1766
|
+
const cwd = activeCwd ?? context.cwd;
|
|
1767
|
+
if (cwd !== void 0) installWrappers(cwd);
|
|
1768
|
+
if (!wrappersInstalled) throw fixedFailure("enforcement is unavailable");
|
|
1769
|
+
const activeTools = [...pi.getActiveTools()].sort();
|
|
1770
|
+
let projectTrusted;
|
|
1771
|
+
try {
|
|
1772
|
+
projectTrusted = context.isProjectTrusted?.() ?? true;
|
|
1773
|
+
} catch {
|
|
1774
|
+
throw fixedFailure("attestation context is unavailable");
|
|
1775
|
+
}
|
|
1776
|
+
if (context.mode !== "rpc" || context.hasUI !== true || context.ui.confirm === void 0 || projectTrusted !== false || typeof cwd !== "string" || cwd === "" || context.model === void 0 || typeof context.model.provider !== "string" || context.model.provider === "" || typeof context.model.id !== "string" || context.model.id === "" || new Set(activeTools).size !== activeTools.length || activeTools.some((tool) => !CHILD_TOOLS.has(tool) || descriptor[tool] === void 0)) {
|
|
1777
|
+
throw fixedFailure("attestation context is invalid");
|
|
1778
|
+
}
|
|
1779
|
+
const digest = childAttestationDigest({
|
|
1780
|
+
nonce,
|
|
1781
|
+
challenge,
|
|
1782
|
+
cwd,
|
|
1783
|
+
provider: context.model.provider,
|
|
1784
|
+
model: context.model.id,
|
|
1785
|
+
tools: activeTools,
|
|
1786
|
+
projectTrusted: false,
|
|
1787
|
+
mode: "rpc"
|
|
1788
|
+
});
|
|
1789
|
+
let confirmed;
|
|
1790
|
+
try {
|
|
1791
|
+
confirmed = await context.ui.confirm(CHILD_ATTESTATION_TITLE, digest, { timeout: CHILD_ATTESTATION_TIMEOUT_MS });
|
|
1792
|
+
} catch {
|
|
1793
|
+
throw fixedFailure("attestation confirmation failed");
|
|
1794
|
+
}
|
|
1795
|
+
if (!confirmed) throw fixedFailure("attestation confirmation was rejected");
|
|
1796
|
+
enforcement.markReady();
|
|
1797
|
+
}
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
async function readChildCapability(source = createReadStream("", { fd: 3, autoClose: true })) {
|
|
1801
|
+
let value = "";
|
|
1802
|
+
let bytes = 0;
|
|
1803
|
+
try {
|
|
1804
|
+
for await (const chunk of source) {
|
|
1805
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
1806
|
+
bytes += Buffer.byteLength(text, "utf8");
|
|
1807
|
+
if (bytes > 32) throw fixedFailure("capability is oversized");
|
|
1808
|
+
value += text;
|
|
1809
|
+
}
|
|
1810
|
+
} catch (error) {
|
|
1811
|
+
if (error instanceof Error && error.message.startsWith("codeArbiter child handshake")) throw error;
|
|
1812
|
+
throw fixedFailure("capability pipe is unavailable");
|
|
1813
|
+
}
|
|
1814
|
+
if (!NONCE.test(value)) throw fixedFailure("capability is malformed");
|
|
1815
|
+
return value;
|
|
1816
|
+
}
|
|
1817
|
+
function loadToolClasses(value) {
|
|
1818
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw fixedFailure("descriptor is unavailable");
|
|
1819
|
+
const allowed = /* @__PURE__ */ new Set(["EXEC", "WRITE", "EDIT", "READ", "OTHER"]);
|
|
1820
|
+
const descriptor = {};
|
|
1821
|
+
for (const [name, category] of Object.entries(value)) {
|
|
1822
|
+
if (name === "" || typeof category !== "string" || !allowed.has(category)) throw fixedFailure("descriptor is invalid");
|
|
1823
|
+
descriptor[name] = category;
|
|
1824
|
+
}
|
|
1825
|
+
return Object.freeze(descriptor);
|
|
1826
|
+
}
|
|
1827
|
+
async function codeArbiterPiChild(pi) {
|
|
1828
|
+
if (process.env.CODEARBITER_SUBAGENT !== "1") throw fixedFailure("has no validated subagent marker");
|
|
1829
|
+
const expectedNonce = await readChildCapability();
|
|
1830
|
+
const runtimeIdentity = await resolvePiRuntimeIdentity();
|
|
1831
|
+
const direction = compatibilityDirection({ piVersion: runtimeIdentity.version, nodeVersion: process.versions.node, pythonMajor: 3 });
|
|
1832
|
+
if (direction !== null) throw new Error(direction);
|
|
1833
|
+
const runtime = await loadPiRuntime(runtimeIdentity);
|
|
1834
|
+
const modulePath = await realpath4(fileURLToPath2(import.meta.url));
|
|
1835
|
+
let packageRoot = dirname3(modulePath);
|
|
1836
|
+
while (true) {
|
|
1837
|
+
try {
|
|
1838
|
+
const manifest = JSON.parse(await readFile2(resolve5(packageRoot, "package.json"), "utf8"));
|
|
1839
|
+
if (manifest.name === "@arbiterforge/ca-pi") break;
|
|
1840
|
+
} catch {
|
|
1841
|
+
}
|
|
1842
|
+
const parent = dirname3(packageRoot);
|
|
1843
|
+
if (parent === packageRoot) throw fixedFailure("could not locate the ca-pi package");
|
|
1844
|
+
packageRoot = parent;
|
|
1845
|
+
}
|
|
1846
|
+
const cwd = process.cwd();
|
|
1847
|
+
const python = resolvePythonCommand(process.platform, void 0, packageRoot, cwd);
|
|
1848
|
+
const gitExecutable = resolveGitExecutable(cwd);
|
|
1849
|
+
const descriptor = loadToolClasses(define_CODEARBITER_PI_TOOL_CLASSES_default);
|
|
1850
|
+
const bridge = new BridgeClient({
|
|
1851
|
+
bridgeScript: resolve5(packageRoot, "hooks", "pi-bridge.py"),
|
|
1852
|
+
packageRoot,
|
|
1853
|
+
pythonExecutable: python.executable,
|
|
1854
|
+
pythonPrefixArgs: python.prefixArgs,
|
|
1855
|
+
gitExecutable,
|
|
1856
|
+
toolClasses: descriptor
|
|
1857
|
+
});
|
|
1858
|
+
const factories = {
|
|
1859
|
+
bash: (root) => {
|
|
1860
|
+
const settings = runtime.SettingsManager.create(root, runtime.getAgentDir(), { projectTrusted: false });
|
|
1861
|
+
return runtime.createBashToolDefinition(root, { commandPrefix: settings.getShellCommandPrefix(), shellPath: settings.getShellPath() });
|
|
1862
|
+
},
|
|
1863
|
+
read: (root) => {
|
|
1864
|
+
const settings = runtime.SettingsManager.create(root, runtime.getAgentDir(), { projectTrusted: false });
|
|
1865
|
+
return runtime.createReadToolDefinition(root, { autoResizeImages: settings.getImageAutoResize() });
|
|
1866
|
+
},
|
|
1867
|
+
edit: (root) => runtime.createEditToolDefinition(root),
|
|
1868
|
+
write: (root) => runtime.createWriteToolDefinition(root)
|
|
1869
|
+
};
|
|
1870
|
+
installChild(pi, {
|
|
1871
|
+
marker: process.env.CODEARBITER_SUBAGENT,
|
|
1872
|
+
expectedNonce,
|
|
1873
|
+
cwd,
|
|
1874
|
+
wrapperSourcePath: modulePath,
|
|
1875
|
+
descriptor,
|
|
1876
|
+
bridge,
|
|
1877
|
+
factories,
|
|
1878
|
+
nativeFactories: factories
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
export {
|
|
1882
|
+
codeArbiterPiChild as default,
|
|
1883
|
+
installChild,
|
|
1884
|
+
readChildCapability
|
|
1885
|
+
};
|