@guilz-dev/belay 0.4.0 → 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.
Files changed (158) hide show
  1. package/README.md +7 -1
  2. package/dist/adapters/layouts/scope.js +3 -2
  3. package/dist/adapters/shared/gate-runtime.d.ts +1 -2
  4. package/dist/adapters/shared/gate-runtime.js +226 -28
  5. package/dist/bundle/claude-runtime.mjs +6967 -4078
  6. package/dist/bundle/codex-runtime.mjs +5951 -3064
  7. package/dist/bundle/cursor-runtime.mjs +5951 -3064
  8. package/dist/cli.js +43 -1
  9. package/dist/commands/classify-for-report.js +1 -1
  10. package/dist/commands/config.d.ts +8 -0
  11. package/dist/commands/config.js +134 -11
  12. package/dist/commands/doctor.js +36 -1
  13. package/dist/commands/explain.js +11 -1
  14. package/dist/commands/judge.js +3 -2
  15. package/dist/commands/session.d.ts +20 -0
  16. package/dist/commands/session.js +51 -0
  17. package/dist/config-io.js +49 -12
  18. package/dist/conformance/guarantee-posture.d.ts +21 -0
  19. package/dist/conformance/guarantee-posture.js +46 -0
  20. package/dist/conformance/guarantee-table.d.ts +1 -0
  21. package/dist/conformance/guarantee-table.js +14 -9
  22. package/dist/core/approval-service.js +6 -2
  23. package/dist/core/approval.d.ts +2 -0
  24. package/dist/core/approval.js +14 -3
  25. package/dist/core/capability/approval-state-mutation.d.ts +13 -0
  26. package/dist/core/capability/approval-state-mutation.js +92 -0
  27. package/dist/core/capability/approval-v3.d.ts +15 -0
  28. package/dist/core/capability/approval-v3.js +74 -0
  29. package/dist/core/capability/attestation.d.ts +13 -0
  30. package/dist/core/capability/attestation.js +55 -0
  31. package/dist/core/capability/boundary-attestation-sign.d.ts +21 -0
  32. package/dist/core/capability/boundary-attestation-sign.js +84 -0
  33. package/dist/core/capability/boundary-driver-container.d.ts +7 -0
  34. package/dist/core/capability/boundary-driver-container.js +122 -0
  35. package/dist/core/capability/boundary-driver.d.ts +29 -0
  36. package/dist/core/capability/boundary-driver.js +43 -0
  37. package/dist/core/capability/boundary-egress.d.ts +21 -0
  38. package/dist/core/capability/boundary-egress.js +109 -0
  39. package/dist/core/capability/boundary-grant-materialize.d.ts +13 -0
  40. package/dist/core/capability/boundary-grant-materialize.js +78 -0
  41. package/dist/core/capability/boundary-profile.d.ts +15 -0
  42. package/dist/core/capability/boundary-profile.js +25 -0
  43. package/dist/core/capability/boundary-run.d.ts +23 -0
  44. package/dist/core/capability/boundary-run.js +23 -0
  45. package/dist/core/capability/boundary-session.d.ts +57 -0
  46. package/dist/core/capability/boundary-session.js +129 -0
  47. package/dist/core/capability/capability-request-hash.d.ts +2 -0
  48. package/dist/core/capability/capability-request-hash.js +8 -0
  49. package/dist/core/capability/gate-latency-ratchet.d.ts +7 -0
  50. package/dist/core/capability/gate-latency-ratchet.js +18 -0
  51. package/dist/core/capability/gate-policy-shadow.d.ts +25 -0
  52. package/dist/core/capability/gate-policy-shadow.js +108 -0
  53. package/dist/core/capability/gate-shadow-audit.d.ts +1 -0
  54. package/dist/core/capability/gate-shadow-audit.js +1 -0
  55. package/dist/core/capability/gate-shadow-ratchet.d.ts +14 -0
  56. package/dist/core/capability/gate-shadow-ratchet.js +81 -0
  57. package/dist/core/capability/grant-lease.d.ts +9 -0
  58. package/dist/core/capability/grant-lease.js +46 -0
  59. package/dist/core/capability/grant-loader.d.ts +12 -0
  60. package/dist/core/capability/grant-loader.js +23 -0
  61. package/dist/core/capability/grant-match.d.ts +6 -0
  62. package/dist/core/capability/grant-match.js +93 -0
  63. package/dist/core/capability/grant.d.ts +16 -0
  64. package/dist/core/capability/grant.js +16 -0
  65. package/dist/core/capability/index.d.ts +8 -0
  66. package/dist/core/capability/index.js +7 -0
  67. package/dist/core/capability/limits.d.ts +7 -0
  68. package/dist/core/capability/limits.js +55 -0
  69. package/dist/core/capability/policy-bridge.d.ts +18 -0
  70. package/dist/core/capability/policy-bridge.js +51 -0
  71. package/dist/core/capability/policy-engine.d.ts +70 -0
  72. package/dist/core/capability/policy-engine.js +476 -0
  73. package/dist/core/capability/policy-types.d.ts +20 -0
  74. package/dist/core/capability/policy-types.js +1 -0
  75. package/dist/core/capability/request.d.ts +45 -0
  76. package/dist/core/capability/request.js +1 -0
  77. package/dist/core/capability/resolver.d.ts +4 -0
  78. package/dist/core/capability/resolver.js +4 -0
  79. package/dist/core/classify-subagent.d.ts +2 -1
  80. package/dist/core/classify-subagent.js +38 -2
  81. package/dist/core/classify-tool.js +126 -62
  82. package/dist/core/config.d.ts +14 -3
  83. package/dist/core/config.js +56 -3
  84. package/dist/core/gate-contract.d.ts +3 -0
  85. package/dist/core/gate-contract.js +3 -0
  86. package/dist/core/gate-engine.js +6 -1
  87. package/dist/core/judge-doctor.js +39 -14
  88. package/dist/core/judge-fallback-hints.d.ts +13 -0
  89. package/dist/core/judge-fallback-hints.js +108 -0
  90. package/dist/core/path-utils.d.ts +1 -0
  91. package/dist/core/path-utils.js +5 -2
  92. package/dist/core/recovery/capability.d.ts +3 -0
  93. package/dist/core/recovery/capability.js +22 -0
  94. package/dist/core/recovery/fail-closed.d.ts +6 -0
  95. package/dist/core/recovery/fail-closed.js +25 -0
  96. package/dist/core/recovery/index.d.ts +3 -0
  97. package/dist/core/recovery/index.js +2 -0
  98. package/dist/core/recovery/types.d.ts +20 -0
  99. package/dist/core/recovery/types.js +1 -0
  100. package/dist/core/standing-allow.js +3 -0
  101. package/dist/core/transactional/diff-evaluator.js +4 -1
  102. package/dist/core/transactional/eligibility.js +4 -0
  103. package/dist/core/transactional/git-worktree.d.ts +10 -3
  104. package/dist/core/transactional/git-worktree.js +81 -11
  105. package/dist/core/transactional/reasons.d.ts +1 -0
  106. package/dist/core/transactional/reasons.js +4 -0
  107. package/dist/core/transactional/runner.js +22 -6
  108. package/dist/core/transactional/types.d.ts +4 -0
  109. package/dist/core/types.d.ts +21 -3
  110. package/dist/core/verdict/adapter.d.ts +3 -4
  111. package/dist/core/verdict/adapter.js +16 -12
  112. package/dist/core/verdict/cursor-acp-client.d.ts +67 -0
  113. package/dist/core/verdict/cursor-acp-client.js +345 -0
  114. package/dist/core/verdict/egress-classify.d.ts +2 -2
  115. package/dist/core/verdict/egress-classify.js +2 -2
  116. package/dist/core/verdict/git-classifier.d.ts +26 -0
  117. package/dist/core/verdict/git-classifier.js +533 -0
  118. package/dist/core/verdict/judge-audit.js +1 -0
  119. package/dist/core/verdict/judge-baseline.d.ts +1 -1
  120. package/dist/core/verdict/judge-baseline.js +1 -0
  121. package/dist/core/verdict/judge-broker-service.js +12 -0
  122. package/dist/core/verdict/judge-catalog.js +1 -0
  123. package/dist/core/verdict/judge-cli-fingerprint.d.ts +1 -0
  124. package/dist/core/verdict/judge-cli-fingerprint.js +6 -0
  125. package/dist/core/verdict/judge-cli.js +5 -3
  126. package/dist/core/verdict/judge-provider-matrix.js +1 -1
  127. package/dist/core/verdict/judge-runtime-config.d.ts +2 -0
  128. package/dist/core/verdict/judge-runtime-config.js +10 -6
  129. package/dist/core/verdict/judge-session-broker.d.ts +1 -1
  130. package/dist/core/verdict/judge-session-broker.js +10 -5
  131. package/dist/core/verdict/judge-transport.d.ts +5 -1
  132. package/dist/core/verdict/judge-transport.js +24 -14
  133. package/dist/core/verdict/judge.d.ts +2 -15
  134. package/dist/core/verdict/judge.js +5 -68
  135. package/dist/core/verdict/launcher-resolve.d.ts +2 -0
  136. package/dist/core/verdict/launcher-resolve.js +16 -0
  137. package/dist/core/verdict/prescan.d.ts +15 -0
  138. package/dist/core/verdict/prescan.js +68 -0
  139. package/dist/core/verdict/shell-policy.d.ts +25 -0
  140. package/dist/core/verdict/shell-policy.js +40 -0
  141. package/dist/core/verdict/shell-semantics.d.ts +24 -0
  142. package/dist/core/verdict/shell-semantics.js +225 -0
  143. package/dist/core/verdict/types.d.ts +13 -1
  144. package/dist/core/verdict/verdict.js +296 -178
  145. package/dist/corpus/evaluate.js +3 -7
  146. package/dist/corpus/gate-latency-budget.d.ts +39 -0
  147. package/dist/corpus/gate-latency-budget.js +33 -0
  148. package/dist/corpus/mutators.d.ts +1 -1
  149. package/dist/corpus/mutators.js +11 -0
  150. package/dist/corpus/runtime-match.js +1 -3
  151. package/dist/corpus/standing-allow-catalog.generated.js +29 -9
  152. package/dist/judge-broker-daemon.js +15 -1
  153. package/dist/services/sandbox-service.d.ts +4 -0
  154. package/dist/services/sandbox-service.js +28 -2
  155. package/dist/types.d.ts +2 -2
  156. package/dist/version.d.ts +1 -1
  157. package/dist/version.js +1 -1
  158. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -12,6 +12,7 @@ import { formatQualityReport, qualityCheck } from './commands/quality.js';
12
12
  import { formatRecoverReport, recoverProject } from './commands/recover.js';
13
13
  import { formatReport, reportProject } from './commands/report.js';
14
14
  import { revokeApproval } from './commands/revoke.js';
15
+ import { formatSessionStatusReport, sessionStartProject, sessionStatusProject, } from './commands/session.js';
15
16
  import { formatSimulateReport, simulateProject } from './commands/simulate.js';
16
17
  import { revokeStandingAllow } from './commands/standing-allow.js';
17
18
  import { formatStatusReport, statusProject } from './commands/status.js';
@@ -381,7 +382,13 @@ function parseArgs(argv) {
381
382
  return { command: 'help', options };
382
383
  }
383
384
  if (token === '--') {
384
- options.explainCommand = rest.slice(index + 1).join(' ');
385
+ const remainder = rest.slice(index + 1).join(' ');
386
+ if (command === 'session' && options.sessionSubcommand === 'start') {
387
+ options.sessionAgentCommand = remainder;
388
+ }
389
+ else {
390
+ options.explainCommand = remainder;
391
+ }
385
392
  break;
386
393
  }
387
394
  if (command === 'audit' && !options.auditSubcommand) {
@@ -405,6 +412,13 @@ function parseArgs(argv) {
405
412
  }
406
413
  throw new Error('sandbox requires subcommand: status');
407
414
  }
415
+ if (command === 'session' && !options.sessionSubcommand) {
416
+ if (token === 'start' || token === 'status') {
417
+ options.sessionSubcommand = token;
418
+ continue;
419
+ }
420
+ throw new Error('session requires subcommand: start or status');
421
+ }
408
422
  if (command === 'standing-allow' && !options.standingAllowSubcommand) {
409
423
  if (token === 'revoke') {
410
424
  options.standingAllowSubcommand = token;
@@ -521,6 +535,8 @@ Usage:
521
535
  ${c} explain [--target <dir>] [--cwd <dir>] [--kind shell|tool|subagent] [--tool <name>] [--payload-json <json>] [--command <text>] [--json] [-- <command>]
522
536
  ${c} egress <start|stop|status|env> [--target <dir>] [--json]
523
537
  ${c} sandbox status [--target <dir>] [--json]
538
+ ${c} session <start|status> [--target <dir>] [--json]
539
+ ${c} session start [--target <dir>] [-- <agent command>]
524
540
  ${c} judge <status|list|use|test|bench|consent> [--target <dir>] [--json]
525
541
  ${c} judge test [--target <dir>] [--json] [--live-probe]
526
542
  ${c} judge use <ollama|codex|claude|cursor> [--model <id>] [--endpoint <url>] [--timeout <ms>] [--accept-cloud] [--cloud-consent-approval-id <id>] [--credential project|apiKey] [--key-stdin] [--key-env <NAME>]
@@ -907,6 +923,32 @@ async function main() {
907
923
  process.exitCode = report.issues.length > 0 && report.sandboxEnabled ? 1 : 0;
908
924
  return;
909
925
  }
926
+ if (command === 'session') {
927
+ if (!options.sessionSubcommand) {
928
+ throw new Error('session requires subcommand: start or status');
929
+ }
930
+ if (options.sessionSubcommand === 'start') {
931
+ const result = await sessionStartProject({
932
+ targetDir: options.targetDir,
933
+ agentCommand: options.sessionAgentCommand,
934
+ });
935
+ process.stdout.write(`${result.message}\n`);
936
+ process.exitCode = result.ok ? 0 : 1;
937
+ return;
938
+ }
939
+ const report = await sessionStatusProject({
940
+ targetDir: options.targetDir,
941
+ json: options.json,
942
+ });
943
+ if (options.json) {
944
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
945
+ }
946
+ else {
947
+ process.stdout.write(`${formatSessionStatusReport(report)}\n`);
948
+ }
949
+ process.exitCode = report.ok ? 0 : 1;
950
+ return;
951
+ }
910
952
  if (command === 'approve') {
911
953
  if (!options.approvalId) {
912
954
  throw new Error('approve requires an approval ID.');
@@ -36,7 +36,7 @@ export async function classifyForReport(params) {
36
36
  if (!params.command && !params.payload) {
37
37
  throw new Error('classify-for-report requires command or payload for subagent classification.');
38
38
  }
39
- result = classifySubagent(payload, repoRoot, classifierOptions);
39
+ result = classifySubagent(payload, repoRoot, classifierOptions, config);
40
40
  input = params.command ?? JSON.stringify(payload);
41
41
  }
42
42
  else if (kind === 'tool') {
@@ -1,5 +1,6 @@
1
1
  import { type JudgeProviderId } from '../core/verdict/judge-catalog.js';
2
2
  import type { AdapterName, InitOptions } from '../types.js';
3
+ import { type SelectOptions } from './tui.js';
3
4
  export declare const BELAY_CONFIG_SUBCOMMANDS: readonly ["list", "get", "set", "unset", "credential", "judge"];
4
5
  export type BelayConfigSubcommand = (typeof BELAY_CONFIG_SUBCOMMANDS)[number];
5
6
  export interface BelayConfigOptions {
@@ -39,6 +40,13 @@ export interface BelayConfigInteractiveOptions {
39
40
  /** Suppress the interactive header when nested under runBelayConfigInteractive */
40
41
  skipBanner?: boolean;
41
42
  }
43
+ export declare const JUDGE_CREDENTIAL_MODE_PROMPT = "Judge API key source";
44
+ export declare const JUDGE_CREDENTIAL_STORE_KEY_PROMPT = "Paste API key to store locally (visible input; prefer belay config credential set --key-stdin): ";
45
+ export declare const JUDGE_TRANSPORT_MODE_PROMPT = "How should Belay reach the judge?";
46
+ export declare const JUDGE_HTTP_ENDPOINT_PROMPT = "Judge HTTP API URL:";
47
+ export type JudgeTransportWizardMode = 'cli' | 'http';
48
+ export declare function buildJudgeTransportSelectOptions(judgeProviderId: JudgeProviderId): SelectOptions<JudgeTransportWizardMode>;
49
+ export declare function buildJudgeCredentialModeSelectOptions(judgeProviderId: JudgeProviderId): SelectOptions<'project' | 'apiKey'>;
42
50
  export declare function resolveBelayConfigInteractiveMode(repoRoot: string): Promise<'full' | 'judge-only'>;
43
51
  export declare function runBelayConfigFullInteractive(options?: BelayConfigInteractiveOptions): Promise<{
44
52
  repoRoot: string;
@@ -8,8 +8,9 @@ import { clearJudgeCredentialStore, writeJudgeCredentialStore } from '../core/cr
8
8
  import { refreshIntegrityIfPinned } from '../core/integrity.js';
9
9
  import { defaultJudgeProviderForAdapter, hasValidCloudConsent, isCloudJudgeConfig, resolveJudgeUsePatch, } from '../core/judge-config.js';
10
10
  import { rejectDeprecatedJudgeModelAuto } from '../core/judge-model-policy.js';
11
- import { resolveJudgeTransport } from '../core/judge-runtime-detection.js';
12
- import { isJudgeProviderId, JUDGE_PROVIDER_IDS, normalizeLegacyProviderId, } from '../core/verdict/judge-catalog.js';
11
+ import { detectJudgeRuntimeCapabilities, resolveJudgeTransport, } from '../core/judge-runtime-detection.js';
12
+ import { getJudgeProviderSpec, isJudgeProviderId, JUDGE_PROVIDER_IDS, normalizeLegacyProviderId, } from '../core/verdict/judge-catalog.js';
13
+ import { normalizeJudgeRuntimeConfig } from '../core/verdict/judge-runtime-config.js';
13
14
  import { initProject } from '../installer.js';
14
15
  import { isBelayFloorInstalled } from './health-snapshot.js';
15
16
  import { judgeStatus } from './judge.js';
@@ -31,6 +32,8 @@ const JUDGE_CONFIG_PATHS = [
31
32
  'judge.timeoutMs',
32
33
  'judge.credential.mode',
33
34
  'judge.credential.ref',
35
+ 'judge.runtime.session.enabled',
36
+ 'judge.runtime.shadow.enabled',
34
37
  ];
35
38
  export function parseAdapter(value) {
36
39
  const normalized = (value?.trim() || 'cursor').toLowerCase();
@@ -98,6 +101,12 @@ function getJudgeField(judge, pathKey) {
98
101
  return judge.credential?.mode ?? null;
99
102
  if (pathKey === 'judge.credential.ref')
100
103
  return judge.credential?.ref ?? null;
104
+ if (pathKey === 'judge.runtime.session.enabled') {
105
+ return normalizeJudgeRuntimeConfig(judge.runtime).session.enabled;
106
+ }
107
+ if (pathKey === 'judge.runtime.shadow.enabled') {
108
+ return normalizeJudgeRuntimeConfig(judge.runtime).shadow.enabled;
109
+ }
101
110
  throw new Error(`Unknown judge config path: ${pathKey}`);
102
111
  }
103
112
  function listJudgeFields(judge) {
@@ -181,6 +190,36 @@ async function applyJudgeSet(repoRoot, config, pathKey, rawValue) {
181
190
  await persistJudge(repoRoot, config, judge, adapter);
182
191
  return judge;
183
192
  }
193
+ if (pathKey === 'judge.runtime.session.enabled') {
194
+ if (value !== 'true' && value !== 'false') {
195
+ throw new Error('judge.runtime.session.enabled must be true or false.');
196
+ }
197
+ const runtime = normalizeJudgeRuntimeConfig(config.judge.runtime);
198
+ const judge = normalizeJudgeConfig({
199
+ ...config.judge,
200
+ runtime: {
201
+ ...runtime,
202
+ session: { ...runtime.session, enabled: value === 'true' },
203
+ },
204
+ });
205
+ await persistJudge(repoRoot, config, judge, adapter);
206
+ return judge;
207
+ }
208
+ if (pathKey === 'judge.runtime.shadow.enabled') {
209
+ if (value !== 'true' && value !== 'false') {
210
+ throw new Error('judge.runtime.shadow.enabled must be true or false.');
211
+ }
212
+ const runtime = normalizeJudgeRuntimeConfig(config.judge.runtime);
213
+ const judge = normalizeJudgeConfig({
214
+ ...config.judge,
215
+ runtime: {
216
+ ...runtime,
217
+ shadow: { ...runtime.shadow, enabled: value === 'true' },
218
+ },
219
+ });
220
+ await persistJudge(repoRoot, config, judge, adapter);
221
+ return judge;
222
+ }
184
223
  throw new Error(`Unknown judge config path: ${pathKey}`);
185
224
  }
186
225
  async function applyJudgeUnset(repoRoot, config, pathKey) {
@@ -289,7 +328,75 @@ export async function runBelayConfigCredential(options) {
289
328
  }
290
329
  throw new Error('credential requires action: mode, set, or clear.');
291
330
  }
331
+ export const JUDGE_CREDENTIAL_MODE_PROMPT = 'Judge API key source';
332
+ export const JUDGE_CREDENTIAL_STORE_KEY_PROMPT = 'Paste API key to store locally (visible input; prefer belay config credential set --key-stdin): ';
333
+ export const JUDGE_TRANSPORT_MODE_PROMPT = 'How should Belay reach the judge?';
334
+ export const JUDGE_HTTP_ENDPOINT_PROMPT = 'Judge HTTP API URL:';
335
+ const CLOUD_CLI_LABELS = {
336
+ codex: 'Codex CLI',
337
+ claude: 'Claude CLI',
338
+ cursor: 'Cursor CLI',
339
+ };
340
+ export function buildJudgeTransportSelectOptions(judgeProviderId) {
341
+ const cliLabel = CLOUD_CLI_LABELS[judgeProviderId];
342
+ const runtime = detectJudgeRuntimeCapabilities(judgeProviderId);
343
+ const cliHint = runtime.cliTransport
344
+ ? 'Uses your logged-in host session; no URL needed.'
345
+ : 'Host CLI not found on PATH — install it or choose custom HTTP API.';
346
+ return {
347
+ message: JUDGE_TRANSPORT_MODE_PROMPT,
348
+ defaultValue: 'cli',
349
+ choices: [
350
+ {
351
+ value: 'cli',
352
+ label: `Use ${cliLabel} (recommended)`,
353
+ hint: cliHint,
354
+ },
355
+ {
356
+ value: 'http',
357
+ label: 'Custom HTTP API endpoint',
358
+ hint: 'Requires a provider API URL and cloud egress consent.',
359
+ },
360
+ ],
361
+ };
362
+ }
363
+ export function buildJudgeCredentialModeSelectOptions(judgeProviderId) {
364
+ const spec = getJudgeProviderSpec(judgeProviderId);
365
+ const providerEnvVars = spec?.apiKeyEnvVars.filter((name) => name !== 'BELAY_JUDGE_API_KEY') ?? [];
366
+ const envNames = providerEnvVars.length > 0
367
+ ? ['BELAY_JUDGE_API_KEY', ...providerEnvVars].join(', ')
368
+ : 'BELAY_JUDGE_API_KEY';
369
+ const projectHint = providerEnvVars.length > 0
370
+ ? `Uses ${envNames} from the shell; does not read .env files. Or host CLI login when available.`
371
+ : 'Uses BELAY_JUDGE_API_KEY from the shell; does not read .env files.';
372
+ return {
373
+ message: JUDGE_CREDENTIAL_MODE_PROMPT,
374
+ defaultValue: 'project',
375
+ choices: [
376
+ {
377
+ value: 'project',
378
+ label: 'Environment variables or host CLI',
379
+ hint: projectHint,
380
+ },
381
+ {
382
+ value: 'apiKey',
383
+ label: 'Store in Belay credential store',
384
+ hint: 'Saved to credentials.json (mode 0600) on the next step.',
385
+ },
386
+ ],
387
+ };
388
+ }
292
389
  function formatSelectPrompt(options) {
390
+ const hasDetails = options.choices.some((choice) => choice.hint || choice.label);
391
+ if (hasDetails) {
392
+ const lines = options.choices.map((choice) => {
393
+ const label = choice.label ?? choice.value;
394
+ const hint = choice.hint ? ` — ${choice.hint}` : '';
395
+ return ` ${choice.value}: ${label}${hint}`;
396
+ });
397
+ const values = options.choices.map((choice) => choice.value).join(' | ');
398
+ return `${options.message}\n${lines.join('\n')}\n[${values}] (${options.defaultValue}): `;
399
+ }
293
400
  const values = options.choices.map((choice) => choice.value).join(' | ');
294
401
  return `${options.message} [${values}] (${options.defaultValue}): `;
295
402
  }
@@ -374,20 +481,36 @@ function writeConfigWizardBanner(options, title) {
374
481
  }
375
482
  output.write(`${title}\n`);
376
483
  }
484
+ async function askJudgeHttpEndpoint(prompter) {
485
+ while (true) {
486
+ const endpoint = (await prompter.askText(JUDGE_HTTP_ENDPOINT_PROMPT)).trim();
487
+ if (endpoint) {
488
+ return endpoint;
489
+ }
490
+ process.stderr.write('Warning: HTTP endpoint URL is required when using custom HTTP API. Paste a URL or re-run belay config and choose host CLI.\n');
491
+ }
492
+ }
493
+ async function askJudgeCredentialStoreKey(prompter) {
494
+ while (true) {
495
+ const key = (await prompter.askText(JUDGE_CREDENTIAL_STORE_KEY_PROMPT)).trim();
496
+ if (key) {
497
+ return key;
498
+ }
499
+ process.stderr.write('Warning: API key cannot be empty. Paste a key or re-run belay config and choose environment variables.\n');
500
+ }
501
+ }
377
502
  async function collectCloudJudgeWizardAnswers(prompter, judgeProviderId) {
378
503
  if (judgeProviderId === 'ollama') {
379
504
  return { acceptCloud: false };
380
505
  }
381
- const judgeCredentialMode = (await prompter.askConfirm('Use project env for credentials?', true))
382
- ? 'project'
383
- : 'apiKey';
384
- const optionalEndpoint = (await prompter.askText('Judge endpoint URL (optional): ')).trim();
385
- const judgeEndpoint = optionalEndpoint || undefined;
506
+ const judgeCredentialMode = await prompter.askSelect(buildJudgeCredentialModeSelectOptions(judgeProviderId));
507
+ const transportMode = await prompter.askSelect(buildJudgeTransportSelectOptions(judgeProviderId));
508
+ let judgeEndpoint;
509
+ if (transportMode === 'http') {
510
+ judgeEndpoint = await askJudgeHttpEndpoint(prompter);
511
+ }
386
512
  if (judgeCredentialMode === 'apiKey') {
387
- const key = await prompter.askText('Paste API key (hidden input not available in all shells): ');
388
- if (key.trim()) {
389
- process.env.BELAY_CONFIG_WIZARD_JUDGE_KEY = key.trim();
390
- }
513
+ process.env.BELAY_CONFIG_WIZARD_JUDGE_KEY = await askJudgeCredentialStoreKey(prompter);
391
514
  }
392
515
  let acceptCloud = false;
393
516
  if (judgeEndpoint) {
@@ -8,9 +8,12 @@ import { resolveScopedPaths } from '../adapters/layouts/scope.js';
8
8
  import { cleanupOrphanApprovalState } from '../cleanup-orphans.js';
9
9
  import { approvedApprovalsPath, belayStateDir, detectAdapterName, loadLayeredConfig, pendingApprovalsPath, repoLocalStateDirFor, } from '../config-io.js';
10
10
  import { detectFenceDrift, summarizeAuditVisibility } from '../core/audit-summary.js';
11
+ import { inspectBoundaryAttestationFile } from '../core/capability/boundary-attestation-sign.js';
12
+ import { boundaryAttestationPath, boundarySessionStatus, } from '../core/capability/boundary-session.js';
11
13
  import { defaultControlPlaneDir } from '../core/config.js';
12
14
  import { verifyIntegrityManifest } from '../core/integrity.js';
13
15
  import { diagnoseJudge, stopJudgeSessionBrokers } from '../core/judge-doctor.js';
16
+ import { resolveJudgeTransport } from '../core/judge-runtime-detection.js';
14
17
  import { getManagedHookEntries } from '../defaults.js';
15
18
  import { resolveNodeBinary } from '../node-resolution.js';
16
19
  import { egressStatus } from '../services/egress-service.js';
@@ -78,7 +81,9 @@ export async function doctorProject(options = {}) {
78
81
  if (loadedConfig.version !== 4) {
79
82
  warnings.push(`Config version is ${loadedConfig.version}; expected 4. Run belay upgrade to migrate.`);
80
83
  }
81
- const judgeDoctor = await diagnoseJudge(loadedConfig);
84
+ const transport = resolveJudgeTransport(loadedConfig.judge);
85
+ const shouldProbe = transport.endsWith('-cli');
86
+ const judgeDoctor = await diagnoseJudge(loadedConfig, repoRoot, { liveProbe: shouldProbe });
82
87
  issues.push(...judgeDoctor.issues);
83
88
  warnings.push(...judgeDoctor.warnings);
84
89
  notes.push(...judgeDoctor.notes);
@@ -294,6 +299,36 @@ export async function doctorProject(options = {}) {
294
299
  for (const issue of sandbox.issues) {
295
300
  warnings.push(issue);
296
301
  }
302
+ for (const advisory of sandbox.advisories) {
303
+ warnings.push(advisory);
304
+ }
305
+ }
306
+ const boundaryDriver = loadedConfig.capability?.boundaryDriver ??
307
+ (loadedConfig.sandbox.runtime === 'container' ? 'container' : null);
308
+ if (loadedConfig.capability || boundaryDriver === 'container') {
309
+ const attestationPath = boundaryAttestationPath(repoRoot, loadedConfig);
310
+ const attestationFormat = await inspectBoundaryAttestationFile(attestationPath);
311
+ if (attestationFormat === 'legacy') {
312
+ warnings.push('Boundary attestation uses an unsigned legacy format. Run belay session start to re-sign it.');
313
+ }
314
+ else if (attestationFormat === 'invalid') {
315
+ warnings.push('Boundary attestation file is present but invalid. Run belay session start to regenerate it.');
316
+ }
317
+ }
318
+ if (boundaryDriver === 'container') {
319
+ const session = await boundarySessionStatus({ repoRoot, config: loadedConfig });
320
+ if (!session.attestation) {
321
+ warnings.push('Container boundary driver is configured but no attestation was found. Run belay session start.');
322
+ }
323
+ else if (!session.fresh) {
324
+ warnings.push('Boundary attestation is stale or expired. Run belay session start to refresh.');
325
+ }
326
+ else if (session.attestation.probeSignals.includes('repo-mount-ro-default')) {
327
+ notes.push('Container boundary defaults to read-only directory mounts; predicted writes use read-write mounts for the working directory only.');
328
+ }
329
+ else if (session.attestation.probeSignals.includes('repo-mount-rw')) {
330
+ notes.push('Container boundary mounts the working directory read-write (repo-mount-rw). Policy grants scope widening; paths outside the mount are not container-enforced.');
331
+ }
297
332
  }
298
333
  if (loadedConfig.egress.enabled) {
299
334
  const egress = await egressStatus({ targetDir: repoRoot });
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { loadApprovalState, loadConfigFile } from '../config-io.js';
3
3
  import { compactApprovals } from '../core/approval.js';
4
+ import { extractJudgeFallbackReason, formatJudgeRecoveryHint, inferProviderIdFromFallbackReason, isJudgeInfrastructureFailure, } from '../core/judge-fallback-hints.js';
4
5
  import { classifyForReport } from './classify-for-report.js';
5
6
  export async function explainCommand(options) {
6
7
  const repoRoot = path.resolve(options.targetDir ?? process.cwd());
@@ -134,5 +135,14 @@ export function formatExplainReport(report) {
134
135
  ? 'Observed assessment: measured in an isolated git worktree at gate time. Observed-safe commands are applied once and the hook denies re-execution (transactional_already_applied).'
135
136
  : 'Observed assessment: not applicable (transactional path not eligible).',
136
137
  ];
137
- return `${lines.join('\n')}\n`;
138
+ const recoveryLines = [];
139
+ if (isJudgeInfrastructureFailure(result)) {
140
+ const fallbackReason = extractJudgeFallbackReason(result);
141
+ const providerId = inferProviderIdFromFallbackReason(fallbackReason);
142
+ const recoveryHint = formatJudgeRecoveryHint(providerId, fallbackReason);
143
+ if (recoveryHint) {
144
+ recoveryLines.push('', 'Recovery:', ` ${recoveryHint}`, ' Then verify: belay judge test --live-probe');
145
+ }
146
+ }
147
+ return `${[...lines, ...recoveryLines].join('\n')}\n`;
138
148
  }
@@ -63,10 +63,11 @@ export async function judgeStatus(options = {}) {
63
63
  });
64
64
  const { resolved } = resolveJudgeModel(judge);
65
65
  const transport = resolveJudgeTransport(judge);
66
+ const effectiveTransport = transport === 'cursor-cli' && judge.runtime?.session.enabled ? 'cursor-acp' : transport;
66
67
  const lines = [
67
68
  `Judge providerId : ${judge.providerId ?? '(inferred)'}`,
68
69
  `Judge driver : ${judge.provider}`,
69
- `Transport : ${transport}`,
70
+ `Transport : ${effectiveTransport}`,
70
71
  `Endpoint : ${judge.endpoint ?? '(none)'}`,
71
72
  `Model : ${resolved} (requested: ${judge.model})`,
72
73
  `Credential : ${credential.mode} (${credential.sourceKind})${credential.source ? ` → ${credential.source}` : ''} ${credential.key ? '✓ set' : '✗ missing'}`,
@@ -82,7 +83,7 @@ export async function judgeStatus(options = {}) {
82
83
  endpoint: judge.endpoint,
83
84
  model: judge.model,
84
85
  modelResolved: resolved,
85
- transport,
86
+ transport: effectiveTransport,
86
87
  credential,
87
88
  cloudConsent: judge.cloudConsent ?? null,
88
89
  cloudJudgeActive: isCloudJudgeConfig(judge) && hasValidCloudConsent(judge),
@@ -0,0 +1,20 @@
1
+ import { boundarySessionStatus } from '../core/capability/boundary-session.js';
2
+ export declare function sessionStartProject(params: {
3
+ targetDir?: string;
4
+ agentCommand?: string;
5
+ }): Promise<{
6
+ ok: boolean;
7
+ message: string;
8
+ attestationPath?: string;
9
+ exitCode?: number | null;
10
+ }>;
11
+ export declare function sessionStatusProject(params: {
12
+ targetDir?: string;
13
+ json?: boolean;
14
+ }): Promise<{
15
+ ok: boolean;
16
+ attestationPath: string;
17
+ fresh: boolean;
18
+ attestation: Awaited<ReturnType<typeof boundarySessionStatus>>['attestation'];
19
+ }>;
20
+ export declare function formatSessionStatusReport(report: Awaited<ReturnType<typeof sessionStatusProject>>): string;
@@ -0,0 +1,51 @@
1
+ import path from 'node:path';
2
+ import process from 'node:process';
3
+ import { loadConfigFile } from '../config-io.js';
4
+ import { boundarySessionStatus, runBoundaryAgentCommand, startBoundarySession, } from '../core/capability/boundary-session.js';
5
+ export async function sessionStartProject(params) {
6
+ const repoRoot = path.resolve(params.targetDir ?? process.cwd());
7
+ const config = await loadConfigFile(repoRoot);
8
+ const started = await startBoundarySession({ repoRoot, config });
9
+ if (!params.agentCommand) {
10
+ return {
11
+ ok: true,
12
+ message: `Boundary session started (${started.attestation.driver}); attestation written to ${path.relative(repoRoot, started.attestationPath)}.`,
13
+ attestationPath: started.attestationPath,
14
+ };
15
+ }
16
+ const run = await runBoundaryAgentCommand({
17
+ repoRoot,
18
+ config,
19
+ command: params.agentCommand,
20
+ });
21
+ const exitCode = run.exitCode ?? 1;
22
+ return {
23
+ ok: exitCode === 0,
24
+ message: `Boundary session started (${started.attestation.driver}); agent exited with code ${exitCode}.`,
25
+ attestationPath: started.attestationPath,
26
+ exitCode,
27
+ };
28
+ }
29
+ export async function sessionStatusProject(params) {
30
+ const repoRoot = path.resolve(params.targetDir ?? process.cwd());
31
+ const config = await loadConfigFile(repoRoot);
32
+ const status = await boundarySessionStatus({ repoRoot, config });
33
+ return {
34
+ ok: status.fresh,
35
+ attestationPath: status.attestationPath,
36
+ fresh: status.fresh,
37
+ attestation: status.attestation,
38
+ };
39
+ }
40
+ export function formatSessionStatusReport(report) {
41
+ if (!report.attestation) {
42
+ return `No boundary attestation at ${report.attestationPath}. Run belay session start.`;
43
+ }
44
+ return [
45
+ `Attestation: ${report.attestationPath}`,
46
+ `Driver: ${report.attestation.driver}`,
47
+ `Fresh: ${report.fresh ? 'yes' : 'no'}`,
48
+ `Expires: ${report.attestation.expiresAt}`,
49
+ `Materializes grants: ${report.attestation.materializesGrants ? 'yes' : 'no'}`,
50
+ ].join('\n');
51
+ }
package/dist/config-io.js CHANGED
@@ -49,6 +49,42 @@ const APPROVAL_STATE_FILES = ['pending-approvals.json', 'approved-approvals.json
49
49
  function approvalFilesExist(dir) {
50
50
  return APPROVAL_STATE_FILES.some((fileName) => existsSync(path.join(dir, fileName)));
51
51
  }
52
+ async function isCorruptApprovalStateFile(filePath) {
53
+ const raw = await readFile(filePath, 'utf8');
54
+ try {
55
+ JSON.parse(raw);
56
+ return false;
57
+ }
58
+ catch (error) {
59
+ return error instanceof SyntaxError;
60
+ }
61
+ }
62
+ function normalizeApprovalStateFile(parsed) {
63
+ const version = parsed.version === 3 ? 3 : parsed.version === 2 ? 2 : 1;
64
+ return {
65
+ version,
66
+ revision: typeof parsed.revision === 'number' ? parsed.revision : undefined,
67
+ approvals: Array.isArray(parsed.approvals) ? parsed.approvals : [],
68
+ };
69
+ }
70
+ /** Gate reads: corrupt JSON is treated as empty state (fail-closed). */
71
+ async function readApprovalStateFileForGate(filePath) {
72
+ const raw = await readFile(filePath, 'utf8');
73
+ try {
74
+ return normalizeApprovalStateFile(JSON.parse(raw));
75
+ }
76
+ catch (error) {
77
+ if (error instanceof SyntaxError) {
78
+ return { version: 1, approvals: [] };
79
+ }
80
+ throw error;
81
+ }
82
+ }
83
+ /** Migration reads: corrupt JSON must not be treated as empty. */
84
+ async function readApprovalStateFileStrict(filePath) {
85
+ const raw = await readFile(filePath, 'utf8');
86
+ return normalizeApprovalStateFile(JSON.parse(raw));
87
+ }
52
88
  async function repoLocalApprovalsEmpty(repoRoot, config) {
53
89
  const repoLocalDir = repoLocalStateDirFor(repoRoot, config);
54
90
  if (!approvalFilesExist(repoLocalDir)) {
@@ -59,21 +95,16 @@ async function repoLocalApprovalsEmpty(repoRoot, config) {
59
95
  if (!existsSync(filePath)) {
60
96
  continue;
61
97
  }
62
- const state = await readApprovalStateFile(filePath);
98
+ if (await isCorruptApprovalStateFile(filePath)) {
99
+ return false;
100
+ }
101
+ const state = await readApprovalStateFileForGate(filePath);
63
102
  if (state.approvals.length > 0) {
64
103
  return false;
65
104
  }
66
105
  }
67
106
  return true;
68
107
  }
69
- async function readApprovalStateFile(filePath) {
70
- const raw = await readFile(filePath, 'utf8');
71
- const parsed = JSON.parse(raw);
72
- return {
73
- version: parsed.version === 2 ? 2 : 1,
74
- approvals: Array.isArray(parsed.approvals) ? parsed.approvals : [],
75
- };
76
- }
77
108
  async function writeApprovalStateFile(filePath, state) {
78
109
  await mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
79
110
  await writeFile(filePath, `${JSON.stringify(compactApprovals(state), null, 2)}\n`, {
@@ -91,12 +122,18 @@ async function migrateApprovalFilesBetween(sourceDir, targetDir) {
91
122
  continue;
92
123
  }
93
124
  if (!existsSync(to)) {
125
+ if (await isCorruptApprovalStateFile(from)) {
126
+ continue;
127
+ }
94
128
  await copyFile(from, to);
95
129
  await chmod(to, 0o600);
96
130
  continue;
97
131
  }
98
- const targetState = await readApprovalStateFile(to);
99
- const sourceState = await readApprovalStateFile(from);
132
+ if ((await isCorruptApprovalStateFile(from)) || (await isCorruptApprovalStateFile(to))) {
133
+ continue;
134
+ }
135
+ const targetState = await readApprovalStateFileStrict(to);
136
+ const sourceState = await readApprovalStateFileStrict(from);
100
137
  await writeApprovalStateFile(to, mergeApprovalStates(targetState, sourceState));
101
138
  }
102
139
  }
@@ -172,7 +209,7 @@ export async function loadApprovalState(repoRoot, fileName, config) {
172
209
  if (!existsSync(filePath)) {
173
210
  return { version: 1, approvals: [] };
174
211
  }
175
- return readApprovalStateFile(filePath);
212
+ return readApprovalStateFileForGate(filePath);
176
213
  }
177
214
  export async function saveApprovalState(repoRoot, fileName, state, config) {
178
215
  const filePath = fileName === 'pending-approvals.json'
@@ -0,0 +1,21 @@
1
+ import type { BoundaryAttestation } from '../core/capability/attestation.js';
2
+ import type { BelayConfigV4 } from '../core/config.js';
3
+ import type { LayerProfileId } from './types.js';
4
+ export interface GuaranteePosture {
5
+ configuredProfile: LayerProfileId;
6
+ attestedProfile: LayerProfileId;
7
+ attestationFresh: boolean;
8
+ l1FullConfigured: boolean;
9
+ l1FullAttested: boolean;
10
+ postureMismatch: boolean;
11
+ }
12
+ export declare function resolveConfiguredProfile(config: BelayConfigV4, egressProxyRunning: boolean): LayerProfileId;
13
+ export declare function resolveAttestedProfile(params: {
14
+ attestation: BoundaryAttestation | null;
15
+ l1FullConfigured: boolean;
16
+ }): LayerProfileId;
17
+ export declare function evaluateGuaranteePosture(params: {
18
+ config: BelayConfigV4;
19
+ attestation: BoundaryAttestation | null;
20
+ egressProxyRunning: boolean;
21
+ }): GuaranteePosture;
@@ -0,0 +1,46 @@
1
+ import { isAttestationFresh } from '../core/capability/attestation.js';
2
+ import { evaluateL1FullStatus } from '../core/capability/broker.js';
3
+ export function resolveConfiguredProfile(config, egressProxyRunning) {
4
+ const l1Full = evaluateL1FullStatus({ config, egressProxyRunning });
5
+ if (l1Full.active) {
6
+ return 'l1-full';
7
+ }
8
+ if (config.egress.enabled) {
9
+ return 'l1-partial-egress';
10
+ }
11
+ if (config.policy.transactional.enabled) {
12
+ return 'l1-l2-transactional';
13
+ }
14
+ return 'l3-l4-only';
15
+ }
16
+ export function resolveAttestedProfile(params) {
17
+ if (!params.attestation || !isAttestationFresh(params.attestation)) {
18
+ return 'l3-l4-only';
19
+ }
20
+ if (params.l1FullConfigured &&
21
+ params.attestation.deniesUngrantedEffects &&
22
+ params.attestation.materializesGrants) {
23
+ return 'l1-full';
24
+ }
25
+ return 'l3-l4-only';
26
+ }
27
+ export function evaluateGuaranteePosture(params) {
28
+ const l1Full = evaluateL1FullStatus({
29
+ config: params.config,
30
+ egressProxyRunning: params.egressProxyRunning,
31
+ });
32
+ const configuredProfile = resolveConfiguredProfile(params.config, params.egressProxyRunning);
33
+ const attestedProfile = resolveAttestedProfile({
34
+ attestation: params.attestation,
35
+ l1FullConfigured: l1Full.active,
36
+ });
37
+ return {
38
+ configuredProfile,
39
+ attestedProfile,
40
+ attestationFresh: params.attestation ? isAttestationFresh(params.attestation) : false,
41
+ l1FullConfigured: l1Full.active,
42
+ l1FullAttested: attestedProfile === 'l1-full' &&
43
+ Boolean(params.attestation?.deniesUngrantedEffects && params.attestation.materializesGrants),
44
+ postureMismatch: configuredProfile === 'l1-full' && attestedProfile !== 'l1-full',
45
+ };
46
+ }
@@ -1,4 +1,5 @@
1
1
  import type { LayerConformanceScenario, LayerProfileId } from './types.js';
2
+ export { evaluateGuaranteePosture, type GuaranteePosture } from './guarantee-posture.js';
2
3
  export type { LayerConformanceScenario, LayerProfileId } from './types.js';
3
4
  export interface GuaranteeTableRow {
4
5
  profile: LayerProfileId;