@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/CHANGELOG.md +11 -0
- package/README.md +67 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/runner.mjs +1621 -163
- package/docs/README.md +3 -0
- package/docs/conformance.md +91 -0
- package/docs/migrating-to-synapsor-spec.md +191 -0
- package/docs/production.md +289 -0
- package/docs/release-notes.md +11 -0
- package/examples/app-owned-writeback/command-handler.mjs +0 -0
- package/examples/mcp-postgres-billing-app-handler/scripts/run-demo.sh +0 -0
- package/examples/reference-support-billing-app/scripts/run-demo.sh +0 -0
- package/examples/support-billing-agent/scripts/run-demo.sh +0 -0
- package/package.json +6 -7
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
|
-
|
|
2724
|
-
|
|
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 (!
|
|
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,
|
|
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 =
|
|
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) =>
|
|
3205
|
-
return
|
|
3858
|
+
const request = payload.find((message) => isRecord4(message) && "id" in message);
|
|
3859
|
+
return isRecord4(request) ? request.id ?? null : null;
|
|
3206
3860
|
}
|
|
3207
|
-
return
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3617
|
-
const target2 =
|
|
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:
|
|
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 =
|
|
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:
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
4182
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
[
|
|
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
|
-
|
|
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
|
|
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
|
-
`
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
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 (!
|
|
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 =
|
|
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) =>
|
|
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 (!
|
|
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 || !
|
|
6237
|
+
if (depth > 10 || !isRecord5(value)) return;
|
|
5476
6238
|
const properties = value.properties;
|
|
5477
|
-
if (
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
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
|
|
7679
|
+
function isRecord6(value) {
|
|
6510
7680
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6511
7681
|
}
|
|
6512
7682
|
function asRecord(value) {
|
|
6513
|
-
return
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
11215
|
-
const snippetServers =
|
|
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 =
|
|
11220
|
-
const snippetServers =
|
|
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
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
12846
|
-
|
|
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 =
|
|
13286
|
-
const visibleRow =
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
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]
|