@guilz-dev/belay 0.1.1 → 0.3.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.
Files changed (106) hide show
  1. package/README.md +55 -15
  2. package/dist/adapters/shared/gate-runtime.js +25 -9
  3. package/dist/bundle/claude-runtime.mjs +1786 -379
  4. package/dist/bundle/codex-runtime.mjs +1785 -379
  5. package/dist/bundle/cursor-runtime.mjs +1786 -379
  6. package/dist/cli.js +201 -11
  7. package/dist/commands/approve.js +11 -0
  8. package/dist/commands/classify-for-report.js +3 -3
  9. package/dist/commands/config.d.ts +70 -0
  10. package/dist/commands/config.js +494 -0
  11. package/dist/commands/doctor.js +3 -2
  12. package/dist/commands/explain.js +14 -14
  13. package/dist/commands/health-snapshot.d.ts +6 -0
  14. package/dist/commands/health-snapshot.js +34 -0
  15. package/dist/commands/judge.d.ts +90 -0
  16. package/dist/commands/judge.js +285 -0
  17. package/dist/commands/recover.js +2 -2
  18. package/dist/commands/status.js +2 -2
  19. package/dist/commands/stdin-key.d.ts +1 -0
  20. package/dist/commands/stdin-key.js +8 -0
  21. package/dist/conformance/guarantee-table.js +12 -0
  22. package/dist/conformance/types.d.ts +2 -0
  23. package/dist/core/approval.d.ts +3 -0
  24. package/dist/core/approval.js +18 -3
  25. package/dist/core/audit-io.d.ts +2 -0
  26. package/dist/core/audit-io.js +14 -0
  27. package/dist/core/audit-query.js +5 -1
  28. package/dist/core/capability/index.d.ts +2 -2
  29. package/dist/core/capability/index.js +2 -2
  30. package/dist/core/capability/paths.d.ts +1 -0
  31. package/dist/core/capability/paths.js +74 -6
  32. package/dist/core/capability/reasons.d.ts +3 -0
  33. package/dist/core/capability/reasons.js +8 -0
  34. package/dist/core/classify-tool.js +97 -27
  35. package/dist/core/config-layers.js +2 -1
  36. package/dist/core/config.d.ts +23 -3
  37. package/dist/core/config.js +112 -13
  38. package/dist/core/credential-store.d.ts +11 -0
  39. package/dist/core/credential-store.js +60 -0
  40. package/dist/core/gate-contract.d.ts +1 -1
  41. package/dist/core/gate-contract.js +1 -1
  42. package/dist/core/gate-engine.js +105 -14
  43. package/dist/core/index.d.ts +2 -2
  44. package/dist/core/index.js +2 -2
  45. package/dist/core/integrity.d.ts +2 -0
  46. package/dist/core/integrity.js +13 -0
  47. package/dist/core/judge-api-key.d.ts +19 -0
  48. package/dist/core/judge-api-key.js +74 -0
  49. package/dist/core/judge-cloud-consent.d.ts +13 -0
  50. package/dist/core/judge-cloud-consent.js +38 -0
  51. package/dist/core/judge-config.d.ts +45 -4
  52. package/dist/core/judge-config.js +266 -44
  53. package/dist/core/judge-doctor.d.ts +6 -1
  54. package/dist/core/judge-doctor.js +148 -97
  55. package/dist/core/judge-model-discovery.d.ts +24 -0
  56. package/dist/core/judge-model-discovery.js +168 -0
  57. package/dist/core/judge-model-policy.d.ts +5 -0
  58. package/dist/core/judge-model-policy.js +21 -0
  59. package/dist/core/judge-runtime-detection.d.ts +9 -0
  60. package/dist/core/judge-runtime-detection.js +68 -0
  61. package/dist/core/transactional/diff-evaluator.js +1 -19
  62. package/dist/core/types.d.ts +7 -3
  63. package/dist/core/{v2 → verdict}/adapter.d.ts +1 -0
  64. package/dist/core/{v2 → verdict}/adapter.js +16 -4
  65. package/dist/core/{v2 → verdict}/containment.d.ts +5 -0
  66. package/dist/core/{v2 → verdict}/containment.js +32 -2
  67. package/dist/core/{v2 → verdict}/egress-classify.js +3 -0
  68. package/dist/core/verdict/judge-catalog.d.ts +40 -0
  69. package/dist/core/verdict/judge-catalog.js +148 -0
  70. package/dist/core/verdict/judge-cli.d.ts +23 -0
  71. package/dist/core/verdict/judge-cli.js +280 -0
  72. package/dist/core/{v2 → verdict}/judge-factory.d.ts +15 -4
  73. package/dist/core/verdict/judge-factory.js +178 -0
  74. package/dist/core/{v2 → verdict}/judge.d.ts +20 -1
  75. package/dist/core/{v2 → verdict}/judge.js +85 -18
  76. package/dist/core/{v2 → verdict}/launcher-resolve.js +72 -1
  77. package/dist/core/verdict/persistent-paths.d.ts +8 -0
  78. package/dist/core/verdict/persistent-paths.js +52 -0
  79. package/dist/core/{v2 → verdict}/types.d.ts +6 -2
  80. package/dist/core/{v2 → verdict}/verdict.js +176 -46
  81. package/dist/corpus/evaluate.js +2 -2
  82. package/dist/installer.js +66 -14
  83. package/dist/types.d.ts +8 -1
  84. package/dist/version.d.ts +1 -1
  85. package/dist/version.js +1 -1
  86. package/package.json +2 -1
  87. package/skills/belay/SKILL.md +8 -7
  88. package/dist/commands/init-wizard.d.ts +0 -16
  89. package/dist/commands/init-wizard.js +0 -50
  90. package/dist/core/v2/judge-factory.js +0 -75
  91. /package/dist/core/{v2 → verdict}/egress-classify.d.ts +0 -0
  92. /package/dist/core/{v2 → verdict}/fingerprint.d.ts +0 -0
  93. /package/dist/core/{v2 → verdict}/fingerprint.js +0 -0
  94. /package/dist/core/{v2 → verdict}/index.d.ts +0 -0
  95. /package/dist/core/{v2 → verdict}/index.js +0 -0
  96. /package/dist/core/{v2 → verdict}/judge-audit.d.ts +0 -0
  97. /package/dist/core/{v2 → verdict}/judge-audit.js +0 -0
  98. /package/dist/core/{v2 → verdict}/judge-outbound.d.ts +0 -0
  99. /package/dist/core/{v2 → verdict}/judge-outbound.js +0 -0
  100. /package/dist/core/{v2 → verdict}/launcher-resolve.d.ts +0 -0
  101. /package/dist/core/{v2 → verdict}/overrides.d.ts +0 -0
  102. /package/dist/core/{v2 → verdict}/overrides.js +0 -0
  103. /package/dist/core/{v2 → verdict}/parser.d.ts +0 -0
  104. /package/dist/core/{v2 → verdict}/parser.js +0 -0
  105. /package/dist/core/{v2 → verdict}/types.js +0 -0
  106. /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
  [![npm version](https://img.shields.io/npm/v/@guilz-dev/belay)](https://www.npmjs.com/package/@guilz-dev/belay)
4
+ [![skills.sh](https://skills.sh/b/guilz-dev/belay)](https://skills.sh/guilz-dev/belay)
4
5
  [![CI](https://github.com/guilz-dev/belay/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/guilz-dev/belay/actions/workflows/ci.yml)
5
6
  [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
7
 
@@ -33,8 +34,8 @@ policy to maintain.
33
34
  | **Claude Code** | Supported | `.claude/settings.json` | `.claude/belay.config.json` |
34
35
  | **Codex** | Experimental | `.codex/config.toml` | `.codex/belay.config.json` |
35
36
 
36
- Pick the adapter at install time with `--adapter cursor|claude|codex` (or let
37
- `init-wizard` prompt). Hosts use different hook event names, but Belay registers
37
+ Pick the adapter at install time with `--adapter cursor|claude|codex` (or use
38
+ `belay config` interactively). Hosts use different hook event names, but Belay registers
38
39
  the same runners (`belay-tool-gate`, `belay-before-submit`, `belay-audit`) at
39
40
  equivalent lifecycle points:
40
41
 
@@ -67,8 +68,8 @@ audit instead of guessing.
67
68
  ## Quick start
68
69
 
69
70
  ```bash
70
- # Interactive setup (prompts for adapter, scope, skill, mode)
71
- npx @guilz-dev/belay init-wizard
71
+ # Interactive setup (adapter, scope, skill, judge provider, credentials)
72
+ belay config
72
73
 
73
74
  # Or non-interactive
74
75
  npx @guilz-dev/belay init --adapter claude # Claude Code
@@ -130,6 +131,9 @@ Belay is a layered hook gate, not a static denylist. Higher layers are opt-in.
130
131
  - L3 command lists are **not security boundaries** by themselves — see
131
132
  [docs/ops/semver-policy.md](./docs/ops/semver-policy.md) and
132
133
  [docs/guarantee-table.md](./docs/guarantee-table.md).
134
+ - At default L3, **local-recoverable mutations outside the repo** pass after Tier1
135
+ (e.g. IDE plan files). To deny repo-external writes at the OS boundary, enable L1-full
136
+ (`sandbox.runtime` ≠ `none`). Tier1 requires a working judge (local Ollama by default).
133
137
  - Adversarial resistance requires the full L1 stack:
134
138
  `belay init --preset l1-full-recommended`, verified with `belay sandbox status`.
135
139
 
@@ -148,12 +152,23 @@ and skill under `~/.cursor/`, so the gate is user-wide while `belay.config.json`
148
152
  approvals, and audit stay repo-local.
149
153
 
150
154
  **Skill-only.** The skill is just a UX layer (slash commands + guidance) and does
151
- **not** enable gating on its own:
155
+ **not** enable gating on its own. Install from [skills.sh](https://skills.sh/guilz-dev/belay)
156
+ or GitHub:
152
157
 
153
158
  ```bash
159
+ # Cursor
154
160
  npx skills add guilz-dev/belay --skill belay -a cursor -y
161
+
162
+ # Claude Code
163
+ npx skills add guilz-dev/belay --skill belay -a claude-code -y
164
+
165
+ # Codex
166
+ npx skills add guilz-dev/belay --skill belay -a codex -y
155
167
  ```
156
168
 
169
+ Running `npx skills add` also registers anonymous install telemetry on skills.sh,
170
+ which is how the skill appears in the directory leaderboard.
171
+
157
172
  Runtime enforcement still requires `belay init` in the target repository.
158
173
 
159
174
  ## Dogfood → enforce
@@ -213,8 +228,9 @@ load.
213
228
 
214
229
  Notable settings:
215
230
 
216
- - **`policy.unknownLocalEffect: "deny"`** — fail-closed classification for
217
- unrecognized local commands.
231
+ - **`policy.unknownLocalEffect: "allow_flagged"`** (fresh default) after Tier1
232
+ says recoverable, structurally unknown local commands run with an audit flag. Use
233
+ `"deny"` (via `belay dogfood`) to ask on those commands instead.
218
234
  - **`classifier.strictChains: true`** (default) — scans every `&&`, `|`, and `;`
219
235
  segment and keeps the strictest verdict. Override lists match exact command or
220
236
  segment keys only.
@@ -223,20 +239,37 @@ Notable settings:
223
239
  current OS user. `upgrade` migrates repo-local approvals in; disabling merges
224
240
  them back. File-mutation tools and shell redirects cannot write control-plane
225
241
  paths while it is enabled.
226
- - **Cloud judge** — for `judge.provider: "openai-compatible"`, set
227
- `judge.endpoint` and provide `BELAY_JUDGE_API_KEY` (or `OPENAI_API_KEY`), or
228
- opt in with
229
- `belay init --judge-provider openai-compatible --judge-endpoint <url> --accept-cloud-judge`.
230
- Fresh installs default to local Ollama (`local-ollama`).
242
+ - **Cloud judge** — configure with `belay config` (interactive) or `belay config set judge.providerId <id>`.
243
+ Providers: `ollama`, `codex`, `claude`, `cursor`. **Provider** is the vendor/service
244
+ (`judge.providerId`); **driver** is the API compatibility layer (`judge.provider`:
245
+ `ollama`, `openai-compatible`, or `anthropic`); **host** is where hooks install
246
+ (`config.adapter`: `cursor`, `claude`, `codex`). Set `judge.endpoint` when needed;
247
+ credentials via `belay config credential mode project|apiKey` or env vars. Record egress
248
+ consent during `belay config` or via `belay judge consent` → `belay approve` →
249
+ `belay judge use … --cloud-consent-approval-id`. Cloud providers can use native CLI
250
+ transport without `judge.endpoint` when the host CLI is available (`codex-cli`,
251
+ `cursor-cli`, `claude-cli`); HTTP transport still requires endpoint and recorded consent.
252
+ Fresh installs default to the
253
+ host-matched provider (`cursor` → `cursor`, etc.). `belay judge use` remains available
254
+ as a secondary path.
255
+
256
+ Legacy `judge.model: auto` in config files is normalized to the provider catalog default on load
257
+ (with a warning); new `auto` values are rejected on CLI, `belay config set`, and `belay judge use`.
258
+ On an installed repo, interactive `belay config` defaults to judge-only setup without re-running
259
+ `init`. Model discovery is covered by unit tests with mocks; set `BELAY_LIVE_CLI_DISCOVERY=1` locally
260
+ for optional live CLI probes. Cloud egress consent is enforced for HTTP transport; native CLI
261
+ transport uses the host session and does not require `judge.endpoint`. `BELAY_JUDGE_MODEL_RESOLVED`
262
+ applies only under Vitest (test overrides).
231
263
 
232
264
  ## Command reference
233
265
 
234
266
  ```bash
235
267
  belay init [--adapter cursor|claude|codex] [--scope project|global]
236
268
  [--preset strict|standard|audit-first|l1-full-recommended]
237
- [--with-skill] [--dogfood]
238
- belay init-wizard # interactive install
239
- belay upgrade # refresh hooks + runtime, migrate config
269
+ [--migrate-judge-default] [--with-skill] [--dogfood]
270
+ belay config # interactive setup (primary)
271
+ belay config list|get|set|unset|judge|credential
272
+ belay upgrade [--migrate-judge-default] # refresh hooks + runtime, migrate config
240
273
  belay dogfood [--enforce] # toggle audit / enforce mode
241
274
  belay doctor [--fix] # check (and repair) floor health
242
275
  belay status # show install scope / skill-only state
@@ -250,6 +283,13 @@ belay egress <start|stop|status|env>
250
283
  belay sandbox status
251
284
  belay approve <approval-id> [--scope once|domain|path]
252
285
  belay revoke <approval-id>
286
+ belay judge status
287
+ belay judge list
288
+ belay judge use <ollama|codex|claude|cursor> [--model <id>] [--endpoint <url>]
289
+ [--accept-cloud] [--cloud-consent-approval-id <id>]
290
+ [--credential project|apiKey] [--key-stdin] [--key-env <NAME>]
291
+ belay judge test
292
+ belay judge consent <provider-id> [--endpoint <url>]
253
293
  ```
254
294
 
255
295
  ## Coexisting with existing hooks
@@ -4,11 +4,11 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import { recordApproval } from '../../core/approval-service.js';
6
6
  import { issueApprovalToken } from '../../core/approval-token.js';
7
- import { fsScopeAllowlistPath, isCapabilityBrokerDemotionActive, loadFsScopeAllowlistSync, shouldSkipBrokerApprovedOnce, } from '../../core/capability/index.js';
7
+ import { fsScopeAllowlistPath, isCapabilityBrokerDemotionActive, loadFsScopeAllowlistSync, shouldSkipBrokerApprovedOnce, shouldSkipBrokerApprovedRecord, } from '../../core/capability/index.js';
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 [approval] = approved.state.approvals.splice(index, 1);
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.v2 ? 2 : 1,
294
- ...(result.v2 ?? {}),
301
+ schemaVersion: result.axes ? 2 : 1,
302
+ ...(result.axes ?? {}),
295
303
  ...auditExtras.transactionalLayer,
296
304
  };
297
305
  if (result.reason === TRANSACTIONAL_ALREADY_APPLIED) {
@@ -314,10 +322,18 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
314
322
  });
315
323
  }
316
324
  const brokerActive = isCapabilityBrokerDemotionActive(ctx.config);
317
- const approved = TRANSACTIONAL_APPROVAL_BYPASS_REASONS.has(result.reason) ||
318
- shouldSkipBrokerApprovedOnce(brokerActive, result.reason)
319
- ? null
320
- : await consumeApprovedApproval(ctx, deps, kind, result.fingerprint);
325
+ let approved = null;
326
+ if (!TRANSACTIONAL_APPROVAL_BYPASS_REASONS.has(result.reason) &&
327
+ !shouldSkipBrokerApprovedOnce(brokerActive, result.reason)) {
328
+ const approvedState = await deps.loadApprovals(ctx, 'approved-approvals.json');
329
+ approvedState.state = compactApprovals(approvedState.state);
330
+ const matchedApproval = approvedState.state.approvals.find((entry) => entry.kind === kind &&
331
+ entry.fingerprint === result.fingerprint &&
332
+ entry.repoRoot === ctx.repoRoot);
333
+ if (!shouldSkipBrokerApprovedRecord(brokerActive, matchedApproval?.reason)) {
334
+ approved = await consumeApprovedApproval(ctx, deps, kind, result.fingerprint);
335
+ }
336
+ }
321
337
  if (approved) {
322
338
  await deps.appendAudit(ctx, {
323
339
  ...gateBase,