@synapsor/runner 0.1.8 → 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))
3427
+ return;
3428
+ const proposal = capability.proposal;
3429
+ if (!proposal)
3253
3430
  return;
3254
- validateArrayRecords(value, "$.policies", POLICY_KEYS, "POLICY", errors);
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
  }
@@ -8379,6 +8757,11 @@ async function main(argv) {
8379
8757
  usage([]);
8380
8758
  return 0;
8381
8759
  }
8760
+ if (command === "--version" || command === "-v" || command === "version") {
8761
+ process2.stdout.write(`${await runnerPackageVersion()}
8762
+ `);
8763
+ return 0;
8764
+ }
8382
8765
  if (!isKnownTopLevelCommand(command)) {
8383
8766
  process2.stderr.write(`Unknown command: ${cliCommandName()} ${command}
8384
8767
 
@@ -9889,6 +10272,32 @@ function proposalWritebackResolutionDoctorCheck(config, capability) {
9889
10272
  };
9890
10273
  }
9891
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
+ }
9892
10301
  async function httpHandlerReachabilityCheck(executorName, rawUrl, timeoutMs) {
9893
10302
  try {
9894
10303
  const url = new URL(rawUrl);
@@ -10396,6 +10805,7 @@ async function localDoctor(args) {
10396
10805
  if (parsed.mode === "review") {
10397
10806
  for (const capability of (parsed.capabilities ?? []).filter((item) => item.kind === "proposal")) {
10398
10807
  checks.push(proposalWritebackResolutionDoctorCheck(parsed, capability));
10808
+ checks.push(proposalApprovalPolicyResolutionDoctorCheck(parsed, capability));
10399
10809
  }
10400
10810
  }
10401
10811
  for (const [sourceName, source] of Object.entries(sources)) {
@@ -11576,6 +11986,8 @@ async function cloudPush(args) {
11576
11986
  process2.stdout.write(`Workflows: ${payload.summary.workflows}
11577
11987
  `);
11578
11988
  process2.stdout.write(`Proposal capabilities: ${payload.summary.proposal_capabilities}
11989
+ `);
11990
+ process2.stdout.write(`Approval policies: ${payload.summary.approval_policies}
11579
11991
  `);
11580
11992
  process2.stdout.write(`Kept-out fields: ${payload.summary.kept_out_fields}
11581
11993
  `);
@@ -11623,12 +12035,17 @@ async function cloudPush(args) {
11623
12035
  return 0;
11624
12036
  }
11625
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
+ }
11626
12042
  return {
11627
12043
  contexts: contract.contexts.length,
11628
12044
  capabilities: contract.capabilities.length,
11629
12045
  workflows: contract.workflows?.length ?? 0,
11630
12046
  proposal_capabilities: contract.capabilities.filter((capability) => capability.kind === "proposal").length,
11631
- 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
11632
12049
  };
11633
12050
  }
11634
12051
  async function postCloudContractPush(input) {
@@ -12797,8 +13214,11 @@ function formatProposeResult(capabilityName, result, storePath) {
12797
13214
  const proposalId = String(result.proposal_id ?? "");
12798
13215
  const evidenceId = String(result.evidence_bundle_id ?? "");
12799
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";
12800
13220
  const lines = [
12801
- "Proposal created.",
13221
+ autoApproved ? "Proposal created and policy-approved." : "Proposal created.",
12802
13222
  "",
12803
13223
  "Capability:",
12804
13224
  capabilityName,
@@ -12812,9 +13232,12 @@ function formatProposeResult(capabilityName, result, storePath) {
12812
13232
  "Source DB changed:",
12813
13233
  sourceChanged ? "yes" : "no",
12814
13234
  "",
13235
+ "Approval:",
13236
+ autoApproved ? `approved by policy ${String(approval?.policy ?? "")}` : "required outside MCP",
13237
+ "",
12815
13238
  "Review:",
12816
13239
  `${cliCommandName()} proposals show ${proposalId || "latest"} --store ${storePath}`,
12817
- `${cliCommandName()} proposals approve ${proposalId || "latest"} --store ${storePath}`,
13240
+ ...autoApproved ? [] : [`${cliCommandName()} proposals approve ${proposalId || "latest"} --store ${storePath}`],
12818
13241
  `${cliCommandName()} apply ${proposalId || "latest"} --store ${storePath}`,
12819
13242
  `${cliCommandName()} replay ${proposalId || "latest"} --store ${storePath}`,
12820
13243
  ""
@@ -13032,6 +13455,7 @@ async function toolsPreview(args) {
13032
13455
  config_path: boundary.configPath,
13033
13456
  store_path: boundary.storePath,
13034
13457
  alias_mode: boundary.aliasMode,
13458
+ auto_approval: boundary.autoApprovalDisabled ? "disabled" : "enabled",
13035
13459
  exposed_to_mcp: boundary.names,
13036
13460
  alias_mappings: boundary.exposures,
13037
13461
  not_exposed_to_mcp: defaultBlockedToolSurface(),
@@ -13060,6 +13484,7 @@ Run ${cliCommandName()} onboard db --from-env DATABASE_URL, or pass --config <pa
13060
13484
  const runtime = createMcpRuntime(parsed, { storePath });
13061
13485
  try {
13062
13486
  const tools2 = runtime.listTools();
13487
+ const autoApprovalDisabled = parsed.approvals?.disable_auto_approval === true;
13063
13488
  const exposures = toolNameExposures(tools2.map((tool) => tool.name), aliasMode);
13064
13489
  const names = exposures.map((item) => item.exposedName);
13065
13490
  const serialized = JSON.stringify(tools2);
@@ -13072,7 +13497,7 @@ Run ${cliCommandName()} onboard db --from-env DATABASE_URL, or pass --config <pa
13072
13497
  { name: "write credentials absent", ok: !/(password|secret|bearer|private[_-]?key|token)/i.test(serialized), detail: "MCP tools do not include write credentials" }
13073
13498
  ];
13074
13499
  const ok = checks.every((check) => check.ok);
13075
- return { ok, configPath, storePath, aliasMode, names, exposures, checks };
13500
+ return { ok, configPath, storePath, aliasMode, names, exposures, autoApprovalDisabled, checks };
13076
13501
  } finally {
13077
13502
  runtime.close();
13078
13503
  }
@@ -13095,6 +13520,7 @@ function formatToolsPreview(input) {
13095
13520
  `Config: ${input.configPath}`,
13096
13521
  `Store: ${input.storePath}`,
13097
13522
  `Alias mode: ${input.aliasMode}`,
13523
+ `auto-approval: ${input.autoApprovalDisabled ? "disabled" : "enabled"}`,
13098
13524
  "",
13099
13525
  "Exposed to MCP:",
13100
13526
  ...exposedLines,
@@ -15889,11 +16315,14 @@ function formatReceiptId(receiptId) {
15889
16315
  return `rct_${String(receiptId).padStart(6, "0")}`;
15890
16316
  }
15891
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;
15892
16321
  if (proposal.state === "pending_review") return "required outside MCP";
15893
- if (proposal.state === "approved" || proposal.state === "pending_worker") return "approved outside MCP; waiting for trusted worker";
15894
- if (proposal.state === "applied") return "approved outside MCP; writeback applied";
15895
- if (proposal.state === "conflict") return "approved outside MCP; writeback blocked by conflict guard";
15896
- 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`;
15897
16326
  if (proposal.state === "rejected") return "rejected outside MCP";
15898
16327
  return humanStatus(proposal.state);
15899
16328
  }
@@ -16105,6 +16534,16 @@ function cliCommandName() {
16105
16534
  if (process2.env.SYNAPSOR_RUNNER_COMMAND_NAME) return process2.env.SYNAPSOR_RUNNER_COMMAND_NAME;
16106
16535
  return "synapsor-runner";
16107
16536
  }
16537
+ async function runnerPackageVersion() {
16538
+ if (process2.env.npm_package_version) return process2.env.npm_package_version;
16539
+ const packageUrl = new URL("../package.json", import.meta.url);
16540
+ try {
16541
+ const parsed = JSON.parse(await fs3.readFile(packageUrl, "utf8"));
16542
+ if (typeof parsed.version === "string" && parsed.version.trim()) return parsed.version.trim();
16543
+ } catch {
16544
+ }
16545
+ return "unknown";
16546
+ }
16108
16547
  function usage(args = []) {
16109
16548
  const [command, subcommand] = args;
16110
16549
  const key = (command === "mcp" || command === "handler") && subcommand ? `${command} ${subcommand}` : command ?? "";