@guilz-dev/belay 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -12
- package/dist/adapters/shared/gate-runtime.js +12 -4
- package/dist/bundle/claude-runtime.mjs +155 -36
- package/dist/bundle/codex-runtime.mjs +155 -36
- package/dist/bundle/cursor-runtime.mjs +155 -36
- package/dist/cli.js +15 -4
- package/dist/commands/classify-for-report.js +3 -3
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/explain.js +14 -14
- package/dist/commands/init-wizard.d.ts +5 -0
- package/dist/commands/init-wizard.js +24 -11
- package/dist/commands/recover.js +2 -2
- package/dist/core/approval.d.ts +3 -0
- package/dist/core/approval.js +18 -3
- package/dist/core/audit-query.js +5 -1
- package/dist/core/classify-tool.js +1 -1
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.js +2 -2
- package/dist/core/gate-contract.d.ts +1 -1
- package/dist/core/gate-contract.js +1 -1
- package/dist/core/gate-engine.js +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +2 -2
- package/dist/core/judge-config.d.ts +5 -1
- package/dist/core/judge-config.js +17 -1
- package/dist/core/judge-doctor.js +2 -2
- package/dist/core/types.d.ts +5 -3
- package/dist/core/{v2 → verdict}/adapter.js +9 -3
- package/dist/core/{v2 → verdict}/egress-classify.js +3 -0
- package/dist/core/{v2 → verdict}/judge.js +10 -12
- package/dist/core/{v2 → verdict}/launcher-resolve.js +72 -1
- package/dist/core/{v2 → verdict}/verdict.js +16 -0
- package/dist/corpus/evaluate.js +2 -2
- package/dist/installer.js +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +2 -1
- package/package.json +5 -2
- package/skills/belay/SKILL.md +19 -5
- /package/dist/core/{v2 → verdict}/adapter.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/containment.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/containment.js +0 -0
- /package/dist/core/{v2 → verdict}/egress-classify.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/fingerprint.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/fingerprint.js +0 -0
- /package/dist/core/{v2 → verdict}/index.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/index.js +0 -0
- /package/dist/core/{v2 → verdict}/judge-audit.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/judge-audit.js +0 -0
- /package/dist/core/{v2 → verdict}/judge-factory.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/judge-factory.js +0 -0
- /package/dist/core/{v2 → verdict}/judge-outbound.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/judge-outbound.js +0 -0
- /package/dist/core/{v2 → verdict}/judge.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/launcher-resolve.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/overrides.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/overrides.js +0 -0
- /package/dist/core/{v2 → verdict}/parser.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/parser.js +0 -0
- /package/dist/core/{v2 → verdict}/types.d.ts +0 -0
- /package/dist/core/{v2 → verdict}/types.js +0 -0
- /package/dist/core/{v2 → verdict}/verdict.d.ts +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Belay
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@guilz-dev/belay)
|
|
4
|
+
[](https://skills.sh/guilz-dev/belay)
|
|
4
5
|
[](https://github.com/guilz-dev/belay/actions/workflows/ci.yml)
|
|
5
6
|
[](https://opensource.org/licenses/MIT)
|
|
6
7
|
|
|
@@ -8,10 +9,10 @@
|
|
|
8
9
|
|
|
9
10
|
[Documentation (日本語)](./docs/README.ja.md)
|
|
10
11
|
|
|
11
|
-
`@guilz-dev/belay` hooks into agent runtimes (Cursor, Claude Code) and
|
|
12
|
-
each shell command, subagent launch, and file mutation *before* it runs.
|
|
13
|
-
actions pass through untouched. Only the irreversible-and-catastrophic ones
|
|
14
|
-
held back for one-shot human approval — and every decision is written to an
|
|
12
|
+
`@guilz-dev/belay` hooks into agent runtimes (Cursor, Claude Code, Codex) and
|
|
13
|
+
inspects each shell command, subagent launch, and file mutation *before* it runs.
|
|
14
|
+
Most actions pass through untouched. Only the irreversible-and-catastrophic ones
|
|
15
|
+
are held back for one-shot human approval — and every decision is written to an
|
|
15
16
|
audit log.
|
|
16
17
|
|
|
17
18
|
<p align="center">
|
|
@@ -21,6 +22,30 @@ audit log.
|
|
|
21
22
|
> **0.0.x early release** — APIs and behavior may change. Cursor and Claude Code
|
|
22
23
|
> are the supported adapters; Codex is experimental.
|
|
23
24
|
|
|
25
|
+
## Supported agents
|
|
26
|
+
|
|
27
|
+
Belay works across three coding agents. Each one runs the **same classifier**,
|
|
28
|
+
wired in through that agent's native **hook** mechanism — no agent-specific
|
|
29
|
+
policy to maintain.
|
|
30
|
+
|
|
31
|
+
| Agent | Status | Hook config | belay config |
|
|
32
|
+
|-------|--------|-------------|--------------|
|
|
33
|
+
| **Cursor** | Supported | `.cursor/hooks.json` | `.cursor/belay.config.json` |
|
|
34
|
+
| **Claude Code** | Supported | `.claude/settings.json` | `.claude/belay.config.json` |
|
|
35
|
+
| **Codex** | Experimental | `.codex/config.toml` | `.codex/belay.config.json` |
|
|
36
|
+
|
|
37
|
+
Pick the adapter at install time with `--adapter cursor|claude|codex` (or let
|
|
38
|
+
`init-wizard` prompt). Hosts use different hook event names, but Belay registers
|
|
39
|
+
the same runners (`belay-tool-gate`, `belay-before-submit`, `belay-audit`) at
|
|
40
|
+
equivalent lifecycle points:
|
|
41
|
+
|
|
42
|
+
| Role | belay hook | Cursor | Claude Code | Codex |
|
|
43
|
+
|------|-----------|--------|-------------|-------|
|
|
44
|
+
| Gate shell / tools / file mutations | `belay-tool-gate` | `beforeShellExecution`, `preToolUse` | `PreToolUse` | `PreToolUse` |
|
|
45
|
+
| Gate subagent launches | `belay-tool-gate` | `subagentStart` | (via `PreToolUse`) | `SubagentStart` |
|
|
46
|
+
| One-shot approvals | `belay-before-submit` | `beforeSubmitPrompt` | `UserPromptSubmit` | `UserPromptSubmit` |
|
|
47
|
+
| Audit log | `belay-audit` | `postToolUse`, `stop`, `sessionEnd` | `PostToolUse` | `PostToolUse` |
|
|
48
|
+
|
|
24
49
|
## Why
|
|
25
50
|
|
|
26
51
|
Static denylists don't work for agents. The same command (`rm`, `curl`, a
|
|
@@ -48,6 +73,7 @@ npx @guilz-dev/belay init-wizard
|
|
|
48
73
|
|
|
49
74
|
# Or non-interactive
|
|
50
75
|
npx @guilz-dev/belay init --adapter claude # Claude Code
|
|
76
|
+
npx @guilz-dev/belay init --adapter codex # Codex (experimental)
|
|
51
77
|
npx @guilz-dev/belay init # Cursor (default)
|
|
52
78
|
```
|
|
53
79
|
|
|
@@ -64,10 +90,10 @@ verdict and `overrides.allow` to whitelist commands you trust.
|
|
|
64
90
|
|
|
65
91
|
## How it works
|
|
66
92
|
|
|
67
|
-
Belay registers hooks on the host runtime (`.cursor/hooks.json
|
|
68
|
-
`.claude/settings.json`) and gates shell execution,
|
|
69
|
-
mutations through one shared classifier. It always
|
|
70
|
-
does not trust an assessment supplied by the agent.
|
|
93
|
+
Belay registers hooks on the host runtime (`.cursor/hooks.json`,
|
|
94
|
+
`.claude/settings.json`, or `.codex/config.toml`) and gates shell execution,
|
|
95
|
+
subagent launches, and file mutations through one shared classifier. It always
|
|
96
|
+
forms its own judgment — it does not trust an assessment supplied by the agent.
|
|
71
97
|
|
|
72
98
|
Every gated action gets one of three verdicts:
|
|
73
99
|
|
|
@@ -84,7 +110,8 @@ When an action is denied, approve the **next matching action once** by sending:
|
|
|
84
110
|
```
|
|
85
111
|
|
|
86
112
|
Approvals are one-shot and expire after 15 minutes by default. Every decision is
|
|
87
|
-
written to `.cursor/belay/audit.ndjson
|
|
113
|
+
written to `.cursor/belay/audit.ndjson`, `.claude/belay/audit.ndjson`, or
|
|
114
|
+
`.codex/belay/audit.ndjson` (depending on the adapter).
|
|
88
115
|
|
|
89
116
|
In **audit mode** (`mode: "audit"`), would-be denials are recorded
|
|
90
117
|
(`wouldBlock: true`) but execution still continues, and no approval IDs are
|
|
@@ -122,12 +149,23 @@ and skill under `~/.cursor/`, so the gate is user-wide while `belay.config.json`
|
|
|
122
149
|
approvals, and audit stay repo-local.
|
|
123
150
|
|
|
124
151
|
**Skill-only.** The skill is just a UX layer (slash commands + guidance) and does
|
|
125
|
-
**not** enable gating on its own
|
|
152
|
+
**not** enable gating on its own. Install from [skills.sh](https://skills.sh/guilz-dev/belay)
|
|
153
|
+
or GitHub:
|
|
126
154
|
|
|
127
155
|
```bash
|
|
156
|
+
# Cursor
|
|
128
157
|
npx skills add guilz-dev/belay --skill belay -a cursor -y
|
|
158
|
+
|
|
159
|
+
# Claude Code
|
|
160
|
+
npx skills add guilz-dev/belay --skill belay -a claude-code -y
|
|
161
|
+
|
|
162
|
+
# Codex
|
|
163
|
+
npx skills add guilz-dev/belay --skill belay -a codex -y
|
|
129
164
|
```
|
|
130
165
|
|
|
166
|
+
Running `npx skills add` also registers anonymous install telemetry on skills.sh,
|
|
167
|
+
which is how the skill appears in the directory leaderboard.
|
|
168
|
+
|
|
131
169
|
Runtime enforcement still requires `belay init` in the target repository.
|
|
132
170
|
|
|
133
171
|
## Dogfood → enforce
|
|
@@ -187,8 +225,9 @@ load.
|
|
|
187
225
|
|
|
188
226
|
Notable settings:
|
|
189
227
|
|
|
190
|
-
- **`policy.unknownLocalEffect: "
|
|
191
|
-
|
|
228
|
+
- **`policy.unknownLocalEffect: "allow_flagged"`** (fresh default) — after Tier1
|
|
229
|
+
says recoverable, structurally unknown local commands run with an audit flag. Use
|
|
230
|
+
`"deny"` (via `belay dogfood`) to ask on those commands instead.
|
|
192
231
|
- **`classifier.strictChains: true`** (default) — scans every `&&`, `|`, and `;`
|
|
193
232
|
segment and keeps the strictest verdict. Override lists match exact command or
|
|
194
233
|
segment keys only.
|
|
@@ -247,6 +286,14 @@ Belay state files are local runtime artifacts and should usually stay out of git
|
|
|
247
286
|
.cursor/hooks/belay-*
|
|
248
287
|
.cursor/skills/belay/
|
|
249
288
|
.cursor/commands/belay-approve.md
|
|
289
|
+
|
|
290
|
+
.claude/belay/
|
|
291
|
+
.claude/belay.config.json
|
|
292
|
+
.claude/hooks/belay-*
|
|
293
|
+
|
|
294
|
+
.codex/belay/
|
|
295
|
+
.codex/belay.config.json
|
|
296
|
+
.codex/hooks/belay-*
|
|
250
297
|
```
|
|
251
298
|
|
|
252
299
|
## Library exports
|
|
@@ -8,7 +8,7 @@ import { fsScopeAllowlistPath, isCapabilityBrokerDemotionActive, loadFsScopeAllo
|
|
|
8
8
|
import { resolveLayeredConfig, teamConfigPath } from '../../core/config-layers.js';
|
|
9
9
|
import { classifyResultToGateVerdict, unnormalizedGateVerdict, } from '../../core/gate-contract.js';
|
|
10
10
|
import { classifyGatedActionAsync, extractAgentAssessment, GateNormalizationError, gateEnabledForAction, normalizeGatedAction, } from '../../core/gate-engine.js';
|
|
11
|
-
import { approvalCommandMatch, approvedApprovalsFile, buildRetryInstruction, canonicalStringify, classifierOptionsFromConfig, compactApprovals, configuredControlPlaneDir, createApprovalRecord, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, scrubValue, toolFingerprint, } from '../../core/index.js';
|
|
11
|
+
import { APPROVAL_EXECUTION_LEASE_MS, approvalCommandMatch, approvedApprovalsFile, buildRetryInstruction, canonicalStringify, classifierOptionsFromConfig, compactApprovals, configuredControlPlaneDir, createApprovalRecord, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, scrubValue, toolFingerprint, } from '../../core/index.js';
|
|
12
12
|
import { notifyDeny } from '../../core/notify.js';
|
|
13
13
|
import { isTransactionalEligible, runTransactionalExecution, TRANSACTIONAL_ALREADY_APPLIED, TRANSACTIONAL_APPROVAL_BYPASS_REASONS, } from '../../core/transactional/index.js';
|
|
14
14
|
import { protectedArtifactRoots } from '../layouts/protected-paths.js';
|
|
@@ -142,7 +142,15 @@ async function consumeApprovedApproval(ctx, deps, kind, fingerprint) {
|
|
|
142
142
|
await deps.writeApprovals(approved.filePath, approved.state);
|
|
143
143
|
return null;
|
|
144
144
|
}
|
|
145
|
-
const
|
|
145
|
+
const approval = approved.state.approvals[index];
|
|
146
|
+
if (approval.executionLeaseExpiresAt) {
|
|
147
|
+
await deps.writeApprovals(approved.filePath, approved.state);
|
|
148
|
+
return approval;
|
|
149
|
+
}
|
|
150
|
+
approved.state.approvals[index] = {
|
|
151
|
+
...approval,
|
|
152
|
+
executionLeaseExpiresAt: new Date(Date.now() + APPROVAL_EXECUTION_LEASE_MS).toISOString(),
|
|
153
|
+
};
|
|
146
154
|
await deps.writeApprovals(approved.filePath, approved.state);
|
|
147
155
|
return approval;
|
|
148
156
|
}
|
|
@@ -290,8 +298,8 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
290
298
|
predictedAssessment: auditExtras.predictedAssessment,
|
|
291
299
|
observedAssessment: auditExtras.observedAssessment,
|
|
292
300
|
mode: ctx.config.mode,
|
|
293
|
-
schemaVersion: result.
|
|
294
|
-
...(result.
|
|
301
|
+
schemaVersion: result.axes ? 2 : 1,
|
|
302
|
+
...(result.axes ?? {}),
|
|
295
303
|
...auditExtras.transactionalLayer,
|
|
296
304
|
};
|
|
297
305
|
if (result.reason === TRANSACTIONAL_ALREADY_APPLIED) {
|
|
@@ -52,7 +52,7 @@ function classifyResultToGateVerdict(params) {
|
|
|
52
52
|
approvalId,
|
|
53
53
|
user_message,
|
|
54
54
|
agent_message,
|
|
55
|
-
|
|
55
|
+
axes: result.axes
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
function unnormalizedGateVerdict(params) {
|
|
@@ -128,7 +128,7 @@ var LEGACY_POLICY_V3 = {
|
|
|
128
128
|
fenceWarnThreshold: DEFAULT_FENCE_WARN_THRESHOLD
|
|
129
129
|
};
|
|
130
130
|
var DEFAULT_POLICY_V3 = {
|
|
131
|
-
unknownLocalEffect: "
|
|
131
|
+
unknownLocalEffect: "allow_flagged",
|
|
132
132
|
unparseableShell: "deny",
|
|
133
133
|
codexUnmappedTool: "deny",
|
|
134
134
|
confidenceThresholds: { ...DEFAULT_CONFIDENCE_THRESHOLDS },
|
|
@@ -745,16 +745,25 @@ import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile2 } from
|
|
|
745
745
|
import path16 from "node:path";
|
|
746
746
|
|
|
747
747
|
// src/core/approval.ts
|
|
748
|
+
var APPROVAL_EXECUTION_LEASE_MS = 6e4;
|
|
748
749
|
function nowIso() {
|
|
749
750
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
750
751
|
}
|
|
751
752
|
function isExpired(approval) {
|
|
752
753
|
return Date.parse(approval.expiresAt) <= Date.now();
|
|
753
754
|
}
|
|
755
|
+
function isExecutionLeaseExpired(approval) {
|
|
756
|
+
if (!approval.executionLeaseExpiresAt) {
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
return Date.parse(approval.executionLeaseExpiresAt) <= Date.now();
|
|
760
|
+
}
|
|
754
761
|
function compactApprovals(state) {
|
|
755
762
|
return {
|
|
756
763
|
version: state.version,
|
|
757
|
-
approvals: state.approvals.filter(
|
|
764
|
+
approvals: state.approvals.filter(
|
|
765
|
+
(approval) => !isExpired(approval) && !isExecutionLeaseExpired(approval)
|
|
766
|
+
)
|
|
758
767
|
};
|
|
759
768
|
}
|
|
760
769
|
function escapeRegex(value) {
|
|
@@ -763,8 +772,15 @@ function escapeRegex(value) {
|
|
|
763
772
|
}
|
|
764
773
|
function approvalCommandMatch(prompt, tokenPrefix) {
|
|
765
774
|
const escapedPrefix = escapeRegex(tokenPrefix);
|
|
766
|
-
const
|
|
767
|
-
|
|
775
|
+
const linePattern = new RegExp(`^\\s*${escapedPrefix}\\s+(\\S+)\\s*$`, "i");
|
|
776
|
+
for (const line of prompt.split(/\r?\n/)) {
|
|
777
|
+
if (!line.trim()) {
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
const match = line.match(linePattern);
|
|
781
|
+
return match?.[1] ?? null;
|
|
782
|
+
}
|
|
783
|
+
return null;
|
|
768
784
|
}
|
|
769
785
|
function buildRetryInstruction(tokenPrefix, approvalId) {
|
|
770
786
|
return `To allow the next matching action once, send ${tokenPrefix} ${approvalId} and then retry the original action unchanged.`;
|
|
@@ -1503,7 +1519,7 @@ function matchesSensitivePath(filePath, patterns) {
|
|
|
1503
1519
|
return false;
|
|
1504
1520
|
}
|
|
1505
1521
|
|
|
1506
|
-
// src/core/
|
|
1522
|
+
// src/core/verdict/judge-audit.ts
|
|
1507
1523
|
function judgeTraceAuditFields(trace) {
|
|
1508
1524
|
if (!trace) {
|
|
1509
1525
|
return {};
|
|
@@ -1518,7 +1534,7 @@ function judgeTraceAuditFields(trace) {
|
|
|
1518
1534
|
};
|
|
1519
1535
|
}
|
|
1520
1536
|
|
|
1521
|
-
// src/core/
|
|
1537
|
+
// src/core/verdict/judge-outbound.ts
|
|
1522
1538
|
var PATH_LIKE = /(?:^|[\s"'`=])(~\/[^\s"'`]+|\/[^\s"'`]+|\.\/[^\s"'`]+|\.\.\/[^\s"'`]+|[A-Za-z]:\\[^\s"'`]+)/g;
|
|
1523
1539
|
var REDACTED_PLACEHOLDER = /^(?:<redacted>|\[REDACTED\]|<secret>|<high-entropy>|<approval-id>)$/i;
|
|
1524
1540
|
var URL_CREDENTIALS_PATTERN2 = /\b[A-Za-z][A-Za-z0-9+.-]*:\/\/([^/\s:@]+):([^@\s/]+)@/gi;
|
|
@@ -1587,16 +1603,16 @@ function scrubOutboundForJudge(text, options) {
|
|
|
1587
1603
|
}
|
|
1588
1604
|
}
|
|
1589
1605
|
|
|
1590
|
-
// src/core/
|
|
1606
|
+
// src/core/verdict/judge.ts
|
|
1591
1607
|
var SECRET_PATTERNS = [/\.env\b/i, /\.pem\b/i, /id_rsa\b/i, /credentials/i, /secrets?\b/i];
|
|
1592
1608
|
var DESTRUCTIVE_VERBS = /\b(rm|rmtree|unlink|delete|truncate|shred|destroy|drop)\b/i;
|
|
1593
1609
|
var GIT_PATTERNS = /\.git\b/i;
|
|
1594
|
-
var TIER1_PROMPT = `Answer ONLY with JSON: {"external_change":boolean,"
|
|
1595
|
-
|
|
1610
|
+
var TIER1_PROMPT = `Answer ONLY with JSON: {"external_change":boolean,"reason":string}
|
|
1611
|
+
reason should be a short snake_case label.
|
|
1596
1612
|
|
|
1597
|
-
external_change:
|
|
1598
|
-
|
|
1599
|
-
|
|
1613
|
+
external_change: true if this operation CREATEs, MODIFYs, DELETEs, or SENDs data to any system outside the local machine and its git repository working tree.
|
|
1614
|
+
READ/QUERY/GET/list/describe/SELECT/HTTP GET is NOT a change. Local build, test, and package install inside the repo are NOT external changes.
|
|
1615
|
+
If unsure, answer true.
|
|
1600
1616
|
|
|
1601
1617
|
Command/code:
|
|
1602
1618
|
`;
|
|
@@ -1611,13 +1627,13 @@ function failClosedVerdict(reason) {
|
|
|
1611
1627
|
function parseTier1Json(raw) {
|
|
1612
1628
|
try {
|
|
1613
1629
|
const parsed = JSON.parse(raw);
|
|
1614
|
-
if (typeof parsed.external_change !== "boolean"
|
|
1630
|
+
if (typeof parsed.external_change !== "boolean") {
|
|
1615
1631
|
return null;
|
|
1616
1632
|
}
|
|
1617
1633
|
return {
|
|
1618
|
-
external_change: parsed.external_change
|
|
1619
|
-
destroys_outside_repo:
|
|
1620
|
-
destroys_history_or_secrets:
|
|
1634
|
+
external_change: parsed.external_change,
|
|
1635
|
+
destroys_outside_repo: false,
|
|
1636
|
+
destroys_history_or_secrets: false,
|
|
1621
1637
|
reason: typeof parsed.reason === "string" ? parsed.reason : "tier1_llm"
|
|
1622
1638
|
};
|
|
1623
1639
|
} catch {
|
|
@@ -1840,10 +1856,10 @@ function createOpenAiCompatibleJudge(options) {
|
|
|
1840
1856
|
return judge;
|
|
1841
1857
|
}
|
|
1842
1858
|
function tier1RequiresAsk(verdict2) {
|
|
1843
|
-
return verdict2.external_change || verdict2.
|
|
1859
|
+
return verdict2.external_change || verdict2.destroys_history_or_secrets;
|
|
1844
1860
|
}
|
|
1845
1861
|
|
|
1846
|
-
// src/core/
|
|
1862
|
+
// src/core/verdict/judge-factory.ts
|
|
1847
1863
|
var FIXTURE_MODELS_URL = new URL("../../../fixtures/judge-models.json", import.meta.url);
|
|
1848
1864
|
function resolveCloudModel(requested, pinned) {
|
|
1849
1865
|
if (requested === "auto") {
|
|
@@ -1890,10 +1906,10 @@ function createJudgeFromConfig(config, options = {}) {
|
|
|
1890
1906
|
return createDeterministicJudgeStub();
|
|
1891
1907
|
}
|
|
1892
1908
|
|
|
1893
|
-
// src/core/
|
|
1909
|
+
// src/core/verdict/verdict.ts
|
|
1894
1910
|
import path11 from "node:path";
|
|
1895
1911
|
|
|
1896
|
-
// src/core/
|
|
1912
|
+
// src/core/verdict/containment.ts
|
|
1897
1913
|
import path8 from "node:path";
|
|
1898
1914
|
function expandHome(token) {
|
|
1899
1915
|
if (token === "~" || token.startsWith("~/")) {
|
|
@@ -1985,7 +2001,7 @@ function cwdRelative(repoRoot, cwd) {
|
|
|
1985
2001
|
return relativeWithinRepo(repoRoot, cwd) ?? cwd;
|
|
1986
2002
|
}
|
|
1987
2003
|
|
|
1988
|
-
// src/core/
|
|
2004
|
+
// src/core/verdict/egress-classify.ts
|
|
1989
2005
|
var EGRESS_TOOL_HEADS = /* @__PURE__ */ new Set([
|
|
1990
2006
|
"aws",
|
|
1991
2007
|
"curl",
|
|
@@ -2083,6 +2099,9 @@ function classifyAws(tokens) {
|
|
|
2083
2099
|
if (/\bs3\s+rm\b/.test(joined)) {
|
|
2084
2100
|
return "destructive";
|
|
2085
2101
|
}
|
|
2102
|
+
if (/\bs3\s+mb\b/.test(joined)) {
|
|
2103
|
+
return "destructive";
|
|
2104
|
+
}
|
|
2086
2105
|
if (/\bs3\s+sync\b/.test(joined)) {
|
|
2087
2106
|
return "destructive";
|
|
2088
2107
|
}
|
|
@@ -2193,15 +2212,59 @@ function classifyNetlify(tokens) {
|
|
|
2193
2212
|
return "ambiguous";
|
|
2194
2213
|
}
|
|
2195
2214
|
|
|
2196
|
-
// src/core/
|
|
2215
|
+
// src/core/verdict/fingerprint.ts
|
|
2197
2216
|
function verdictFingerprint(cwdRelative2, commandRedacted) {
|
|
2198
2217
|
return hashValue(`v2:${cwdRelative2}:${commandRedacted}`);
|
|
2199
2218
|
}
|
|
2200
2219
|
|
|
2201
|
-
// src/core/
|
|
2220
|
+
// src/core/verdict/launcher-resolve.ts
|
|
2202
2221
|
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
|
|
2203
2222
|
import path9 from "node:path";
|
|
2204
2223
|
var MAX_RESOLVE_DEPTH = 8;
|
|
2224
|
+
var PNPM_BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
|
|
2225
|
+
"add",
|
|
2226
|
+
"audit",
|
|
2227
|
+
"cache",
|
|
2228
|
+
"config",
|
|
2229
|
+
"deploy",
|
|
2230
|
+
"dlx",
|
|
2231
|
+
"exec",
|
|
2232
|
+
"fetch",
|
|
2233
|
+
"help",
|
|
2234
|
+
"import",
|
|
2235
|
+
"init",
|
|
2236
|
+
"install",
|
|
2237
|
+
"i",
|
|
2238
|
+
"licenses",
|
|
2239
|
+
"link",
|
|
2240
|
+
"list",
|
|
2241
|
+
"outdated",
|
|
2242
|
+
"pack",
|
|
2243
|
+
"patch",
|
|
2244
|
+
"patch-commit",
|
|
2245
|
+
"patch-remove",
|
|
2246
|
+
"publish",
|
|
2247
|
+
"prune",
|
|
2248
|
+
"rebuild",
|
|
2249
|
+
"remove",
|
|
2250
|
+
"rm",
|
|
2251
|
+
"store",
|
|
2252
|
+
"unlink",
|
|
2253
|
+
"update",
|
|
2254
|
+
"up",
|
|
2255
|
+
"why"
|
|
2256
|
+
]);
|
|
2257
|
+
var PNPM_EXEC_LIKE_HEADS = /* @__PURE__ */ new Set([
|
|
2258
|
+
"vitest",
|
|
2259
|
+
"vite",
|
|
2260
|
+
"biome",
|
|
2261
|
+
"eslint",
|
|
2262
|
+
"jest",
|
|
2263
|
+
"mocha",
|
|
2264
|
+
"tsc",
|
|
2265
|
+
"tsx",
|
|
2266
|
+
"node"
|
|
2267
|
+
]);
|
|
2205
2268
|
function readPackageJson(dir) {
|
|
2206
2269
|
const packagePath = path9.join(dir, "package.json");
|
|
2207
2270
|
if (!existsSync4(packagePath)) {
|
|
@@ -2256,6 +2319,12 @@ function npmScriptName(tokens) {
|
|
|
2256
2319
|
if (launcher[0] === "pnpm" && launcher[1] === "run" && launcher[2]) {
|
|
2257
2320
|
return launcher[2];
|
|
2258
2321
|
}
|
|
2322
|
+
if (launcher[0] === "pnpm" && launcher[1] === "test") {
|
|
2323
|
+
return "test";
|
|
2324
|
+
}
|
|
2325
|
+
if (launcher[0] === "pnpm" && launcher[1] && !launcher[1].startsWith("-") && !PNPM_BUILTIN_COMMANDS.has(launcher[1])) {
|
|
2326
|
+
return launcher[1];
|
|
2327
|
+
}
|
|
2259
2328
|
if (launcher[0] === "npm" && launcher[1] && launcher[1] !== "run" && launcher[1] !== "install") {
|
|
2260
2329
|
return null;
|
|
2261
2330
|
}
|
|
@@ -2377,11 +2446,31 @@ function resolveLauncherRecipe(params) {
|
|
|
2377
2446
|
const tokens = params.tokens;
|
|
2378
2447
|
const scriptName = npmScriptName(tokens);
|
|
2379
2448
|
if (scriptName) {
|
|
2380
|
-
|
|
2449
|
+
const resolution = resolveNpmRecipe(
|
|
2450
|
+
params.cwd,
|
|
2451
|
+
params.repoRoot,
|
|
2452
|
+
scriptName,
|
|
2453
|
+
forwardedArgs(tokens)
|
|
2454
|
+
);
|
|
2455
|
+
if (tokens[0] === "pnpm" && tokens[1] && PNPM_EXEC_LIKE_HEADS.has(tokens[1]) && resolution.reason === "npm_script_undefined") {
|
|
2456
|
+
return {
|
|
2457
|
+
recipes: [tokens.slice(1).join(" ")],
|
|
2458
|
+
opaque: false,
|
|
2459
|
+
reason: "pnpm_exec_like"
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
return resolution;
|
|
2381
2463
|
}
|
|
2382
2464
|
if (tokens[0] === "make" && tokens[1] && !tokens[1].startsWith("-")) {
|
|
2383
2465
|
return resolveMakeRecipe(params.cwd, params.repoRoot, tokens[1]);
|
|
2384
2466
|
}
|
|
2467
|
+
if (tokens[0] === "pnpm" && tokens[1] && PNPM_EXEC_LIKE_HEADS.has(tokens[1])) {
|
|
2468
|
+
return {
|
|
2469
|
+
recipes: [tokens.slice(1).join(" ")],
|
|
2470
|
+
opaque: false,
|
|
2471
|
+
reason: "pnpm_exec_like"
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2385
2474
|
return null;
|
|
2386
2475
|
}
|
|
2387
2476
|
function isRoutineLauncher(tokens) {
|
|
@@ -2397,7 +2486,7 @@ function matchesCustomCommand(normalizedCommand, key, pattern) {
|
|
|
2397
2486
|
return normalizedCommand === trimmed || key === trimmed;
|
|
2398
2487
|
}
|
|
2399
2488
|
|
|
2400
|
-
// src/core/
|
|
2489
|
+
// src/core/verdict/overrides.ts
|
|
2401
2490
|
function matchesCustomPatterns(command, segment, patterns) {
|
|
2402
2491
|
if (!patterns || patterns.length === 0) {
|
|
2403
2492
|
return false;
|
|
@@ -2436,7 +2525,7 @@ function askFromCustomExternal(opacity) {
|
|
|
2436
2525
|
};
|
|
2437
2526
|
}
|
|
2438
2527
|
|
|
2439
|
-
// src/core/
|
|
2528
|
+
// src/core/verdict/parser.ts
|
|
2440
2529
|
import path10 from "node:path";
|
|
2441
2530
|
|
|
2442
2531
|
// src/core/shell-substitution.ts
|
|
@@ -2660,7 +2749,7 @@ function hasUnbalancedDollarParen(command) {
|
|
|
2660
2749
|
return depth > 0;
|
|
2661
2750
|
}
|
|
2662
2751
|
|
|
2663
|
-
// src/core/
|
|
2752
|
+
// src/core/verdict/parser.ts
|
|
2664
2753
|
var ENV_PREFIX_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=(?:'[^']*'|"[^"]*"|\S+)$/;
|
|
2665
2754
|
var TRANSPARENT_WRAPPERS = /* @__PURE__ */ new Set([
|
|
2666
2755
|
"sudo",
|
|
@@ -2860,7 +2949,7 @@ function redactCommand(command) {
|
|
|
2860
2949
|
return command.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]").replace(/sk-[A-Za-z0-9]{8,}/g, "sk-[REDACTED]").trim();
|
|
2861
2950
|
}
|
|
2862
2951
|
|
|
2863
|
-
// src/core/
|
|
2952
|
+
// src/core/verdict/verdict.ts
|
|
2864
2953
|
var DEFAULT_MAX_DEPTH = 8;
|
|
2865
2954
|
var TIER0_EXTERNAL_KEYS = /* @__PURE__ */ new Set([
|
|
2866
2955
|
"git push",
|
|
@@ -2943,6 +3032,7 @@ var LOCAL_ROUTINE_HEADS = /* @__PURE__ */ new Set([
|
|
|
2943
3032
|
"make",
|
|
2944
3033
|
"cmake"
|
|
2945
3034
|
]);
|
|
3035
|
+
var BELAY_SELF_COMMANDS = /* @__PURE__ */ new Set(["approve", "revoke"]);
|
|
2946
3036
|
var FIND_DANGEROUS_FLAGS = /* @__PURE__ */ new Set(["-delete", "-exec", "-execdir", "-ok", "-okdir"]);
|
|
2947
3037
|
function isFindDangerous(tokens) {
|
|
2948
3038
|
return tokens.some(
|
|
@@ -3090,6 +3180,11 @@ function tier0ExternalMatch(key, head, tokens) {
|
|
|
3090
3180
|
}
|
|
3091
3181
|
return false;
|
|
3092
3182
|
}
|
|
3183
|
+
function isBelaySelfCommand(tokens) {
|
|
3184
|
+
const head = tokens[0] ?? "";
|
|
3185
|
+
const subcommand = tokens[1] ?? "";
|
|
3186
|
+
return head === "belay" && BELAY_SELF_COMMANDS.has(subcommand);
|
|
3187
|
+
}
|
|
3093
3188
|
function tier0HighStakesRm(tokens, context) {
|
|
3094
3189
|
const head = tokens[0] ?? "";
|
|
3095
3190
|
if (head !== "rm") {
|
|
@@ -3321,6 +3416,16 @@ async function evaluateSegment(command, context, depth) {
|
|
|
3321
3416
|
if (rmVerdict) {
|
|
3322
3417
|
return rmVerdict;
|
|
3323
3418
|
}
|
|
3419
|
+
if (isBelaySelfCommand(peeled)) {
|
|
3420
|
+
return allowVerdict({
|
|
3421
|
+
location: "unknown",
|
|
3422
|
+
opacity: "transparent",
|
|
3423
|
+
effect: "local_mutation",
|
|
3424
|
+
confidence: "deterministic",
|
|
3425
|
+
reason: "belay_control_plane_command",
|
|
3426
|
+
signals: ["belay_control_plane_command", segment.head]
|
|
3427
|
+
});
|
|
3428
|
+
}
|
|
3324
3429
|
let effect = "unknown";
|
|
3325
3430
|
if (READ_ONLY_KEYS.has(segment.key) || READ_ONLY_KEYS.has(segment.head)) {
|
|
3326
3431
|
effect = "read_only";
|
|
@@ -3541,7 +3646,7 @@ async function verdict(command, context) {
|
|
|
3541
3646
|
);
|
|
3542
3647
|
}
|
|
3543
3648
|
|
|
3544
|
-
// src/core/
|
|
3649
|
+
// src/core/verdict/adapter.ts
|
|
3545
3650
|
function buildVerdictContext(params) {
|
|
3546
3651
|
const protectedArtifactRoots2 = [
|
|
3547
3652
|
...params.options?.protectedArtifactRoots ?? [],
|
|
@@ -3588,6 +3693,12 @@ function mapLegacyReason(result) {
|
|
|
3588
3693
|
if (result.reason === "repo_local_mutation") {
|
|
3589
3694
|
return "local_mutation";
|
|
3590
3695
|
}
|
|
3696
|
+
if (result.reason === "tier1_not_restorable") {
|
|
3697
|
+
return "tier1_catastrophic";
|
|
3698
|
+
}
|
|
3699
|
+
if (result.reason === "tier0_restorable" || result.reason === "tier1_restorable") {
|
|
3700
|
+
return result.effect === "local_mutation" ? "local_mutation" : result.reason;
|
|
3701
|
+
}
|
|
3591
3702
|
return result.reason;
|
|
3592
3703
|
}
|
|
3593
3704
|
function verdictToClassifyResult(result) {
|
|
@@ -3608,13 +3719,13 @@ function verdictToClassifyResult(result) {
|
|
|
3608
3719
|
assessment,
|
|
3609
3720
|
normalizedCommand: result.commandRedacted,
|
|
3610
3721
|
summary: result.commandRedacted,
|
|
3611
|
-
|
|
3722
|
+
axes: {
|
|
3612
3723
|
location: result.location,
|
|
3613
3724
|
opacity: result.opacity,
|
|
3614
3725
|
effect: result.effect,
|
|
3615
3726
|
confidence: result.confidence,
|
|
3616
3727
|
would: result.permission,
|
|
3617
|
-
by: "
|
|
3728
|
+
by: "verdict",
|
|
3618
3729
|
commandRedacted: result.commandRedacted,
|
|
3619
3730
|
commandFingerprint: result.fingerprint,
|
|
3620
3731
|
signals: result.signals,
|
|
@@ -4064,7 +4175,7 @@ function normalizeGatedAction(params) {
|
|
|
4064
4175
|
};
|
|
4065
4176
|
}
|
|
4066
4177
|
function applyShellPeripheralPolicy(command, action, result, options) {
|
|
4067
|
-
if (options.brokerFsScope && result.verdict === "deny_pending_approval" && (result.reason === "outside_repo_mutation" || result.reason === "outside_repo_redirect" || result.reason === "repo_outside_mutation" || result.
|
|
4178
|
+
if (options.brokerFsScope && result.verdict === "deny_pending_approval" && (result.reason === "outside_repo_mutation" || result.reason === "outside_repo_redirect" || result.reason === "repo_outside_mutation" || result.axes?.location === "repo_outside")) {
|
|
4068
4179
|
const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot);
|
|
4069
4180
|
if (outsideRepoPaths.length > 0 && options.fsScopeAllowlist && allPathsAllowlisted(outsideRepoPaths, options.fsScopeAllowlist)) {
|
|
4070
4181
|
return {
|
|
@@ -4743,7 +4854,15 @@ async function consumeApprovedApproval(ctx, deps, kind, fingerprint) {
|
|
|
4743
4854
|
await deps.writeApprovals(approved.filePath, approved.state);
|
|
4744
4855
|
return null;
|
|
4745
4856
|
}
|
|
4746
|
-
const
|
|
4857
|
+
const approval = approved.state.approvals[index];
|
|
4858
|
+
if (approval.executionLeaseExpiresAt) {
|
|
4859
|
+
await deps.writeApprovals(approved.filePath, approved.state);
|
|
4860
|
+
return approval;
|
|
4861
|
+
}
|
|
4862
|
+
approved.state.approvals[index] = {
|
|
4863
|
+
...approval,
|
|
4864
|
+
executionLeaseExpiresAt: new Date(Date.now() + APPROVAL_EXECUTION_LEASE_MS).toISOString()
|
|
4865
|
+
};
|
|
4747
4866
|
await deps.writeApprovals(approved.filePath, approved.state);
|
|
4748
4867
|
return approval;
|
|
4749
4868
|
}
|
|
@@ -4859,8 +4978,8 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
4859
4978
|
predictedAssessment: auditExtras.predictedAssessment,
|
|
4860
4979
|
observedAssessment: auditExtras.observedAssessment,
|
|
4861
4980
|
mode: ctx.config.mode,
|
|
4862
|
-
schemaVersion: result.
|
|
4863
|
-
...result.
|
|
4981
|
+
schemaVersion: result.axes ? 2 : 1,
|
|
4982
|
+
...result.axes ?? {},
|
|
4864
4983
|
...auditExtras.transactionalLayer
|
|
4865
4984
|
};
|
|
4866
4985
|
if (result.reason === TRANSACTIONAL_ALREADY_APPLIED) {
|