@guilz-dev/belay 0.7.0 → 0.8.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 (75) hide show
  1. package/README.md +19 -19
  2. package/dist/adapters/shared/gate-runtime.js +23 -5
  3. package/dist/bundle/claude-runtime.mjs +4909 -3504
  4. package/dist/bundle/codex-runtime.mjs +5050 -3645
  5. package/dist/bundle/cursor-runtime.mjs +5050 -3645
  6. package/dist/commands/doctor.js +7 -0
  7. package/dist/commands/dogfood.js +1 -1
  8. package/dist/commands/explain.js +2 -2
  9. package/dist/commands/harvest.d.ts +0 -1
  10. package/dist/commands/harvest.js +1 -4
  11. package/dist/commands/metrics.js +6 -0
  12. package/dist/commands/quality.js +1 -3
  13. package/dist/conformance/guarantee-table.js +133 -11
  14. package/dist/conformance/types.d.ts +1 -0
  15. package/dist/core/audit-metrics.d.ts +1 -0
  16. package/dist/core/audit-metrics.js +12 -4
  17. package/dist/core/audit-types.d.ts +3 -0
  18. package/dist/core/capability/index.d.ts +1 -1
  19. package/dist/core/capability/policy-engine.d.ts +31 -1
  20. package/dist/core/capability/policy-engine.js +186 -0
  21. package/dist/core/capability/request.d.ts +9 -0
  22. package/dist/core/capability/resolver.d.ts +0 -1
  23. package/dist/core/capability/resolver.js +0 -1
  24. package/dist/core/config.js +0 -2
  25. package/dist/core/effect-ir/audit.js +1 -0
  26. package/dist/core/effect-ir/build.js +5 -1
  27. package/dist/core/effect-ir/index.d.ts +6 -1
  28. package/dist/core/effect-ir/index.js +2 -0
  29. package/dist/core/effect-ir/normalize.js +9 -3
  30. package/dist/core/effect-ir/policy.d.ts +2 -1
  31. package/dist/core/effect-ir/policy.js +93 -6
  32. package/dist/core/effect-ir/shell-build.d.ts +43 -0
  33. package/dist/core/effect-ir/shell-build.js +77 -0
  34. package/dist/core/effect-ir/shell-lower.d.ts +14 -0
  35. package/dist/core/effect-ir/shell-lower.js +1509 -0
  36. package/dist/core/effect-ir/types.d.ts +9 -2
  37. package/dist/core/gate-engine.js +63 -23
  38. package/dist/core/git-resource-identity.d.ts +26 -0
  39. package/dist/core/git-resource-identity.js +284 -0
  40. package/dist/core/harvest.d.ts +3 -7
  41. package/dist/core/harvest.js +4 -27
  42. package/dist/core/index.d.ts +2 -0
  43. package/dist/core/index.js +1 -0
  44. package/dist/core/path-utils.js +42 -23
  45. package/dist/core/shell-substitution.d.ts +1 -0
  46. package/dist/core/shell-substitution.js +61 -0
  47. package/dist/core/standing-allow.d.ts +1 -2
  48. package/dist/core/standing-allow.js +3 -20
  49. package/dist/core/types.d.ts +4 -1
  50. package/dist/core/verdict/adapter.js +15 -15
  51. package/dist/core/verdict/containment.js +4 -0
  52. package/dist/core/verdict/egress-classify.d.ts +12 -0
  53. package/dist/core/verdict/egress-classify.js +746 -0
  54. package/dist/core/verdict/git-classifier.d.ts +12 -0
  55. package/dist/core/verdict/git-classifier.js +320 -0
  56. package/dist/core/verdict/launcher-resolve.js +77 -17
  57. package/dist/core/verdict/parser.d.ts +2 -0
  58. package/dist/core/verdict/parser.js +95 -1
  59. package/dist/core/verdict/types.d.ts +2 -3
  60. package/dist/core/verdict/verdict.js +103 -952
  61. package/dist/corpus/must-allow-commands.d.ts +2 -1
  62. package/dist/corpus/must-allow-commands.js +2 -1
  63. package/dist/corpus/runtime-match.d.ts +2 -1
  64. package/dist/corpus/runtime-match.js +2 -1
  65. package/dist/corpus/types.d.ts +2 -2
  66. package/dist/templates.js +7 -2
  67. package/dist/version.d.ts +1 -1
  68. package/dist/version.js +1 -1
  69. package/package.json +2 -2
  70. package/dist/core/verdict/overrides.d.ts +0 -7
  71. package/dist/core/verdict/overrides.js +0 -37
  72. package/dist/core/verdict/shell-policy.d.ts +0 -26
  73. package/dist/core/verdict/shell-policy.js +0 -40
  74. package/dist/corpus/standing-allow-catalog.generated.d.ts +0 -13
  75. package/dist/corpus/standing-allow-catalog.generated.js +0 -99
package/README.md CHANGED
@@ -86,7 +86,8 @@ npx @guilz-dev/belay status
86
86
 
87
87
  Fresh installs default to **fail-closed** shell policy: unknown or unparseable
88
88
  shell commands are denied until approved. Use `belay explain` to inspect a
89
- verdict and `overrides.allow` to whitelist commands you trust.
89
+ verdict. Correct inaccurate EffectPlan semantics or resource scope; otherwise
90
+ approve the exact request for one-shot, resource-scoped authorization.
90
91
 
91
92
  ## How it works
92
93
 
@@ -138,12 +139,12 @@ Belay is a layered hook gate, not a static denylist. Higher layers are opt-in.
138
139
  | **L3** Prediction | Policy rules + command heuristics | default |
139
140
  | **L4** Approval | Human one-shot / scoped approvals | default |
140
141
 
141
- - L3 command lists are **not security boundaries** by themselves — see
142
- [docs/ops/semver-policy.md](./docs/ops/semver-policy.md) and
143
- [docs/guarantee-table.md](./docs/guarantee-table.md).
144
- - At default L3, **local-recoverable mutations outside the repo** pass after Tier1
145
- (e.g. IDE plan files). To deny repo-external writes at the OS boundary, enable L1-full
146
- (`sandbox.runtime` ≠ `none`). Tier1 requires a working judge (local Ollama by default).
142
+ - Normalized shell authorization uses only canonical `EffectPlan` requirements.
143
+ Command lists, legacy overrides, corpus labels, and shell standing allows are inert.
144
+ - Payload-free reads are `allow`; reversible repository-local writes (including implicit
145
+ download output) are `allow_flagged`. Outside-repository writes, external mutation,
146
+ explicit payload/file/secret sends, high-stakes effects, and partial/indeterminate plans
147
+ require approval. See [ADR-004](./docs/adr/ADR-004-effectplan-shell-authority.md).
147
148
  - Adversarial resistance requires the full L1 stack:
148
149
  `belay init --preset l1-full-recommended`, verified with `belay sandbox status`.
149
150
 
@@ -188,18 +189,18 @@ npx @guilz-dev/belay dogfood # mode: audit, unknownLocalEffect: deny
188
189
  # ...run normal agent work...
189
190
  npx @guilz-dev/belay metrics # review what would have been blocked
190
191
  npx @guilz-dev/belay status # check readiness
191
- # tune overrides.allow with `belay explain`, then:
192
+ # inspect EffectPlan semantics and resource scope with `belay explain`, then:
192
193
  npx @guilz-dev/belay dogfood --enforce
193
194
  ```
194
195
 
195
196
  ## Configuration
196
197
 
197
- `belay.config.json` uses `version: 3`. v1/v2 configs migrate automatically on
198
+ `belay.config.json` uses `version: 4`. v1/v2/v3 configs migrate automatically on
198
199
  load.
199
200
 
200
201
  ```json
201
202
  {
202
- "version": 3,
203
+ "version": 4,
203
204
  "mode": "enforce",
204
205
  "gates": {
205
206
  "shell": true,
@@ -214,10 +215,6 @@ load.
214
215
  "policy": {
215
216
  "unknownLocalEffect": "allow_flagged"
216
217
  },
217
- "overrides": {
218
- "allow": ["pnpm release:staging"],
219
- "external": ["./scripts/release.sh"]
220
- },
221
218
  "redaction": {
222
219
  "maskApprovalIds": true,
223
220
  "maskBearerTokens": true,
@@ -238,12 +235,15 @@ load.
238
235
 
239
236
  Notable settings:
240
237
 
241
- - **`policy.unknownLocalEffect: "allow_flagged"`** (fresh default) — after Tier1
242
- says recoverable, structurally unknown local commands run with an audit flag. Use
243
- `"deny"` (via `belay dogfood`) to ask on those commands instead.
238
+ - **`policy.unknownLocalEffect: "allow_flagged"`** (fresh default) — compatibility fallback
239
+ for legacy, non-EffectPlan classification paths. It cannot loosen a normalized shell
240
+ EffectPlan: partial/indeterminate plans and outside-repository writes still ask. Use
241
+ `"deny"` (via `belay dogfood`) for a stricter fallback.
244
242
  - **`classifier.strictChains: true`** (default) — scans every `&&`, `|`, and `;`
245
- segment and keeps the strictest verdict. Override lists match exact command or
246
- segment keys only.
243
+ segment into the EffectPlan and keeps the strictest policy projection. Legacy
244
+ `overrides.allow` / `overrides.external` lists are accepted only for config
245
+ compatibility; shell authorization ignores them and `belay doctor` warns when
246
+ either list is non-empty.
247
247
  - **`controlPlane.enabled: true`** — stores approval state under
248
248
  `~/.config/belay/` (or `XDG_CONFIG_HOME/belay`), shared across repos for the
249
249
  current OS user. `upgrade` migrates repo-local approvals in; disabling merges
@@ -32,11 +32,25 @@ import { fingerprintReplayPayload } from '../../core/replay-scrub.js';
32
32
  import { loadStandingAllow, resolveStandingAllowMatch, standingAllowFile, } from '../../core/standing-allow.js';
33
33
  import { FILE_CHECKPOINT_ISOLATION_UNAVAILABLE, isTransactionalEligible, runTransactionalExecution, TRANSACTIONAL_ALREADY_APPLIED, TRANSACTIONAL_APPROVAL_BYPASS_REASONS, } from '../../core/transactional/index.js';
34
34
  import { egressStatus } from '../../services/egress-service.js';
35
+ import { PACKAGE_VERSION } from '../../version.js';
35
36
  import { protectedArtifactRoots } from '../layouts/protected-paths.js';
36
37
  const EMPTY_APPROVALS = {
37
38
  version: 1,
38
39
  approvals: [],
39
40
  };
41
+ const RUNTIME_PROVENANCE_KEY = Symbol.for('agent-belay.runtime-provenance');
42
+ function auditProvenance(config) {
43
+ const runtime = globalThis[RUNTIME_PROVENANCE_KEY];
44
+ const runtimeVersion = typeof runtime?.runtimeVersion === 'string' ? runtime.runtimeVersion : PACKAGE_VERSION;
45
+ const runtimeBuildStamp = typeof runtime?.runtimeBuildStamp === 'string'
46
+ ? runtime.runtimeBuildStamp
47
+ : `${PACKAGE_VERSION}@source`;
48
+ return {
49
+ runtimeVersion,
50
+ runtimeBuildStamp,
51
+ configFingerprint: hashValue(canonicalStringify(config)),
52
+ };
53
+ }
40
54
  function adapterIdFromContext(ctx) {
41
55
  if (ctx.config.adapter === 'cursor' ||
42
56
  ctx.config.adapter === 'claude' ||
@@ -75,11 +89,17 @@ export function createDefaultGateRuntimeDeps() {
75
89
  async appendAudit(ctx, event) {
76
90
  const auditPath = path.join(ctx.repoRoot, ctx.config.audit.logPath);
77
91
  await mkdir(path.dirname(auditPath), { recursive: true });
78
- const record = { timestamp: new Date().toISOString(), ...event };
92
+ const provenance = auditProvenance(ctx.config);
93
+ const record = {
94
+ timestamp: new Date().toISOString(),
95
+ ...event,
96
+ ...provenance,
97
+ };
79
98
  if (!ctx.config.audit.includeAssessment) {
80
99
  delete record.assessment;
81
100
  }
82
101
  const scrubbed = scrubValue(record, scrubOptionsFromConfig(ctx.config));
102
+ Object.assign(scrubbed, provenance);
83
103
  await writeFile(auditPath, `${JSON.stringify(scrubbed)}\n`, {
84
104
  encoding: 'utf8',
85
105
  flag: 'a',
@@ -767,7 +787,8 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
767
787
  agent_message: agentMessage,
768
788
  });
769
789
  }
770
- if (result.verdict === 'deny_pending_approval' &&
790
+ if (kind !== 'shell' &&
791
+ result.verdict === 'deny_pending_approval' &&
771
792
  ctx.config.mode === 'enforce' &&
772
793
  !TRANSACTIONAL_APPROVAL_BYPASS_REASONS.has(result.reason)) {
773
794
  const standingAllowPath = standingAllowFile(ctx.config, ctx.layout.repoLocalStateDir(ctx.repoRoot));
@@ -784,9 +805,6 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
784
805
  verdict: 'allow',
785
806
  reason: 'standing_allow',
786
807
  standingAllowSource: standingMatch.source,
787
- ...(standingMatch.catalogCommand
788
- ? { standingAllowCatalogCommand: standingMatch.catalogCommand }
789
- : {}),
790
808
  wouldBlock: false,
791
809
  permission: 'allow',
792
810
  });