@guilz-dev/belay 0.1.1 → 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.
Files changed (61) hide show
  1. package/README.md +16 -3
  2. package/dist/adapters/shared/gate-runtime.js +12 -4
  3. package/dist/bundle/claude-runtime.mjs +155 -36
  4. package/dist/bundle/codex-runtime.mjs +155 -36
  5. package/dist/bundle/cursor-runtime.mjs +155 -36
  6. package/dist/cli.js +4 -4
  7. package/dist/commands/classify-for-report.js +3 -3
  8. package/dist/commands/doctor.js +1 -1
  9. package/dist/commands/explain.js +14 -14
  10. package/dist/commands/init-wizard.d.ts +5 -0
  11. package/dist/commands/init-wizard.js +24 -11
  12. package/dist/commands/recover.js +2 -2
  13. package/dist/core/approval.d.ts +3 -0
  14. package/dist/core/approval.js +18 -3
  15. package/dist/core/audit-query.js +5 -1
  16. package/dist/core/classify-tool.js +1 -1
  17. package/dist/core/config.d.ts +1 -1
  18. package/dist/core/config.js +2 -2
  19. package/dist/core/gate-contract.d.ts +1 -1
  20. package/dist/core/gate-contract.js +1 -1
  21. package/dist/core/gate-engine.js +2 -2
  22. package/dist/core/index.d.ts +2 -2
  23. package/dist/core/index.js +2 -2
  24. package/dist/core/judge-config.d.ts +5 -1
  25. package/dist/core/judge-config.js +17 -1
  26. package/dist/core/judge-doctor.js +2 -2
  27. package/dist/core/types.d.ts +5 -3
  28. package/dist/core/{v2 → verdict}/adapter.js +9 -3
  29. package/dist/core/{v2 → verdict}/egress-classify.js +3 -0
  30. package/dist/core/{v2 → verdict}/judge.js +10 -12
  31. package/dist/core/{v2 → verdict}/launcher-resolve.js +72 -1
  32. package/dist/core/{v2 → verdict}/verdict.js +16 -0
  33. package/dist/corpus/evaluate.js +2 -2
  34. package/dist/installer.js +1 -0
  35. package/dist/types.d.ts +1 -1
  36. package/dist/version.d.ts +1 -1
  37. package/dist/version.js +1 -1
  38. package/package.json +2 -1
  39. /package/dist/core/{v2 → verdict}/adapter.d.ts +0 -0
  40. /package/dist/core/{v2 → verdict}/containment.d.ts +0 -0
  41. /package/dist/core/{v2 → verdict}/containment.js +0 -0
  42. /package/dist/core/{v2 → verdict}/egress-classify.d.ts +0 -0
  43. /package/dist/core/{v2 → verdict}/fingerprint.d.ts +0 -0
  44. /package/dist/core/{v2 → verdict}/fingerprint.js +0 -0
  45. /package/dist/core/{v2 → verdict}/index.d.ts +0 -0
  46. /package/dist/core/{v2 → verdict}/index.js +0 -0
  47. /package/dist/core/{v2 → verdict}/judge-audit.d.ts +0 -0
  48. /package/dist/core/{v2 → verdict}/judge-audit.js +0 -0
  49. /package/dist/core/{v2 → verdict}/judge-factory.d.ts +0 -0
  50. /package/dist/core/{v2 → verdict}/judge-factory.js +0 -0
  51. /package/dist/core/{v2 → verdict}/judge-outbound.d.ts +0 -0
  52. /package/dist/core/{v2 → verdict}/judge-outbound.js +0 -0
  53. /package/dist/core/{v2 → verdict}/judge.d.ts +0 -0
  54. /package/dist/core/{v2 → verdict}/launcher-resolve.d.ts +0 -0
  55. /package/dist/core/{v2 → verdict}/overrides.d.ts +0 -0
  56. /package/dist/core/{v2 → verdict}/overrides.js +0 -0
  57. /package/dist/core/{v2 → verdict}/parser.d.ts +0 -0
  58. /package/dist/core/{v2 → verdict}/parser.js +0 -0
  59. /package/dist/core/{v2 → verdict}/types.d.ts +0 -0
  60. /package/dist/core/{v2 → verdict}/types.js +0 -0
  61. /package/dist/core/{v2 → verdict}/verdict.d.ts +0 -0
@@ -86,7 +86,7 @@ var LEGACY_POLICY_V3 = {
86
86
  fenceWarnThreshold: DEFAULT_FENCE_WARN_THRESHOLD
87
87
  };
88
88
  var DEFAULT_POLICY_V3 = {
89
- unknownLocalEffect: "deny",
89
+ unknownLocalEffect: "allow_flagged",
90
90
  unparseableShell: "deny",
91
91
  codexUnmappedTool: "deny",
92
92
  confidenceThresholds: { ...DEFAULT_CONFIDENCE_THRESHOLDS },
@@ -704,16 +704,25 @@ import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile2 } from
704
704
  import path16 from "node:path";
705
705
 
706
706
  // src/core/approval.ts
707
+ var APPROVAL_EXECUTION_LEASE_MS = 6e4;
707
708
  function nowIso() {
708
709
  return (/* @__PURE__ */ new Date()).toISOString();
709
710
  }
710
711
  function isExpired(approval) {
711
712
  return Date.parse(approval.expiresAt) <= Date.now();
712
713
  }
714
+ function isExecutionLeaseExpired(approval) {
715
+ if (!approval.executionLeaseExpiresAt) {
716
+ return false;
717
+ }
718
+ return Date.parse(approval.executionLeaseExpiresAt) <= Date.now();
719
+ }
713
720
  function compactApprovals(state) {
714
721
  return {
715
722
  version: state.version,
716
- approvals: state.approvals.filter((approval) => !isExpired(approval))
723
+ approvals: state.approvals.filter(
724
+ (approval) => !isExpired(approval) && !isExecutionLeaseExpired(approval)
725
+ )
717
726
  };
718
727
  }
719
728
  function escapeRegex(value) {
@@ -722,8 +731,15 @@ function escapeRegex(value) {
722
731
  }
723
732
  function approvalCommandMatch(prompt, tokenPrefix) {
724
733
  const escapedPrefix = escapeRegex(tokenPrefix);
725
- const match = prompt.match(new RegExp(`^\\s*${escapedPrefix}\\s+(\\S+)\\s*$`, "i"));
726
- return match?.[1] ?? null;
734
+ const linePattern = new RegExp(`^\\s*${escapedPrefix}\\s+(\\S+)\\s*$`, "i");
735
+ for (const line of prompt.split(/\r?\n/)) {
736
+ if (!line.trim()) {
737
+ continue;
738
+ }
739
+ const match = line.match(linePattern);
740
+ return match?.[1] ?? null;
741
+ }
742
+ return null;
727
743
  }
728
744
  function buildRetryInstruction(tokenPrefix, approvalId) {
729
745
  return `To allow the next matching action once, send ${tokenPrefix} ${approvalId} and then retry the original action unchanged.`;
@@ -1250,7 +1266,7 @@ function classifyResultToGateVerdict(params) {
1250
1266
  approvalId,
1251
1267
  user_message,
1252
1268
  agent_message,
1253
- v2: result.v2
1269
+ axes: result.axes
1254
1270
  };
1255
1271
  }
1256
1272
  function unnormalizedGateVerdict(params) {
@@ -1504,7 +1520,7 @@ function matchesSensitivePath(filePath, patterns) {
1504
1520
  return false;
1505
1521
  }
1506
1522
 
1507
- // src/core/v2/judge-audit.ts
1523
+ // src/core/verdict/judge-audit.ts
1508
1524
  function judgeTraceAuditFields(trace) {
1509
1525
  if (!trace) {
1510
1526
  return {};
@@ -1519,7 +1535,7 @@ function judgeTraceAuditFields(trace) {
1519
1535
  };
1520
1536
  }
1521
1537
 
1522
- // src/core/v2/judge-outbound.ts
1538
+ // src/core/verdict/judge-outbound.ts
1523
1539
  var PATH_LIKE = /(?:^|[\s"'`=])(~\/[^\s"'`]+|\/[^\s"'`]+|\.\/[^\s"'`]+|\.\.\/[^\s"'`]+|[A-Za-z]:\\[^\s"'`]+)/g;
1524
1540
  var REDACTED_PLACEHOLDER = /^(?:<redacted>|\[REDACTED\]|<secret>|<high-entropy>|<approval-id>)$/i;
1525
1541
  var URL_CREDENTIALS_PATTERN2 = /\b[A-Za-z][A-Za-z0-9+.-]*:\/\/([^/\s:@]+):([^@\s/]+)@/gi;
@@ -1588,16 +1604,16 @@ function scrubOutboundForJudge(text, options) {
1588
1604
  }
1589
1605
  }
1590
1606
 
1591
- // src/core/v2/judge.ts
1607
+ // src/core/verdict/judge.ts
1592
1608
  var SECRET_PATTERNS = [/\.env\b/i, /\.pem\b/i, /id_rsa\b/i, /credentials/i, /secrets?\b/i];
1593
1609
  var DESTRUCTIVE_VERBS = /\b(rm|rmtree|unlink|delete|truncate|shred|destroy|drop)\b/i;
1594
1610
  var GIT_PATTERNS = /\.git\b/i;
1595
- var TIER1_PROMPT = `Answer ONLY with JSON: {"external_change":boolean,"destroys_outside_repo":boolean,"destroys_history_or_secrets":boolean,"reason":string}
1596
- Default all three booleans to true when unsure. reason should be a short snake_case label.
1611
+ var TIER1_PROMPT = `Answer ONLY with JSON: {"external_change":boolean,"reason":string}
1612
+ reason should be a short snake_case label.
1597
1613
 
1598
- external_change: CREATE/MODIFY/DELETE on database, network endpoint, cloud, remote git, registry, queue. READ/QUERY/GET is NOT a change.
1599
- destroys_outside_repo: DELETE/OVERWRITE files OUTSIDE the current repository working tree.
1600
- destroys_history_or_secrets: DELETE/OVERWRITE .git directory OR secret/credential files (.env, *.pem, id_rsa, credentials).
1614
+ 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.
1615
+ 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.
1616
+ If unsure, answer true.
1601
1617
 
1602
1618
  Command/code:
1603
1619
  `;
@@ -1612,13 +1628,13 @@ function failClosedVerdict(reason) {
1612
1628
  function parseTier1Json(raw) {
1613
1629
  try {
1614
1630
  const parsed = JSON.parse(raw);
1615
- if (typeof parsed.external_change !== "boolean" || typeof parsed.destroys_outside_repo !== "boolean" || typeof parsed.destroys_history_or_secrets !== "boolean") {
1631
+ if (typeof parsed.external_change !== "boolean") {
1616
1632
  return null;
1617
1633
  }
1618
1634
  return {
1619
- external_change: parsed.external_change !== false,
1620
- destroys_outside_repo: parsed.destroys_outside_repo !== false,
1621
- destroys_history_or_secrets: parsed.destroys_history_or_secrets !== false,
1635
+ external_change: parsed.external_change,
1636
+ destroys_outside_repo: false,
1637
+ destroys_history_or_secrets: false,
1622
1638
  reason: typeof parsed.reason === "string" ? parsed.reason : "tier1_llm"
1623
1639
  };
1624
1640
  } catch {
@@ -1841,10 +1857,10 @@ function createOpenAiCompatibleJudge(options) {
1841
1857
  return judge;
1842
1858
  }
1843
1859
  function tier1RequiresAsk(verdict2) {
1844
- return verdict2.external_change || verdict2.destroys_outside_repo || verdict2.destroys_history_or_secrets;
1860
+ return verdict2.external_change || verdict2.destroys_history_or_secrets;
1845
1861
  }
1846
1862
 
1847
- // src/core/v2/judge-factory.ts
1863
+ // src/core/verdict/judge-factory.ts
1848
1864
  var FIXTURE_MODELS_URL = new URL("../../../fixtures/judge-models.json", import.meta.url);
1849
1865
  function resolveCloudModel(requested, pinned) {
1850
1866
  if (requested === "auto") {
@@ -1891,10 +1907,10 @@ function createJudgeFromConfig(config, options = {}) {
1891
1907
  return createDeterministicJudgeStub();
1892
1908
  }
1893
1909
 
1894
- // src/core/v2/verdict.ts
1910
+ // src/core/verdict/verdict.ts
1895
1911
  import path11 from "node:path";
1896
1912
 
1897
- // src/core/v2/containment.ts
1913
+ // src/core/verdict/containment.ts
1898
1914
  import path8 from "node:path";
1899
1915
  function expandHome(token) {
1900
1916
  if (token === "~" || token.startsWith("~/")) {
@@ -1986,7 +2002,7 @@ function cwdRelative(repoRoot, cwd) {
1986
2002
  return relativeWithinRepo(repoRoot, cwd) ?? cwd;
1987
2003
  }
1988
2004
 
1989
- // src/core/v2/egress-classify.ts
2005
+ // src/core/verdict/egress-classify.ts
1990
2006
  var EGRESS_TOOL_HEADS = /* @__PURE__ */ new Set([
1991
2007
  "aws",
1992
2008
  "curl",
@@ -2084,6 +2100,9 @@ function classifyAws(tokens) {
2084
2100
  if (/\bs3\s+rm\b/.test(joined)) {
2085
2101
  return "destructive";
2086
2102
  }
2103
+ if (/\bs3\s+mb\b/.test(joined)) {
2104
+ return "destructive";
2105
+ }
2087
2106
  if (/\bs3\s+sync\b/.test(joined)) {
2088
2107
  return "destructive";
2089
2108
  }
@@ -2194,15 +2213,59 @@ function classifyNetlify(tokens) {
2194
2213
  return "ambiguous";
2195
2214
  }
2196
2215
 
2197
- // src/core/v2/fingerprint.ts
2216
+ // src/core/verdict/fingerprint.ts
2198
2217
  function verdictFingerprint(cwdRelative2, commandRedacted) {
2199
2218
  return hashValue(`v2:${cwdRelative2}:${commandRedacted}`);
2200
2219
  }
2201
2220
 
2202
- // src/core/v2/launcher-resolve.ts
2221
+ // src/core/verdict/launcher-resolve.ts
2203
2222
  import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
2204
2223
  import path9 from "node:path";
2205
2224
  var MAX_RESOLVE_DEPTH = 8;
2225
+ var PNPM_BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
2226
+ "add",
2227
+ "audit",
2228
+ "cache",
2229
+ "config",
2230
+ "deploy",
2231
+ "dlx",
2232
+ "exec",
2233
+ "fetch",
2234
+ "help",
2235
+ "import",
2236
+ "init",
2237
+ "install",
2238
+ "i",
2239
+ "licenses",
2240
+ "link",
2241
+ "list",
2242
+ "outdated",
2243
+ "pack",
2244
+ "patch",
2245
+ "patch-commit",
2246
+ "patch-remove",
2247
+ "publish",
2248
+ "prune",
2249
+ "rebuild",
2250
+ "remove",
2251
+ "rm",
2252
+ "store",
2253
+ "unlink",
2254
+ "update",
2255
+ "up",
2256
+ "why"
2257
+ ]);
2258
+ var PNPM_EXEC_LIKE_HEADS = /* @__PURE__ */ new Set([
2259
+ "vitest",
2260
+ "vite",
2261
+ "biome",
2262
+ "eslint",
2263
+ "jest",
2264
+ "mocha",
2265
+ "tsc",
2266
+ "tsx",
2267
+ "node"
2268
+ ]);
2206
2269
  function readPackageJson(dir) {
2207
2270
  const packagePath = path9.join(dir, "package.json");
2208
2271
  if (!existsSync4(packagePath)) {
@@ -2257,6 +2320,12 @@ function npmScriptName(tokens) {
2257
2320
  if (launcher[0] === "pnpm" && launcher[1] === "run" && launcher[2]) {
2258
2321
  return launcher[2];
2259
2322
  }
2323
+ if (launcher[0] === "pnpm" && launcher[1] === "test") {
2324
+ return "test";
2325
+ }
2326
+ if (launcher[0] === "pnpm" && launcher[1] && !launcher[1].startsWith("-") && !PNPM_BUILTIN_COMMANDS.has(launcher[1])) {
2327
+ return launcher[1];
2328
+ }
2260
2329
  if (launcher[0] === "npm" && launcher[1] && launcher[1] !== "run" && launcher[1] !== "install") {
2261
2330
  return null;
2262
2331
  }
@@ -2378,11 +2447,31 @@ function resolveLauncherRecipe(params) {
2378
2447
  const tokens = params.tokens;
2379
2448
  const scriptName = npmScriptName(tokens);
2380
2449
  if (scriptName) {
2381
- return resolveNpmRecipe(params.cwd, params.repoRoot, scriptName, forwardedArgs(tokens));
2450
+ const resolution = resolveNpmRecipe(
2451
+ params.cwd,
2452
+ params.repoRoot,
2453
+ scriptName,
2454
+ forwardedArgs(tokens)
2455
+ );
2456
+ if (tokens[0] === "pnpm" && tokens[1] && PNPM_EXEC_LIKE_HEADS.has(tokens[1]) && resolution.reason === "npm_script_undefined") {
2457
+ return {
2458
+ recipes: [tokens.slice(1).join(" ")],
2459
+ opaque: false,
2460
+ reason: "pnpm_exec_like"
2461
+ };
2462
+ }
2463
+ return resolution;
2382
2464
  }
2383
2465
  if (tokens[0] === "make" && tokens[1] && !tokens[1].startsWith("-")) {
2384
2466
  return resolveMakeRecipe(params.cwd, params.repoRoot, tokens[1]);
2385
2467
  }
2468
+ if (tokens[0] === "pnpm" && tokens[1] && PNPM_EXEC_LIKE_HEADS.has(tokens[1])) {
2469
+ return {
2470
+ recipes: [tokens.slice(1).join(" ")],
2471
+ opaque: false,
2472
+ reason: "pnpm_exec_like"
2473
+ };
2474
+ }
2386
2475
  return null;
2387
2476
  }
2388
2477
  function isRoutineLauncher(tokens) {
@@ -2398,7 +2487,7 @@ function matchesCustomCommand(normalizedCommand, key, pattern) {
2398
2487
  return normalizedCommand === trimmed || key === trimmed;
2399
2488
  }
2400
2489
 
2401
- // src/core/v2/overrides.ts
2490
+ // src/core/verdict/overrides.ts
2402
2491
  function matchesCustomPatterns(command, segment, patterns) {
2403
2492
  if (!patterns || patterns.length === 0) {
2404
2493
  return false;
@@ -2437,7 +2526,7 @@ function askFromCustomExternal(opacity) {
2437
2526
  };
2438
2527
  }
2439
2528
 
2440
- // src/core/v2/parser.ts
2529
+ // src/core/verdict/parser.ts
2441
2530
  import path10 from "node:path";
2442
2531
 
2443
2532
  // src/core/shell-substitution.ts
@@ -2661,7 +2750,7 @@ function hasUnbalancedDollarParen(command) {
2661
2750
  return depth > 0;
2662
2751
  }
2663
2752
 
2664
- // src/core/v2/parser.ts
2753
+ // src/core/verdict/parser.ts
2665
2754
  var ENV_PREFIX_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=(?:'[^']*'|"[^"]*"|\S+)$/;
2666
2755
  var TRANSPARENT_WRAPPERS = /* @__PURE__ */ new Set([
2667
2756
  "sudo",
@@ -2861,7 +2950,7 @@ function redactCommand(command) {
2861
2950
  return command.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]").replace(/sk-[A-Za-z0-9]{8,}/g, "sk-[REDACTED]").trim();
2862
2951
  }
2863
2952
 
2864
- // src/core/v2/verdict.ts
2953
+ // src/core/verdict/verdict.ts
2865
2954
  var DEFAULT_MAX_DEPTH = 8;
2866
2955
  var TIER0_EXTERNAL_KEYS = /* @__PURE__ */ new Set([
2867
2956
  "git push",
@@ -2944,6 +3033,7 @@ var LOCAL_ROUTINE_HEADS = /* @__PURE__ */ new Set([
2944
3033
  "make",
2945
3034
  "cmake"
2946
3035
  ]);
3036
+ var BELAY_SELF_COMMANDS = /* @__PURE__ */ new Set(["approve", "revoke"]);
2947
3037
  var FIND_DANGEROUS_FLAGS = /* @__PURE__ */ new Set(["-delete", "-exec", "-execdir", "-ok", "-okdir"]);
2948
3038
  function isFindDangerous(tokens) {
2949
3039
  return tokens.some(
@@ -3091,6 +3181,11 @@ function tier0ExternalMatch(key, head, tokens) {
3091
3181
  }
3092
3182
  return false;
3093
3183
  }
3184
+ function isBelaySelfCommand(tokens) {
3185
+ const head = tokens[0] ?? "";
3186
+ const subcommand = tokens[1] ?? "";
3187
+ return head === "belay" && BELAY_SELF_COMMANDS.has(subcommand);
3188
+ }
3094
3189
  function tier0HighStakesRm(tokens, context) {
3095
3190
  const head = tokens[0] ?? "";
3096
3191
  if (head !== "rm") {
@@ -3322,6 +3417,16 @@ async function evaluateSegment(command, context, depth) {
3322
3417
  if (rmVerdict) {
3323
3418
  return rmVerdict;
3324
3419
  }
3420
+ if (isBelaySelfCommand(peeled)) {
3421
+ return allowVerdict({
3422
+ location: "unknown",
3423
+ opacity: "transparent",
3424
+ effect: "local_mutation",
3425
+ confidence: "deterministic",
3426
+ reason: "belay_control_plane_command",
3427
+ signals: ["belay_control_plane_command", segment.head]
3428
+ });
3429
+ }
3325
3430
  let effect = "unknown";
3326
3431
  if (READ_ONLY_KEYS.has(segment.key) || READ_ONLY_KEYS.has(segment.head)) {
3327
3432
  effect = "read_only";
@@ -3542,7 +3647,7 @@ async function verdict(command, context) {
3542
3647
  );
3543
3648
  }
3544
3649
 
3545
- // src/core/v2/adapter.ts
3650
+ // src/core/verdict/adapter.ts
3546
3651
  function buildVerdictContext(params) {
3547
3652
  const protectedArtifactRoots2 = [
3548
3653
  ...params.options?.protectedArtifactRoots ?? [],
@@ -3589,6 +3694,12 @@ function mapLegacyReason(result) {
3589
3694
  if (result.reason === "repo_local_mutation") {
3590
3695
  return "local_mutation";
3591
3696
  }
3697
+ if (result.reason === "tier1_not_restorable") {
3698
+ return "tier1_catastrophic";
3699
+ }
3700
+ if (result.reason === "tier0_restorable" || result.reason === "tier1_restorable") {
3701
+ return result.effect === "local_mutation" ? "local_mutation" : result.reason;
3702
+ }
3592
3703
  return result.reason;
3593
3704
  }
3594
3705
  function verdictToClassifyResult(result) {
@@ -3609,13 +3720,13 @@ function verdictToClassifyResult(result) {
3609
3720
  assessment,
3610
3721
  normalizedCommand: result.commandRedacted,
3611
3722
  summary: result.commandRedacted,
3612
- v2: {
3723
+ axes: {
3613
3724
  location: result.location,
3614
3725
  opacity: result.opacity,
3615
3726
  effect: result.effect,
3616
3727
  confidence: result.confidence,
3617
3728
  would: result.permission,
3618
- by: "v2",
3729
+ by: "verdict",
3619
3730
  commandRedacted: result.commandRedacted,
3620
3731
  commandFingerprint: result.fingerprint,
3621
3732
  signals: result.signals,
@@ -4065,7 +4176,7 @@ function normalizeGatedAction(params) {
4065
4176
  };
4066
4177
  }
4067
4178
  function applyShellPeripheralPolicy(command, action, result, options) {
4068
- if (options.brokerFsScope && result.verdict === "deny_pending_approval" && (result.reason === "outside_repo_mutation" || result.reason === "outside_repo_redirect" || result.reason === "repo_outside_mutation" || result.v2?.location === "repo_outside")) {
4179
+ 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")) {
4069
4180
  const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot);
4070
4181
  if (outsideRepoPaths.length > 0 && options.fsScopeAllowlist && allPathsAllowlisted(outsideRepoPaths, options.fsScopeAllowlist)) {
4071
4182
  return {
@@ -4744,7 +4855,15 @@ async function consumeApprovedApproval(ctx, deps, kind, fingerprint) {
4744
4855
  await deps.writeApprovals(approved.filePath, approved.state);
4745
4856
  return null;
4746
4857
  }
4747
- const [approval] = approved.state.approvals.splice(index, 1);
4858
+ const approval = approved.state.approvals[index];
4859
+ if (approval.executionLeaseExpiresAt) {
4860
+ await deps.writeApprovals(approved.filePath, approved.state);
4861
+ return approval;
4862
+ }
4863
+ approved.state.approvals[index] = {
4864
+ ...approval,
4865
+ executionLeaseExpiresAt: new Date(Date.now() + APPROVAL_EXECUTION_LEASE_MS).toISOString()
4866
+ };
4748
4867
  await deps.writeApprovals(approved.filePath, approved.state);
4749
4868
  return approval;
4750
4869
  }
@@ -4882,8 +5001,8 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
4882
5001
  predictedAssessment: auditExtras.predictedAssessment,
4883
5002
  observedAssessment: auditExtras.observedAssessment,
4884
5003
  mode: ctx.config.mode,
4885
- schemaVersion: result.v2 ? 2 : 1,
4886
- ...result.v2 ?? {},
5004
+ schemaVersion: result.axes ? 2 : 1,
5005
+ ...result.axes ?? {},
4887
5006
  ...auditExtras.transactionalLayer
4888
5007
  };
4889
5008
  if (result.reason === TRANSACTIONAL_ALREADY_APPLIED) {