@synapsor/runner 0.1.1 → 0.1.2

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,7 +444,7 @@ 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", "cloud", "strict", "result_format"]);
447
+ var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["version", "mode", "storage", "sources", "trusted_context", "contexts", "executors", "capabilities", "contracts", "cloud", "strict", "result_format"]);
448
448
  var STORAGE_KEYS = /* @__PURE__ */ new Set(["sqlite_path"]);
449
449
  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
450
  var SOURCE_KEYS = /* @__PURE__ */ new Set([
@@ -559,16 +559,33 @@ function validateRunnerCapabilityConfig(input) {
559
559
  errors.push({ path: "$.mode", code: "INVALID_MODE", message: "mode must be read_only, shadow, review, or cloud." });
560
560
  }
561
561
  validateStorage(input.storage, strict, errors);
562
+ const hasContracts = validateContracts(input.contracts, errors);
562
563
  validateCloud(input.cloud, input.mode, strict, errors);
563
564
  validateSources(input.sources, input.mode, strict, errors, warnings);
564
565
  validateContexts(input.contexts, strict, errors, warnings);
565
- validateTrustedContext(input.trusted_context, input.contexts, input.capabilities, input.mode, strict, errors, warnings);
566
+ validateTrustedContext(input.trusted_context, input.contexts, input.capabilities, input.mode, strict, errors, warnings, hasContracts);
566
567
  validateExecutors(input.executors, input.mode, strict, errors);
567
- validateCapabilities(input.capabilities, input.sources, input.contexts, input.executors, input.mode, strict, errors, warnings);
568
+ validateCapabilities(input.capabilities, input.sources, input.contexts, input.executors, input.mode, strict, errors, warnings, hasContracts);
568
569
  validateWritebackReadiness(input.sources, input.capabilities, input.mode, errors, warnings);
569
570
  scanForForbiddenFields(input, "$", errors);
570
571
  return { ok: errors.length === 0, errors, warnings };
571
572
  }
573
+ function validateContracts(value, errors) {
574
+ if (value === void 0) return false;
575
+ if (!Array.isArray(value) || value.length === 0) {
576
+ errors.push({ path: "$.contracts", code: "INVALID_CONTRACT_REFERENCES", message: "contracts must be a non-empty array of contract file paths." });
577
+ return false;
578
+ }
579
+ value.forEach((contractPath, index) => {
580
+ if (!isNonEmptyString(contractPath)) {
581
+ errors.push({ path: `$.contracts[${index}]`, code: "INVALID_CONTRACT_REFERENCE", message: "contract references must be non-empty file paths." });
582
+ }
583
+ if (typeof contractPath === "string" && /(postgres(?:ql)?:\/\/|mysql:\/\/|password|secret|token)/i.test(contractPath)) {
584
+ errors.push({ path: `$.contracts[${index}]`, code: "CONTRACT_REFERENCE_LOOKS_SECRET", message: "contracts must reference local contract files, not URLs or secrets." });
585
+ }
586
+ });
587
+ return errors.every((error) => !error.path.startsWith("$.contracts"));
588
+ }
572
589
  function assertValidRunnerCapabilityConfig(input) {
573
590
  const result = validateRunnerCapabilityConfig(input);
574
591
  if (!result.ok) {
@@ -765,8 +782,9 @@ function validateContexts(value, strict, errors, warnings) {
765
782
  validateContextObject(context, path4, strict, errors, warnings);
766
783
  }
767
784
  }
768
- function validateTrustedContext(value, contexts, capabilities, mode, strict, errors, warnings) {
785
+ function validateTrustedContext(value, contexts, capabilities, mode, strict, errors, warnings, hasContracts = false) {
769
786
  if (value === void 0) {
787
+ if (hasContracts) return;
770
788
  if (mode === "cloud") return;
771
789
  const allCapabilitiesHaveContext = Array.isArray(capabilities) && capabilities.length > 0 && capabilities.every((capability) => isRecord(capability) && isNonEmptyString(capability.context));
772
790
  if (isRecord(contexts) && allCapabilitiesHaveContext) return;
@@ -861,8 +879,9 @@ function validateExecutorAuth(value, path4, strict, errors) {
861
879
  errors.push({ path: `${path4}.token_env`, code: "EXECUTOR_TOKEN_ENV_REQUIRED", message: "bearer_env auth requires token_env." });
862
880
  }
863
881
  }
864
- function validateCapabilities(value, sources, contexts, executors, mode, strict, errors, warnings) {
882
+ function validateCapabilities(value, sources, contexts, executors, mode, strict, errors, warnings, hasContracts = false) {
865
883
  if (mode === "cloud" && value === void 0) return;
884
+ if (hasContracts && value === void 0) return;
866
885
  if (!Array.isArray(value) || value.length === 0) {
867
886
  errors.push({ path: "$.capabilities", code: "CAPABILITIES_REQUIRED", message: "At least one capability is required." });
868
887
  return;
@@ -2693,6 +2712,557 @@ function isRecord2(value) {
2693
2712
  return typeof value === "object" && value !== null && !Array.isArray(value);
2694
2713
  }
2695
2714
 
2715
+ // packages/spec/dist/version.js
2716
+ var SPEC_VERSION = "0.1";
2717
+
2718
+ // packages/spec/dist/errors.js
2719
+ var SynapsorSpecValidationError = class extends Error {
2720
+ issues;
2721
+ constructor(issues) {
2722
+ super(`Invalid Synapsor contract:
2723
+ ${issues.map((issue) => `${issue.path} ${issue.code}: ${issue.message}`).join("\n")}`);
2724
+ this.issues = issues;
2725
+ this.name = "SynapsorSpecValidationError";
2726
+ }
2727
+ };
2728
+
2729
+ // packages/spec/dist/validate.js
2730
+ var TOP_LEVEL_KEYS2 = /* @__PURE__ */ new Set([
2731
+ "spec_version",
2732
+ "kind",
2733
+ "metadata",
2734
+ "resources",
2735
+ "contexts",
2736
+ "capabilities",
2737
+ "workflows",
2738
+ "policies",
2739
+ "evidence",
2740
+ "proposals",
2741
+ "receipts",
2742
+ "replay",
2743
+ "external_actions"
2744
+ ]);
2745
+ var METADATA_KEYS = /* @__PURE__ */ new Set(["name", "description", "version", "tags"]);
2746
+ var RESOURCE_KEYS = /* @__PURE__ */ new Set(["name", "engine", "schema", "table", "type", "primary_key", "tenant_key", "conflict_key", "single_tenant_dev"]);
2747
+ var CONTEXT_KEYS2 = /* @__PURE__ */ new Set(["name", "description", "bindings", "tenant_binding", "principal_binding"]);
2748
+ var BINDING_KEYS = /* @__PURE__ */ new Set(["name", "source", "key", "required"]);
2749
+ var CAPABILITY_KEYS2 = /* @__PURE__ */ new Set(["name", "description", "kind", "context", "source", "subject", "args", "lookup", "visible_fields", "kept_out_fields", "evidence", "max_rows", "proposal"]);
2750
+ var SUBJECT_KEYS = /* @__PURE__ */ new Set(["resource", "schema", "table", "primary_key", "tenant_key", "conflict_key", "single_tenant_dev"]);
2751
+ var ARG_KEYS2 = /* @__PURE__ */ new Set(["type", "description", "required", "max_length", "minimum", "maximum", "enum"]);
2752
+ var LOOKUP_KEYS2 = /* @__PURE__ */ new Set(["id_from_arg"]);
2753
+ var EVIDENCE_KEYS = /* @__PURE__ */ new Set(["required", "sources", "query_audit", "handle_prefix"]);
2754
+ var PROPOSAL_KEYS = /* @__PURE__ */ new Set(["action", "allowed_fields", "patch", "conflict_guard", "approval", "writeback"]);
2755
+ var PATCH_KEYS = /* @__PURE__ */ new Set(["fixed", "from_arg"]);
2756
+ var CONFLICT_GUARD_KEYS2 = /* @__PURE__ */ new Set(["column", "weak_guard_ack"]);
2757
+ var APPROVAL_KEYS2 = /* @__PURE__ */ new Set(["mode", "required_role"]);
2758
+ var WRITEBACK_KEYS = /* @__PURE__ */ new Set(["mode", "executor", "idempotency_key"]);
2759
+ var WORKFLOW_KEYS = /* @__PURE__ */ new Set(["name", "description", "context", "allowed_capabilities", "required_evidence", "approval", "settlement", "replay"]);
2760
+ var POLICY_KEYS = /* @__PURE__ */ new Set(["name", "kind", "mode", "rules"]);
2761
+ var EVIDENCE_RECORD_KEYS = /* @__PURE__ */ new Set(["handle", "capability", "query_fingerprint", "items"]);
2762
+ var PROPOSAL_RECORD_KEYS = /* @__PURE__ */ new Set(["id", "capability", "subject", "status", "diff", "evidence_handle"]);
2763
+ var RECEIPT_KEYS = /* @__PURE__ */ new Set(["id", "proposal_id", "status", "idempotency_key", "source_database_mutated", "rows_affected"]);
2764
+ var REPLAY_KEYS = /* @__PURE__ */ new Set(["id", "proposal_id", "run_id", "events"]);
2765
+ var EXTERNAL_ACTION_KEYS = /* @__PURE__ */ new Set(["id", "action", "handler", "idempotency_key", "receipt"]);
2766
+ var TRUSTED_ARG_NAMES = /* @__PURE__ */ new Set([
2767
+ "tenant_id",
2768
+ "tenantId",
2769
+ "principal",
2770
+ "principal_id",
2771
+ "principalId",
2772
+ "schema",
2773
+ "table",
2774
+ "column",
2775
+ "columns",
2776
+ "database_url",
2777
+ "write_url",
2778
+ "read_url",
2779
+ "expected_version",
2780
+ "row_version"
2781
+ ]);
2782
+ function validateContract(input) {
2783
+ const errors = [];
2784
+ const warnings = [];
2785
+ if (!isRecord3(input)) {
2786
+ return {
2787
+ ok: false,
2788
+ errors: [{ path: "$", code: "CONTRACT_NOT_OBJECT", message: "Contract must be a JSON object." }],
2789
+ warnings
2790
+ };
2791
+ }
2792
+ checkUnknownKeys2(input, TOP_LEVEL_KEYS2, "$", errors);
2793
+ if (input.spec_version !== SPEC_VERSION) {
2794
+ errors.push({ path: "$.spec_version", code: "UNSUPPORTED_SPEC_VERSION", message: `spec_version must be ${SPEC_VERSION}.` });
2795
+ }
2796
+ if (input.kind !== "SynapsorContract") {
2797
+ errors.push({ path: "$.kind", code: "INVALID_KIND", message: "kind must be SynapsorContract." });
2798
+ }
2799
+ validateMetadata(input.metadata, "$.metadata", errors);
2800
+ const resourceNames = validateResources(input.resources, errors, warnings);
2801
+ const contextNames = validateContexts2(input.contexts, errors, warnings);
2802
+ const capabilityNames = validateCapabilities2(input.capabilities, contextNames, resourceNames, errors, warnings);
2803
+ validateWorkflows(input.workflows, contextNames, capabilityNames, errors);
2804
+ validatePolicies(input.policies, errors);
2805
+ validateEvidenceRecords(input.evidence, errors);
2806
+ validateProposalRecords(input.proposals, capabilityNames, errors);
2807
+ validateReceiptRecords(input.receipts, errors);
2808
+ validateReplay(input.replay, errors);
2809
+ validateExternalActions(input.external_actions, errors);
2810
+ scanForInlineSecrets(input, "$", errors);
2811
+ return { ok: errors.length === 0, errors, warnings };
2812
+ }
2813
+ function assertValidContract(input) {
2814
+ const result = validateContract(input);
2815
+ if (!result.ok)
2816
+ throw new SynapsorSpecValidationError(result.errors);
2817
+ }
2818
+ function validateMetadata(value, path4, errors) {
2819
+ if (value === void 0)
2820
+ return;
2821
+ if (!isRecord3(value)) {
2822
+ errors.push({ path: path4, code: "METADATA_NOT_OBJECT", message: "metadata must be an object." });
2823
+ return;
2824
+ }
2825
+ checkUnknownKeys2(value, METADATA_KEYS, path4, errors);
2826
+ if (value.name !== void 0 && !isNonEmptyString2(value.name))
2827
+ errors.push({ path: `${path4}.name`, code: "INVALID_METADATA_NAME", message: "metadata.name must be a non-empty string." });
2828
+ if (value.tags !== void 0 && (!Array.isArray(value.tags) || value.tags.some((tag) => !isNonEmptyString2(tag)))) {
2829
+ errors.push({ path: `${path4}.tags`, code: "INVALID_METADATA_TAGS", message: "metadata.tags must be an array of non-empty strings." });
2830
+ }
2831
+ }
2832
+ function validateResources(value, errors, warnings) {
2833
+ const names = /* @__PURE__ */ new Set();
2834
+ if (value === void 0)
2835
+ return names;
2836
+ if (!Array.isArray(value)) {
2837
+ errors.push({ path: "$.resources", code: "RESOURCES_NOT_ARRAY", message: "resources must be an array." });
2838
+ return names;
2839
+ }
2840
+ value.forEach((resource, index) => {
2841
+ const path4 = `$.resources[${index}]`;
2842
+ if (!isRecord3(resource)) {
2843
+ errors.push({ path: path4, code: "RESOURCE_NOT_OBJECT", message: "resource must be an object." });
2844
+ return;
2845
+ }
2846
+ checkUnknownKeys2(resource, RESOURCE_KEYS, path4, errors);
2847
+ if (!isQualifiedOrSafeName(resource.name))
2848
+ errors.push({ path: `${path4}.name`, code: "INVALID_RESOURCE_NAME", message: "resource name must be a safe identifier or qualified name." });
2849
+ else
2850
+ addUnique(names, resource.name, `${path4}.name`, "DUPLICATE_RESOURCE_NAME", errors);
2851
+ if (!["postgres", "mysql", "synapsor"].includes(String(resource.engine)))
2852
+ errors.push({ path: `${path4}.engine`, code: "INVALID_RESOURCE_ENGINE", message: "engine must be postgres, mysql, or synapsor." });
2853
+ for (const key of ["schema", "table", "primary_key"]) {
2854
+ if (!isSafeIdentifier2(resource[key]))
2855
+ errors.push({ path: `${path4}.${key}`, code: "INVALID_RESOURCE_IDENTIFIER", message: `${key} must be a fixed safe identifier.` });
2856
+ }
2857
+ if (resource.tenant_key !== void 0 && !isSafeIdentifier2(resource.tenant_key))
2858
+ errors.push({ path: `${path4}.tenant_key`, code: "INVALID_TENANT_KEY", message: "tenant_key must be a fixed safe identifier." });
2859
+ if (!isSafeIdentifier2(resource.tenant_key) && resource.single_tenant_dev !== true) {
2860
+ errors.push({ path: `${path4}.tenant_key`, code: "TENANT_KEY_REQUIRED", message: "tenant_key is required unless single_tenant_dev is explicitly true." });
2861
+ }
2862
+ if (resource.single_tenant_dev === true)
2863
+ warnings.push({ path: `${path4}.single_tenant_dev`, code: "SINGLE_TENANT_DEV", message: "single_tenant_dev is only for local demos and must not be used for shared tenant data." });
2864
+ if (resource.conflict_key !== void 0 && !isSafeIdentifier2(resource.conflict_key))
2865
+ errors.push({ path: `${path4}.conflict_key`, code: "INVALID_CONFLICT_KEY", message: "conflict_key must be a fixed safe identifier." });
2866
+ });
2867
+ return names;
2868
+ }
2869
+ function validateContexts2(value, errors, warnings) {
2870
+ const names = /* @__PURE__ */ new Set();
2871
+ if (!Array.isArray(value) || value.length === 0) {
2872
+ errors.push({ path: "$.contexts", code: "CONTEXTS_REQUIRED", message: "At least one context is required." });
2873
+ return names;
2874
+ }
2875
+ value.forEach((context, index) => {
2876
+ const path4 = `$.contexts[${index}]`;
2877
+ if (!isRecord3(context)) {
2878
+ errors.push({ path: path4, code: "CONTEXT_NOT_OBJECT", message: "context must be an object." });
2879
+ return;
2880
+ }
2881
+ checkUnknownKeys2(context, CONTEXT_KEYS2, path4, errors);
2882
+ if (!isQualifiedOrSafeName(context.name))
2883
+ errors.push({ path: `${path4}.name`, code: "INVALID_CONTEXT_NAME", message: "context name must be a safe identifier or qualified name." });
2884
+ else
2885
+ addUnique(names, context.name, `${path4}.name`, "DUPLICATE_CONTEXT_NAME", errors);
2886
+ if (!Array.isArray(context.bindings) || context.bindings.length === 0) {
2887
+ errors.push({ path: `${path4}.bindings`, code: "BINDINGS_REQUIRED", message: "context.bindings must contain trusted/session bindings." });
2888
+ return;
2889
+ }
2890
+ const bindingNames = /* @__PURE__ */ new Set();
2891
+ context.bindings.forEach((binding, bindingIndex) => {
2892
+ const bindingPath = `${path4}.bindings[${bindingIndex}]`;
2893
+ if (!isRecord3(binding)) {
2894
+ errors.push({ path: bindingPath, code: "BINDING_NOT_OBJECT", message: "binding must be an object." });
2895
+ return;
2896
+ }
2897
+ checkUnknownKeys2(binding, BINDING_KEYS, bindingPath, errors);
2898
+ if (!isSafeIdentifier2(binding.name))
2899
+ errors.push({ path: `${bindingPath}.name`, code: "INVALID_BINDING_NAME", message: "binding name must be a safe identifier." });
2900
+ else
2901
+ addUnique(bindingNames, binding.name, `${bindingPath}.name`, "DUPLICATE_BINDING_NAME", errors);
2902
+ if (!["session", "environment", "cloud_session", "static_dev", "http_claim"].includes(String(binding.source)))
2903
+ errors.push({ path: `${bindingPath}.source`, code: "INVALID_BINDING_SOURCE", message: "binding source must be session, environment, cloud_session, static_dev, or http_claim." });
2904
+ if (!isNonEmptyString2(binding.key))
2905
+ errors.push({ path: `${bindingPath}.key`, code: "INVALID_BINDING_KEY", message: "binding key must be a non-empty string." });
2906
+ if (binding.source === "static_dev")
2907
+ warnings.push({ path: `${bindingPath}.source`, code: "STATIC_DEV_BINDING", message: "static_dev bindings are for local demos only." });
2908
+ });
2909
+ for (const key of ["tenant_binding", "principal_binding"]) {
2910
+ if (context[key] !== void 0 && (!isSafeIdentifier2(context[key]) || !bindingNames.has(String(context[key])))) {
2911
+ errors.push({ path: `${path4}.${key}`, code: "UNKNOWN_CONTEXT_BINDING", message: `${key} must reference a binding name.` });
2912
+ }
2913
+ }
2914
+ });
2915
+ return names;
2916
+ }
2917
+ function validateCapabilities2(value, contextNames, resourceNames, errors, warnings) {
2918
+ const names = /* @__PURE__ */ new Set();
2919
+ if (!Array.isArray(value) || value.length === 0) {
2920
+ errors.push({ path: "$.capabilities", code: "CAPABILITIES_REQUIRED", message: "At least one capability is required." });
2921
+ return names;
2922
+ }
2923
+ value.forEach((capability, index) => {
2924
+ const path4 = `$.capabilities[${index}]`;
2925
+ if (!isRecord3(capability)) {
2926
+ errors.push({ path: path4, code: "CAPABILITY_NOT_OBJECT", message: "capability must be an object." });
2927
+ return;
2928
+ }
2929
+ checkUnknownKeys2(capability, CAPABILITY_KEYS2, path4, errors);
2930
+ if (!isQualifiedName2(capability.name))
2931
+ errors.push({ path: `${path4}.name`, code: "INVALID_CAPABILITY_NAME", message: "capability name must be namespace.name." });
2932
+ else
2933
+ addUnique(names, capability.name, `${path4}.name`, "DUPLICATE_CAPABILITY_NAME", errors);
2934
+ if (!["read", "proposal", "external_action", "answer_with_evidence"].includes(String(capability.kind)))
2935
+ errors.push({ path: `${path4}.kind`, code: "INVALID_CAPABILITY_KIND", message: "kind must be read, proposal, external_action, or answer_with_evidence." });
2936
+ if (!isNonEmptyString2(capability.context) || !contextNames.has(capability.context))
2937
+ errors.push({ path: `${path4}.context`, code: "UNKNOWN_CONTEXT", message: "capability.context must reference a declared context." });
2938
+ validateSubject(capability.subject, `${path4}.subject`, resourceNames, errors, warnings);
2939
+ validateArgs2(capability.args, `${path4}.args`, errors);
2940
+ if (capability.lookup !== void 0)
2941
+ validateLookup2(capability.lookup, `${path4}.lookup`, capability.args, errors);
2942
+ validateFieldList(capability.visible_fields, `${path4}.visible_fields`, "VISIBLE_FIELDS_REQUIRED", errors);
2943
+ if (capability.kept_out_fields !== void 0)
2944
+ validateFieldList(capability.kept_out_fields, `${path4}.kept_out_fields`, "INVALID_KEPT_OUT_FIELDS", errors, true);
2945
+ validateKeptOutExclusion(capability.visible_fields, capability.kept_out_fields, path4, errors);
2946
+ if (capability.evidence !== void 0)
2947
+ validateEvidenceRequirement(capability.evidence, `${path4}.evidence`, errors);
2948
+ if (capability.max_rows !== void 0 && !isPositiveInteger2(capability.max_rows))
2949
+ errors.push({ path: `${path4}.max_rows`, code: "INVALID_MAX_ROWS", message: "max_rows must be a positive integer." });
2950
+ if (capability.kind === "proposal")
2951
+ validateProposalAction(capability.proposal, `${path4}.proposal`, errors);
2952
+ if (capability.kind !== "proposal" && capability.proposal !== void 0)
2953
+ errors.push({ path: `${path4}.proposal`, code: "PROPOSAL_ONLY_FOR_PROPOSAL_KIND", message: "proposal is only valid for proposal capabilities." });
2954
+ });
2955
+ return names;
2956
+ }
2957
+ function validateSubject(value, path4, resourceNames, errors, warnings) {
2958
+ if (!isRecord3(value)) {
2959
+ errors.push({ path: path4, code: "SUBJECT_REQUIRED", message: "subject must be an object." });
2960
+ return;
2961
+ }
2962
+ checkUnknownKeys2(value, SUBJECT_KEYS, path4, errors);
2963
+ if (value.resource !== void 0 && (!isQualifiedOrSafeName(value.resource) || resourceNames.size > 0 && !resourceNames.has(String(value.resource)))) {
2964
+ errors.push({ path: `${path4}.resource`, code: "UNKNOWN_RESOURCE", message: "subject.resource must reference a declared resource." });
2965
+ }
2966
+ if (value.resource === void 0) {
2967
+ for (const key of ["schema", "table", "primary_key"]) {
2968
+ if (!isSafeIdentifier2(value[key]))
2969
+ errors.push({ path: `${path4}.${key}`, code: "INVALID_SUBJECT_IDENTIFIER", message: `${key} must be provided as a fixed safe identifier when resource is omitted.` });
2970
+ }
2971
+ if (!isSafeIdentifier2(value.tenant_key) && value.single_tenant_dev !== true) {
2972
+ errors.push({ path: `${path4}.tenant_key`, code: "TENANT_KEY_REQUIRED", message: "tenant_key is required unless single_tenant_dev is explicitly true." });
2973
+ }
2974
+ }
2975
+ if (value.single_tenant_dev === true)
2976
+ warnings.push({ path: `${path4}.single_tenant_dev`, code: "SINGLE_TENANT_DEV", message: "single_tenant_dev is only for local demos and must not be used for shared tenant data." });
2977
+ }
2978
+ function validateArgs2(value, path4, errors) {
2979
+ if (!isRecord3(value) || Object.keys(value).length === 0) {
2980
+ errors.push({ path: path4, code: "ARGS_REQUIRED", message: "args must define at least one model-facing business argument." });
2981
+ return;
2982
+ }
2983
+ for (const [name, arg] of Object.entries(value)) {
2984
+ const argPath = `${path4}.${name}`;
2985
+ if (!isSafeIdentifier2(name))
2986
+ errors.push({ path: argPath, code: "INVALID_ARG_NAME", message: "arg names must be safe identifiers." });
2987
+ if (TRUSTED_ARG_NAMES.has(name))
2988
+ errors.push({ path: argPath, code: "MODEL_CONTROLLED_TRUST_ARG", message: "model-facing args cannot include trust scope, dynamic identifiers, or version authority." });
2989
+ if (!isRecord3(arg)) {
2990
+ errors.push({ path: argPath, code: "ARG_NOT_OBJECT", message: "arg definition must be an object." });
2991
+ continue;
2992
+ }
2993
+ checkUnknownKeys2(arg, ARG_KEYS2, argPath, errors);
2994
+ if (!["string", "number", "boolean"].includes(String(arg.type)))
2995
+ errors.push({ path: `${argPath}.type`, code: "INVALID_ARG_TYPE", message: "arg type must be string, number, or boolean." });
2996
+ }
2997
+ }
2998
+ function validateLookup2(value, path4, args, errors) {
2999
+ if (!isRecord3(value)) {
3000
+ errors.push({ path: path4, code: "LOOKUP_NOT_OBJECT", message: "lookup must be an object." });
3001
+ return;
3002
+ }
3003
+ checkUnknownKeys2(value, LOOKUP_KEYS2, path4, errors);
3004
+ if (!isSafeIdentifier2(value.id_from_arg) || !isRecord3(args) || !Object.prototype.hasOwnProperty.call(args, String(value.id_from_arg))) {
3005
+ errors.push({ path: `${path4}.id_from_arg`, code: "UNKNOWN_LOOKUP_ARG", message: "lookup.id_from_arg must reference a model-facing arg." });
3006
+ }
3007
+ }
3008
+ function validateEvidenceRequirement(value, path4, errors) {
3009
+ if (!isRecord3(value)) {
3010
+ errors.push({ path: path4, code: "EVIDENCE_NOT_OBJECT", message: "evidence must be an object." });
3011
+ return;
3012
+ }
3013
+ checkUnknownKeys2(value, EVIDENCE_KEYS, path4, errors);
3014
+ if (typeof value.required !== "boolean")
3015
+ errors.push({ path: `${path4}.required`, code: "INVALID_EVIDENCE_REQUIRED", message: "evidence.required must be boolean." });
3016
+ if (value.sources !== void 0 && (!Array.isArray(value.sources) || value.sources.some((source) => !isNonEmptyString2(source))))
3017
+ errors.push({ path: `${path4}.sources`, code: "INVALID_EVIDENCE_SOURCES", message: "evidence.sources must be non-empty strings." });
3018
+ }
3019
+ function validateProposalAction(value, path4, errors) {
3020
+ if (!isRecord3(value)) {
3021
+ errors.push({ path: path4, code: "PROPOSAL_REQUIRED", message: "proposal capabilities must define proposal action semantics." });
3022
+ return;
3023
+ }
3024
+ checkUnknownKeys2(value, PROPOSAL_KEYS, path4, errors);
3025
+ if (!isQualifiedOrSafeName(value.action))
3026
+ errors.push({ path: `${path4}.action`, code: "INVALID_PROPOSAL_ACTION", message: "proposal.action must be a safe action name." });
3027
+ validateFieldList(value.allowed_fields, `${path4}.allowed_fields`, "ALLOWED_FIELDS_REQUIRED", errors);
3028
+ if (!isRecord3(value.patch) || Object.keys(value.patch).length === 0) {
3029
+ errors.push({ path: `${path4}.patch`, code: "PATCH_REQUIRED", message: "proposal.patch must map allowed fields to fixed or arg values." });
3030
+ } else {
3031
+ for (const [field, patch] of Object.entries(value.patch)) {
3032
+ const patchPath = `${path4}.patch.${field}`;
3033
+ if (!isSafeIdentifier2(field))
3034
+ errors.push({ path: patchPath, code: "INVALID_PATCH_FIELD", message: "patch field must be a safe identifier." });
3035
+ if (Array.isArray(value.allowed_fields) && !value.allowed_fields.includes(field))
3036
+ errors.push({ path: patchPath, code: "PATCH_FIELD_NOT_ALLOWED", message: "patch field must be listed in allowed_fields." });
3037
+ if (!isRecord3(patch)) {
3038
+ errors.push({ path: patchPath, code: "PATCH_BINDING_NOT_OBJECT", message: "patch binding must be an object." });
3039
+ continue;
3040
+ }
3041
+ checkUnknownKeys2(patch, PATCH_KEYS, patchPath, errors);
3042
+ if (patch.fixed === void 0 && !isSafeIdentifier2(patch.from_arg))
3043
+ errors.push({ path: patchPath, code: "PATCH_BINDING_REQUIRED", message: "patch binding must include fixed or from_arg." });
3044
+ }
3045
+ }
3046
+ if (value.conflict_guard !== void 0) {
3047
+ if (!isRecord3(value.conflict_guard))
3048
+ errors.push({ path: `${path4}.conflict_guard`, code: "CONFLICT_GUARD_NOT_OBJECT", message: "conflict_guard must be an object." });
3049
+ else {
3050
+ checkUnknownKeys2(value.conflict_guard, CONFLICT_GUARD_KEYS2, `${path4}.conflict_guard`, errors);
3051
+ if (value.conflict_guard.column !== void 0 && !isSafeIdentifier2(value.conflict_guard.column))
3052
+ errors.push({ path: `${path4}.conflict_guard.column`, code: "INVALID_CONFLICT_COLUMN", message: "conflict_guard.column must be a safe identifier." });
3053
+ if (value.conflict_guard.column === void 0 && value.conflict_guard.weak_guard_ack !== true)
3054
+ errors.push({ path: `${path4}.conflict_guard`, code: "CONFLICT_GUARD_REQUIRED", message: "proposal needs conflict_guard.column unless weak_guard_ack is explicit." });
3055
+ }
3056
+ }
3057
+ if (value.approval !== void 0)
3058
+ validateNestedObject(value.approval, APPROVAL_KEYS2, `${path4}.approval`, errors);
3059
+ if (value.writeback !== void 0) {
3060
+ validateNestedObject(value.writeback, WRITEBACK_KEYS, `${path4}.writeback`, errors);
3061
+ if (isRecord3(value.writeback) && !["direct_sql", "app_handler", "cloud_worker", "none"].includes(String(value.writeback.mode)))
3062
+ errors.push({ path: `${path4}.writeback.mode`, code: "INVALID_WRITEBACK_MODE", message: "writeback.mode must be direct_sql, app_handler, cloud_worker, or none." });
3063
+ }
3064
+ }
3065
+ function validateWorkflows(value, contextNames, capabilityNames, errors) {
3066
+ if (value === void 0)
3067
+ return;
3068
+ if (!Array.isArray(value)) {
3069
+ errors.push({ path: "$.workflows", code: "WORKFLOWS_NOT_ARRAY", message: "workflows must be an array." });
3070
+ return;
3071
+ }
3072
+ const names = /* @__PURE__ */ new Set();
3073
+ value.forEach((workflow, index) => {
3074
+ const path4 = `$.workflows[${index}]`;
3075
+ if (!isRecord3(workflow)) {
3076
+ errors.push({ path: path4, code: "WORKFLOW_NOT_OBJECT", message: "workflow must be an object." });
3077
+ return;
3078
+ }
3079
+ checkUnknownKeys2(workflow, WORKFLOW_KEYS, path4, errors);
3080
+ if (!isQualifiedName2(workflow.name))
3081
+ errors.push({ path: `${path4}.name`, code: "INVALID_WORKFLOW_NAME", message: "workflow name must be namespace.name." });
3082
+ else
3083
+ addUnique(names, workflow.name, `${path4}.name`, "DUPLICATE_WORKFLOW_NAME", errors);
3084
+ if (!isNonEmptyString2(workflow.context) || !contextNames.has(workflow.context))
3085
+ errors.push({ path: `${path4}.context`, code: "UNKNOWN_CONTEXT", message: "workflow.context must reference a declared context." });
3086
+ if (!Array.isArray(workflow.allowed_capabilities) || workflow.allowed_capabilities.length === 0) {
3087
+ errors.push({ path: `${path4}.allowed_capabilities`, code: "WORKFLOW_CAPABILITIES_REQUIRED", message: "workflow.allowed_capabilities must list allowed capabilities." });
3088
+ } else {
3089
+ workflow.allowed_capabilities.forEach((name, allowedIndex) => {
3090
+ if (!isNonEmptyString2(name) || !capabilityNames.has(name))
3091
+ errors.push({ path: `${path4}.allowed_capabilities[${allowedIndex}]`, code: "UNKNOWN_CAPABILITY", message: "workflow allowed capability must reference a declared capability." });
3092
+ });
3093
+ }
3094
+ });
3095
+ }
3096
+ function validatePolicies(value, errors) {
3097
+ if (value === void 0)
3098
+ return;
3099
+ validateArrayRecords(value, "$.policies", POLICY_KEYS, "POLICY", errors);
3100
+ }
3101
+ function validateEvidenceRecords(value, errors) {
3102
+ if (value === void 0)
3103
+ return;
3104
+ validateArrayRecords(value, "$.evidence", EVIDENCE_RECORD_KEYS, "EVIDENCE", errors);
3105
+ }
3106
+ function validateProposalRecords(value, capabilityNames, errors) {
3107
+ if (value === void 0)
3108
+ return;
3109
+ if (!Array.isArray(value)) {
3110
+ errors.push({ path: "$.proposals", code: "PROPOSALS_NOT_ARRAY", message: "proposals must be an array." });
3111
+ return;
3112
+ }
3113
+ value.forEach((proposal, index) => {
3114
+ const path4 = `$.proposals[${index}]`;
3115
+ if (!isRecord3(proposal)) {
3116
+ errors.push({ path: path4, code: "PROPOSAL_NOT_OBJECT", message: "proposal must be an object." });
3117
+ return;
3118
+ }
3119
+ checkUnknownKeys2(proposal, PROPOSAL_RECORD_KEYS, path4, errors);
3120
+ if (!isNonEmptyString2(proposal.id))
3121
+ errors.push({ path: `${path4}.id`, code: "INVALID_PROPOSAL_ID", message: "proposal.id must be non-empty." });
3122
+ if (!isNonEmptyString2(proposal.capability) || capabilityNames.size > 0 && !capabilityNames.has(String(proposal.capability)))
3123
+ errors.push({ path: `${path4}.capability`, code: "UNKNOWN_CAPABILITY", message: "proposal.capability must reference a declared capability." });
3124
+ });
3125
+ }
3126
+ function validateReceiptRecords(value, errors) {
3127
+ if (value === void 0)
3128
+ return;
3129
+ validateArrayRecords(value, "$.receipts", RECEIPT_KEYS, "RECEIPT", errors);
3130
+ }
3131
+ function validateReplay(value, errors) {
3132
+ if (value === void 0)
3133
+ return;
3134
+ if (!Array.isArray(value)) {
3135
+ errors.push({ path: "$.replay", code: "REPLAY_NOT_ARRAY", message: "replay must be an array." });
3136
+ return;
3137
+ }
3138
+ value.forEach((record, index) => {
3139
+ const path4 = `$.replay[${index}]`;
3140
+ if (!isRecord3(record)) {
3141
+ errors.push({ path: path4, code: "REPLAY_RECORD_NOT_OBJECT", message: "replay record must be an object." });
3142
+ return;
3143
+ }
3144
+ checkUnknownKeys2(record, REPLAY_KEYS, path4, errors);
3145
+ if (!isNonEmptyString2(record.id))
3146
+ errors.push({ path: `${path4}.id`, code: "INVALID_REPLAY_ID", message: "replay id must be non-empty." });
3147
+ if (!Array.isArray(record.events))
3148
+ errors.push({ path: `${path4}.events`, code: "REPLAY_EVENTS_REQUIRED", message: "replay.events must be an array." });
3149
+ });
3150
+ }
3151
+ function validateExternalActions(value, errors) {
3152
+ if (value === void 0)
3153
+ return;
3154
+ validateArrayRecords(value, "$.external_actions", EXTERNAL_ACTION_KEYS, "EXTERNAL_ACTION", errors);
3155
+ }
3156
+ function validateArrayRecords(value, path4, keys, label, errors) {
3157
+ if (!Array.isArray(value)) {
3158
+ errors.push({ path: path4, code: `${label}_NOT_ARRAY`, message: `${path4} must be an array.` });
3159
+ return;
3160
+ }
3161
+ value.forEach((record, index) => {
3162
+ const itemPath = `${path4}[${index}]`;
3163
+ if (!isRecord3(record)) {
3164
+ errors.push({ path: itemPath, code: `${label}_NOT_OBJECT`, message: `${label.toLowerCase()} entry must be an object.` });
3165
+ return;
3166
+ }
3167
+ checkUnknownKeys2(record, keys, itemPath, errors);
3168
+ });
3169
+ }
3170
+ function validateFieldList(value, path4, code, errors, allowEmpty = false) {
3171
+ if (!Array.isArray(value) || !allowEmpty && value.length === 0) {
3172
+ errors.push({ path: path4, code, message: "field list must be a non-empty array of safe identifiers." });
3173
+ return;
3174
+ }
3175
+ const seen = /* @__PURE__ */ new Set();
3176
+ value.forEach((field, index) => {
3177
+ if (!isSafeIdentifier2(field))
3178
+ errors.push({ path: `${path4}[${index}]`, code: "INVALID_FIELD_IDENTIFIER", message: "field names must be fixed safe identifiers." });
3179
+ else
3180
+ addUnique(seen, field, `${path4}[${index}]`, "DUPLICATE_FIELD", errors);
3181
+ });
3182
+ }
3183
+ function validateKeptOutExclusion(visible, keptOut, path4, errors) {
3184
+ if (!Array.isArray(visible) || !Array.isArray(keptOut))
3185
+ return;
3186
+ const visibleSet = new Set(visible);
3187
+ for (const field of keptOut) {
3188
+ if (visibleSet.has(field))
3189
+ errors.push({ path: `${path4}.kept_out_fields`, code: "KEPT_OUT_FIELD_VISIBLE", message: `kept-out field must not also be visible: ${String(field)}` });
3190
+ }
3191
+ }
3192
+ function validateNestedObject(value, keys, path4, errors) {
3193
+ if (!isRecord3(value)) {
3194
+ errors.push({ path: path4, code: "NESTED_VALUE_NOT_OBJECT", message: "nested value must be an object." });
3195
+ return;
3196
+ }
3197
+ checkUnknownKeys2(value, keys, path4, errors);
3198
+ }
3199
+ function scanForInlineSecrets(value, path4, errors) {
3200
+ if (Array.isArray(value)) {
3201
+ value.forEach((item, index) => scanForInlineSecrets(item, `${path4}[${index}]`, errors));
3202
+ return;
3203
+ }
3204
+ if (!isRecord3(value))
3205
+ return;
3206
+ for (const [key, child] of Object.entries(value)) {
3207
+ const childPath = `${path4}.${key}`;
3208
+ if (/database_url|read_url|write_url|connection_string|password|secret|token/i.test(key) && typeof child === "string" && /(postgres(?:ql)?:\/\/|mysql:\/\/|Bearer\s+|[A-Za-z0-9+/=]{24,})/i.test(child)) {
3209
+ errors.push({ path: childPath, code: "INLINE_SECRET_OR_URL", message: "Contracts must not contain database URLs, passwords, bearer tokens, or secrets. Put runtime wiring in runner config/env vars." });
3210
+ }
3211
+ scanForInlineSecrets(child, childPath, errors);
3212
+ }
3213
+ }
3214
+ function checkUnknownKeys2(value, allowed, path4, errors) {
3215
+ for (const key of Object.keys(value)) {
3216
+ if (allowed.has(key) || isExtensionKey(key))
3217
+ continue;
3218
+ errors.push({ path: `${path4}.${key}`, code: "UNKNOWN_CORE_FIELD", message: `Unknown core field ${key}. Use x-cloud-*, x-runner-*, or x-experimental-* for extensions.` });
3219
+ }
3220
+ }
3221
+ function addUnique(seen, name, path4, code, errors) {
3222
+ const value = String(name);
3223
+ if (seen.has(value))
3224
+ errors.push({ path: path4, code, message: `Duplicate name: ${value}` });
3225
+ seen.add(value);
3226
+ }
3227
+ function isExtensionKey(key) {
3228
+ return /^x-(cloud|runner|experimental)-[A-Za-z0-9_.-]+$/.test(key);
3229
+ }
3230
+ function isRecord3(value) {
3231
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3232
+ }
3233
+ function isNonEmptyString2(value) {
3234
+ return typeof value === "string" && value.trim().length > 0;
3235
+ }
3236
+ function isSafeIdentifier2(value) {
3237
+ return typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
3238
+ }
3239
+ function isQualifiedName2(value) {
3240
+ return typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/.test(value);
3241
+ }
3242
+ function isQualifiedOrSafeName(value) {
3243
+ return isSafeIdentifier2(value) || isQualifiedName2(value);
3244
+ }
3245
+ function isPositiveInteger2(value) {
3246
+ return Number.isInteger(value) && Number(value) > 0;
3247
+ }
3248
+
3249
+ // packages/spec/dist/normalize.js
3250
+ function normalizeContract(input) {
3251
+ assertValidContract(input);
3252
+ return sortJson(input);
3253
+ }
3254
+ function sortJson(value) {
3255
+ if (Array.isArray(value))
3256
+ return value.map(sortJson);
3257
+ if (!value || typeof value !== "object")
3258
+ return value;
3259
+ const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
3260
+ const output = {};
3261
+ for (const [key, child] of entries)
3262
+ output[key] = sortJson(child);
3263
+ return output;
3264
+ }
3265
+
2696
3266
  // packages/mcp-server/src/index.ts
2697
3267
  import mysql from "mysql2/promise";
2698
3268
  import { Pool } from "pg";
@@ -2720,10 +3290,94 @@ var McpRuntimeError = class extends Error {
2720
3290
  function loadRuntimeConfigFromFile(configPath = process.env.SYNAPSOR_MCP_CONFIG || "synapsor.runner.json") {
2721
3291
  const resolved = path.resolve(configPath);
2722
3292
  const parsed = JSON.parse(fs.readFileSync(resolved, "utf8"));
2723
- assertValidRunnerCapabilityConfig(parsed);
2724
- return parsed;
3293
+ const config = resolveRuntimeConfig(parsed, path.dirname(resolved));
3294
+ assertValidRunnerCapabilityConfig(config);
3295
+ return config;
3296
+ }
3297
+ function resolveRuntimeConfig(config, baseDir = process.cwd()) {
3298
+ if (!Array.isArray(config.contracts) || config.contracts.length === 0) return config;
3299
+ const resolved = {
3300
+ ...config,
3301
+ contexts: { ...config.contexts ?? {} },
3302
+ capabilities: [...config.capabilities ?? []]
3303
+ };
3304
+ for (const contractPath of config.contracts) {
3305
+ const fullPath = path.resolve(baseDir, contractPath);
3306
+ const contract = normalizeContract(JSON.parse(fs.readFileSync(fullPath, "utf8")));
3307
+ mergeContractIntoRuntimeConfig(resolved, contract);
3308
+ }
3309
+ delete resolved.contracts;
3310
+ return resolved;
3311
+ }
3312
+ function mergeContractIntoRuntimeConfig(config, contract) {
3313
+ const resources = new Map((contract.resources ?? []).map((resource) => [resource.name, resource]));
3314
+ for (const context of contract.contexts) {
3315
+ if (!config.contexts) config.contexts = {};
3316
+ config.contexts[context.name] ??= runtimeContextFromSpec(context);
3317
+ }
3318
+ if (!config.trusted_context && contract.contexts.length === 1) {
3319
+ const [context] = contract.contexts;
3320
+ if (context) config.trusted_context = runtimeContextFromSpec(context);
3321
+ }
3322
+ if (!config.capabilities) config.capabilities = [];
3323
+ for (const capability of contract.capabilities) {
3324
+ config.capabilities.push(runtimeCapabilityFromSpec(capability, resources, config));
3325
+ }
3326
+ }
3327
+ function runtimeContextFromSpec(context) {
3328
+ const tenantBinding = context.bindings.find((binding) => binding.name === context.tenant_binding) ?? context.bindings.find((binding) => binding.name === "tenant_id");
3329
+ const principalBinding = context.bindings.find((binding) => binding.name === context.principal_binding) ?? context.bindings.find((binding) => binding.name === "principal");
3330
+ const provider = context.bindings.some((binding) => binding.source === "environment") ? "environment" : context.bindings.some((binding) => binding.source === "cloud_session") ? "cloud_session" : context.bindings.some((binding) => binding.source === "http_claim") ? "http_claims" : context.bindings.some((binding) => binding.source === "static_dev") ? "static_dev" : "environment";
3331
+ return {
3332
+ provider,
3333
+ values: {
3334
+ ...tenantBinding ? { tenant_id_env: tenantBinding.key, tenant_id_key: tenantBinding.key } : {},
3335
+ ...principalBinding ? { principal_env: principalBinding.key, principal_key: principalBinding.key } : {}
3336
+ }
3337
+ };
3338
+ }
3339
+ function runtimeCapabilityFromSpec(capability, resources, config) {
3340
+ const subjectResource = capability.subject.resource ? resources.get(capability.subject.resource) : void 0;
3341
+ const source = resolveCapabilitySource(capability, config);
3342
+ const target2 = {
3343
+ schema: subjectResource?.schema ?? capability.subject.schema ?? "",
3344
+ table: subjectResource?.table ?? capability.subject.table ?? "",
3345
+ primary_key: subjectResource?.primary_key ?? capability.subject.primary_key ?? "",
3346
+ tenant_key: subjectResource?.tenant_key ?? capability.subject.tenant_key,
3347
+ single_tenant_dev: subjectResource?.single_tenant_dev ?? capability.subject.single_tenant_dev
3348
+ };
3349
+ const runtime = {
3350
+ name: capability.name,
3351
+ kind: capability.kind === "proposal" ? "proposal" : "read",
3352
+ ...capability.description ? { description: capability.description } : {},
3353
+ source,
3354
+ context: capability.context,
3355
+ target: target2,
3356
+ args: capability.args,
3357
+ lookup: capability.lookup ?? { id_from_arg: Object.keys(capability.args)[0] ?? "id" },
3358
+ visible_columns: capability.visible_fields,
3359
+ evidence: capability.evidence?.required === false ? "optional" : "required",
3360
+ ...capability.max_rows ? { max_rows: capability.max_rows } : {}
3361
+ };
3362
+ if (capability.kind === "proposal" && capability.proposal) {
3363
+ runtime.patch = capability.proposal.patch;
3364
+ runtime.allowed_columns = capability.proposal.allowed_fields;
3365
+ runtime.conflict_guard = capability.proposal.conflict_guard;
3366
+ runtime.approval = capability.proposal.approval;
3367
+ if (capability.proposal.writeback?.mode && capability.proposal.writeback.mode !== "direct_sql" && capability.proposal.writeback.executor) {
3368
+ runtime.executor = capability.proposal.writeback.executor;
3369
+ }
3370
+ }
3371
+ return runtime;
3372
+ }
3373
+ function resolveCapabilitySource(capability, config) {
3374
+ if (capability.source) return capability.source;
3375
+ const sourceNames = Object.keys(config.sources ?? {});
3376
+ if (sourceNames.length === 1 && sourceNames[0]) return sourceNames[0];
3377
+ throw new Error(`contract capability ${capability.name} must set source when runner config has ${sourceNames.length} sources`);
2725
3378
  }
2726
3379
  function createMcpRuntime(config, options = {}) {
3380
+ config = resolveRuntimeConfig(config);
2727
3381
  assertValidRunnerCapabilityConfig(config);
2728
3382
  const env = options.env ?? process.env;
2729
3383
  const storePath = options.storePath ?? config.storage?.sqlite_path ?? "./.synapsor/local.db";
@@ -3126,7 +3780,7 @@ async function handleHttpMcpRequest(input) {
3126
3780
  }
3127
3781
  const body = await readRequestBody(request);
3128
3782
  const payload = JSON.parse(body);
3129
- if (!isRecord3(payload)) {
3783
+ if (!isRecord4(payload)) {
3130
3784
  writeJson(response, 400, jsonRpcError(null, -32600, "JSON-RPC request must be an object."));
3131
3785
  return;
3132
3786
  }
@@ -3136,7 +3790,7 @@ async function handleHttpMcpRequest(input) {
3136
3790
  writeJson(response, 400, jsonRpcError(id, -32600, "JSON-RPC method is required."));
3137
3791
  return;
3138
3792
  }
3139
- const result = await handleHttpJsonRpcMethod(runtime, method, isRecord3(payload.params) ? payload.params : {});
3793
+ const result = await handleHttpJsonRpcMethod(runtime, method, isRecord4(payload.params) ? payload.params : {});
3140
3794
  writeJson(response, 200, {
3141
3795
  jsonrpc: "2.0",
3142
3796
  id,
@@ -3156,7 +3810,7 @@ async function handleHttpJsonRpcMethod(runtime, method, params) {
3156
3810
  if (method === "tools/call") {
3157
3811
  const name = typeof params.name === "string" ? params.name : void 0;
3158
3812
  if (!name) throw new McpRuntimeError("HTTP_TOOL_NAME_REQUIRED", "tools/call requires params.name.");
3159
- const args = isRecord3(params.arguments) ? params.arguments : isRecord3(params.args) ? params.args : {};
3813
+ const args = isRecord4(params.arguments) ? params.arguments : isRecord4(params.args) ? params.args : {};
3160
3814
  return await toolCallResult(runtime, name, args);
3161
3815
  }
3162
3816
  if (method === "resources/read") {
@@ -3201,10 +3855,10 @@ function containsInitializeRequest(payload) {
3201
3855
  }
3202
3856
  function requestIdFromPayload(payload) {
3203
3857
  if (Array.isArray(payload)) {
3204
- const request = payload.find((message) => isRecord3(message) && "id" in message);
3205
- return isRecord3(request) ? request.id ?? null : null;
3858
+ const request = payload.find((message) => isRecord4(message) && "id" in message);
3859
+ return isRecord4(request) ? request.id ?? null : null;
3206
3860
  }
3207
- return isRecord3(payload) ? payload.id ?? null : null;
3861
+ return isRecord4(payload) ? payload.id ?? null : null;
3208
3862
  }
3209
3863
  function openaiToolNameAlias(canonicalName) {
3210
3864
  const sanitized = canonicalName.replace(/[^A-Za-z0-9_-]+/g, "__").replace(/_{3,}/g, "__").replace(/^_+|_+$/g, "");
@@ -3297,7 +3951,7 @@ function sanitizeHttpError(error, authToken) {
3297
3951
  function sanitizeHttpPayload(value, authToken) {
3298
3952
  if (typeof value === "string") return sanitizeHttpString(value, authToken);
3299
3953
  if (Array.isArray(value)) return value.map((item) => sanitizeHttpPayload(item, authToken));
3300
- if (isRecord3(value)) {
3954
+ if (isRecord4(value)) {
3301
3955
  return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitizeHttpPayload(item, authToken)]));
3302
3956
  }
3303
3957
  return value;
@@ -3423,11 +4077,11 @@ function toolDescriptionWithCanonical(description, canonicalName, exposedName) {
3423
4077
  ${description}`;
3424
4078
  }
3425
4079
  function zodInputShapeFromJsonSchema(schema) {
3426
- const properties = isRecord3(schema.properties) ? schema.properties : {};
4080
+ const properties = isRecord4(schema.properties) ? schema.properties : {};
3427
4081
  const required = Array.isArray(schema.required) ? new Set(schema.required.map(String)) : /* @__PURE__ */ new Set();
3428
4082
  const shape = {};
3429
4083
  for (const [name, rawProperty] of Object.entries(properties)) {
3430
- const property = isRecord3(rawProperty) ? rawProperty : {};
4084
+ const property = isRecord4(rawProperty) ? rawProperty : {};
3431
4085
  let valueSchema;
3432
4086
  if (property.type === "number" || property.type === "integer") valueSchema = z2.number();
3433
4087
  else if (property.type === "boolean") valueSchema = z2.boolean();
@@ -3613,8 +4267,8 @@ function resultEnvelopeFromLegacy(legacy, capability, canonicalName) {
3613
4267
  const kind = capability?.kind ?? (typeof legacy.proposal_id === "string" ? "proposal" : "read");
3614
4268
  const evidenceBundleId = typeof legacy.evidence_bundle_id === "string" ? legacy.evidence_bundle_id : void 0;
3615
4269
  const sourceChanged = Boolean(legacy.source_database_changed ?? legacy.source_database_mutated ?? false);
3616
- const context = isRecord3(legacy.trusted_context) ? legacy.trusted_context : void 0;
3617
- const target2 = isRecord3(legacy.target) ? legacy.target : void 0;
4270
+ const context = isRecord4(legacy.trusted_context) ? legacy.trusted_context : void 0;
4271
+ const target2 = isRecord4(legacy.target) ? legacy.target : void 0;
3618
4272
  if (kind === "proposal") {
3619
4273
  const proposalId = typeof legacy.proposal_id === "string" ? legacy.proposal_id : "wrp_unknown";
3620
4274
  const targetType = typeof target2?.type === "string" ? target2.type : capability?.target.table ?? "object";
@@ -3631,7 +4285,7 @@ function resultEnvelopeFromLegacy(legacy, capability, canonicalName) {
3631
4285
  id: proposalId,
3632
4286
  state: typeof legacy.status === "string" ? legacy.status : "review_required",
3633
4287
  target: `${targetType}:${targetId}`,
3634
- diff: isRecord3(legacy.diff) ? legacy.diff : {},
4288
+ diff: isRecord4(legacy.diff) ? legacy.diff : {},
3635
4289
  approval_required: legacy.approval_required !== false,
3636
4290
  writeback: {
3637
4291
  mode: writebackMode,
@@ -3650,7 +4304,7 @@ function resultEnvelopeFromLegacy(legacy, capability, canonicalName) {
3650
4304
  }
3651
4305
  };
3652
4306
  }
3653
- const businessObject = isRecord3(legacy.business_object) ? legacy.business_object : void 0;
4307
+ const businessObject = isRecord4(legacy.business_object) ? legacy.business_object : void 0;
3654
4308
  const objectType = typeof businessObject?.type === "string" ? businessObject.type : capability?.target.table ?? "record";
3655
4309
  const objectId = businessObject?.id !== void 0 ? String(businessObject.id) : String(legacy.action ?? action);
3656
4310
  return {
@@ -3658,7 +4312,7 @@ function resultEnvelopeFromLegacy(legacy, capability, canonicalName) {
3658
4312
  summary: `Read ${objectType} ${objectId} through ${action}. Source database changed: no.`,
3659
4313
  action,
3660
4314
  kind: "read",
3661
- data: isRecord3(legacy.data) ? legacy.data : {},
4315
+ data: isRecord4(legacy.data) ? legacy.data : {},
3662
4316
  proposal: null,
3663
4317
  error: null,
3664
4318
  evidence: evidenceBundleId ? evidenceHandle(evidenceBundleId) : null,
@@ -3672,7 +4326,7 @@ function resultEnvelopeFromLegacy(legacy, capability, canonicalName) {
3672
4326
  };
3673
4327
  }
3674
4328
  function writebackExecutorName(value) {
3675
- if (!isRecord3(value)) return void 0;
4329
+ if (!isRecord4(value)) return void 0;
3676
4330
  return typeof value.executor === "string" ? value.executor : typeof value.mode === "string" ? value.mode : void 0;
3677
4331
  }
3678
4332
  function evidenceHandle(bundleId) {
@@ -4086,7 +4740,7 @@ function stableId(prefix, input) {
4086
4740
  function hashJson(input) {
4087
4741
  return `sha256:${crypto.createHash("sha256").update(JSON.stringify(input)).digest("hex")}`;
4088
4742
  }
4089
- function isRecord3(value) {
4743
+ function isRecord4(value) {
4090
4744
  return typeof value === "object" && value !== null && !Array.isArray(value);
4091
4745
  }
4092
4746
  function toolErrorPayload(error) {
@@ -4159,14 +4813,41 @@ function validateMysqlPatch(job) {
4159
4813
  function resultHash(job, status, version) {
4160
4814
  return `sha256:${crypto2.createHash("sha256").update(JSON.stringify({ job_id: job.job_id, status, version })).digest("hex")}`;
4161
4815
  }
4816
+ function normalizeVersionValue(value) {
4817
+ if (value instanceof Date) return normalizeVersionValue(value.toISOString());
4818
+ const text = String(value ?? "").trim();
4819
+ const timestamp = text.match(/^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}:\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}(?::?\d{2})?)?$/i);
4820
+ if (timestamp) {
4821
+ const fraction = (timestamp[3] ?? "").padEnd(6, "0").slice(0, 6);
4822
+ const offset = timestamp[4];
4823
+ if (offset && offset.toUpperCase() !== "Z") {
4824
+ const dateParts = timestamp[1].split("-");
4825
+ const timeParts = timestamp[2].split(":");
4826
+ const year = Number(dateParts[0]);
4827
+ const month = Number(dateParts[1]);
4828
+ const day = Number(dateParts[2]);
4829
+ const hour = Number(timeParts[0]);
4830
+ const minute = Number(timeParts[1]);
4831
+ const second = Number(timeParts[2]);
4832
+ const utc = new Date(Date.UTC(year, month - 1, day, hour, minute, second) - offsetMinutes(offset) * 6e4);
4833
+ return `${utc.getUTCFullYear()}-${pad2(utc.getUTCMonth() + 1)}-${pad2(utc.getUTCDate())} ${pad2(utc.getUTCHours())}:${pad2(utc.getUTCMinutes())}:${pad2(utc.getUTCSeconds())}.${fraction}`;
4834
+ }
4835
+ return `${timestamp[1]} ${timestamp[2]}.${fraction}`;
4836
+ }
4837
+ return text;
4838
+ }
4839
+ function offsetMinutes(offset) {
4840
+ const sign = offset.startsWith("-") ? -1 : 1;
4841
+ const compact = offset.slice(1).replace(":", "");
4842
+ const hours = Number(compact.slice(0, 2));
4843
+ const minutes = Number(compact.slice(2, 4) || "0");
4844
+ return sign * (hours * 60 + minutes);
4845
+ }
4846
+ function pad2(value) {
4847
+ return String(value).padStart(2, "0");
4848
+ }
4162
4849
  function versionValuesMatch(actual, expected) {
4163
- const normalizeTimestamp = (value) => String(value ?? "").replace("T", " ").replace(/\.\d{3}Z$/, "").replace(/Z$/, "").slice(0, 19);
4164
- const actualNormalized = normalizeTimestamp(actual);
4165
- const expectedNormalized = normalizeTimestamp(expected);
4166
- if (actualNormalized && expectedNormalized && actualNormalized === expectedNormalized) {
4167
- return true;
4168
- }
4169
- return String(actual) === String(expected);
4850
+ return normalizeVersionValue(actual) === normalizeVersionValue(expected);
4170
4851
  }
4171
4852
  async function applyMysqlJob(job, config) {
4172
4853
  if (config.dryRun) {
@@ -4176,15 +4857,22 @@ async function applyMysqlJob(job, config) {
4176
4857
  if (!config.databaseUrl) return failed(job, config, "DATABASE_UNAVAILABLE");
4177
4858
  const connection = await mysql2.createConnection({ uri: config.databaseUrl, dateStrings: true });
4178
4859
  try {
4179
- await connection.beginTransaction();
4860
+ return await applyMysqlJobWithConnection(job, config, connection);
4861
+ } catch (error) {
4862
+ const message = error instanceof Error ? error.message : "TRANSACTION_FAILED";
4863
+ return failed(job, config, message.includes("MULTI_ROW") ? "MULTI_ROW_WRITE_BLOCKED" : message.includes("VERSION") ? "VERSION_CONFLICT" : "TRANSACTION_FAILED");
4864
+ } finally {
4865
+ await connection.end();
4866
+ }
4867
+ }
4868
+ async function applyMysqlJobWithConnection(job, config, connection) {
4869
+ await connection.beginTransaction();
4870
+ try {
4180
4871
  await connection.query(mysqlReceiptMigration);
4181
- const [receiptRows] = await connection.query(
4182
- "SELECT status, result_hash FROM synapsor_writeback_receipts WHERE idempotency_key = ? FOR UPDATE",
4183
- [job.idempotency_key]
4184
- );
4185
- if (receiptRows[0]?.status === "applied") {
4872
+ const existing = await claimReceipt(connection, job, config);
4873
+ if (existing) {
4186
4874
  await connection.commit();
4187
- return { protocol_version: "1.0", job_id: job.job_id, runner_id: config.runnerId, status: "applied", affected_rows: 0, result_hash: receiptRows[0].result_hash, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
4875
+ return existing;
4188
4876
  }
4189
4877
  const [rows] = await connection.query(
4190
4878
  `SELECT * FROM ${quoteMysqlIdentifier(job.target.schema)}.${quoteMysqlIdentifier(job.target.table)}
@@ -4194,14 +4882,16 @@ FOR UPDATE`,
4194
4882
  [job.target.primary_key.value, job.target.tenant_guard.value]
4195
4883
  );
4196
4884
  if (!rows[0]) {
4197
- await recordReceipt(connection, job, "conflict", resultHash(job, "ROW_NOT_FOUND"));
4885
+ const hash2 = resultHash(job, "ROW_NOT_FOUND");
4886
+ await recordReceipt(connection, job, "conflict", hash2);
4198
4887
  await connection.commit();
4199
- return conflict(job, config, "ROW_NOT_FOUND");
4888
+ return conflict(job, config, "ROW_NOT_FOUND", hash2);
4200
4889
  }
4201
4890
  if (job.conflict_guard.kind === "version_column" && !versionValuesMatch(rows[0][job.conflict_guard.column], job.conflict_guard.expected_value)) {
4202
- await recordReceipt(connection, job, "conflict", resultHash(job, "VERSION_CONFLICT", rows[0][job.conflict_guard.column]));
4891
+ const hash2 = resultHash(job, "VERSION_CONFLICT", rows[0][job.conflict_guard.column]);
4892
+ await recordReceipt(connection, job, "conflict", hash2);
4203
4893
  await connection.commit();
4204
- return conflict(job, config, "VERSION_CONFLICT");
4894
+ return conflict(job, config, "VERSION_CONFLICT", hash2);
4205
4895
  }
4206
4896
  const update = buildMysqlUpdate(job);
4207
4897
  const [result] = await connection.query(update.sql, update.values);
@@ -4212,22 +4902,44 @@ FOR UPDATE`,
4212
4902
  return { protocol_version: "1.0", job_id: job.job_id, runner_id: config.runnerId, status: "applied", affected_rows: 1, result_hash: hash, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
4213
4903
  } catch (error) {
4214
4904
  await connection.rollback().catch(() => void 0);
4215
- const message = error instanceof Error ? error.message : "TRANSACTION_FAILED";
4216
- return failed(job, config, message.includes("MULTI_ROW") ? "MULTI_ROW_WRITE_BLOCKED" : message.includes("VERSION") ? "VERSION_CONFLICT" : "TRANSACTION_FAILED");
4217
- } finally {
4218
- await connection.end();
4905
+ throw error;
4219
4906
  }
4220
4907
  }
4908
+ async function claimReceipt(connection, job, config) {
4909
+ const [claim] = await connection.query(
4910
+ `INSERT IGNORE INTO synapsor_writeback_receipts (idempotency_key, job_id, proposal_id, status, result_hash)
4911
+ VALUES (?, ?, ?, 'in_progress', NULL)`,
4912
+ [job.idempotency_key, job.job_id, job.proposal_id]
4913
+ );
4914
+ if (claim.affectedRows === 1) return void 0;
4915
+ const [receiptRows] = await connection.query(
4916
+ "SELECT status, result_hash FROM synapsor_writeback_receipts WHERE idempotency_key = ? FOR UPDATE",
4917
+ [job.idempotency_key]
4918
+ );
4919
+ const row = receiptRows[0];
4920
+ if (!row) return failed(job, config, "IDEMPOTENCY_RECEIPT_UNAVAILABLE");
4921
+ return resultFromExistingReceipt(job, config, row.status, row.result_hash);
4922
+ }
4923
+ function resultFromExistingReceipt(job, config, status, hash) {
4924
+ const result_hash = typeof hash === "string" && hash ? hash : resultHash(job, "idempotent");
4925
+ if (status === "applied" || status === "already_applied") {
4926
+ return { protocol_version: "1.0", job_id: job.job_id, runner_id: config.runnerId, status: "already_applied", affected_rows: 0, result_hash, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
4927
+ }
4928
+ if (status === "conflict") return conflict(job, config, "IDEMPOTENT_CONFLICT", result_hash);
4929
+ if (status === "failed") return failed(job, config, "IDEMPOTENT_FAILED");
4930
+ return failed(job, config, "IDEMPOTENCY_RECEIPT_IN_PROGRESS");
4931
+ }
4221
4932
  async function recordReceipt(connection, job, status, hash) {
4222
- await connection.query(
4223
- `INSERT INTO synapsor_writeback_receipts (idempotency_key, job_id, proposal_id, status, result_hash, completed_at)
4224
- VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
4225
- ON DUPLICATE KEY UPDATE status = VALUES(status), result_hash = VALUES(result_hash), completed_at = VALUES(completed_at)`,
4226
- [job.idempotency_key, job.job_id, job.proposal_id, status, hash]
4933
+ const [result] = await connection.query(
4934
+ `UPDATE synapsor_writeback_receipts
4935
+ SET status = ?, result_hash = ?, completed_at = CURRENT_TIMESTAMP
4936
+ WHERE idempotency_key = ?`,
4937
+ [status, hash, job.idempotency_key]
4227
4938
  );
4939
+ if (result.affectedRows !== 1) throw new Error("IDEMPOTENCY_RECEIPT_UNAVAILABLE");
4228
4940
  }
4229
- function conflict(job, config, code) {
4230
- return { protocol_version: "1.0", job_id: job.job_id, runner_id: config.runnerId, status: "conflict", affected_rows: 0, error_code: code, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
4941
+ function conflict(job, config, code, result_hash) {
4942
+ return { protocol_version: "1.0", job_id: job.job_id, runner_id: config.runnerId, status: "conflict", affected_rows: 0, error_code: code, result_hash, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
4231
4943
  }
4232
4944
  function failed(job, config, code) {
4233
4945
  return { protocol_version: "1.0", job_id: job.job_id, runner_id: config.runnerId, status: "failed", error_code: code, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
@@ -4331,19 +5043,41 @@ function validatePostgresPatch(job) {
4331
5043
  function resultHash2(job, status, version) {
4332
5044
  return `sha256:${crypto3.createHash("sha256").update(JSON.stringify({ job_id: job.job_id, status, version })).digest("hex")}`;
4333
5045
  }
5046
+ function normalizeVersionValue2(value) {
5047
+ if (value instanceof Date) return normalizeVersionValue2(value.toISOString());
5048
+ const text = String(value ?? "").trim();
5049
+ const timestamp = text.match(/^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}:\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}(?::?\d{2})?)?$/i);
5050
+ if (timestamp) {
5051
+ const fraction = (timestamp[3] ?? "").padEnd(6, "0").slice(0, 6);
5052
+ const offset = timestamp[4];
5053
+ if (offset && offset.toUpperCase() !== "Z") {
5054
+ const dateParts = timestamp[1].split("-");
5055
+ const timeParts = timestamp[2].split(":");
5056
+ const year = Number(dateParts[0]);
5057
+ const month = Number(dateParts[1]);
5058
+ const day = Number(dateParts[2]);
5059
+ const hour = Number(timeParts[0]);
5060
+ const minute = Number(timeParts[1]);
5061
+ const second = Number(timeParts[2]);
5062
+ const utc = new Date(Date.UTC(year, month - 1, day, hour, minute, second) - offsetMinutes2(offset) * 6e4);
5063
+ return `${utc.getUTCFullYear()}-${pad22(utc.getUTCMonth() + 1)}-${pad22(utc.getUTCDate())} ${pad22(utc.getUTCHours())}:${pad22(utc.getUTCMinutes())}:${pad22(utc.getUTCSeconds())}.${fraction}`;
5064
+ }
5065
+ return `${timestamp[1]} ${timestamp[2]}.${fraction}`;
5066
+ }
5067
+ return text;
5068
+ }
5069
+ function offsetMinutes2(offset) {
5070
+ const sign = offset.startsWith("-") ? -1 : 1;
5071
+ const compact = offset.slice(1).replace(":", "");
5072
+ const hours = Number(compact.slice(0, 2));
5073
+ const minutes = Number(compact.slice(2, 4) || "0");
5074
+ return sign * (hours * 60 + minutes);
5075
+ }
5076
+ function pad22(value) {
5077
+ return String(value).padStart(2, "0");
5078
+ }
4334
5079
  function versionValuesMatch2(actual, expected) {
4335
- if (actual instanceof Date) {
4336
- const expectedDate = new Date(String(expected));
4337
- return !Number.isNaN(expectedDate.getTime()) && actual.getTime() === expectedDate.getTime();
4338
- }
4339
- if (typeof actual === "string" && typeof expected === "string") {
4340
- const actualDate = new Date(actual);
4341
- const expectedDate = new Date(expected);
4342
- if (!Number.isNaN(actualDate.getTime()) && !Number.isNaN(expectedDate.getTime())) {
4343
- return actualDate.getTime() === expectedDate.getTime();
4344
- }
4345
- }
4346
- return String(actual) === String(expected);
5080
+ return normalizeVersionValue2(actual) === normalizeVersionValue2(expected);
4347
5081
  }
4348
5082
  async function withTransaction(client, fn) {
4349
5083
  await client.query("BEGIN");
@@ -4375,55 +5109,7 @@ async function applyPostgresJob(job, config) {
4375
5109
  const pool = new Pool2({ connectionString: config.databaseUrl });
4376
5110
  const client = await pool.connect();
4377
5111
  try {
4378
- return await withTransaction(client, async () => {
4379
- await client.query(postgresReceiptMigration);
4380
- const receipt = await client.query(
4381
- "SELECT status, result_hash FROM synapsor_writeback_receipts WHERE idempotency_key = $1 FOR UPDATE",
4382
- [job.idempotency_key]
4383
- );
4384
- if (receipt.rowCount && receipt.rows[0]?.status === "applied") {
4385
- return {
4386
- protocol_version: "1.0",
4387
- job_id: job.job_id,
4388
- runner_id: config.runnerId,
4389
- status: "applied",
4390
- affected_rows: 0,
4391
- result_hash: receipt.rows[0]?.result_hash || resultHash2(job, "idempotent"),
4392
- completed_at: (/* @__PURE__ */ new Date()).toISOString()
4393
- };
4394
- }
4395
- const row = await client.query(
4396
- `SELECT * FROM ${quotePostgresIdentifier(job.target.schema)}.${quotePostgresIdentifier(job.target.table)}
4397
- WHERE ${quotePostgresIdentifier(job.target.primary_key.column)} = $1
4398
- AND ${quotePostgresIdentifier(job.target.tenant_guard.column)} = $2
4399
- FOR UPDATE`,
4400
- [job.target.primary_key.value, job.target.tenant_guard.value]
4401
- );
4402
- if (!row.rowCount) {
4403
- await recordReceipt2(client, job, "conflict", resultHash2(job, "ROW_NOT_FOUND"));
4404
- return conflict2(job, config, "ROW_NOT_FOUND");
4405
- }
4406
- if (job.conflict_guard.kind === "version_column" && !versionValuesMatch2(row.rows[0]?.[job.conflict_guard.column], job.conflict_guard.expected_value)) {
4407
- await recordReceipt2(client, job, "conflict", resultHash2(job, "VERSION_CONFLICT", row.rows[0]?.[job.conflict_guard.column]));
4408
- return conflict2(job, config, "VERSION_CONFLICT");
4409
- }
4410
- const update = buildPostgresUpdate(job);
4411
- const applied = await client.query(update.sql, update.values);
4412
- if (applied.rowCount !== 1) {
4413
- throw new Error(applied.rowCount === 0 ? "VERSION_CONFLICT" : "MULTI_ROW_WRITE_BLOCKED");
4414
- }
4415
- const hash = resultHash2(job, "applied", job.conflict_guard.kind === "version_column" ? job.conflict_guard.expected_value : void 0);
4416
- await recordReceipt2(client, job, "applied", hash);
4417
- return {
4418
- protocol_version: "1.0",
4419
- job_id: job.job_id,
4420
- runner_id: config.runnerId,
4421
- status: "applied",
4422
- affected_rows: 1,
4423
- result_hash: hash,
4424
- completed_at: (/* @__PURE__ */ new Date()).toISOString()
4425
- };
4426
- });
5112
+ return await applyPostgresJobWithClient(job, config, client);
4427
5113
  } catch (error) {
4428
5114
  const message = error instanceof Error ? error.message : "TRANSACTION_FAILED";
4429
5115
  return failed2(job, config, message.includes("MULTI_ROW") ? "MULTI_ROW_WRITE_BLOCKED" : message.includes("VERSION") ? "VERSION_CONFLICT" : "TRANSACTION_FAILED");
@@ -4432,17 +5118,93 @@ FOR UPDATE`,
4432
5118
  await pool.end();
4433
5119
  }
4434
5120
  }
5121
+ async function applyPostgresJobWithClient(job, config, client) {
5122
+ return await withTransaction(client, async () => {
5123
+ await client.query(postgresReceiptMigration);
5124
+ const existing = await claimReceipt2(client, job, config);
5125
+ if (existing) return existing;
5126
+ const conflictProjection = job.conflict_guard.kind === "version_column" ? `, ${quotePostgresIdentifier(job.conflict_guard.column)}::text AS "__synapsor_conflict_value"` : "";
5127
+ const row = await client.query(
5128
+ `SELECT *${conflictProjection} FROM ${quotePostgresIdentifier(job.target.schema)}.${quotePostgresIdentifier(job.target.table)}
5129
+ WHERE ${quotePostgresIdentifier(job.target.primary_key.column)} = $1
5130
+ AND ${quotePostgresIdentifier(job.target.tenant_guard.column)} = $2
5131
+ FOR UPDATE`,
5132
+ [job.target.primary_key.value, job.target.tenant_guard.value]
5133
+ );
5134
+ if (!row.rowCount) {
5135
+ const hash2 = resultHash2(job, "ROW_NOT_FOUND");
5136
+ await recordReceipt2(client, job, "conflict", hash2);
5137
+ return conflict2(job, config, "ROW_NOT_FOUND", hash2);
5138
+ }
5139
+ const currentVersion = row.rows[0]?.__synapsor_conflict_value ?? row.rows[0]?.[job.conflict_guard.kind === "version_column" ? job.conflict_guard.column : ""];
5140
+ if (job.conflict_guard.kind === "version_column" && !versionValuesMatch2(currentVersion, job.conflict_guard.expected_value)) {
5141
+ const hash2 = resultHash2(job, "VERSION_CONFLICT", currentVersion);
5142
+ await recordReceipt2(client, job, "conflict", hash2);
5143
+ return conflict2(job, config, "VERSION_CONFLICT", hash2);
5144
+ }
5145
+ const update = buildPostgresUpdate(job);
5146
+ const applied = await client.query(update.sql, update.values);
5147
+ if (applied.rowCount !== 1) {
5148
+ throw new Error(applied.rowCount === 0 ? "VERSION_CONFLICT" : "MULTI_ROW_WRITE_BLOCKED");
5149
+ }
5150
+ const hash = resultHash2(job, "applied", job.conflict_guard.kind === "version_column" ? job.conflict_guard.expected_value : void 0);
5151
+ await recordReceipt2(client, job, "applied", hash);
5152
+ return {
5153
+ protocol_version: "1.0",
5154
+ job_id: job.job_id,
5155
+ runner_id: config.runnerId,
5156
+ status: "applied",
5157
+ affected_rows: 1,
5158
+ result_hash: hash,
5159
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
5160
+ };
5161
+ });
5162
+ }
5163
+ async function claimReceipt2(client, job, config) {
5164
+ const claimed = await client.query(
5165
+ `INSERT INTO synapsor_writeback_receipts (idempotency_key, job_id, proposal_id, status, result_hash)
5166
+ VALUES ($1, $2, $3, 'in_progress', NULL)
5167
+ ON CONFLICT (idempotency_key) DO NOTHING
5168
+ RETURNING status, result_hash`,
5169
+ [job.idempotency_key, job.job_id, job.proposal_id]
5170
+ );
5171
+ if (claimed.rowCount === 1) return void 0;
5172
+ const receipt = await client.query(
5173
+ "SELECT status, result_hash FROM synapsor_writeback_receipts WHERE idempotency_key = $1 FOR UPDATE",
5174
+ [job.idempotency_key]
5175
+ );
5176
+ const row = receipt.rows[0];
5177
+ if (!row) return failed2(job, config, "IDEMPOTENCY_RECEIPT_UNAVAILABLE");
5178
+ return resultFromExistingReceipt2(job, config, row.status, row.result_hash);
5179
+ }
5180
+ function resultFromExistingReceipt2(job, config, status, hash) {
5181
+ const result_hash = typeof hash === "string" && hash ? hash : resultHash2(job, "idempotent");
5182
+ if (status === "applied" || status === "already_applied") {
5183
+ return {
5184
+ protocol_version: "1.0",
5185
+ job_id: job.job_id,
5186
+ runner_id: config.runnerId,
5187
+ status: "already_applied",
5188
+ affected_rows: 0,
5189
+ result_hash,
5190
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
5191
+ };
5192
+ }
5193
+ if (status === "conflict") return conflict2(job, config, "IDEMPOTENT_CONFLICT", result_hash);
5194
+ if (status === "failed") return failed2(job, config, "IDEMPOTENT_FAILED");
5195
+ return failed2(job, config, "IDEMPOTENCY_RECEIPT_IN_PROGRESS");
5196
+ }
4435
5197
  async function recordReceipt2(client, job, status, hash) {
4436
- await client.query(
4437
- `INSERT INTO synapsor_writeback_receipts (idempotency_key, job_id, proposal_id, status, result_hash, completed_at)
4438
- VALUES ($1, $2, $3, $4, $5, now())
4439
- ON CONFLICT (idempotency_key)
4440
- DO UPDATE SET status = EXCLUDED.status, result_hash = EXCLUDED.result_hash, completed_at = EXCLUDED.completed_at`,
4441
- [job.idempotency_key, job.job_id, job.proposal_id, status, hash]
5198
+ const result = await client.query(
5199
+ `UPDATE synapsor_writeback_receipts
5200
+ SET status = $2, result_hash = $3, completed_at = now()
5201
+ WHERE idempotency_key = $1`,
5202
+ [job.idempotency_key, status, hash]
4442
5203
  );
5204
+ if (result.rowCount !== 1) throw new Error("IDEMPOTENCY_RECEIPT_UNAVAILABLE");
4443
5205
  }
4444
- function conflict2(job, config, code) {
4445
- return { protocol_version: "1.0", job_id: job.job_id, runner_id: config.runnerId, status: "conflict", affected_rows: 0, error_code: code, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
5206
+ function conflict2(job, config, code, result_hash) {
5207
+ return { protocol_version: "1.0", job_id: job.job_id, runner_id: config.runnerId, status: "conflict", affected_rows: 0, error_code: code, result_hash, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
4446
5208
  }
4447
5209
  function failed2(job, config, code) {
4448
5210
  return { protocol_version: "1.0", job_id: job.job_id, runner_id: config.runnerId, status: "failed", error_code: code, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
@@ -5432,13 +6194,13 @@ function collectTools(input) {
5432
6194
  const tools2 = [];
5433
6195
  const seen = /* @__PURE__ */ new Set();
5434
6196
  function addTool(value, path4) {
5435
- if (!isRecord4(value)) return;
6197
+ if (!isRecord5(value)) return;
5436
6198
  const name = optionalString(value.name) ?? optionalString(value.tool_name) ?? optionalString(value.toolName);
5437
6199
  if (!name) return;
5438
6200
  const description = optionalString(value.description) ?? optionalString(value.title) ?? optionalString(value.summary) ?? "";
5439
6201
  const inputSchema = value.inputSchema ?? value.input_schema ?? value.parameters ?? value.schema ?? value.args_schema;
5440
6202
  const outputSchema = value.outputSchema ?? value.output_schema ?? value.resultSchema ?? value.result_schema ?? value.output;
5441
- const annotations = isRecord4(value.annotations) ? value.annotations : {};
6203
+ const annotations = isRecord5(value.annotations) ? value.annotations : {};
5442
6204
  const key = `${path4}:${name}`;
5443
6205
  if (seen.has(key)) return;
5444
6206
  seen.add(key);
@@ -5447,14 +6209,14 @@ function collectTools(input) {
5447
6209
  function visit(value, path4, depth) {
5448
6210
  if (depth > 8) return;
5449
6211
  if (Array.isArray(value)) {
5450
- if (value.every((item) => isRecord4(item) && typeof item.name === "string")) {
6212
+ if (value.every((item) => isRecord5(item) && typeof item.name === "string")) {
5451
6213
  value.forEach((item, index) => addTool(item, `${path4}[${index}]`));
5452
6214
  } else {
5453
6215
  value.forEach((item, index) => visit(item, `${path4}[${index}]`, depth + 1));
5454
6216
  }
5455
6217
  return;
5456
6218
  }
5457
- if (!isRecord4(value)) return;
6219
+ if (!isRecord5(value)) return;
5458
6220
  addTool(value, path4);
5459
6221
  for (const [key, child] of Object.entries(value)) {
5460
6222
  if (key === "tools" && Array.isArray(child)) {
@@ -5472,9 +6234,9 @@ function collectTools(input) {
5472
6234
  function collectSchemaPropertyNames(schema) {
5473
6235
  const names = /* @__PURE__ */ new Set();
5474
6236
  function visit(value, depth) {
5475
- if (depth > 10 || !isRecord4(value)) return;
6237
+ if (depth > 10 || !isRecord5(value)) return;
5476
6238
  const properties = value.properties;
5477
- if (isRecord4(properties)) {
6239
+ if (isRecord5(properties)) {
5478
6240
  for (const [name, propertySchema] of Object.entries(properties)) {
5479
6241
  names.add(name);
5480
6242
  visit(propertySchema, depth + 1);
@@ -5548,7 +6310,7 @@ function normalizeToken(value) {
5548
6310
  function optionalString(value) {
5549
6311
  return typeof value === "string" ? value : void 0;
5550
6312
  }
5551
- function isRecord4(value) {
6313
+ function isRecord5(value) {
5552
6314
  return typeof value === "object" && value !== null && !Array.isArray(value);
5553
6315
  }
5554
6316
  function safeStringify(value) {
@@ -5662,6 +6424,414 @@ async function startPolling(config, adapters2, signal) {
5662
6424
  }
5663
6425
  }
5664
6426
 
6427
+ // packages/dsl/dist/index.js
6428
+ var AgentDslError = class extends Error {
6429
+ line;
6430
+ column;
6431
+ code;
6432
+ constructor(line, column, code, message) {
6433
+ super(`${line}:${column} ${code}: ${message}`);
6434
+ this.line = line;
6435
+ this.column = column;
6436
+ this.code = code;
6437
+ this.name = "AgentDslError";
6438
+ }
6439
+ };
6440
+ function parseAgentDsl(source) {
6441
+ const blocks = parseBlocks(source);
6442
+ const contexts = [];
6443
+ const capabilities = [];
6444
+ const workflows = [];
6445
+ for (const block of blocks) {
6446
+ if (block.kind === "context")
6447
+ contexts.push(parseContextBlock(block));
6448
+ if (block.kind === "capability")
6449
+ capabilities.push(parseCapabilityBlock(block));
6450
+ if (block.kind === "workflow")
6451
+ workflows.push(parseWorkflowBlock(block));
6452
+ }
6453
+ return { contexts, capabilities, workflows };
6454
+ }
6455
+ function compileAgentDsl(source) {
6456
+ const ast = parseAgentDsl(source);
6457
+ const contexts = ast.contexts.map((context) => ({
6458
+ name: context.name,
6459
+ bindings: context.bindings,
6460
+ ...context.tenantBinding ? { tenant_binding: context.tenantBinding } : {},
6461
+ ...context.principalBinding ? { principal_binding: context.principalBinding } : {}
6462
+ }));
6463
+ const capabilities = ast.capabilities.map((capability) => {
6464
+ const spec = {
6465
+ name: capability.name,
6466
+ kind: capability.kind,
6467
+ context: capability.context,
6468
+ ...capability.source ? { source: capability.source } : {},
6469
+ subject: {
6470
+ schema: capability.schema,
6471
+ table: capability.table,
6472
+ primary_key: capability.primaryKey,
6473
+ ...capability.tenantKey ? { tenant_key: capability.tenantKey } : {},
6474
+ ...capability.conflictKey ? { conflict_key: capability.conflictKey } : {}
6475
+ },
6476
+ args: capability.args,
6477
+ ...capability.lookup ? { lookup: { id_from_arg: capability.lookup.arg } } : {},
6478
+ visible_fields: capability.visibleFields,
6479
+ ...capability.keptOutFields.length ? { kept_out_fields: capability.keptOutFields } : {},
6480
+ ...capability.evidenceRequired !== void 0 ? { evidence: { required: capability.evidenceRequired, query_audit: true } } : {},
6481
+ ...capability.maxRows ? { max_rows: capability.maxRows } : {}
6482
+ };
6483
+ if (capability.kind === "proposal" && capability.proposal) {
6484
+ spec.proposal = {
6485
+ action: capability.proposal.action,
6486
+ allowed_fields: capability.proposal.allowedFields,
6487
+ patch: capability.proposal.patch,
6488
+ conflict_guard: capability.conflictKey ? { column: capability.conflictKey } : { weak_guard_ack: true },
6489
+ approval: { mode: "human", required_role: capability.proposal.approvalRole ?? "local_reviewer" },
6490
+ writeback: {
6491
+ mode: capability.proposal.writebackMode ?? "direct_sql",
6492
+ ...capability.proposal.executor ? { executor: capability.proposal.executor } : {}
6493
+ }
6494
+ };
6495
+ }
6496
+ return spec;
6497
+ });
6498
+ const workflows = ast.workflows.map((workflow) => ({
6499
+ name: workflow.name,
6500
+ context: workflow.context,
6501
+ allowed_capabilities: workflow.allowedCapabilities,
6502
+ ...workflow.requiredEvidence !== void 0 ? { required_evidence: workflow.requiredEvidence } : {},
6503
+ ...workflow.approvalRole ? { approval: { required: true, role: workflow.approvalRole } } : {},
6504
+ ...workflow.checkpoint ? { replay: { checkpoint: workflow.checkpoint } } : {}
6505
+ }));
6506
+ const contract = {
6507
+ spec_version: "0.1",
6508
+ kind: "SynapsorContract",
6509
+ contexts,
6510
+ capabilities,
6511
+ ...workflows.length ? { workflows } : {}
6512
+ };
6513
+ assertValidContract(contract);
6514
+ return normalizeContract(contract);
6515
+ }
6516
+ function validateAgentDsl(source) {
6517
+ try {
6518
+ compileAgentDsl(source);
6519
+ return { ok: true, errors: [], warnings: [] };
6520
+ } catch (error) {
6521
+ if (error instanceof AgentDslError) {
6522
+ return {
6523
+ ok: false,
6524
+ errors: [{ line: error.line, column: error.column, code: error.code, message: error.message.replace(/^\d+:\d+ [A-Z_]+: /, "") }],
6525
+ warnings: []
6526
+ };
6527
+ }
6528
+ return {
6529
+ ok: false,
6530
+ errors: [{ line: 1, column: 1, code: "DSL_VALIDATION_FAILED", message: error instanceof Error ? error.message : String(error) }],
6531
+ warnings: []
6532
+ };
6533
+ }
6534
+ }
6535
+ function parseBlocks(source) {
6536
+ const lines = source.split(/\r?\n/);
6537
+ const blocks = [];
6538
+ let current;
6539
+ lines.forEach((rawLine, index) => {
6540
+ const lineNumber = index + 1;
6541
+ const stripped = rawLine.replace(/--.*$/, "").trim();
6542
+ if (!stripped)
6543
+ return;
6544
+ const contextMatch = stripped.match(/^CREATE\s+AGENT\s+CONTEXT\s+([A-Za-z_][A-Za-z0-9_.]*)$/i);
6545
+ const capabilityMatch = stripped.match(/^CREATE\s+(?:AGENT\s+)?CAPABILITY\s+([A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*)$/i);
6546
+ const workflowMatch = stripped.match(/^CREATE\s+AGENT\s+WORKFLOW\s+([A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*)$/i);
6547
+ if (contextMatch || capabilityMatch || workflowMatch) {
6548
+ if (current)
6549
+ throw dslError(lineNumber, 1, "BLOCK_NOT_CLOSED", "previous CREATE block must end with END before starting another block");
6550
+ if (contextMatch?.[1])
6551
+ current = { kind: "context", name: contextMatch[1], line: lineNumber, body: [] };
6552
+ else if (capabilityMatch?.[1])
6553
+ current = { kind: "capability", name: capabilityMatch[1], line: lineNumber, body: [] };
6554
+ else if (workflowMatch?.[1])
6555
+ current = { kind: "workflow", name: workflowMatch[1], line: lineNumber, body: [] };
6556
+ return;
6557
+ }
6558
+ if (/^END;?$/i.test(stripped)) {
6559
+ if (!current)
6560
+ throw dslError(lineNumber, 1, "END_WITHOUT_BLOCK", "END appeared without an open CREATE block");
6561
+ blocks.push(current);
6562
+ current = void 0;
6563
+ return;
6564
+ }
6565
+ if (!current)
6566
+ throw dslError(lineNumber, 1, "EXPECTED_CREATE", "expected CREATE AGENT CONTEXT, CREATE CAPABILITY, or CREATE AGENT WORKFLOW");
6567
+ current.body.push({ text: stripped.replace(/;$/, ""), line: lineNumber });
6568
+ });
6569
+ if (current)
6570
+ throw dslError(current.line, 1, "BLOCK_NOT_CLOSED", `${current.name} must end with END`);
6571
+ return blocks;
6572
+ }
6573
+ function parseContextBlock(block) {
6574
+ const context = { name: block.name, bindings: [] };
6575
+ for (const item of block.body) {
6576
+ const bind = item.text.match(/^BIND\s+([A-Za-z_][A-Za-z0-9_]*)\s+FROM\s+(SESSION|ENV|ENVIRONMENT|CLOUD_SESSION|STATIC_DEV|HTTP_CLAIM)\s+([A-Za-z0-9_.-]+)(?:\s+REQUIRED)?$/i);
6577
+ if (bind?.[1] && bind[2] && bind[3]) {
6578
+ const name = bind[1];
6579
+ const source = normalizeBindingSource(bind[2]);
6580
+ context.bindings.push({ name, source, key: bind[3], ...item.text.match(/\sREQUIRED$/i) ? { required: true } : {} });
6581
+ continue;
6582
+ }
6583
+ const tenant = item.text.match(/^TENANT\s+BINDING\s+([A-Za-z_][A-Za-z0-9_]*)$/i);
6584
+ if (tenant?.[1]) {
6585
+ context.tenantBinding = tenant[1];
6586
+ continue;
6587
+ }
6588
+ const principal = item.text.match(/^PRINCIPAL\s+BINDING\s+([A-Za-z_][A-Za-z0-9_]*)$/i);
6589
+ if (principal?.[1]) {
6590
+ context.principalBinding = principal[1];
6591
+ continue;
6592
+ }
6593
+ unsupported(item, "context");
6594
+ }
6595
+ if (context.bindings.length === 0)
6596
+ throw dslError(block.line, 1, "CONTEXT_BINDINGS_REQUIRED", "CREATE AGENT CONTEXT requires at least one BIND line");
6597
+ context.tenantBinding ??= context.bindings.find((binding) => binding.name === "tenant_id")?.name;
6598
+ context.principalBinding ??= context.bindings.find((binding) => binding.name === "principal")?.name;
6599
+ return context;
6600
+ }
6601
+ function parseCapabilityBlock(block) {
6602
+ const capability = {
6603
+ name: block.name,
6604
+ kind: "read",
6605
+ context: "",
6606
+ schema: "",
6607
+ table: "",
6608
+ primaryKey: "id",
6609
+ args: {},
6610
+ visibleFields: [],
6611
+ keptOutFields: []
6612
+ };
6613
+ for (const item of block.body) {
6614
+ const context = item.text.match(/^USING\s+CONTEXT\s+([A-Za-z_][A-Za-z0-9_.]*)$/i);
6615
+ if (context?.[1]) {
6616
+ capability.context = context[1];
6617
+ continue;
6618
+ }
6619
+ const source = item.text.match(/^SOURCE\s+([A-Za-z_][A-Za-z0-9_.-]*)$/i);
6620
+ if (source?.[1]) {
6621
+ capability.source = source[1];
6622
+ continue;
6623
+ }
6624
+ const on = item.text.match(/^ON\s+([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)$/i);
6625
+ if (on?.[1] && on[2]) {
6626
+ capability.schema = on[1];
6627
+ capability.table = on[2];
6628
+ continue;
6629
+ }
6630
+ const primary = item.text.match(/^PRIMARY\s+KEY\s+([A-Za-z_][A-Za-z0-9_]*)$/i);
6631
+ if (primary?.[1]) {
6632
+ capability.primaryKey = primary[1];
6633
+ continue;
6634
+ }
6635
+ const tenant = item.text.match(/^TENANT\s+KEY\s+([A-Za-z_][A-Za-z0-9_]*)$/i);
6636
+ if (tenant?.[1]) {
6637
+ capability.tenantKey = tenant[1];
6638
+ continue;
6639
+ }
6640
+ const conflict3 = item.text.match(/^CONFLICT\s+GUARD\s+([A-Za-z_][A-Za-z0-9_]*)$/i);
6641
+ if (conflict3?.[1]) {
6642
+ capability.conflictKey = conflict3[1];
6643
+ continue;
6644
+ }
6645
+ const arg = item.text.match(/^ARG\s+([A-Za-z_][A-Za-z0-9_]*)\s+(STRING|TEXT|NUMBER|BOOLEAN|BOOL)(?:\s+REQUIRED)?(?:\s+MAX\s+(\d+))?$/i);
6646
+ if (arg?.[1] && arg[2]) {
6647
+ capability.args[arg[1]] = {
6648
+ type: normalizeArgType(arg[2]),
6649
+ ...item.text.match(/\sREQUIRED(?:\s|$)/i) ? { required: true } : {},
6650
+ ...arg[3] ? { max_length: Number(arg[3]) } : {}
6651
+ };
6652
+ continue;
6653
+ }
6654
+ const lookup = item.text.match(/^LOOKUP\s+([A-Za-z_][A-Za-z0-9_]*)\s+BY\s+([A-Za-z_][A-Za-z0-9_]*)$/i);
6655
+ if (lookup?.[1] && lookup[2]) {
6656
+ capability.lookup = { arg: lookup[1], column: lookup[2] };
6657
+ capability.primaryKey = lookup[2];
6658
+ if (!capability.args[lookup[1]])
6659
+ capability.args[lookup[1]] = { type: "string", required: true, max_length: 128 };
6660
+ continue;
6661
+ }
6662
+ const read = item.text.match(/^ALLOW\s+READ\s+(.+)$/i);
6663
+ if (read?.[1]) {
6664
+ capability.visibleFields = parseList(read[1]);
6665
+ continue;
6666
+ }
6667
+ const keptOut = item.text.match(/^KEEP\s+OUT\s+(.+)$/i);
6668
+ if (keptOut?.[1]) {
6669
+ capability.keptOutFields = parseList(keptOut[1]);
6670
+ continue;
6671
+ }
6672
+ if (/^REQUIRE\s+EVIDENCE$/i.test(item.text)) {
6673
+ capability.evidenceRequired = true;
6674
+ continue;
6675
+ }
6676
+ const maxRows = item.text.match(/^MAX\s+ROWS\s+(\d+)$/i);
6677
+ if (maxRows?.[1]) {
6678
+ capability.maxRows = Number(maxRows[1]);
6679
+ continue;
6680
+ }
6681
+ const propose2 = item.text.match(/^PROPOSE\s+ACTION\s+([A-Za-z_][A-Za-z0-9_.]*)$/i);
6682
+ if (propose2?.[1]) {
6683
+ capability.kind = "proposal";
6684
+ capability.proposal = { action: propose2[1], allowedFields: [], patch: {} };
6685
+ continue;
6686
+ }
6687
+ const allowWrite = item.text.match(/^ALLOW\s+WRITE\s+(.+)$/i);
6688
+ if (allowWrite?.[1]) {
6689
+ ensureProposal(capability, item);
6690
+ capability.proposal.allowedFields = parseList(allowWrite[1]);
6691
+ continue;
6692
+ }
6693
+ const patch = item.text.match(/^PATCH\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$/i);
6694
+ if (patch?.[1] && patch[2]) {
6695
+ ensureProposal(capability, item);
6696
+ capability.proposal.patch[patch[1]] = parsePatchBinding(patch[2]);
6697
+ if (!capability.proposal.allowedFields.includes(patch[1]))
6698
+ capability.proposal.allowedFields.push(patch[1]);
6699
+ continue;
6700
+ }
6701
+ const approval = item.text.match(/^APPROVAL\s+ROLE\s+([A-Za-z_][A-Za-z0-9_.-]*)$/i);
6702
+ if (approval?.[1]) {
6703
+ ensureProposal(capability, item);
6704
+ capability.proposal.approvalRole = approval[1];
6705
+ continue;
6706
+ }
6707
+ 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);
6708
+ if (writeback2?.[1]) {
6709
+ ensureProposal(capability, item);
6710
+ capability.proposal.writebackMode = normalizeWritebackMode(writeback2[1]);
6711
+ if (writeback2[2])
6712
+ capability.proposal.executor = writeback2[2];
6713
+ continue;
6714
+ }
6715
+ unsupported(item, "capability");
6716
+ }
6717
+ if (!capability.context)
6718
+ throw dslError(block.line, 1, "CAPABILITY_CONTEXT_REQUIRED", `${block.name} requires USING CONTEXT`);
6719
+ if (!capability.schema || !capability.table)
6720
+ throw dslError(block.line, 1, "CAPABILITY_SUBJECT_REQUIRED", `${block.name} requires ON schema.table`);
6721
+ if (!capability.tenantKey)
6722
+ throw dslError(block.line, 1, "CAPABILITY_TENANT_REQUIRED", `${block.name} requires TENANT KEY for 0.1 DSL`);
6723
+ if (capability.visibleFields.length === 0)
6724
+ throw dslError(block.line, 1, "CAPABILITY_VISIBLE_FIELDS_REQUIRED", `${block.name} requires ALLOW READ`);
6725
+ if (Object.keys(capability.args).length === 0 && capability.lookup)
6726
+ capability.args[capability.lookup.arg] = { type: "string", required: true, max_length: 128 };
6727
+ if (Object.keys(capability.args).length === 0)
6728
+ throw dslError(block.line, 1, "CAPABILITY_ARGS_REQUIRED", `${block.name} requires ARG or LOOKUP`);
6729
+ if (capability.kind === "proposal" && (!capability.proposal || Object.keys(capability.proposal.patch).length === 0))
6730
+ throw dslError(block.line, 1, "PROPOSAL_PATCH_REQUIRED", `${block.name} proposal requires at least one PATCH line`);
6731
+ return capability;
6732
+ }
6733
+ function parseWorkflowBlock(block) {
6734
+ const workflow = { name: block.name, context: "", allowedCapabilities: [] };
6735
+ for (const item of block.body) {
6736
+ const context = item.text.match(/^USING\s+CONTEXT\s+([A-Za-z_][A-Za-z0-9_.]*)$/i);
6737
+ if (context?.[1]) {
6738
+ workflow.context = context[1];
6739
+ continue;
6740
+ }
6741
+ const capability = item.text.match(/^ALLOW\s+CAPABILITY\s+([A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*)$/i);
6742
+ if (capability?.[1]) {
6743
+ workflow.allowedCapabilities.push(capability[1]);
6744
+ continue;
6745
+ }
6746
+ if (/^REQUIRE\s+EVIDENCE$/i.test(item.text)) {
6747
+ workflow.requiredEvidence = true;
6748
+ continue;
6749
+ }
6750
+ const approval = item.text.match(/^APPROVAL\s+REQUIRED\s+ROLE\s+([A-Za-z_][A-Za-z0-9_.-]*)$/i);
6751
+ if (approval?.[1]) {
6752
+ workflow.approvalRole = approval[1];
6753
+ continue;
6754
+ }
6755
+ const checkpoint = item.text.match(/^CHECKPOINT\s+(NONE|EVERY\s+STEP|PROPOSAL\s+ONLY)$/i);
6756
+ if (checkpoint?.[1]) {
6757
+ workflow.checkpoint = checkpoint[1].toLowerCase().replace(/\s+/g, "_");
6758
+ continue;
6759
+ }
6760
+ unsupported(item, "workflow");
6761
+ }
6762
+ if (!workflow.context)
6763
+ throw dslError(block.line, 1, "WORKFLOW_CONTEXT_REQUIRED", `${block.name} requires USING CONTEXT`);
6764
+ if (workflow.allowedCapabilities.length === 0)
6765
+ throw dslError(block.line, 1, "WORKFLOW_CAPABILITIES_REQUIRED", `${block.name} requires ALLOW CAPABILITY`);
6766
+ return workflow;
6767
+ }
6768
+ function ensureProposal(capability, item) {
6769
+ if (!capability.proposal)
6770
+ throw dslError(item.line, 1, "PROPOSAL_ACTION_REQUIRED", "proposal clauses require PROPOSE ACTION first");
6771
+ }
6772
+ function parsePatchBinding(raw) {
6773
+ const trimmed = raw.trim();
6774
+ const arg = trimmed.match(/^ARG\s+([A-Za-z_][A-Za-z0-9_]*)$/i);
6775
+ if (arg?.[1])
6776
+ return { from_arg: arg[1] };
6777
+ if (/^NULL$/i.test(trimmed))
6778
+ return { fixed: null };
6779
+ if (/^TRUE$/i.test(trimmed))
6780
+ return { fixed: true };
6781
+ if (/^FALSE$/i.test(trimmed))
6782
+ return { fixed: false };
6783
+ if (/^-?\d+(?:\.\d+)?$/.test(trimmed))
6784
+ return { fixed: Number(trimmed) };
6785
+ const quoted = trimmed.match(/^'(.*)'$/);
6786
+ if (quoted)
6787
+ return { fixed: quoted[1] ?? "" };
6788
+ return { fixed: trimmed };
6789
+ }
6790
+ function normalizeBindingSource(source) {
6791
+ const normalized = source.toUpperCase();
6792
+ if (normalized === "ENV")
6793
+ return "environment";
6794
+ if (normalized === "ENVIRONMENT")
6795
+ return "environment";
6796
+ if (normalized === "SESSION")
6797
+ return "session";
6798
+ if (normalized === "CLOUD_SESSION")
6799
+ return "cloud_session";
6800
+ if (normalized === "STATIC_DEV")
6801
+ return "static_dev";
6802
+ return "http_claim";
6803
+ }
6804
+ function normalizeArgType(type) {
6805
+ const normalized = type.toUpperCase();
6806
+ if (normalized === "NUMBER")
6807
+ return "number";
6808
+ if (normalized === "BOOLEAN" || normalized === "BOOL")
6809
+ return "boolean";
6810
+ return "string";
6811
+ }
6812
+ function normalizeWritebackMode(mode) {
6813
+ const normalized = mode.toUpperCase().replace(/\s+/g, "_");
6814
+ if (normalized === "DIRECT_SQL")
6815
+ return "direct_sql";
6816
+ if (normalized === "APP_HANDLER")
6817
+ return "app_handler";
6818
+ if (normalized === "CLOUD_WORKER")
6819
+ return "cloud_worker";
6820
+ return "none";
6821
+ }
6822
+ function parseList(value) {
6823
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
6824
+ }
6825
+ function unsupported(item, blockKind) {
6826
+ if (/ROOT\s+EXTERNAL|JOIN\s+EXTERNAL|RETURN\s+ANSWER|AUTO\s+BRANCH|AUTO\s+MERGE/i.test(item.text)) {
6827
+ throw dslError(item.line, 1, "UNSUPPORTED_PREVIEW_SYNTAX", `${blockKind} clause is not supported by @synapsor/dsl 0.1 preview: ${item.text}`);
6828
+ }
6829
+ throw dslError(item.line, 1, "UNSUPPORTED_DSL_CLAUSE", `unsupported ${blockKind} clause: ${item.text}`);
6830
+ }
6831
+ function dslError(line, column, code, message) {
6832
+ return new AgentDslError(line, column, code, message);
6833
+ }
6834
+
5665
6835
  // apps/runner/src/local-ui.ts
5666
6836
  import crypto4 from "node:crypto";
5667
6837
  import fs2 from "node:fs/promises";
@@ -5814,7 +6984,7 @@ async function handleRequest(input) {
5814
6984
  async function readRunnerConfig(configPath) {
5815
6985
  const raw = await fs2.readFile(configPath, "utf8");
5816
6986
  const parsed = JSON.parse(raw);
5817
- if (!isRecord5(parsed)) throw new Error("runner config must be a JSON object");
6987
+ if (!isRecord6(parsed)) throw new Error("runner config must be a JSON object");
5818
6988
  return parsed;
5819
6989
  }
5820
6990
  function buildSummary(config, configPath, storePath) {
@@ -5983,7 +7153,7 @@ async function readJsonBody(request) {
5983
7153
  }
5984
7154
  if (chunks.length === 0) return {};
5985
7155
  const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
5986
- if (!isRecord5(parsed)) throw new Error("request body must be a JSON object");
7156
+ if (!isRecord6(parsed)) throw new Error("request body must be a JSON object");
5987
7157
  return parsed;
5988
7158
  }
5989
7159
  function hasValidCsrf(request, csrfToken) {
@@ -6489,7 +7659,7 @@ init().catch((error) => {
6489
7659
  }
6490
7660
  function redactSecrets(value, key = "") {
6491
7661
  if (Array.isArray(value)) return value.map((item) => redactSecrets(item));
6492
- if (isRecord5(value)) {
7662
+ if (isRecord6(value)) {
6493
7663
  return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [entryKey, redactSecrets(entryValue, entryKey)]));
6494
7664
  }
6495
7665
  if (typeof value === "string") {
@@ -6506,11 +7676,11 @@ function escapeScriptString(value) {
6506
7676
  function isLocalHost(host) {
6507
7677
  return host === "127.0.0.1" || host === "localhost" || host === "::1";
6508
7678
  }
6509
- function isRecord5(value) {
7679
+ function isRecord6(value) {
6510
7680
  return typeof value === "object" && value !== null && !Array.isArray(value);
6511
7681
  }
6512
7682
  function asRecord(value) {
6513
- return isRecord5(value) ? value : {};
7683
+ return isRecord6(value) ? value : {};
6514
7684
  }
6515
7685
  function stringOrDefault(value, fallback) {
6516
7686
  return typeof value === "string" && value.trim() ? value.trim() : fallback;
@@ -6856,6 +8026,8 @@ ${cliCommandName()} --help
6856
8026
  if (command === "init") return init(rest);
6857
8027
  if (command === "inspect") return inspect(rest);
6858
8028
  if (command === "config") return configCommand(rest);
8029
+ if (command === "contract") return contractCommand(rest);
8030
+ if (command === "dsl") return dslCommand(rest);
6859
8031
  if (command === "doctor") return doctor(rest);
6860
8032
  if (command === "validate") return validate(rest);
6861
8033
  if (command === "apply") return apply(rest);
@@ -7333,7 +8505,7 @@ function isScriptedOnboardingArgs(args) {
7333
8505
  return args.includes("--yes") || args.includes("--non-interactive") || args.includes("--dry-run") || Boolean(optionalArg(args, "--answers")) || Boolean(optionalArg(args, "--inspection-json")) || Boolean(optionalArg(args, "--table"));
7334
8506
  }
7335
8507
  function answersToSelectionSpec(raw) {
7336
- if (!isRecord6(raw)) throw new Error("--answers file must contain a JSON object");
8508
+ if (!isRecord7(raw)) throw new Error("--answers file must contain a JSON object");
7337
8509
  const mode = stringValue(raw.mode) ?? "review";
7338
8510
  if (!["read_only", "shadow", "review"].includes(mode)) throw new Error("answers.mode must be read_only, shadow, or review");
7339
8511
  const engine = stringValue(raw.engine) ?? "postgres";
@@ -8002,6 +9174,182 @@ async function configCommand(args) {
8002
9174
  usage();
8003
9175
  return 2;
8004
9176
  }
9177
+ async function contractCommand(args) {
9178
+ const [subcommand, ...rest] = args;
9179
+ if (subcommand === "validate") return contractValidate(rest);
9180
+ if (subcommand === "normalize") return contractNormalize(rest);
9181
+ if (subcommand === "bundle") return contractBundle(rest);
9182
+ usage(["contract"]);
9183
+ return 2;
9184
+ }
9185
+ async function dslCommand(args) {
9186
+ const [subcommand, ...rest] = args;
9187
+ if (subcommand === "validate") return dslValidate(rest);
9188
+ if (subcommand === "compile") return dslCompile(rest);
9189
+ usage(["dsl"]);
9190
+ return 2;
9191
+ }
9192
+ async function dslValidate(args) {
9193
+ const target2 = firstPositional(args);
9194
+ if (!target2) throw new Error("dsl validate requires <contract.synapsor>");
9195
+ const source = await fs3.readFile(target2, "utf8");
9196
+ const result = validateAgentDsl(source);
9197
+ if (args.includes("--json")) {
9198
+ process2.stdout.write(`${JSON.stringify(result, null, 2)}
9199
+ `);
9200
+ } else if (result.ok) {
9201
+ process2.stdout.write(`dsl valid: ${target2}
9202
+ `);
9203
+ } else {
9204
+ process2.stdout.write(`dsl invalid: ${target2}
9205
+ `);
9206
+ for (const error of result.errors) process2.stdout.write(`error ${error.line}:${error.column} ${error.code}: ${error.message}
9207
+ `);
9208
+ }
9209
+ return result.ok ? 0 : 1;
9210
+ }
9211
+ async function dslCompile(args) {
9212
+ const target2 = firstPositional(args);
9213
+ if (!target2) throw new Error("dsl compile requires <contract.synapsor>");
9214
+ const source = await fs3.readFile(target2, "utf8");
9215
+ const contract = compileAgentDsl(source);
9216
+ const output = outputArg(args);
9217
+ const text = `${JSON.stringify(contract, null, 2)}
9218
+ `;
9219
+ if (output) {
9220
+ await fs3.writeFile(output, text, "utf8");
9221
+ process2.stdout.write(`wrote contract: ${output}
9222
+ `);
9223
+ } else {
9224
+ process2.stdout.write(text);
9225
+ }
9226
+ return 0;
9227
+ }
9228
+ async function contractValidate(args) {
9229
+ const target2 = firstPositional(args);
9230
+ if (!target2) throw new Error("contract validate requires <synapsor.contract.json>");
9231
+ const parsed = JSON.parse(await fs3.readFile(target2, "utf8"));
9232
+ const result = validateContract(parsed);
9233
+ if (args.includes("--json")) {
9234
+ process2.stdout.write(`${JSON.stringify(result, null, 2)}
9235
+ `);
9236
+ } else if (result.ok) {
9237
+ process2.stdout.write(`contract valid: ${target2}
9238
+ `);
9239
+ for (const warning of result.warnings) process2.stdout.write(`warning ${warning.path} ${warning.code}: ${warning.message}
9240
+ `);
9241
+ } else {
9242
+ process2.stdout.write(`contract invalid: ${target2}
9243
+ `);
9244
+ for (const error of result.errors) process2.stdout.write(`error ${error.path} ${error.code}: ${error.message}
9245
+ `);
9246
+ }
9247
+ return result.ok ? 0 : 1;
9248
+ }
9249
+ async function contractNormalize(args) {
9250
+ const target2 = firstPositional(args);
9251
+ if (!target2) throw new Error("contract normalize requires <synapsor.contract.json>");
9252
+ const parsed = JSON.parse(await fs3.readFile(target2, "utf8"));
9253
+ const normalized = normalizeContract(parsed);
9254
+ const output = outputArg(args);
9255
+ const text = `${JSON.stringify(normalized, null, 2)}
9256
+ `;
9257
+ if (output) {
9258
+ await fs3.writeFile(output, text, "utf8");
9259
+ process2.stdout.write(`wrote normalized contract: ${output}
9260
+ `);
9261
+ } else {
9262
+ process2.stdout.write(text);
9263
+ }
9264
+ return 0;
9265
+ }
9266
+ async function contractBundle(args) {
9267
+ const target2 = firstPositional(args);
9268
+ if (!target2) throw new Error("contract bundle requires <synapsor.contract.json>");
9269
+ const outDir = outputArg(args) ?? "synapsor-runner-bundle";
9270
+ const parsed = JSON.parse(await fs3.readFile(target2, "utf8"));
9271
+ const contract = normalizeContract(parsed);
9272
+ const firstCapability = contract.capabilities[0];
9273
+ const firstSource = firstCapability?.source ?? "local_postgres";
9274
+ const engine = inferContractBundleEngine(contract);
9275
+ const readUrlEnv = engine === "mysql" ? "SYNAPSOR_DATABASE_READ_URL" : "SYNAPSOR_DATABASE_READ_URL";
9276
+ const runnerConfig = {
9277
+ version: 1,
9278
+ mode: contract.capabilities.some((capability) => capability.kind === "proposal") ? "review" : "read_only",
9279
+ result_format: 2,
9280
+ storage: { sqlite_path: "./.synapsor/local.db" },
9281
+ contracts: ["./synapsor.contract.json"],
9282
+ sources: {
9283
+ [firstSource]: {
9284
+ engine,
9285
+ read_url_env: readUrlEnv,
9286
+ statement_timeout_ms: 3e3
9287
+ }
9288
+ }
9289
+ };
9290
+ await fs3.mkdir(outDir, { recursive: true });
9291
+ await fs3.mkdir(path3.join(outDir, "mcp-client-examples"), { recursive: true });
9292
+ await fs3.writeFile(path3.join(outDir, "synapsor.contract.json"), `${JSON.stringify(contract, null, 2)}
9293
+ `, "utf8");
9294
+ await fs3.writeFile(path3.join(outDir, "synapsor.runner.json"), `${JSON.stringify(runnerConfig, null, 2)}
9295
+ `, "utf8");
9296
+ await fs3.writeFile(path3.join(outDir, ".env.example"), bundleEnvExample(contract, readUrlEnv, engine), "utf8");
9297
+ await fs3.writeFile(path3.join(outDir, "README.md"), bundleReadme(contract), "utf8");
9298
+ await fs3.writeFile(path3.join(outDir, "mcp-client-examples", "generic-stdio.json"), `${JSON.stringify({
9299
+ command: cliCommandName(),
9300
+ args: ["mcp", "serve", "--config", "./synapsor.runner.json", "--store", "./.synapsor/local.db"]
9301
+ }, null, 2)}
9302
+ `, "utf8");
9303
+ process2.stdout.write(`created runner bundle: ${outDir}
9304
+ `);
9305
+ process2.stdout.write("No database URLs, write credentials, tokens, or customer rows were included.\n");
9306
+ return 0;
9307
+ }
9308
+ function inferContractBundleEngine(contract) {
9309
+ const engine = contract.resources?.find((resource) => resource.engine === "postgres" || resource.engine === "mysql")?.engine;
9310
+ return engine === "mysql" ? "mysql" : "postgres";
9311
+ }
9312
+ function bundleEnvExample(contract, readUrlEnv, engine) {
9313
+ const context = contract.contexts[0];
9314
+ const tenantBinding = context?.bindings.find((binding) => binding.name === context.tenant_binding) ?? context?.bindings.find((binding) => binding.name === "tenant_id");
9315
+ const principalBinding = context?.bindings.find((binding) => binding.name === context.principal_binding) ?? context?.bindings.find((binding) => binding.name === "principal");
9316
+ return [
9317
+ "# Synapsor Runner bundle environment.",
9318
+ "# Fill these locally. Do not commit real values.",
9319
+ `# Set ${readUrlEnv} to your read-only ${engine === "mysql" ? "MySQL" : "Postgres"} URL.`,
9320
+ `${readUrlEnv}=`,
9321
+ `${tenantBinding?.key ?? "SYNAPSOR_TENANT_ID"}=acme`,
9322
+ `${principalBinding?.key ?? "SYNAPSOR_PRINCIPAL"}=local_operator`,
9323
+ ""
9324
+ ].join("\n");
9325
+ }
9326
+ function bundleReadme(contract) {
9327
+ const contractName = contract.metadata?.name ?? "Synapsor contract";
9328
+ return [
9329
+ `# ${contractName} Runner Bundle`,
9330
+ "",
9331
+ "This bundle lets you run a Cloud/exported Synapsor contract locally with Synapsor Runner.",
9332
+ "",
9333
+ "It includes:",
9334
+ "",
9335
+ "- `synapsor.contract.json`: canonical Synapsor contract;",
9336
+ "- `synapsor.runner.json`: local runtime wiring with env-var placeholders;",
9337
+ "- `.env.example`: placeholder runtime values only;",
9338
+ "- `mcp-client-examples/`: client snippets with command paths only.",
9339
+ "",
9340
+ "It does not include database passwords, write credentials, bearer tokens, or table rows.",
9341
+ "",
9342
+ "## Run Locally",
9343
+ "",
9344
+ "```bash",
9345
+ "cp .env.example .env",
9346
+ "# edit .env, then export the values in your shell",
9347
+ `${cliCommandName()} contract validate ./synapsor.contract.json`,
9348
+ `${cliCommandName()} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db`,
9349
+ "```",
9350
+ ""
9351
+ ].join("\n");
9352
+ }
8005
9353
  async function configValidate(args) {
8006
9354
  const configPath = optionalArg(args, "--config") ?? "synapsor.runner.json";
8007
9355
  const parsed = JSON.parse(await fs3.readFile(configPath, "utf8"));
@@ -8334,7 +9682,7 @@ function looksLikeRecipePath(value) {
8334
9682
  return value.endsWith(".json") || value.includes("/") || value.includes("\\") || value.startsWith(".");
8335
9683
  }
8336
9684
  function normalizeRecipe(value, source) {
8337
- if (!isRecord6(value)) throw new Error(`recipe ${source} must be a JSON object`);
9685
+ if (!isRecord7(value)) throw new Error(`recipe ${source} must be a JSON object`);
8338
9686
  const recipe = {
8339
9687
  id: requiredString(value, "id", source),
8340
9688
  title: requiredString(value, "title", source),
@@ -8369,7 +9717,7 @@ function requiredStringArray(value, key, source) {
8369
9717
  }
8370
9718
  function requiredRecord(value, key, source) {
8371
9719
  const item = value[key];
8372
- if (!isRecord6(item)) throw new Error(`recipe ${source} requires object ${key}`);
9720
+ if (!isRecord7(item)) throw new Error(`recipe ${source} requires object ${key}`);
8373
9721
  return item;
8374
9722
  }
8375
9723
  async function doctor(args = []) {
@@ -8640,7 +9988,7 @@ async function localDoctor(args) {
8640
9988
  await inspectConfiguredSource({ config: parsed, sourceName, source, checks });
8641
9989
  }
8642
9990
  for (const [executorName, executor] of Object.entries(parsed.executors ?? {})) {
8643
- if (!isRecord6(executor)) continue;
9991
+ if (!isRecord7(executor)) continue;
8644
9992
  if (executor.type === "http_handler") {
8645
9993
  const urlEnv = String(executor.url_env ?? "");
8646
9994
  if (urlEnv) {
@@ -8657,7 +10005,7 @@ async function localDoctor(args) {
8657
10005
  });
8658
10006
  }
8659
10007
  }
8660
- const auth = isRecord6(executor.auth) ? executor.auth : void 0;
10008
+ const auth = isRecord7(executor.auth) ? executor.auth : void 0;
8661
10009
  const tokenEnv = typeof auth?.token_env === "string" ? auth.token_env : void 0;
8662
10010
  if (tokenEnv) checks.push(envPresenceCheck(tokenEnv, `${tokenEnv} is required for http_handler executor ${executorName} bearer auth.`));
8663
10011
  const signingSecretEnv = typeof executor.signing_secret_env === "string" ? executor.signing_secret_env : void 0;
@@ -8874,6 +10222,28 @@ async function localDoctorStoreStats(storePath) {
8874
10222
  }
8875
10223
  }
8876
10224
  async function validate(args) {
10225
+ const target2 = firstPositional(args);
10226
+ if (target2) {
10227
+ const parsed = JSON.parse(await fs3.readFile(target2, "utf8"));
10228
+ if (isSynapsorContractLike(parsed)) {
10229
+ const result = validateContract(parsed);
10230
+ if (args.includes("--json")) {
10231
+ process2.stdout.write(`${JSON.stringify(result, null, 2)}
10232
+ `);
10233
+ } else if (result.ok) {
10234
+ process2.stdout.write(`contract valid: ${target2}
10235
+ `);
10236
+ for (const warning of result.warnings) process2.stdout.write(`warning ${warning.path} ${warning.code}: ${warning.message}
10237
+ `);
10238
+ } else {
10239
+ process2.stdout.write(`contract invalid: ${target2}
10240
+ `);
10241
+ for (const error of result.errors) process2.stdout.write(`error ${error.path} ${error.code}: ${error.message}
10242
+ `);
10243
+ }
10244
+ return result.ok ? 0 : 1;
10245
+ }
10246
+ }
8877
10247
  const job = await readJob(args);
8878
10248
  parseWritebackJob(job);
8879
10249
  process2.stdout.write("job valid\n");
@@ -9035,7 +10405,7 @@ function proposalExecutorName(proposal, capability) {
9035
10405
  }
9036
10406
  function executorConfig(config, executorName) {
9037
10407
  const raw = config.executors?.[executorName];
9038
- if (!isRecord6(raw)) throw new Error(`executor ${executorName} is not configured`);
10408
+ if (!isRecord7(raw)) throw new Error(`executor ${executorName} is not configured`);
9039
10409
  if (raw.type === "http_handler") return raw;
9040
10410
  if (raw.type === "command_handler") return raw;
9041
10411
  if (raw.type === "sql_update") return { type: "sql_update" };
@@ -9193,7 +10563,7 @@ function alreadyAppliedReceipt(receipt, runnerId) {
9193
10563
  };
9194
10564
  }
9195
10565
  function handlerReceiptFromBody(input) {
9196
- const body = isRecord6(input.body) ? input.body : {};
10566
+ const body = isRecord7(input.body) ? input.body : {};
9197
10567
  const rawStatus = String(body.status ?? "failed");
9198
10568
  const status = handlerReceiptStatuses.has(rawStatus) ? rawStatus : "failed";
9199
10569
  const rowsAffected = Number.isInteger(body.rows_affected) && Number(body.rows_affected) >= 0 ? Number(body.rows_affected) : status === "applied" && !input.dryRun ? 1 : 0;
@@ -9640,10 +11010,10 @@ function formatUpWritebackLines(config) {
9640
11010
  function formatUpHandlerLines(config) {
9641
11011
  const lines = [];
9642
11012
  for (const [name, executor] of Object.entries(config.executors ?? {})) {
9643
- if (!isRecord6(executor)) continue;
11013
+ if (!isRecord7(executor)) continue;
9644
11014
  if (executor.type === "http_handler") {
9645
11015
  const urlEnv = typeof executor.url_env === "string" ? executor.url_env : "";
9646
- const auth = isRecord6(executor.auth) ? executor.auth : void 0;
11016
+ const auth = isRecord7(executor.auth) ? executor.auth : void 0;
9647
11017
  const tokenEnv = typeof auth?.token_env === "string" ? auth.token_env : void 0;
9648
11018
  const signingSecretEnv = typeof executor.signing_secret_env === "string" ? executor.signing_secret_env : void 0;
9649
11019
  lines.push(` - ${name}: http_handler`);
@@ -9689,9 +11059,62 @@ async function runnerCommand(args) {
9689
11059
  async function cloud(args) {
9690
11060
  const [subcommand, ...rest] = args;
9691
11061
  if (subcommand === "connect") return cloudConnect(rest);
11062
+ if (subcommand === "push") return cloudPush(rest);
9692
11063
  usage();
9693
11064
  return 2;
9694
11065
  }
11066
+ async function cloudPush(args) {
11067
+ const target2 = firstPositional(args);
11068
+ if (!target2) throw new Error("cloud push requires <synapsor.contract.json>");
11069
+ const parsed = JSON.parse(await fs3.readFile(target2, "utf8"));
11070
+ const contract = normalizeContract(parsed);
11071
+ const payload = {
11072
+ schema_version: "synapsor.cloud-contract-push.v0.1",
11073
+ contract,
11074
+ summary: contractSummary(contract),
11075
+ workspace: optionalArg(args, "--workspace") ?? process2.env.SYNAPSOR_WORKSPACE_ID,
11076
+ name: optionalArg(args, "--name") ?? contract.metadata?.name,
11077
+ pushed_at: (/* @__PURE__ */ new Date()).toISOString()
11078
+ };
11079
+ const dryRun = args.includes("--dry-run");
11080
+ if (args.includes("--json")) {
11081
+ process2.stdout.write(`${JSON.stringify({ ok: dryRun, dry_run: dryRun, payload }, null, 2)}
11082
+ `);
11083
+ } else {
11084
+ process2.stdout.write("Synapsor Cloud contract push preview\n");
11085
+ process2.stdout.write(`Contract: ${target2}
11086
+ `);
11087
+ process2.stdout.write(`Contexts: ${payload.summary.contexts}
11088
+ `);
11089
+ process2.stdout.write(`Capabilities: ${payload.summary.capabilities}
11090
+ `);
11091
+ process2.stdout.write(`Workflows: ${payload.summary.workflows}
11092
+ `);
11093
+ process2.stdout.write(`Proposal capabilities: ${payload.summary.proposal_capabilities}
11094
+ `);
11095
+ process2.stdout.write(`Kept-out fields: ${payload.summary.kept_out_fields}
11096
+ `);
11097
+ }
11098
+ if (dryRun) {
11099
+ if (!args.includes("--json")) process2.stdout.write("Dry run only. No Cloud upload attempted.\n");
11100
+ return 0;
11101
+ }
11102
+ const apiUrl = optionalArg(args, "--api-url") ?? process2.env.SYNAPSOR_CLOUD_BASE_URL;
11103
+ const token = optionalArg(args, "--token") ?? process2.env.SYNAPSOR_CLOUD_TOKEN ?? process2.env.SYNAPSOR_RUNNER_TOKEN;
11104
+ if (!apiUrl || !token) {
11105
+ throw new Error("cloud push upload requires --dry-run, or real --api-url plus --token/SYNAPSOR_CLOUD_TOKEN after the Cloud registry endpoint is available.");
11106
+ }
11107
+ throw new Error("cloud push upload is not wired to a Cloud registry endpoint yet. Use --dry-run for local validation; do not treat this as uploaded.");
11108
+ }
11109
+ function contractSummary(contract) {
11110
+ return {
11111
+ contexts: contract.contexts.length,
11112
+ capabilities: contract.capabilities.length,
11113
+ workflows: contract.workflows?.length ?? 0,
11114
+ proposal_capabilities: contract.capabilities.filter((capability) => capability.kind === "proposal").length,
11115
+ kept_out_fields: contract.capabilities.reduce((count, capability) => count + (capability.kept_out_fields?.length ?? 0), 0)
11116
+ };
11117
+ }
9695
11118
  async function cloudConnect(args) {
9696
11119
  const configPath = optionalArg(args, "--config") ?? process2.env.SYNAPSOR_MCP_CONFIG ?? "synapsor.cloud.json";
9697
11120
  const parsed = JSON.parse(await fs3.readFile(configPath, "utf8"));
@@ -10733,12 +12156,12 @@ async function proposalInput(args, capability) {
10733
12156
  if (selected > 1) throw new Error("propose accepts only one of --sample, --input, or --json");
10734
12157
  if (jsonInput) {
10735
12158
  const parsed = JSON.parse(jsonInput);
10736
- if (!isRecord6(parsed)) throw new Error("propose --json must be a JSON object");
12159
+ if (!isRecord7(parsed)) throw new Error("propose --json must be a JSON object");
10737
12160
  return parsed;
10738
12161
  }
10739
12162
  if (inputPath) {
10740
12163
  const parsed = JSON.parse(await fs3.readFile(inputPath, "utf8"));
10741
- if (!isRecord6(parsed)) throw new Error("propose --input must point to a JSON object");
12164
+ if (!isRecord7(parsed)) throw new Error("propose --input must point to a JSON object");
10742
12165
  return parsed;
10743
12166
  }
10744
12167
  if (sample) return sampleInputForCapability(capability);
@@ -11042,7 +12465,7 @@ The MCP server needs a reviewed config before it can expose semantic tools.
11042
12465
  Fix:
11043
12466
  Run ${cliCommandName()} onboard db --from-env DATABASE_URL, or pass --config <path>.`);
11044
12467
  }
11045
- const parsed = JSON.parse(await fs3.readFile(configPath, "utf8"));
12468
+ const parsed = await readRuntimeConfig(configPath);
11046
12469
  const runtime = createMcpRuntime(parsed, { storePath });
11047
12470
  try {
11048
12471
  const tools2 = runtime.listTools();
@@ -11131,12 +12554,12 @@ async function smokeInputFromArgs(args, capability) {
11131
12554
  if (selected > 1) throw new Error("smoke call accepts only one of --sample, --input, or --json");
11132
12555
  if (jsonInput) {
11133
12556
  const parsed = JSON.parse(jsonInput);
11134
- if (!isRecord6(parsed)) throw new Error("smoke call --json must be a JSON object");
12557
+ if (!isRecord7(parsed)) throw new Error("smoke call --json must be a JSON object");
11135
12558
  return parsed;
11136
12559
  }
11137
12560
  if (inputPath) {
11138
12561
  const parsed = JSON.parse(await fs3.readFile(inputPath, "utf8"));
11139
- if (!isRecord6(parsed)) throw new Error("smoke call --input must point to a JSON object");
12562
+ if (!isRecord7(parsed)) throw new Error("smoke call --input must point to a JSON object");
11140
12563
  return parsed;
11141
12564
  }
11142
12565
  if (sample && capability) return sampleInputForCapability(capability);
@@ -11211,13 +12634,13 @@ async function writeMcpClientSnippet(destination, client, snippet, yes) {
11211
12634
  function mergeMcpClientSnippet(client, existing, snippet) {
11212
12635
  if (client === "generic" || client === "generic-stdio") return snippet;
11213
12636
  if (client === "claude-desktop" || client === "cursor") {
11214
- const existingServers = isRecord6(existing.mcpServers) ? existing.mcpServers : {};
11215
- const snippetServers = isRecord6(snippet.mcpServers) ? snippet.mcpServers : {};
12637
+ const existingServers = isRecord7(existing.mcpServers) ? existing.mcpServers : {};
12638
+ const snippetServers = isRecord7(snippet.mcpServers) ? snippet.mcpServers : {};
11216
12639
  return { ...existing, mcpServers: { ...existingServers, ...snippetServers } };
11217
12640
  }
11218
12641
  if (client === "vscode") {
11219
- const existingServers = isRecord6(existing.servers) ? existing.servers : {};
11220
- const snippetServers = isRecord6(snippet.servers) ? snippet.servers : {};
12642
+ const existingServers = isRecord7(existing.servers) ? existing.servers : {};
12643
+ const snippetServers = isRecord7(snippet.servers) ? snippet.servers : {};
11221
12644
  return { ...existing, servers: { ...existingServers, ...snippetServers } };
11222
12645
  }
11223
12646
  throw new Error(`unsupported MCP client: ${client}`);
@@ -11315,7 +12738,7 @@ async function recipesInit(args) {
11315
12738
  process2.stdout.write("Review the generated table and column names against your staging database before serving MCP tools.\n");
11316
12739
  return 0;
11317
12740
  }
11318
- function isRecord6(value) {
12741
+ function isRecord7(value) {
11319
12742
  return typeof value === "object" && value !== null && !Array.isArray(value);
11320
12743
  }
11321
12744
  async function benchmark(args) {
@@ -11561,7 +12984,7 @@ function builtInMcpAuditExample(example) {
11561
12984
  throw new Error(`unknown audit example: ${example}. Available examples: dangerous-db-mcp`);
11562
12985
  }
11563
12986
  function isRunnerConfigLike(value) {
11564
- return isRecord6(value) && value.version === 1 && Array.isArray(value.capabilities);
12987
+ return isRecord7(value) && value.version === 1 && Array.isArray(value.capabilities);
11565
12988
  }
11566
12989
  async function fetchRemoteMcpTools(target2, args, timeoutMs) {
11567
12990
  const bearerEnv = optionalArg(args, "--bearer-env") ?? "SYNAPSOR_MCP_AUDIT_BEARER";
@@ -11770,7 +13193,7 @@ async function shadowRecordHumanAction(args) {
11770
13193
  const patchPath = optionalArg(args, "--patch");
11771
13194
  if (!patchPath) throw new Error("shadow record-human-action requires --patch <human-action.json>");
11772
13195
  const patch = JSON.parse(await fs3.readFile(patchPath, "utf8"));
11773
- if (!isRecord6(patch)) throw new Error("shadow human-action patch must be a JSON object");
13196
+ if (!isRecord7(patch)) throw new Error("shadow human-action patch must be a JSON object");
11774
13197
  const store = await openLocalStore(args);
11775
13198
  try {
11776
13199
  const action = store.recordShadowHumanAction(proposalId, {
@@ -12842,8 +14265,10 @@ function missingLocalStoreError(storePath) {
12842
14265
  ].join("\n"));
12843
14266
  }
12844
14267
  async function readRuntimeConfig(configPath) {
12845
- const parsed = JSON.parse(await fs3.readFile(configPath, "utf8"));
12846
- return parsed;
14268
+ return loadRuntimeConfigFromFile(configPath);
14269
+ }
14270
+ function isSynapsorContractLike(value) {
14271
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value) && (value.kind === "SynapsorContract" || value.spec_version === "0.1");
12847
14272
  }
12848
14273
  function envWithDemoDefaults(config, configPath) {
12849
14274
  if (!isReferenceDemoConfig(config, configPath)) return process2.env;
@@ -13282,16 +14707,16 @@ function formatEvidenceDetail(evidence2) {
13282
14707
  `;
13283
14708
  }
13284
14709
  function formatEvidenceItem(item, index) {
13285
- const payload = isRecord6(item.item) ? item.item : item;
13286
- const visibleRow = isRecord6(payload.visible_row) ? payload.visible_row : payload;
14710
+ const payload = isRecord7(item.item) ? item.item : item;
14711
+ const visibleRow = isRecord7(payload.visible_row) ? payload.visible_row : payload;
13287
14712
  const title = stringField(payload, "kind") ?? "item";
13288
- const primaryKey = isRecord6(payload.primary_key) ? payload.primary_key : void 0;
14713
+ const primaryKey = isRecord7(payload.primary_key) ? payload.primary_key : void 0;
13289
14714
  const heading = primaryKey ? `* ${title} ${formatScalar(primaryKey.value)}` : `* ${title} ${index}`;
13290
14715
  const rows = Object.entries(visibleRow).filter(([key]) => !["kind", "source_id", "table", "primary_key", "tenant"].includes(key)).flatMap(([key, value]) => formatEvidenceFieldLines(key, value)).slice(0, 12);
13291
14716
  return [heading, ...rows.length ? rows : [" (no scalar preview fields)"]];
13292
14717
  }
13293
14718
  function formatEvidenceFieldLines(key, value) {
13294
- if (isRecord6(value)) {
14719
+ if (isRecord7(value)) {
13295
14720
  const nested = Object.entries(value).filter(([, nestedValue]) => nestedValue === null || ["string", "number", "boolean"].includes(typeof nestedValue)).slice(0, 6).map(([nestedKey, nestedValue]) => ` ${key}.${nestedKey}: ${formatScalar(nestedValue)}`);
13296
14721
  return nested.length ? nested : [` ${key}: [object]`];
13297
14722
  }
@@ -13353,7 +14778,7 @@ function formatQueryAuditFirstLook(row, storeSuffix) {
13353
14778
  ].join("\n");
13354
14779
  }
13355
14780
  function formatQueryAuditDetail(row) {
13356
- const payload = isRecord6(row.payload) ? row.payload : {};
14781
+ const payload = isRecord7(row.payload) ? row.payload : {};
13357
14782
  return [
13358
14783
  `Query audit: ${row.audit_id}`,
13359
14784
  `Created at: ${row.created_at}`,
@@ -13504,7 +14929,7 @@ function formatReplayMarkdown(replay2) {
13504
14929
  const approvalEvents = replay2.events.filter((event) => /approved|rejected|canceled/i.test(event.kind));
13505
14930
  const evidenceLines = replay2.evidence.length > 0 ? replay2.evidence.flatMap((evidence2) => {
13506
14931
  const record = evidence2;
13507
- const payload = isRecord6(record.payload) ? record.payload : {};
14932
+ const payload = isRecord7(record.payload) ? record.payload : {};
13508
14933
  const sourceId = stringField(payload, "source_id") ?? proposal.source_id;
13509
14934
  const table = stringField(payload, "target") ?? `${proposal.source_schema}.${proposal.source_table}`;
13510
14935
  const queryFingerprint = stringField(payload, "query_fingerprint") ?? proposal.change_set.evidence.query_fingerprint;
@@ -13820,7 +15245,7 @@ function formatMcpAuditMarkdown(report) {
13820
15245
  `;
13821
15246
  }
13822
15247
  function stringField(record, key) {
13823
- if (!isRecord6(record)) return void 0;
15248
+ if (!isRecord7(record)) return void 0;
13824
15249
  const value = record[key];
13825
15250
  if (typeof value === "string") return value;
13826
15251
  if (typeof value === "number") return String(value);
@@ -14039,6 +15464,8 @@ function isKnownTopLevelCommand(command) {
14039
15464
  "init",
14040
15465
  "inspect",
14041
15466
  "config",
15467
+ "contract",
15468
+ "dsl",
14042
15469
  "doctor",
14043
15470
  "validate",
14044
15471
  "apply",
@@ -14091,6 +15518,9 @@ Commands:
14091
15518
  up Bring up local review mode guidance/server
14092
15519
  init Generate a Synapsor capability contract
14093
15520
  mcp Serve safe semantic tools over MCP
15521
+ contract Validate and normalize canonical Synapsor contract files
15522
+ dsl Compile SQL-like Synapsor authoring DSL to contract JSON
15523
+ cloud Register runner metadata or dry-run contract push to Cloud
14094
15524
  onboard One-command own-database setup
14095
15525
  smoke Test generated tool calls before wiring an MCP client
14096
15526
  tools List model-facing MCP tools and aliases
@@ -14116,12 +15546,40 @@ Examples:
14116
15546
  ${cmd} onboard db --from-env DATABASE_URL
14117
15547
  ${cmd} inspect --from-env DATABASE_URL
14118
15548
  ${cmd} init --wizard --from-env DATABASE_URL
15549
+ ${cmd} contract validate ./synapsor.contract.json
15550
+ ${cmd} contract normalize ./synapsor.contract.json --out ./synapsor.contract.normalized.json
15551
+ ${cmd} dsl compile ./contract.synapsor --out ./synapsor.contract.json
15552
+ ${cmd} cloud push ./synapsor.contract.json --dry-run
14119
15553
  ${cmd} smoke call --config ./synapsor.runner.json --store ./.synapsor/local.db
14120
15554
  ${cmd} tools list --aliases --config ./synapsor.runner.json --store ./.synapsor/local.db
14121
15555
  ${cmd} handler template node-fastify --output ./synapsor-writeback-handler.mjs
14122
15556
  ${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db
14123
15557
  ${cmd} propose billing.propose_late_fee_waiver --sample
14124
15558
  ${cmd} audit ./synapsor.runner.json
15559
+ `,
15560
+ contract: `Usage:
15561
+ ${cmd} contract validate ./synapsor.contract.json [--json]
15562
+ ${cmd} contract normalize ./synapsor.contract.json [--out ./synapsor.contract.normalized.json]
15563
+
15564
+ Validate or normalize canonical Synapsor contract files. Contracts describe
15565
+ contexts, capabilities, workflows, evidence, proposal, receipt, and replay
15566
+ semantics. Local database URLs, ports, and store paths stay in runner config.
15567
+ `,
15568
+ dsl: `Usage:
15569
+ ${cmd} dsl validate ./contract.synapsor [--json]
15570
+ ${cmd} dsl compile ./contract.synapsor --out ./synapsor.contract.json
15571
+
15572
+ Compile the preview SQL-like Synapsor authoring DSL into canonical
15573
+ @synapsor/spec JSON. Unsupported Cloud-only/generated clauses fail explicitly
15574
+ instead of being ignored.
15575
+ `,
15576
+ cloud: `Usage:
15577
+ ${cmd} cloud connect --config ./synapsor.cloud.json
15578
+ ${cmd} cloud push ./synapsor.contract.json --dry-run [--workspace <id>] [--name <registry-name>]
15579
+
15580
+ cloud push validates and normalizes the contract locally, then prints the
15581
+ payload summary. Upload is intentionally not reported as successful until a
15582
+ real Cloud registry endpoint is wired.
14125
15583
  `,
14126
15584
  up: `Usage:
14127
15585
  ${cmd} up --config ./synapsor.runner.json --store ./.synapsor/local.db [--transport stdio|streamable-http]