@synapsor/runner 0.1.9 → 0.1.10

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/dist/runner.mjs CHANGED
@@ -444,8 +444,9 @@ function sleep(ms) {
444
444
  }
445
445
 
446
446
  // packages/config/src/index.ts
447
- var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["version", "mode", "storage", "sources", "trusted_context", "contexts", "executors", "capabilities", "contracts", "cloud", "strict", "result_format"]);
447
+ var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["version", "mode", "storage", "sources", "trusted_context", "contexts", "executors", "capabilities", "contracts", "policies", "approvals", "cloud", "strict", "result_format"]);
448
448
  var STORAGE_KEYS = /* @__PURE__ */ new Set(["sqlite_path"]);
449
+ var APPROVALS_KEYS = /* @__PURE__ */ new Set(["disable_auto_approval"]);
449
450
  var CLOUD_KEYS = /* @__PURE__ */ new Set(["base_url_env", "runner_token_env", "runner_id", "runner_version", "project_id", "adapter_id", "source_id", "engines", "capabilities", "session"]);
450
451
  var SOURCE_KEYS = /* @__PURE__ */ new Set([
451
452
  "engine",
@@ -489,7 +490,9 @@ var PATCH_BINDING_KEYS = /* @__PURE__ */ new Set(["fixed", "from_arg"]);
489
490
  var NUMERIC_BOUND_KEYS = /* @__PURE__ */ new Set(["minimum", "maximum"]);
490
491
  var TRANSITION_GUARD_KEYS = /* @__PURE__ */ new Set(["from_column", "allowed"]);
491
492
  var CONFLICT_GUARD_KEYS = /* @__PURE__ */ new Set(["column", "weak_guard_ack"]);
492
- var APPROVAL_KEYS = /* @__PURE__ */ new Set(["mode", "required_role"]);
493
+ var APPROVAL_KEYS = /* @__PURE__ */ new Set(["mode", "required_role", "policy"]);
494
+ var POLICY_KEYS = /* @__PURE__ */ new Set(["name", "kind", "mode", "rules"]);
495
+ var APPROVAL_POLICY_RULE_KEYS = /* @__PURE__ */ new Set(["field", "max"]);
493
496
  var WRITEBACK_KEYS = /* @__PURE__ */ new Set(["mode", "executor"]);
494
497
  var WRITEBACK_MODES = /* @__PURE__ */ new Set(["direct_sql", "app_handler", "cloud_worker", "none"]);
495
498
  var MODEL_CONTROLLED_TRUST_FIELDS = /* @__PURE__ */ new Set([
@@ -562,17 +565,60 @@ function validateRunnerCapabilityConfig(input) {
562
565
  errors.push({ path: "$.mode", code: "INVALID_MODE", message: "mode must be read_only, shadow, review, or cloud." });
563
566
  }
564
567
  validateStorage(input.storage, strict, errors);
568
+ validateApprovals(input.approvals, strict, errors);
565
569
  const hasContracts = validateContracts(input.contracts, errors);
566
570
  validateCloud(input.cloud, input.mode, strict, errors);
567
571
  validateSources(input.sources, input.mode, strict, errors, warnings);
568
572
  validateContexts(input.contexts, strict, errors, warnings);
569
573
  validateTrustedContext(input.trusted_context, input.contexts, input.capabilities, input.mode, strict, errors, warnings, hasContracts);
570
574
  validateExecutors(input.executors, input.mode, strict, errors);
575
+ validatePolicies(input.policies, strict, errors);
571
576
  validateCapabilities(input.capabilities, input.sources, input.contexts, input.executors, input.mode, strict, errors, warnings, hasContracts);
577
+ validateApprovalPolicyReferences(input.capabilities, input.policies, errors);
572
578
  validateWritebackReadiness(input.sources, input.capabilities, input.mode, errors, warnings);
573
579
  scanForForbiddenFields(input, "$", errors);
574
580
  return { ok: errors.length === 0, errors, warnings };
575
581
  }
582
+ function validateApprovalPolicyReferences(capabilities, policies, errors) {
583
+ if (!Array.isArray(capabilities)) return;
584
+ const policyByName = /* @__PURE__ */ new Map();
585
+ if (Array.isArray(policies)) {
586
+ policies.forEach((policy) => {
587
+ if (isRecord(policy) && typeof policy.name === "string") policyByName.set(policy.name, policy);
588
+ });
589
+ }
590
+ capabilities.forEach((capability, index) => {
591
+ if (!isRecord(capability) || capability.kind !== "proposal" || !isRecord(capability.approval)) return;
592
+ const approval = capability.approval;
593
+ const path4 = `$.capabilities[${index}].approval`;
594
+ if (approval.mode === "policy") {
595
+ if (!isNonEmptyString(approval.required_role)) {
596
+ errors.push({ path: `${path4}.required_role`, code: "APPROVAL_POLICY_ROLE_REQUIRED", message: "policy approval still requires required_role for human fallback." });
597
+ }
598
+ if (!isSafeName(approval.policy)) {
599
+ errors.push({ path: `${path4}.policy`, code: "APPROVAL_POLICY_REQUIRED", message: "approval.mode policy requires approval.policy." });
600
+ return;
601
+ }
602
+ const policy = policyByName.get(String(approval.policy));
603
+ if (!policy || policy.kind !== "approval") {
604
+ errors.push({ path: `${path4}.policy`, code: "APPROVAL_POLICY_UNRESOLVED", message: "approval.policy must reference a top-level approval policy." });
605
+ }
606
+ } else if (approval.policy !== void 0) {
607
+ errors.push({ path: `${path4}.policy`, code: "APPROVAL_POLICY_MODE_REQUIRED", message: "approval.policy can only be set when approval.mode is policy." });
608
+ }
609
+ });
610
+ }
611
+ function validateApprovals(value, strict, errors) {
612
+ if (value === void 0) return;
613
+ if (!isRecord(value)) {
614
+ errors.push({ path: "$.approvals", code: "APPROVALS_NOT_OBJECT", message: "approvals must be an object." });
615
+ return;
616
+ }
617
+ if (strict) checkUnknownKeys(value, APPROVALS_KEYS, "$.approvals", errors);
618
+ if (value.disable_auto_approval !== void 0 && typeof value.disable_auto_approval !== "boolean") {
619
+ errors.push({ path: "$.approvals.disable_auto_approval", code: "INVALID_DISABLE_AUTO_APPROVAL", message: "disable_auto_approval must be true or false." });
620
+ }
621
+ }
576
622
  function validateContracts(value, errors) {
577
623
  if (value === void 0) return false;
578
624
  if (!Array.isArray(value) || value.length === 0) {
@@ -873,6 +919,44 @@ function validateExecutors(value, mode, strict, errors) {
873
919
  }
874
920
  }
875
921
  }
922
+ function validatePolicies(value, strict, errors) {
923
+ if (value === void 0) return;
924
+ if (!Array.isArray(value)) {
925
+ errors.push({ path: "$.policies", code: "POLICIES_NOT_ARRAY", message: "policies must be an array." });
926
+ return;
927
+ }
928
+ const names = /* @__PURE__ */ new Set();
929
+ value.forEach((policy, index) => {
930
+ const path4 = `$.policies[${index}]`;
931
+ if (!isRecord(policy)) {
932
+ errors.push({ path: path4, code: "POLICY_NOT_OBJECT", message: "policy must be an object." });
933
+ return;
934
+ }
935
+ if (strict) checkUnknownKeys(policy, POLICY_KEYS, path4, errors);
936
+ if (!isSafeName(policy.name)) errors.push({ path: `${path4}.name`, code: "INVALID_POLICY_NAME", message: "policy name must be a safe or qualified identifier." });
937
+ else if (names.has(String(policy.name))) errors.push({ path: `${path4}.name`, code: "DUPLICATE_POLICY_NAME", message: `Duplicate policy name: ${String(policy.name)}` });
938
+ else names.add(String(policy.name));
939
+ if (!["approval", "settlement", "scope", "custom"].includes(String(policy.kind))) {
940
+ errors.push({ path: `${path4}.kind`, code: "INVALID_POLICY_KIND", message: "policy.kind must be approval, settlement, scope, or custom." });
941
+ }
942
+ if (policy.rules !== void 0) {
943
+ if (!Array.isArray(policy.rules)) {
944
+ errors.push({ path: `${path4}.rules`, code: "POLICY_RULES_NOT_ARRAY", message: "policy.rules must be an array." });
945
+ } else if (policy.kind === "approval") {
946
+ policy.rules.forEach((rule, ruleIndex) => validateApprovalPolicyRule(rule, `${path4}.rules[${ruleIndex}]`, strict, errors));
947
+ }
948
+ }
949
+ });
950
+ }
951
+ function validateApprovalPolicyRule(value, path4, strict, errors) {
952
+ if (!isRecord(value)) {
953
+ errors.push({ path: path4, code: "APPROVAL_POLICY_RULE_NOT_OBJECT", message: "approval policy rules must be objects." });
954
+ return;
955
+ }
956
+ if (strict) checkUnknownKeys(value, APPROVAL_POLICY_RULE_KEYS, path4, errors);
957
+ if (!isSafeIdentifier(value.field)) errors.push({ path: `${path4}.field`, code: "INVALID_APPROVAL_POLICY_FIELD", message: "approval policy rule field must be a safe identifier." });
958
+ if (!Number.isInteger(value.max) || Number(value.max) < 0) errors.push({ path: `${path4}.max`, code: "INVALID_APPROVAL_POLICY_MAX", message: "approval policy rule max must be a non-negative integer." });
959
+ }
876
960
  function validateExecutorAuth(value, path4, strict, errors) {
877
961
  if (value === void 0) return;
878
962
  if (!isRecord(value)) {
@@ -2823,10 +2907,10 @@ var PATCH_KEYS = /* @__PURE__ */ new Set(["fixed", "from_arg"]);
2823
2907
  var NUMERIC_BOUND_KEYS2 = /* @__PURE__ */ new Set(["minimum", "maximum"]);
2824
2908
  var TRANSITION_GUARD_KEYS2 = /* @__PURE__ */ new Set(["from_column", "allowed"]);
2825
2909
  var CONFLICT_GUARD_KEYS2 = /* @__PURE__ */ new Set(["column", "weak_guard_ack"]);
2826
- var APPROVAL_KEYS2 = /* @__PURE__ */ new Set(["mode", "required_role"]);
2910
+ var APPROVAL_KEYS2 = /* @__PURE__ */ new Set(["mode", "required_role", "policy"]);
2827
2911
  var WRITEBACK_KEYS2 = /* @__PURE__ */ new Set(["mode", "executor", "idempotency_key"]);
2828
2912
  var WORKFLOW_KEYS = /* @__PURE__ */ new Set(["name", "description", "context", "allowed_capabilities", "required_evidence", "approval", "settlement", "replay"]);
2829
- var POLICY_KEYS = /* @__PURE__ */ new Set(["name", "kind", "mode", "rules"]);
2913
+ var POLICY_KEYS2 = /* @__PURE__ */ new Set(["name", "kind", "mode", "rules"]);
2830
2914
  var EVIDENCE_RECORD_KEYS = /* @__PURE__ */ new Set(["handle", "capability", "query_fingerprint", "items"]);
2831
2915
  var PROPOSAL_RECORD_KEYS = /* @__PURE__ */ new Set(["id", "capability", "subject", "status", "diff", "evidence_handle"]);
2832
2916
  var RECEIPT_KEYS = /* @__PURE__ */ new Set(["id", "proposal_id", "status", "idempotency_key", "source_database_mutated", "rows_affected"]);
@@ -2868,9 +2952,12 @@ function validateContract(input) {
2868
2952
  validateMetadata(input.metadata, "$.metadata", errors);
2869
2953
  const resourceNames = validateResources(input.resources, errors, warnings);
2870
2954
  const contextNames = validateContexts2(input.contexts, errors, warnings);
2955
+ const capabilities = Array.isArray(input.capabilities) ? input.capabilities : [];
2956
+ const policies = Array.isArray(input.policies) ? input.policies : [];
2871
2957
  const capabilityNames = validateCapabilities2(input.capabilities, contextNames, resourceNames, errors, warnings);
2872
2958
  validateWorkflows(input.workflows, contextNames, capabilityNames, errors);
2873
- validatePolicies(input.policies, errors);
2959
+ const policyByName = validatePolicies2(input.policies, errors);
2960
+ validateCapabilityApprovalPolicies(capabilities, policies, policyByName, errors);
2874
2961
  validateEvidenceRecords(input.evidence, errors);
2875
2962
  validateProposalRecords(input.proposals, capabilityNames, errors);
2876
2963
  validateReceiptRecords(input.receipts, errors);
@@ -3248,10 +3335,135 @@ function validateWorkflows(value, contextNames, capabilityNames, errors) {
3248
3335
  }
3249
3336
  });
3250
3337
  }
3251
- function validatePolicies(value, errors) {
3338
+ function validatePolicies2(value, errors) {
3339
+ const names = /* @__PURE__ */ new Map();
3252
3340
  if (value === void 0)
3341
+ return names;
3342
+ if (!Array.isArray(value)) {
3343
+ errors.push({ path: "$.policies", code: "POLICIES_NOT_ARRAY", message: "$.policies must be an array." });
3344
+ return names;
3345
+ }
3346
+ value.forEach((policy, index) => {
3347
+ const path4 = `$.policies[${index}]`;
3348
+ if (!isRecord3(policy)) {
3349
+ errors.push({ path: path4, code: "POLICY_NOT_OBJECT", message: "policy entry must be an object." });
3350
+ return;
3351
+ }
3352
+ checkUnknownKeys2(policy, POLICY_KEYS2, path4, errors);
3353
+ if (!isQualifiedOrSafeName(policy.name))
3354
+ errors.push({ path: `${path4}.name`, code: "INVALID_POLICY_NAME", message: "policy name must be a safe identifier or qualified name." });
3355
+ else {
3356
+ if (names.has(String(policy.name)))
3357
+ errors.push({ path: `${path4}.name`, code: "DUPLICATE_POLICY_NAME", message: `Duplicate name: ${String(policy.name)}` });
3358
+ names.set(String(policy.name), policy);
3359
+ }
3360
+ if (!["approval", "settlement", "scope", "custom"].includes(String(policy.kind)))
3361
+ errors.push({ path: `${path4}.kind`, code: "INVALID_POLICY_KIND", message: "policy.kind must be approval, settlement, scope, or custom." });
3362
+ if (policy.mode !== void 0 && !["green", "yellow", "red", "manual", "block"].includes(String(policy.mode)))
3363
+ errors.push({ path: `${path4}.mode`, code: "INVALID_POLICY_MODE", message: "policy.mode must be green, yellow, red, manual, or block." });
3364
+ if (policy.rules !== void 0) {
3365
+ if (!Array.isArray(policy.rules)) {
3366
+ errors.push({ path: `${path4}.rules`, code: "POLICY_RULES_NOT_ARRAY", message: "policy.rules must be an array." });
3367
+ } else if (policy.kind === "approval") {
3368
+ policy.rules.forEach((rule, ruleIndex) => validateApprovalPolicyRuleShape(rule, `${path4}.rules[${ruleIndex}]`, errors));
3369
+ }
3370
+ }
3371
+ });
3372
+ return names;
3373
+ }
3374
+ function validateApprovalPolicyRuleShape(rule, path4, errors) {
3375
+ if (!isRecord3(rule)) {
3376
+ errors.push({ path: path4, code: "APPROVAL_POLICY_RULE_NOT_OBJECT", message: "approval policy rules must be objects." });
3377
+ return;
3378
+ }
3379
+ const keys = /* @__PURE__ */ new Set(["field", "max"]);
3380
+ checkUnknownKeys2(rule, keys, path4, errors);
3381
+ if (!isSafeIdentifier2(rule.field))
3382
+ errors.push({ path: `${path4}.field`, code: "INVALID_APPROVAL_POLICY_FIELD", message: "approval policy rule field must be a safe identifier." });
3383
+ if (!Number.isInteger(rule.max) || Number(rule.max) < 0)
3384
+ errors.push({ path: `${path4}.max`, code: "INVALID_APPROVAL_POLICY_MAX", message: "approval policy rule max must be a non-negative integer." });
3385
+ }
3386
+ function validateCapabilityApprovalPolicies(capabilities, policies, policyByName, errors) {
3387
+ const policyIndexByName = /* @__PURE__ */ new Map();
3388
+ policies.forEach((policy, index) => {
3389
+ if (isRecord3(policy) && typeof policy.name === "string")
3390
+ policyIndexByName.set(policy.name, index);
3391
+ });
3392
+ capabilities.forEach((capability, index) => {
3393
+ if (!isRecord3(capability) || !isRecord3(capability.proposal))
3394
+ return;
3395
+ const proposal = capability.proposal;
3396
+ const approval = proposal.approval;
3397
+ if (!isRecord3(approval))
3398
+ return;
3399
+ const path4 = `$.capabilities[${index}].proposal.approval`;
3400
+ const mode = approval.mode;
3401
+ const policyName = approval.policy;
3402
+ if (mode === "policy") {
3403
+ if (!isNonEmptyString2(approval.required_role)) {
3404
+ errors.push({ path: `${path4}.required_role`, code: "APPROVAL_POLICY_ROLE_REQUIRED", message: "approval.mode policy still requires required_role for human fallback." });
3405
+ }
3406
+ if (!isQualifiedOrSafeName(policyName)) {
3407
+ errors.push({ path: `${path4}.policy`, code: "APPROVAL_POLICY_REQUIRED", message: "approval.mode policy requires approval.policy." });
3408
+ return;
3409
+ }
3410
+ const policy = policyByName.get(policyName);
3411
+ if (!policy) {
3412
+ errors.push({ path: `${path4}.policy`, code: "UNKNOWN_APPROVAL_POLICY", message: `approval.policy must reference an existing approval policy: ${policyName}` });
3413
+ return;
3414
+ }
3415
+ if (policy.kind !== "approval") {
3416
+ errors.push({ path: `${path4}.policy`, code: "APPROVAL_POLICY_KIND_REQUIRED", message: "approval.policy must reference a policy with kind approval." });
3417
+ return;
3418
+ }
3419
+ validateApprovalPolicyRulesAgainstCapability(policy, policyIndexByName.get(policyName) ?? 0, capability, index, errors);
3420
+ } else if (policyName !== void 0) {
3421
+ errors.push({ path: `${path4}.policy`, code: "APPROVAL_POLICY_MODE_REQUIRED", message: "approval.policy can only be set when approval.mode is policy." });
3422
+ }
3423
+ });
3424
+ }
3425
+ function validateApprovalPolicyRulesAgainstCapability(policy, policyIndex, capability, capabilityIndex, errors) {
3426
+ if (!Array.isArray(policy.rules))
3253
3427
  return;
3254
- validateArrayRecords(value, "$.policies", POLICY_KEYS, "POLICY", errors);
3428
+ const proposal = capability.proposal;
3429
+ if (!proposal)
3430
+ return;
3431
+ policy.rules.forEach((rule, ruleIndex) => {
3432
+ if (!isRecord3(rule))
3433
+ return;
3434
+ const field = rule.field;
3435
+ const max = rule.max;
3436
+ const rulePath = `$.policies[${policyIndex}].rules[${ruleIndex}]`;
3437
+ if (!isSafeIdentifier2(field) || !Number.isInteger(max))
3438
+ return;
3439
+ if (!isNumericProposalField(proposal, capability.args, field)) {
3440
+ errors.push({
3441
+ path: `${rulePath}.field`,
3442
+ code: "APPROVAL_POLICY_FIELD_NOT_NUMERIC",
3443
+ message: `approval policy field ${field} must be numeric for capability ${capability.name}.`
3444
+ });
3445
+ }
3446
+ const bound = proposal.numeric_bounds?.[field];
3447
+ if (bound?.maximum !== void 0 && Number(max) > Number(bound.maximum)) {
3448
+ errors.push({
3449
+ path: `${rulePath}.max`,
3450
+ code: "APPROVAL_POLICY_MAX_EXCEEDS_BOUND",
3451
+ message: `approval policy max for ${field} must be <= numeric_bounds maximum on $.capabilities[${capabilityIndex}].proposal.numeric_bounds.${field}.maximum.`
3452
+ });
3453
+ }
3454
+ });
3455
+ }
3456
+ function isNumericProposalField(proposal, args, field) {
3457
+ if (proposal.numeric_bounds?.[field] !== void 0)
3458
+ return true;
3459
+ const patch = proposal.patch?.[field];
3460
+ if (!patch)
3461
+ return false;
3462
+ if (typeof patch.fixed === "number" && Number.isInteger(patch.fixed))
3463
+ return true;
3464
+ if (patch.from_arg && args[patch.from_arg]?.type === "number")
3465
+ return true;
3466
+ return false;
3255
3467
  }
3256
3468
  function validateEvidenceRecords(value, errors) {
3257
3469
  if (value === void 0)
@@ -3455,18 +3667,23 @@ function loadRuntimeConfigFromFile(configPath = process.env.SYNAPSOR_MCP_CONFIG
3455
3667
  function resolveRuntimeConfig(config, baseDir = process.cwd()) {
3456
3668
  if (!Array.isArray(config.contracts) || config.contracts.length === 0) return config;
3457
3669
  const seenCapabilities = /* @__PURE__ */ new Map();
3670
+ const seenPolicies = /* @__PURE__ */ new Map();
3458
3671
  for (const [index, capability] of (config.capabilities ?? []).entries()) {
3459
3672
  rememberCapabilityName(seenCapabilities, capability.name, `embedded capabilities[${index}]`);
3460
3673
  }
3674
+ for (const [index, policy] of (config.policies ?? []).entries()) {
3675
+ rememberPolicyName(seenPolicies, policy.name, `embedded policies[${index}]`);
3676
+ }
3461
3677
  const resolved = {
3462
3678
  ...config,
3463
3679
  contexts: { ...config.contexts ?? {} },
3464
- capabilities: [...config.capabilities ?? []]
3680
+ capabilities: [...config.capabilities ?? []],
3681
+ policies: [...config.policies ?? []]
3465
3682
  };
3466
3683
  for (const [contractIndex, contractPath] of config.contracts.entries()) {
3467
3684
  const fullPath = path.resolve(baseDir, contractPath);
3468
3685
  const contract = normalizeContract(JSON.parse(fs.readFileSync(fullPath, "utf8")));
3469
- mergeContractIntoRuntimeConfig(resolved, contract, `contracts[${contractIndex}] ${contractPath}`, seenCapabilities);
3686
+ mergeContractIntoRuntimeConfig(resolved, contract, `contracts[${contractIndex}] ${contractPath}`, seenCapabilities, seenPolicies);
3470
3687
  }
3471
3688
  delete resolved.contracts;
3472
3689
  return resolved;
@@ -3478,7 +3695,14 @@ function rememberCapabilityName(seen, name, origin) {
3478
3695
  }
3479
3696
  seen.set(name, origin);
3480
3697
  }
3481
- function mergeContractIntoRuntimeConfig(config, contract, origin, seenCapabilities) {
3698
+ function rememberPolicyName(seen, name, origin) {
3699
+ const previous = seen.get(name);
3700
+ if (previous) {
3701
+ throw new Error(`Duplicate policy ${name}: ${origin} conflicts with ${previous}. Policy names must be unique across embedded runner config and referenced contracts.`);
3702
+ }
3703
+ seen.set(name, origin);
3704
+ }
3705
+ function mergeContractIntoRuntimeConfig(config, contract, origin, seenCapabilities, seenPolicies) {
3482
3706
  const resources = new Map((contract.resources ?? []).map((resource) => [resource.name, resource]));
3483
3707
  for (const context of contract.contexts) {
3484
3708
  if (!config.contexts) config.contexts = {};
@@ -3493,6 +3717,13 @@ function mergeContractIntoRuntimeConfig(config, contract, origin, seenCapabiliti
3493
3717
  rememberCapabilityName(seenCapabilities, capability.name, `${origin} capabilities[${capabilityIndex}]`);
3494
3718
  config.capabilities.push(runtimeCapabilityFromSpec(capability, resources, config));
3495
3719
  }
3720
+ if (contract.policies?.length) {
3721
+ if (!config.policies) config.policies = [];
3722
+ for (const [policyIndex, policy] of contract.policies.entries()) {
3723
+ rememberPolicyName(seenPolicies, policy.name, `${origin} policies[${policyIndex}]`);
3724
+ config.policies.push(policy);
3725
+ }
3726
+ }
3496
3727
  }
3497
3728
  function runtimeContextFromSpec(context) {
3498
3729
  const tenantBinding = context.bindings.find((binding) => binding.name === context.tenant_binding) ?? context.bindings.find((binding) => binding.name === "tenant_id");
@@ -4285,6 +4516,7 @@ async function callConfiguredTool(input) {
4285
4516
  }
4286
4517
  if (capability.kind === "proposal" && input.config.mode === "review") {
4287
4518
  assertProposalWritebackResolvable(input.config, capability);
4519
+ assertApprovalPolicyResolvable(input.config, capability);
4288
4520
  }
4289
4521
  const source = input.config.sources?.[capability.source];
4290
4522
  if (!source) throw new McpRuntimeError("SOURCE_NOT_FOUND", `Unknown source: ${capability.source}`);
@@ -4380,6 +4612,13 @@ async function callConfiguredTool(input) {
4380
4612
  objectId
4381
4613
  });
4382
4614
  const proposal = input.store.createProposal(changeSet);
4615
+ const approvalResult = maybeAutoApproveProposal({
4616
+ config: input.config,
4617
+ capability,
4618
+ store: input.store,
4619
+ proposal,
4620
+ patch: changeSet.patch
4621
+ });
4383
4622
  input.store.recordEvidenceBundle({
4384
4623
  evidence_bundle_id: evidenceBundleId,
4385
4624
  proposal_id: proposal.proposal_id,
@@ -4388,7 +4627,7 @@ async function callConfiguredTool(input) {
4388
4627
  capability: capability.name,
4389
4628
  proposal_id: proposal.proposal_id,
4390
4629
  source_database_changed: false,
4391
- approval_status: changeSet.approval.status
4630
+ approval_status: approvalResult.proposal.state === "approved" ? "approved" : changeSet.approval.status
4392
4631
  },
4393
4632
  items: [
4394
4633
  {
@@ -4413,11 +4652,11 @@ async function callConfiguredTool(input) {
4413
4652
  }
4414
4653
  });
4415
4654
  return {
4416
- status: input.config.mode === "shadow" ? "shadow_proposal_created" : "review_required",
4655
+ status: input.config.mode === "shadow" ? "shadow_proposal_created" : approvalResult.proposal.state === "approved" ? "approved" : "review_required",
4417
4656
  action: capability.name,
4418
- proposal_id: proposal.proposal_id,
4419
- proposal_version: proposal.proposal_version,
4420
- proposal_hash: proposal.proposal_hash,
4657
+ proposal_id: approvalResult.proposal.proposal_id,
4658
+ proposal_version: approvalResult.proposal.proposal_version,
4659
+ proposal_hash: approvalResult.proposal.proposal_hash,
4421
4660
  target: {
4422
4661
  type: capability.target.table,
4423
4662
  id: objectId,
@@ -4428,7 +4667,12 @@ async function callConfiguredTool(input) {
4428
4667
  evidence_resource: `synapsor://evidence/${evidenceBundleId}`,
4429
4668
  proposal_resource: `synapsor://proposals/${proposal.proposal_id}`,
4430
4669
  replay_resource: `synapsor://replay/replay_${proposal.proposal_id}`,
4431
- approval_required: true,
4670
+ approval: approvalResult.approved ? { mode: "policy", policy: approvalResult.policy } : {
4671
+ mode: capability.approval?.mode ?? "human",
4672
+ ...capability.approval?.policy ? { policy: capability.approval.policy } : {},
4673
+ ...capability.approval?.required_role ? { required_role: capability.approval.required_role } : {}
4674
+ },
4675
+ approval_required: approvalResult.proposal.state === "pending_review",
4432
4676
  writeback: changeSet.writeback,
4433
4677
  source_database_changed: false,
4434
4678
  source_database_mutated: false
@@ -4454,6 +4698,9 @@ function resultEnvelopeFromLegacy(legacy, capability, canonicalName) {
4454
4698
  const proposalId = typeof legacy.proposal_id === "string" ? legacy.proposal_id : "wrp_unknown";
4455
4699
  const targetType = typeof target2?.type === "string" ? target2.type : capability?.target.table ?? "object";
4456
4700
  const targetId = target2?.id !== void 0 ? String(target2.id) : "unknown";
4701
+ const approval = isRecord4(legacy.approval) ? legacy.approval : void 0;
4702
+ const state = typeof legacy.status === "string" ? legacy.status : "review_required";
4703
+ const approvalRequired = legacy.approval_required !== false;
4457
4704
  const executor = writebackExecutorName(legacy.writeback);
4458
4705
  const writebackMode = executor === "read_only" || executor === "none" ? "proposal_only" : executor && executor !== "sql_update" && executor !== "trusted_worker_required" ? "app_handler" : "direct_update";
4459
4706
  return {
@@ -4464,15 +4711,16 @@ function resultEnvelopeFromLegacy(legacy, capability, canonicalName) {
4464
4711
  data: null,
4465
4712
  proposal: {
4466
4713
  id: proposalId,
4467
- state: typeof legacy.status === "string" ? legacy.status : "review_required",
4714
+ state,
4468
4715
  target: `${targetType}:${targetId}`,
4469
4716
  diff: isRecord4(legacy.diff) ? legacy.diff : {},
4470
- approval_required: legacy.approval_required !== false,
4717
+ approval_required: approvalRequired,
4718
+ ...approval ? { approval } : {},
4471
4719
  writeback: {
4472
4720
  mode: writebackMode,
4473
4721
  applied: false
4474
4722
  },
4475
- next: "A human must approve outside this model-facing tool surface; nothing is committed yet."
4723
+ next: approvalRequired ? "A human must approve outside this model-facing tool surface; nothing is committed yet." : "The proposal is approved outside the model-facing tool surface; trusted writeback/apply is still separate."
4476
4724
  },
4477
4725
  error: null,
4478
4726
  evidence: evidenceBundleId ? evidenceHandle(evidenceBundleId) : null,
@@ -4548,6 +4796,68 @@ function assertProposalWritebackResolvable(config, capability) {
4548
4796
  throw new McpRuntimeError("WRITEBACK_UNRESOLVED", `capability ${capability.name} declares HANDLER writeback but executor ${executorName} is not an app-owned handler.`);
4549
4797
  }
4550
4798
  }
4799
+ function assertApprovalPolicyResolvable(config, capability) {
4800
+ if (capability.kind !== "proposal" || capability.approval?.mode !== "policy") return;
4801
+ const policyName = capability.approval.policy;
4802
+ if (!policyName) {
4803
+ throw new McpRuntimeError("APPROVAL_POLICY_UNRESOLVED", `capability ${capability.name} uses policy approval but does not name approval.policy.`);
4804
+ }
4805
+ const policy = approvalPolicyByName(config, policyName);
4806
+ if (!policy) {
4807
+ throw new McpRuntimeError("APPROVAL_POLICY_UNRESOLVED", `capability ${capability.name} references missing approval policy ${policyName}.`);
4808
+ }
4809
+ }
4810
+ function maybeAutoApproveProposal(input) {
4811
+ if (input.config.mode !== "review") return { proposal: input.proposal, approved: false };
4812
+ if (input.config.approvals?.disable_auto_approval === true) return { proposal: input.proposal, approved: false };
4813
+ if (input.capability.approval?.mode !== "policy" || !input.capability.approval.policy) return { proposal: input.proposal, approved: false };
4814
+ if (input.proposal.state === "approved") return { proposal: input.proposal, approved: true, policy: input.capability.approval.policy };
4815
+ if (input.proposal.state !== "pending_review") return { proposal: input.proposal, approved: false };
4816
+ const policyName = input.capability.approval.policy;
4817
+ const policy = approvalPolicyByName(input.config, policyName);
4818
+ if (!policy) throw new McpRuntimeError("APPROVAL_POLICY_UNRESOLVED", `capability ${input.capability.name} references missing approval policy ${policyName}.`);
4819
+ const evaluation = evaluateApprovalPolicy(input.capability, policy, input.patch);
4820
+ if (!evaluation.qualifies) return { proposal: input.proposal, approved: false, policy: policyName };
4821
+ const updated = input.store.approveProposal(input.proposal.proposal_id, {
4822
+ approver: `policy:${policyName}`,
4823
+ proposal_hash: input.proposal.proposal_hash,
4824
+ proposal_version: input.proposal.proposal_version,
4825
+ reason: `auto-approved by policy ${policyName}: ${evaluation.reason}`
4826
+ });
4827
+ return { proposal: updated, approved: true, policy: policyName };
4828
+ }
4829
+ function approvalPolicyByName(config, policyName) {
4830
+ return (config.policies ?? []).find((policy) => policy.kind === "approval" && policy.name === policyName);
4831
+ }
4832
+ function evaluateApprovalPolicy(capability, policy, patch) {
4833
+ const ruleByField = /* @__PURE__ */ new Map();
4834
+ for (const rule of policy.rules ?? []) {
4835
+ const field = typeof rule.field === "string" ? rule.field : void 0;
4836
+ const max = typeof rule.max === "number" && Number.isInteger(rule.max) ? rule.max : void 0;
4837
+ if (field && max !== void 0) ruleByField.set(field, max);
4838
+ }
4839
+ const numericFields = Object.keys(patch).filter((field) => isNumericRuntimeProposalField(capability, field));
4840
+ if (numericFields.length === 0) return { qualifies: false, reason: "no numeric patch fields covered by policy" };
4841
+ const reasons = [];
4842
+ for (const field of numericFields) {
4843
+ const max = ruleByField.get(field);
4844
+ const value = patch[field];
4845
+ if (max === void 0) return { qualifies: false, reason: `${field} has no matching policy rule` };
4846
+ if (typeof value !== "number" || !Number.isInteger(value)) return { qualifies: false, reason: `${field} is not an integer` };
4847
+ if (value > max) return { qualifies: false, reason: `${field} ${value} > ${max}` };
4848
+ reasons.push(`${field} ${value} <= ${max}`);
4849
+ }
4850
+ return { qualifies: true, reason: reasons.join(", ") };
4851
+ }
4852
+ function isNumericRuntimeProposalField(capability, field) {
4853
+ const proposal = {
4854
+ action: capability.name,
4855
+ allowed_fields: capability.allowed_columns ?? Object.keys(capability.patch ?? {}),
4856
+ patch: capability.patch ?? {},
4857
+ ...capability.numeric_bounds ? { numeric_bounds: capability.numeric_bounds } : {}
4858
+ };
4859
+ return isNumericProposalField(proposal, capability.args, field);
4860
+ }
4551
4861
  function evidenceHandle(bundleId) {
4552
4862
  return {
4553
4863
  bundle_id: bundleId,
@@ -4653,6 +4963,8 @@ function buildChangeSet(input) {
4653
4963
  },
4654
4964
  approval: {
4655
4965
  status: "pending",
4966
+ ...input.capability.approval?.mode ? { mode: input.capability.approval.mode } : {},
4967
+ ...input.capability.approval?.policy ? { policy: input.capability.approval.policy } : {},
4656
4968
  required_role: input.capability.approval?.required_role
4657
4969
  },
4658
4970
  writeback: {
@@ -6693,6 +7005,7 @@ function compileAgentDslWithWarnings(source) {
6693
7005
  ...context.tenantBinding ? { tenant_binding: context.tenantBinding } : {},
6694
7006
  ...context.principalBinding ? { principal_binding: context.principalBinding } : {}
6695
7007
  }));
7008
+ const policies = [];
6696
7009
  const capabilities = ast.capabilities.map((capability) => {
6697
7010
  const spec = {
6698
7011
  name: capability.name,
@@ -6716,6 +7029,15 @@ function compileAgentDslWithWarnings(source) {
6716
7029
  ...capability.maxRows ? { max_rows: capability.maxRows } : {}
6717
7030
  };
6718
7031
  if (capability.kind === "proposal" && capability.proposal) {
7032
+ const autoApprovalPolicyName = capability.proposal.autoApprovalRules?.length ? autoApprovalPolicyNameForCapability(capability.name) : void 0;
7033
+ if (autoApprovalPolicyName) {
7034
+ policies.push({
7035
+ name: autoApprovalPolicyName,
7036
+ kind: "approval",
7037
+ mode: "green",
7038
+ rules: capability.proposal.autoApprovalRules?.map((rule) => ({ field: rule.field, max: rule.max }))
7039
+ });
7040
+ }
6719
7041
  spec.proposal = {
6720
7042
  action: capability.proposal.action,
6721
7043
  allowed_fields: capability.proposal.allowedFields,
@@ -6723,7 +7045,7 @@ function compileAgentDslWithWarnings(source) {
6723
7045
  ...capability.proposal.numericBounds ? { numeric_bounds: capability.proposal.numericBounds } : {},
6724
7046
  ...capability.proposal.transitionGuards ? { transition_guards: capability.proposal.transitionGuards } : {},
6725
7047
  conflict_guard: capability.conflictKey ? { column: capability.conflictKey } : { weak_guard_ack: true },
6726
- approval: { mode: "human", required_role: capability.proposal.approvalRole ?? "local_reviewer" },
7048
+ approval: autoApprovalPolicyName ? { mode: "policy", required_role: capability.proposal.approvalRole ?? "local_reviewer", policy: autoApprovalPolicyName } : { mode: "human", required_role: capability.proposal.approvalRole ?? "local_reviewer" },
6727
7049
  writeback: {
6728
7050
  mode: capability.proposal.writebackMode ?? "direct_sql",
6729
7051
  ...capability.proposal.executor ? { executor: capability.proposal.executor } : {}
@@ -6745,7 +7067,8 @@ function compileAgentDslWithWarnings(source) {
6745
7067
  kind: "SynapsorContract",
6746
7068
  contexts,
6747
7069
  capabilities,
6748
- ...workflows.length ? { workflows } : {}
7070
+ ...workflows.length ? { workflows } : {},
7071
+ ...policies.length ? { policies } : {}
6749
7072
  };
6750
7073
  assertValidContract(contract);
6751
7074
  return { contract: normalizeContract(contract), warnings: collectDslWarnings(ast) };
@@ -6972,6 +7295,12 @@ function parseCapabilityBlock(block) {
6972
7295
  capability.proposal.approvalRole = approval[1];
6973
7296
  continue;
6974
7297
  }
7298
+ const autoApproval = item.text.match(/^AUTO\s+APPROVE\s+WHEN\s+(.+)$/i);
7299
+ if (autoApproval?.[1]) {
7300
+ ensureProposal(capability, item);
7301
+ parseAutoApprovalClause(capability, autoApproval[1], item.line);
7302
+ continue;
7303
+ }
6975
7304
  const writeback2 = item.text.match(/^WRITEBACK\s+(DIRECT\s+SQL|APP\s+HANDLER|CLOUD\s+WORKER|NONE)(?:\s+EXECUTOR\s+([A-Za-z_][A-Za-z0-9_.-]*))?$/i);
6976
7305
  if (writeback2?.[1]) {
6977
7306
  ensureProposal(capability, item);
@@ -7032,6 +7361,52 @@ function collectDslWarnings(ast) {
7032
7361
  }
7033
7362
  return warnings;
7034
7363
  }
7364
+ function parseAutoApprovalClause(capability, rawCondition, line) {
7365
+ if (!capability.proposal.approvalRole) {
7366
+ throw dslError(line, 1, "AUTO_APPROVE_APPROVAL_ROLE_REQUIRED", "AUTO APPROVE WHEN requires an explicit APPROVAL ROLE before it");
7367
+ }
7368
+ const condition = rawCondition.trim();
7369
+ const match = condition.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*(<=)\s*(.+)$/);
7370
+ if (!match?.[1] || !match[3]) {
7371
+ throw dslError(line, 1, "AUTO_APPROVE_UNSUPPORTED", "AUTO APPROVE WHEN supports only: <field> <= <integer>");
7372
+ }
7373
+ const field = match[1];
7374
+ const rawMax = match[3].trim();
7375
+ if (!/^\d+$/.test(rawMax)) {
7376
+ throw dslError(line, 1, "AUTO_APPROVE_MAX_INVALID", "AUTO APPROVE WHEN max must be a non-negative integer");
7377
+ }
7378
+ const max = Number(rawMax);
7379
+ if (!Number.isSafeInteger(max)) {
7380
+ throw dslError(line, 1, "AUTO_APPROVE_MAX_INVALID", "AUTO APPROVE WHEN max must be a safe non-negative integer");
7381
+ }
7382
+ if (!Object.prototype.hasOwnProperty.call(capability.proposal.patch, field)) {
7383
+ throw dslError(line, 1, "AUTO_APPROVE_FIELD_NOT_PATCHED", `AUTO APPROVE WHEN field ${field} must be in the PATCH list`);
7384
+ }
7385
+ if (!isNumericDslProposalField(capability, field)) {
7386
+ throw dslError(line, 1, "AUTO_APPROVE_FIELD_NOT_NUMERIC", `AUTO APPROVE WHEN field ${field} must be numeric by BOUND, NUMBER arg patch, or integer literal patch`);
7387
+ }
7388
+ const boundMax = capability.proposal.numericBounds?.[field]?.maximum;
7389
+ if (boundMax !== void 0 && max > boundMax) {
7390
+ throw dslError(line, 1, "AUTO_APPROVE_MAX_EXCEEDS_BOUND", `AUTO APPROVE WHEN max for ${field} must be <= BOUND maximum ${boundMax}`);
7391
+ }
7392
+ capability.proposal.autoApprovalRules ??= [];
7393
+ if (capability.proposal.autoApprovalRules.some((rule) => rule.field === field)) {
7394
+ throw dslError(line, 1, "AUTO_APPROVE_DUPLICATE_FIELD", `AUTO APPROVE WHEN already defines a rule for ${field}`);
7395
+ }
7396
+ capability.proposal.autoApprovalRules.push({ field, max, line });
7397
+ }
7398
+ function isNumericDslProposalField(capability, field) {
7399
+ if (capability.proposal.numericBounds?.[field] !== void 0)
7400
+ return true;
7401
+ const patch = capability.proposal.patch[field];
7402
+ if (!patch)
7403
+ return false;
7404
+ if (typeof patch.fixed === "number" && Number.isInteger(patch.fixed))
7405
+ return true;
7406
+ if (patch.from_arg && capability.args[patch.from_arg]?.type === "number")
7407
+ return true;
7408
+ return false;
7409
+ }
7035
7410
  function parseWorkflowBlock(block) {
7036
7411
  const workflow = { name: block.name, context: "", allowedCapabilities: [] };
7037
7412
  for (const item of block.body) {
@@ -7191,6 +7566,9 @@ function normalizeWritebackMode(mode) {
7191
7566
  return "cloud_worker";
7192
7567
  return "none";
7193
7568
  }
7569
+ function autoApprovalPolicyNameForCapability(name) {
7570
+ return `${name.replace(/[^A-Za-z0-9_]/g, "_")}_auto_approval`;
7571
+ }
7194
7572
  function parseList(value) {
7195
7573
  return value.split(",").map((item) => item.trim()).filter(Boolean);
7196
7574
  }
@@ -9894,6 +10272,32 @@ function proposalWritebackResolutionDoctorCheck(config, capability) {
9894
10272
  };
9895
10273
  }
9896
10274
  }
10275
+ function proposalApprovalPolicyResolutionDoctorCheck(config, capability) {
10276
+ if (capability.approval?.mode !== "policy") {
10277
+ return {
10278
+ name: `capability:${capability.name}:approval-policy-resolution`,
10279
+ ok: true,
10280
+ level: "pass",
10281
+ message: "Capability does not use policy auto-approval."
10282
+ };
10283
+ }
10284
+ try {
10285
+ assertApprovalPolicyResolvable(config, capability);
10286
+ return {
10287
+ name: `capability:${capability.name}:approval-policy-resolution`,
10288
+ ok: true,
10289
+ level: "pass",
10290
+ message: `Approval policy ${capability.approval.policy} resolves from the reviewed contract/config.`
10291
+ };
10292
+ } catch (error) {
10293
+ return {
10294
+ name: `capability:${capability.name}:approval-policy-resolution`,
10295
+ ok: false,
10296
+ level: "fail",
10297
+ message: error instanceof Error ? error.message : String(error)
10298
+ };
10299
+ }
10300
+ }
9897
10301
  async function httpHandlerReachabilityCheck(executorName, rawUrl, timeoutMs) {
9898
10302
  try {
9899
10303
  const url = new URL(rawUrl);
@@ -10401,6 +10805,7 @@ async function localDoctor(args) {
10401
10805
  if (parsed.mode === "review") {
10402
10806
  for (const capability of (parsed.capabilities ?? []).filter((item) => item.kind === "proposal")) {
10403
10807
  checks.push(proposalWritebackResolutionDoctorCheck(parsed, capability));
10808
+ checks.push(proposalApprovalPolicyResolutionDoctorCheck(parsed, capability));
10404
10809
  }
10405
10810
  }
10406
10811
  for (const [sourceName, source] of Object.entries(sources)) {
@@ -11581,6 +11986,8 @@ async function cloudPush(args) {
11581
11986
  process2.stdout.write(`Workflows: ${payload.summary.workflows}
11582
11987
  `);
11583
11988
  process2.stdout.write(`Proposal capabilities: ${payload.summary.proposal_capabilities}
11989
+ `);
11990
+ process2.stdout.write(`Approval policies: ${payload.summary.approval_policies}
11584
11991
  `);
11585
11992
  process2.stdout.write(`Kept-out fields: ${payload.summary.kept_out_fields}
11586
11993
  `);
@@ -11628,12 +12035,17 @@ async function cloudPush(args) {
11628
12035
  return 0;
11629
12036
  }
11630
12037
  function contractSummary(contract) {
12038
+ const keptOutFields = /* @__PURE__ */ new Set();
12039
+ for (const capability of contract.capabilities) {
12040
+ for (const field of capability.kept_out_fields ?? []) keptOutFields.add(field);
12041
+ }
11631
12042
  return {
11632
12043
  contexts: contract.contexts.length,
11633
12044
  capabilities: contract.capabilities.length,
11634
12045
  workflows: contract.workflows?.length ?? 0,
11635
12046
  proposal_capabilities: contract.capabilities.filter((capability) => capability.kind === "proposal").length,
11636
- kept_out_fields: contract.capabilities.reduce((count, capability) => count + (capability.kept_out_fields?.length ?? 0), 0)
12047
+ approval_policies: contract.policies?.filter((policy) => policy.kind === "approval").length ?? 0,
12048
+ kept_out_fields: keptOutFields.size
11637
12049
  };
11638
12050
  }
11639
12051
  async function postCloudContractPush(input) {
@@ -12802,8 +13214,11 @@ function formatProposeResult(capabilityName, result, storePath) {
12802
13214
  const proposalId = String(result.proposal_id ?? "");
12803
13215
  const evidenceId = String(result.evidence_bundle_id ?? "");
12804
13216
  const sourceChanged = result.source_database_changed === true || result.source_database_mutated === true;
13217
+ const status = String(result.status ?? "review_required");
13218
+ const approval = isRecord7(result.approval) ? result.approval : void 0;
13219
+ const autoApproved = status === "approved" && approval?.mode === "policy";
12805
13220
  const lines = [
12806
- "Proposal created.",
13221
+ autoApproved ? "Proposal created and policy-approved." : "Proposal created.",
12807
13222
  "",
12808
13223
  "Capability:",
12809
13224
  capabilityName,
@@ -12817,9 +13232,12 @@ function formatProposeResult(capabilityName, result, storePath) {
12817
13232
  "Source DB changed:",
12818
13233
  sourceChanged ? "yes" : "no",
12819
13234
  "",
13235
+ "Approval:",
13236
+ autoApproved ? `approved by policy ${String(approval?.policy ?? "")}` : "required outside MCP",
13237
+ "",
12820
13238
  "Review:",
12821
13239
  `${cliCommandName()} proposals show ${proposalId || "latest"} --store ${storePath}`,
12822
- `${cliCommandName()} proposals approve ${proposalId || "latest"} --store ${storePath}`,
13240
+ ...autoApproved ? [] : [`${cliCommandName()} proposals approve ${proposalId || "latest"} --store ${storePath}`],
12823
13241
  `${cliCommandName()} apply ${proposalId || "latest"} --store ${storePath}`,
12824
13242
  `${cliCommandName()} replay ${proposalId || "latest"} --store ${storePath}`,
12825
13243
  ""
@@ -13037,6 +13455,7 @@ async function toolsPreview(args) {
13037
13455
  config_path: boundary.configPath,
13038
13456
  store_path: boundary.storePath,
13039
13457
  alias_mode: boundary.aliasMode,
13458
+ auto_approval: boundary.autoApprovalDisabled ? "disabled" : "enabled",
13040
13459
  exposed_to_mcp: boundary.names,
13041
13460
  alias_mappings: boundary.exposures,
13042
13461
  not_exposed_to_mcp: defaultBlockedToolSurface(),
@@ -13065,6 +13484,7 @@ Run ${cliCommandName()} onboard db --from-env DATABASE_URL, or pass --config <pa
13065
13484
  const runtime = createMcpRuntime(parsed, { storePath });
13066
13485
  try {
13067
13486
  const tools2 = runtime.listTools();
13487
+ const autoApprovalDisabled = parsed.approvals?.disable_auto_approval === true;
13068
13488
  const exposures = toolNameExposures(tools2.map((tool) => tool.name), aliasMode);
13069
13489
  const names = exposures.map((item) => item.exposedName);
13070
13490
  const serialized = JSON.stringify(tools2);
@@ -13077,7 +13497,7 @@ Run ${cliCommandName()} onboard db --from-env DATABASE_URL, or pass --config <pa
13077
13497
  { name: "write credentials absent", ok: !/(password|secret|bearer|private[_-]?key|token)/i.test(serialized), detail: "MCP tools do not include write credentials" }
13078
13498
  ];
13079
13499
  const ok = checks.every((check) => check.ok);
13080
- return { ok, configPath, storePath, aliasMode, names, exposures, checks };
13500
+ return { ok, configPath, storePath, aliasMode, names, exposures, autoApprovalDisabled, checks };
13081
13501
  } finally {
13082
13502
  runtime.close();
13083
13503
  }
@@ -13100,6 +13520,7 @@ function formatToolsPreview(input) {
13100
13520
  `Config: ${input.configPath}`,
13101
13521
  `Store: ${input.storePath}`,
13102
13522
  `Alias mode: ${input.aliasMode}`,
13523
+ `auto-approval: ${input.autoApprovalDisabled ? "disabled" : "enabled"}`,
13103
13524
  "",
13104
13525
  "Exposed to MCP:",
13105
13526
  ...exposedLines,
@@ -15894,11 +16315,14 @@ function formatReceiptId(receiptId) {
15894
16315
  return `rct_${String(receiptId).padStart(6, "0")}`;
15895
16316
  }
15896
16317
  function approvalBoundary(proposal) {
16318
+ const approval = proposal.change_set.approval;
16319
+ const policy = typeof approval.policy === "string" ? approval.policy : void 0;
16320
+ const policyActor = policy ? `policy:${policy}` : void 0;
15897
16321
  if (proposal.state === "pending_review") return "required outside MCP";
15898
- if (proposal.state === "approved" || proposal.state === "pending_worker") return "approved outside MCP; waiting for trusted worker";
15899
- if (proposal.state === "applied") return "approved outside MCP; writeback applied";
15900
- if (proposal.state === "conflict") return "approved outside MCP; writeback blocked by conflict guard";
15901
- if (proposal.state === "failed") return "approved outside MCP; writeback failed safely";
16322
+ if (proposal.state === "approved" || proposal.state === "pending_worker") return `${policyActor ? `approved by ${policyActor}` : "approved outside MCP"}; waiting for trusted worker`;
16323
+ if (proposal.state === "applied") return `${policyActor ? `approved by ${policyActor}` : "approved outside MCP"}; writeback applied`;
16324
+ if (proposal.state === "conflict") return `${policyActor ? `approved by ${policyActor}` : "approved outside MCP"}; writeback blocked by conflict guard`;
16325
+ if (proposal.state === "failed") return `${policyActor ? `approved by ${policyActor}` : "approved outside MCP"}; writeback failed safely`;
15902
16326
  if (proposal.state === "rejected") return "rejected outside MCP";
15903
16327
  return humanStatus(proposal.state);
15904
16328
  }