@synapsor/runner 0.1.6 → 0.1.8
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 +52 -1
- package/README.md +39 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/runner.mjs +736 -132
- package/docs/capability-authoring.md +68 -5
- package/docs/conformance.md +11 -0
- package/docs/local-mode.md +22 -0
- package/docs/migrating-to-synapsor-spec.md +1 -1
- package/docs/release-notes.md +30 -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
|
@@ -440,7 +440,7 @@ function normalizeRecord(value) {
|
|
|
440
440
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
441
441
|
}
|
|
442
442
|
function sleep(ms) {
|
|
443
|
-
return new Promise((
|
|
443
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
444
444
|
}
|
|
445
445
|
|
|
446
446
|
// packages/config/src/index.ts
|
|
@@ -479,6 +479,7 @@ var CAPABILITY_KEYS = /* @__PURE__ */ new Set([
|
|
|
479
479
|
"transition_guards",
|
|
480
480
|
"conflict_guard",
|
|
481
481
|
"approval",
|
|
482
|
+
"writeback",
|
|
482
483
|
"single_tenant_dev_ack"
|
|
483
484
|
]);
|
|
484
485
|
var TARGET_KEYS = /* @__PURE__ */ new Set(["schema", "table", "primary_key", "tenant_key", "single_tenant_dev"]);
|
|
@@ -489,6 +490,8 @@ var NUMERIC_BOUND_KEYS = /* @__PURE__ */ new Set(["minimum", "maximum"]);
|
|
|
489
490
|
var TRANSITION_GUARD_KEYS = /* @__PURE__ */ new Set(["from_column", "allowed"]);
|
|
490
491
|
var CONFLICT_GUARD_KEYS = /* @__PURE__ */ new Set(["column", "weak_guard_ack"]);
|
|
491
492
|
var APPROVAL_KEYS = /* @__PURE__ */ new Set(["mode", "required_role"]);
|
|
493
|
+
var WRITEBACK_KEYS = /* @__PURE__ */ new Set(["mode", "executor"]);
|
|
494
|
+
var WRITEBACK_MODES = /* @__PURE__ */ new Set(["direct_sql", "app_handler", "cloud_worker", "none"]);
|
|
492
495
|
var MODEL_CONTROLLED_TRUST_FIELDS = /* @__PURE__ */ new Set([
|
|
493
496
|
"tenant_id",
|
|
494
497
|
"tenantId",
|
|
@@ -653,9 +656,7 @@ function validateWritebackReadiness(sources, capabilities, mode, errors, warning
|
|
|
653
656
|
const sourceName = isNonEmptyString(capability.source) ? capability.source : void 0;
|
|
654
657
|
const source = sourceName ? sources[sourceName] : void 0;
|
|
655
658
|
if (!sourceName || !isRecord(source)) return;
|
|
656
|
-
|
|
657
|
-
const directSql = executor === "sql_update";
|
|
658
|
-
if (!directSql) return;
|
|
659
|
+
if (capabilityWritebackMode(capability) !== "direct_sql") return;
|
|
659
660
|
if (source.read_only === true) {
|
|
660
661
|
errors.push({
|
|
661
662
|
path: `$.capabilities[${index}].executor`,
|
|
@@ -673,6 +674,13 @@ function validateWritebackReadiness(sources, capabilities, mode, errors, warning
|
|
|
673
674
|
}
|
|
674
675
|
});
|
|
675
676
|
}
|
|
677
|
+
function capabilityWritebackMode(capability) {
|
|
678
|
+
if (isRecord(capability.writeback) && typeof capability.writeback.mode === "string" && WRITEBACK_MODES.has(capability.writeback.mode)) {
|
|
679
|
+
return capability.writeback.mode;
|
|
680
|
+
}
|
|
681
|
+
if (isNonEmptyString(capability.executor) && capability.executor !== "sql_update") return "app_handler";
|
|
682
|
+
return "direct_sql";
|
|
683
|
+
}
|
|
676
684
|
function validateCloud(value, mode, strict, errors) {
|
|
677
685
|
if (mode !== "cloud") {
|
|
678
686
|
if (value !== void 0) {
|
|
@@ -882,6 +890,7 @@ function validateExecutorAuth(value, path4, strict, errors) {
|
|
|
882
890
|
function validateCapabilities(value, sources, contexts, executors, mode, strict, errors, warnings, hasContracts = false) {
|
|
883
891
|
if (mode === "cloud" && value === void 0) return;
|
|
884
892
|
if (hasContracts && value === void 0) return;
|
|
893
|
+
if (hasContracts && Array.isArray(value) && value.length === 0) return;
|
|
885
894
|
if (!Array.isArray(value) || value.length === 0) {
|
|
886
895
|
errors.push({ path: "$.capabilities", code: "CAPABILITIES_REQUIRED", message: "At least one capability is required." });
|
|
887
896
|
return;
|
|
@@ -889,7 +898,21 @@ function validateCapabilities(value, sources, contexts, executors, mode, strict,
|
|
|
889
898
|
const sourceNames = isRecord(sources) ? new Set(Object.keys(sources)) : /* @__PURE__ */ new Set();
|
|
890
899
|
const contextNames = isRecord(contexts) ? new Set(Object.keys(contexts)) : /* @__PURE__ */ new Set();
|
|
891
900
|
const executorNames = isRecord(executors) ? new Set(Object.keys(executors)) : /* @__PURE__ */ new Set();
|
|
901
|
+
const capabilityNames = /* @__PURE__ */ new Map();
|
|
892
902
|
value.forEach((capability, index) => validateCapability(capability, index, sourceNames, contextNames, executorNames, strict, errors, warnings));
|
|
903
|
+
value.forEach((capability, index) => {
|
|
904
|
+
if (!isRecord(capability) || !isQualifiedName(capability.name)) return;
|
|
905
|
+
const previous = capabilityNames.get(capability.name);
|
|
906
|
+
if (previous !== void 0) {
|
|
907
|
+
errors.push({
|
|
908
|
+
path: `$.capabilities[${index}].name`,
|
|
909
|
+
code: "DUPLICATE_CAPABILITY_NAME",
|
|
910
|
+
message: `Capability ${capability.name} is already defined at $.capabilities[${previous}]. Capability names must be unique.`
|
|
911
|
+
});
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
capabilityNames.set(capability.name, index);
|
|
915
|
+
});
|
|
893
916
|
}
|
|
894
917
|
function validateCapability(value, index, sourceNames, contextNames, executorNames, strict, errors, warnings) {
|
|
895
918
|
const path4 = `$.capabilities[${index}]`;
|
|
@@ -923,6 +946,7 @@ function validateCapability(value, index, sourceNames, contextNames, executorNam
|
|
|
923
946
|
errors.push({ path: `${path4}.executor`, code: "UNKNOWN_EXECUTOR", message: "Capability executor must be sql_update or reference a configured executor." });
|
|
924
947
|
}
|
|
925
948
|
}
|
|
949
|
+
validateCapabilityWriteback(value, path4, executorNames, strict, errors);
|
|
926
950
|
validateTarget(value.target, `${path4}.target`, strict, errors, warnings);
|
|
927
951
|
validateArgs(value.args, `${path4}.args`, strict, errors);
|
|
928
952
|
validateLookup(value.lookup, `${path4}.lookup`, strict, errors);
|
|
@@ -932,6 +956,40 @@ function validateCapability(value, index, sourceNames, contextNames, executorNam
|
|
|
932
956
|
}
|
|
933
957
|
if (value.kind === "proposal") {
|
|
934
958
|
validateProposalCapability(value, path4, strict, errors);
|
|
959
|
+
} else if (value.writeback !== void 0) {
|
|
960
|
+
errors.push({ path: `${path4}.writeback`, code: "WRITEBACK_ONLY_FOR_PROPOSAL", message: "writeback is only valid on proposal capabilities." });
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
function validateCapabilityWriteback(capability, path4, executorNames, strict, errors) {
|
|
964
|
+
if (capability.writeback === void 0) return;
|
|
965
|
+
if (!isRecord(capability.writeback)) {
|
|
966
|
+
errors.push({ path: `${path4}.writeback`, code: "WRITEBACK_NOT_OBJECT", message: "writeback must be an object." });
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
if (strict) checkUnknownKeys(capability.writeback, WRITEBACK_KEYS, `${path4}.writeback`, errors);
|
|
970
|
+
if (!WRITEBACK_MODES.has(String(capability.writeback.mode))) {
|
|
971
|
+
errors.push({ path: `${path4}.writeback.mode`, code: "INVALID_WRITEBACK_MODE", message: "writeback.mode must be direct_sql, app_handler, cloud_worker, or none." });
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
const mode = String(capability.writeback.mode);
|
|
975
|
+
const executor = isNonEmptyString(capability.writeback.executor) ? capability.writeback.executor : isNonEmptyString(capability.executor) ? capability.executor : void 0;
|
|
976
|
+
if (mode === "direct_sql") {
|
|
977
|
+
if (executor !== void 0 && executor !== "sql_update") {
|
|
978
|
+
errors.push({ path: `${path4}.writeback.executor`, code: "WRITEBACK_EXECUTOR_MISMATCH", message: "direct_sql writeback cannot name an app-owned executor." });
|
|
979
|
+
}
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
if (mode === "none") {
|
|
983
|
+
if (executor !== void 0) {
|
|
984
|
+
errors.push({ path: `${path4}.writeback.executor`, code: "WRITEBACK_EXECUTOR_MISMATCH", message: "WRITEBACK NONE must not name an executor." });
|
|
985
|
+
}
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
if (mode === "cloud_worker") return;
|
|
989
|
+
if (!executor || executor === "sql_update") {
|
|
990
|
+
errors.push({ path: `${path4}.writeback.executor`, code: "WRITEBACK_EXECUTOR_REQUIRED", message: "app_handler writeback must name a configured executor." });
|
|
991
|
+
} else if (!executorNames.has(executor)) {
|
|
992
|
+
errors.push({ path: `${path4}.writeback.executor`, code: "UNKNOWN_EXECUTOR", message: "app_handler writeback executor must reference a configured executor." });
|
|
935
993
|
}
|
|
936
994
|
}
|
|
937
995
|
function validateTarget(value, path4, strict, errors, warnings) {
|
|
@@ -1250,6 +1308,8 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
|
1250
1308
|
|
|
1251
1309
|
// packages/proposal-store/src/index.ts
|
|
1252
1310
|
import { DatabaseSync } from "node:sqlite";
|
|
1311
|
+
import { mkdirSync } from "node:fs";
|
|
1312
|
+
import { dirname, resolve } from "node:path";
|
|
1253
1313
|
var ProposalStoreError = class extends Error {
|
|
1254
1314
|
constructor(code, message) {
|
|
1255
1315
|
super(message);
|
|
@@ -1263,6 +1323,9 @@ var ProposalStore = class {
|
|
|
1263
1323
|
path;
|
|
1264
1324
|
constructor(path4 = ":memory:") {
|
|
1265
1325
|
this.path = path4;
|
|
1326
|
+
if (path4 !== ":memory:") {
|
|
1327
|
+
mkdirSync(dirname(resolve(path4)), { recursive: true });
|
|
1328
|
+
}
|
|
1266
1329
|
this.db = new DatabaseSync(path4);
|
|
1267
1330
|
this.migrate();
|
|
1268
1331
|
}
|
|
@@ -1952,6 +2015,10 @@ var ProposalStore = class {
|
|
|
1952
2015
|
if (changeSet.writeback.mode !== "trusted_worker_required") {
|
|
1953
2016
|
throw new ProposalStoreError("WRITEBACK_NOT_REQUIRED", `proposal ${proposalId} uses ${changeSet.writeback.mode}`);
|
|
1954
2017
|
}
|
|
2018
|
+
const writebackExecutor = changeSet.writeback.executor;
|
|
2019
|
+
if (typeof writebackExecutor === "string" && writebackExecutor !== "sql_update" && writebackExecutor !== "trusted_worker_required") {
|
|
2020
|
+
throw new ProposalStoreError("WRITEBACK_NOT_DIRECT_SQL", `proposal ${proposalId} uses app-owned or non-local writeback executor ${writebackExecutor}`);
|
|
2021
|
+
}
|
|
1955
2022
|
if (changeSet.source.kind !== "external_postgres" && changeSet.source.kind !== "external_mysql") {
|
|
1956
2023
|
throw new ProposalStoreError("WRITEBACK_TARGET_NOT_EXTERNAL", `proposal ${proposalId} targets ${changeSet.source.kind}`);
|
|
1957
2024
|
}
|
|
@@ -2746,16 +2813,18 @@ var METADATA_KEYS = /* @__PURE__ */ new Set(["name", "description", "version", "
|
|
|
2746
2813
|
var RESOURCE_KEYS = /* @__PURE__ */ new Set(["name", "engine", "schema", "table", "type", "primary_key", "tenant_key", "conflict_key", "single_tenant_dev"]);
|
|
2747
2814
|
var CONTEXT_KEYS2 = /* @__PURE__ */ new Set(["name", "description", "bindings", "tenant_binding", "principal_binding"]);
|
|
2748
2815
|
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"]);
|
|
2816
|
+
var CAPABILITY_KEYS2 = /* @__PURE__ */ new Set(["name", "description", "returns_hint", "kind", "context", "source", "subject", "args", "lookup", "visible_fields", "kept_out_fields", "evidence", "max_rows", "proposal"]);
|
|
2750
2817
|
var SUBJECT_KEYS = /* @__PURE__ */ new Set(["resource", "schema", "table", "primary_key", "tenant_key", "conflict_key", "single_tenant_dev"]);
|
|
2751
2818
|
var ARG_KEYS2 = /* @__PURE__ */ new Set(["type", "description", "required", "max_length", "minimum", "maximum", "enum"]);
|
|
2752
2819
|
var LOOKUP_KEYS2 = /* @__PURE__ */ new Set(["id_from_arg"]);
|
|
2753
2820
|
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"]);
|
|
2821
|
+
var PROPOSAL_KEYS = /* @__PURE__ */ new Set(["action", "allowed_fields", "patch", "numeric_bounds", "transition_guards", "conflict_guard", "approval", "writeback"]);
|
|
2755
2822
|
var PATCH_KEYS = /* @__PURE__ */ new Set(["fixed", "from_arg"]);
|
|
2823
|
+
var NUMERIC_BOUND_KEYS2 = /* @__PURE__ */ new Set(["minimum", "maximum"]);
|
|
2824
|
+
var TRANSITION_GUARD_KEYS2 = /* @__PURE__ */ new Set(["from_column", "allowed"]);
|
|
2756
2825
|
var CONFLICT_GUARD_KEYS2 = /* @__PURE__ */ new Set(["column", "weak_guard_ack"]);
|
|
2757
2826
|
var APPROVAL_KEYS2 = /* @__PURE__ */ new Set(["mode", "required_role"]);
|
|
2758
|
-
var
|
|
2827
|
+
var WRITEBACK_KEYS2 = /* @__PURE__ */ new Set(["mode", "executor", "idempotency_key"]);
|
|
2759
2828
|
var WORKFLOW_KEYS = /* @__PURE__ */ new Set(["name", "description", "context", "allowed_capabilities", "required_evidence", "approval", "settlement", "replay"]);
|
|
2760
2829
|
var POLICY_KEYS = /* @__PURE__ */ new Set(["name", "kind", "mode", "rules"]);
|
|
2761
2830
|
var EVIDENCE_RECORD_KEYS = /* @__PURE__ */ new Set(["handle", "capability", "query_fingerprint", "items"]);
|
|
@@ -2931,6 +3000,8 @@ function validateCapabilities2(value, contextNames, resourceNames, errors, warni
|
|
|
2931
3000
|
errors.push({ path: `${path4}.name`, code: "INVALID_CAPABILITY_NAME", message: "capability name must be namespace.name." });
|
|
2932
3001
|
else
|
|
2933
3002
|
addUnique(names, capability.name, `${path4}.name`, "DUPLICATE_CAPABILITY_NAME", errors);
|
|
3003
|
+
if (capability.returns_hint !== void 0 && !isNonEmptyString2(capability.returns_hint))
|
|
3004
|
+
errors.push({ path: `${path4}.returns_hint`, code: "INVALID_RETURNS_HINT", message: "returns_hint must be a non-empty string." });
|
|
2934
3005
|
if (!["read", "proposal", "external_action", "answer_with_evidence"].includes(String(capability.kind)))
|
|
2935
3006
|
errors.push({ path: `${path4}.kind`, code: "INVALID_CAPABILITY_KIND", message: "kind must be read, proposal, external_action, or answer_with_evidence." });
|
|
2936
3007
|
if (!isNonEmptyString2(capability.context) || !contextNames.has(capability.context))
|
|
@@ -2993,6 +3064,20 @@ function validateArgs2(value, path4, errors) {
|
|
|
2993
3064
|
checkUnknownKeys2(arg, ARG_KEYS2, argPath, errors);
|
|
2994
3065
|
if (!["string", "number", "boolean"].includes(String(arg.type)))
|
|
2995
3066
|
errors.push({ path: `${argPath}.type`, code: "INVALID_ARG_TYPE", message: "arg type must be string, number, or boolean." });
|
|
3067
|
+
if (arg.description !== void 0 && !isNonEmptyString2(arg.description))
|
|
3068
|
+
errors.push({ path: `${argPath}.description`, code: "INVALID_ARG_DESCRIPTION", message: "arg description must be a non-empty string." });
|
|
3069
|
+
if (arg.max_length !== void 0 && (!Number.isInteger(arg.max_length) || Number(arg.max_length) <= 0))
|
|
3070
|
+
errors.push({ path: `${argPath}.max_length`, code: "INVALID_MAX_LENGTH", message: "max_length must be a positive integer." });
|
|
3071
|
+
if ((arg.minimum !== void 0 || arg.maximum !== void 0) && arg.type !== "number")
|
|
3072
|
+
errors.push({ path: argPath, code: "NUMERIC_BOUNDS_REQUIRE_NUMBER", message: "minimum/maximum can only be used with number args." });
|
|
3073
|
+
if (arg.minimum !== void 0 && !isFiniteNumber2(arg.minimum))
|
|
3074
|
+
errors.push({ path: `${argPath}.minimum`, code: "INVALID_MINIMUM", message: "minimum must be a finite number." });
|
|
3075
|
+
if (arg.maximum !== void 0 && !isFiniteNumber2(arg.maximum))
|
|
3076
|
+
errors.push({ path: `${argPath}.maximum`, code: "INVALID_MAXIMUM", message: "maximum must be a finite number." });
|
|
3077
|
+
if (isFiniteNumber2(arg.minimum) && isFiniteNumber2(arg.maximum) && Number(arg.minimum) > Number(arg.maximum))
|
|
3078
|
+
errors.push({ path: argPath, code: "INVALID_ARG_NUMERIC_RANGE", message: "minimum must be less than or equal to maximum." });
|
|
3079
|
+
if (arg.enum !== void 0 && (!Array.isArray(arg.enum) || arg.enum.length === 0))
|
|
3080
|
+
errors.push({ path: `${argPath}.enum`, code: "INVALID_ARG_ENUM", message: "enum must be a non-empty array when provided." });
|
|
2996
3081
|
}
|
|
2997
3082
|
}
|
|
2998
3083
|
function validateLookup2(value, path4, args, errors) {
|
|
@@ -3043,6 +3128,8 @@ function validateProposalAction(value, path4, errors) {
|
|
|
3043
3128
|
errors.push({ path: patchPath, code: "PATCH_BINDING_REQUIRED", message: "patch binding must include fixed or from_arg." });
|
|
3044
3129
|
}
|
|
3045
3130
|
}
|
|
3131
|
+
validateNumericBounds2(value.numeric_bounds, value.patch, `${path4}.numeric_bounds`, errors);
|
|
3132
|
+
validateTransitionGuards2(value.transition_guards, value.patch, `${path4}.transition_guards`, errors);
|
|
3046
3133
|
if (value.conflict_guard !== void 0) {
|
|
3047
3134
|
if (!isRecord3(value.conflict_guard))
|
|
3048
3135
|
errors.push({ path: `${path4}.conflict_guard`, code: "CONFLICT_GUARD_NOT_OBJECT", message: "conflict_guard must be an object." });
|
|
@@ -3057,11 +3144,79 @@ function validateProposalAction(value, path4, errors) {
|
|
|
3057
3144
|
if (value.approval !== void 0)
|
|
3058
3145
|
validateNestedObject(value.approval, APPROVAL_KEYS2, `${path4}.approval`, errors);
|
|
3059
3146
|
if (value.writeback !== void 0) {
|
|
3060
|
-
validateNestedObject(value.writeback,
|
|
3147
|
+
validateNestedObject(value.writeback, WRITEBACK_KEYS2, `${path4}.writeback`, errors);
|
|
3061
3148
|
if (isRecord3(value.writeback) && !["direct_sql", "app_handler", "cloud_worker", "none"].includes(String(value.writeback.mode)))
|
|
3062
3149
|
errors.push({ path: `${path4}.writeback.mode`, code: "INVALID_WRITEBACK_MODE", message: "writeback.mode must be direct_sql, app_handler, cloud_worker, or none." });
|
|
3063
3150
|
}
|
|
3064
3151
|
}
|
|
3152
|
+
function validateNumericBounds2(value, patch, path4, errors) {
|
|
3153
|
+
if (value === void 0)
|
|
3154
|
+
return;
|
|
3155
|
+
if (!isRecord3(value)) {
|
|
3156
|
+
errors.push({ path: path4, code: "NUMERIC_BOUNDS_NOT_OBJECT", message: "numeric_bounds must map patch fields to numeric bounds." });
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
const patchFields = isRecord3(patch) ? new Set(Object.keys(patch)) : /* @__PURE__ */ new Set();
|
|
3160
|
+
for (const [field, bounds] of Object.entries(value)) {
|
|
3161
|
+
const boundPath = `${path4}.${field}`;
|
|
3162
|
+
if (!isSafeIdentifier2(field))
|
|
3163
|
+
errors.push({ path: boundPath, code: "INVALID_NUMERIC_BOUND_FIELD", message: "numeric_bounds keys must be safe patch fields." });
|
|
3164
|
+
if (!patchFields.has(field))
|
|
3165
|
+
errors.push({ path: boundPath, code: "NUMERIC_BOUND_PATCH_FIELD_REQUIRED", message: "numeric_bounds can only constrain fields in proposal.patch." });
|
|
3166
|
+
if (!isRecord3(bounds)) {
|
|
3167
|
+
errors.push({ path: boundPath, code: "NUMERIC_BOUND_NOT_OBJECT", message: "numeric bound must be an object." });
|
|
3168
|
+
continue;
|
|
3169
|
+
}
|
|
3170
|
+
checkUnknownKeys2(bounds, NUMERIC_BOUND_KEYS2, boundPath, errors);
|
|
3171
|
+
const hasMinimum = bounds.minimum !== void 0;
|
|
3172
|
+
const hasMaximum = bounds.maximum !== void 0;
|
|
3173
|
+
if (!hasMinimum && !hasMaximum)
|
|
3174
|
+
errors.push({ path: boundPath, code: "NUMERIC_BOUND_EMPTY", message: "numeric bound must define minimum, maximum, or both." });
|
|
3175
|
+
if (hasMinimum && !isFiniteNumber2(bounds.minimum))
|
|
3176
|
+
errors.push({ path: `${boundPath}.minimum`, code: "INVALID_MINIMUM", message: "minimum must be a finite number." });
|
|
3177
|
+
if (hasMaximum && !isFiniteNumber2(bounds.maximum))
|
|
3178
|
+
errors.push({ path: `${boundPath}.maximum`, code: "INVALID_MAXIMUM", message: "maximum must be a finite number." });
|
|
3179
|
+
if (isFiniteNumber2(bounds.minimum) && isFiniteNumber2(bounds.maximum) && Number(bounds.minimum) > Number(bounds.maximum)) {
|
|
3180
|
+
errors.push({ path: boundPath, code: "INVALID_NUMERIC_RANGE", message: "minimum must be less than or equal to maximum." });
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
function validateTransitionGuards2(value, patch, path4, errors) {
|
|
3185
|
+
if (value === void 0)
|
|
3186
|
+
return;
|
|
3187
|
+
if (!isRecord3(value)) {
|
|
3188
|
+
errors.push({ path: path4, code: "TRANSITION_GUARDS_NOT_OBJECT", message: "transition_guards must map patch fields to allowed state transitions." });
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
const patchFields = isRecord3(patch) ? new Set(Object.keys(patch)) : /* @__PURE__ */ new Set();
|
|
3192
|
+
for (const [field, guard] of Object.entries(value)) {
|
|
3193
|
+
const guardPath = `${path4}.${field}`;
|
|
3194
|
+
if (!isSafeIdentifier2(field))
|
|
3195
|
+
errors.push({ path: guardPath, code: "INVALID_TRANSITION_GUARD_FIELD", message: "transition_guards keys must be safe patch fields." });
|
|
3196
|
+
if (!patchFields.has(field))
|
|
3197
|
+
errors.push({ path: guardPath, code: "TRANSITION_GUARD_PATCH_FIELD_REQUIRED", message: "transition_guards can only constrain fields in proposal.patch." });
|
|
3198
|
+
if (!isRecord3(guard)) {
|
|
3199
|
+
errors.push({ path: guardPath, code: "TRANSITION_GUARD_NOT_OBJECT", message: "transition guard must be an object." });
|
|
3200
|
+
continue;
|
|
3201
|
+
}
|
|
3202
|
+
checkUnknownKeys2(guard, TRANSITION_GUARD_KEYS2, guardPath, errors);
|
|
3203
|
+
if (guard.from_column !== void 0 && !isSafeIdentifier2(guard.from_column)) {
|
|
3204
|
+
errors.push({ path: `${guardPath}.from_column`, code: "INVALID_TRANSITION_FROM_COLUMN", message: "from_column must be a safe identifier." });
|
|
3205
|
+
}
|
|
3206
|
+
if (!isRecord3(guard.allowed) || Object.keys(guard.allowed).length === 0) {
|
|
3207
|
+
errors.push({ path: `${guardPath}.allowed`, code: "TRANSITION_ALLOWED_REQUIRED", message: "transition guard must define allowed transitions." });
|
|
3208
|
+
continue;
|
|
3209
|
+
}
|
|
3210
|
+
for (const [from, toValues] of Object.entries(guard.allowed)) {
|
|
3211
|
+
const allowedPath = `${guardPath}.allowed.${from}`;
|
|
3212
|
+
if (!isNonEmptyString2(from))
|
|
3213
|
+
errors.push({ path: allowedPath, code: "TRANSITION_FROM_REQUIRED", message: "transition source state must be a non-empty string." });
|
|
3214
|
+
if (!Array.isArray(toValues) || toValues.length === 0 || toValues.some((item) => !isNonEmptyString2(item))) {
|
|
3215
|
+
errors.push({ path: allowedPath, code: "TRANSITION_TO_VALUES_REQUIRED", message: "transition target states must be non-empty strings." });
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3065
3220
|
function validateWorkflows(value, contextNames, capabilityNames, errors) {
|
|
3066
3221
|
if (value === void 0)
|
|
3067
3222
|
return;
|
|
@@ -3245,6 +3400,9 @@ function isQualifiedOrSafeName(value) {
|
|
|
3245
3400
|
function isPositiveInteger2(value) {
|
|
3246
3401
|
return Number.isInteger(value) && Number(value) > 0;
|
|
3247
3402
|
}
|
|
3403
|
+
function isFiniteNumber2(value) {
|
|
3404
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
3405
|
+
}
|
|
3248
3406
|
|
|
3249
3407
|
// packages/spec/dist/normalize.js
|
|
3250
3408
|
function normalizeContract(input) {
|
|
@@ -3296,20 +3454,31 @@ function loadRuntimeConfigFromFile(configPath = process.env.SYNAPSOR_MCP_CONFIG
|
|
|
3296
3454
|
}
|
|
3297
3455
|
function resolveRuntimeConfig(config, baseDir = process.cwd()) {
|
|
3298
3456
|
if (!Array.isArray(config.contracts) || config.contracts.length === 0) return config;
|
|
3457
|
+
const seenCapabilities = /* @__PURE__ */ new Map();
|
|
3458
|
+
for (const [index, capability] of (config.capabilities ?? []).entries()) {
|
|
3459
|
+
rememberCapabilityName(seenCapabilities, capability.name, `embedded capabilities[${index}]`);
|
|
3460
|
+
}
|
|
3299
3461
|
const resolved = {
|
|
3300
3462
|
...config,
|
|
3301
3463
|
contexts: { ...config.contexts ?? {} },
|
|
3302
3464
|
capabilities: [...config.capabilities ?? []]
|
|
3303
3465
|
};
|
|
3304
|
-
for (const contractPath of config.contracts) {
|
|
3466
|
+
for (const [contractIndex, contractPath] of config.contracts.entries()) {
|
|
3305
3467
|
const fullPath = path.resolve(baseDir, contractPath);
|
|
3306
3468
|
const contract = normalizeContract(JSON.parse(fs.readFileSync(fullPath, "utf8")));
|
|
3307
|
-
mergeContractIntoRuntimeConfig(resolved, contract);
|
|
3469
|
+
mergeContractIntoRuntimeConfig(resolved, contract, `contracts[${contractIndex}] ${contractPath}`, seenCapabilities);
|
|
3308
3470
|
}
|
|
3309
3471
|
delete resolved.contracts;
|
|
3310
3472
|
return resolved;
|
|
3311
3473
|
}
|
|
3312
|
-
function
|
|
3474
|
+
function rememberCapabilityName(seen, name, origin) {
|
|
3475
|
+
const previous = seen.get(name);
|
|
3476
|
+
if (previous) {
|
|
3477
|
+
throw new Error(`Duplicate capability ${name}: ${origin} conflicts with ${previous}. Capability names must be unique across embedded runner config and referenced contracts.`);
|
|
3478
|
+
}
|
|
3479
|
+
seen.set(name, origin);
|
|
3480
|
+
}
|
|
3481
|
+
function mergeContractIntoRuntimeConfig(config, contract, origin, seenCapabilities) {
|
|
3313
3482
|
const resources = new Map((contract.resources ?? []).map((resource) => [resource.name, resource]));
|
|
3314
3483
|
for (const context of contract.contexts) {
|
|
3315
3484
|
if (!config.contexts) config.contexts = {};
|
|
@@ -3320,7 +3489,8 @@ function mergeContractIntoRuntimeConfig(config, contract) {
|
|
|
3320
3489
|
if (context) config.trusted_context = runtimeContextFromSpec(context);
|
|
3321
3490
|
}
|
|
3322
3491
|
if (!config.capabilities) config.capabilities = [];
|
|
3323
|
-
for (const capability of contract.capabilities) {
|
|
3492
|
+
for (const [capabilityIndex, capability] of contract.capabilities.entries()) {
|
|
3493
|
+
rememberCapabilityName(seenCapabilities, capability.name, `${origin} capabilities[${capabilityIndex}]`);
|
|
3324
3494
|
config.capabilities.push(runtimeCapabilityFromSpec(capability, resources, config));
|
|
3325
3495
|
}
|
|
3326
3496
|
}
|
|
@@ -3350,6 +3520,7 @@ function runtimeCapabilityFromSpec(capability, resources, config) {
|
|
|
3350
3520
|
name: capability.name,
|
|
3351
3521
|
kind: capability.kind === "proposal" ? "proposal" : "read",
|
|
3352
3522
|
...capability.description ? { description: capability.description } : {},
|
|
3523
|
+
...capability.returns_hint ? { returns_hint: capability.returns_hint } : {},
|
|
3353
3524
|
source,
|
|
3354
3525
|
context: capability.context,
|
|
3355
3526
|
target: target2,
|
|
@@ -3362,9 +3533,15 @@ function runtimeCapabilityFromSpec(capability, resources, config) {
|
|
|
3362
3533
|
if (capability.kind === "proposal" && capability.proposal) {
|
|
3363
3534
|
runtime.patch = capability.proposal.patch;
|
|
3364
3535
|
runtime.allowed_columns = capability.proposal.allowed_fields;
|
|
3536
|
+
runtime.numeric_bounds = capability.proposal.numeric_bounds;
|
|
3537
|
+
runtime.transition_guards = capability.proposal.transition_guards;
|
|
3365
3538
|
runtime.conflict_guard = capability.proposal.conflict_guard;
|
|
3366
3539
|
runtime.approval = capability.proposal.approval;
|
|
3367
|
-
|
|
3540
|
+
runtime.writeback = {
|
|
3541
|
+
mode: capability.proposal.writeback?.mode ?? "direct_sql",
|
|
3542
|
+
...capability.proposal.writeback?.executor ? { executor: capability.proposal.writeback.executor } : {}
|
|
3543
|
+
};
|
|
3544
|
+
if (capability.proposal.writeback?.executor) {
|
|
3368
3545
|
runtime.executor = capability.proposal.writeback.executor;
|
|
3369
3546
|
}
|
|
3370
3547
|
}
|
|
@@ -3522,11 +3699,11 @@ async function serveStdio(options = {}) {
|
|
|
3522
3699
|
const transport = new StdioServerTransport();
|
|
3523
3700
|
await server.connect(transport);
|
|
3524
3701
|
process.stderr.write("synapsor-runner MCP stdio server ready. Waiting for an MCP client on stdio; logs stay on stderr.\n");
|
|
3525
|
-
await new Promise((
|
|
3702
|
+
await new Promise((resolve2) => {
|
|
3526
3703
|
const previousOnClose = transport.onclose;
|
|
3527
3704
|
const close = () => {
|
|
3528
3705
|
runtime.close();
|
|
3529
|
-
|
|
3706
|
+
resolve2();
|
|
3530
3707
|
};
|
|
3531
3708
|
transport.onclose = () => {
|
|
3532
3709
|
previousOnClose?.();
|
|
@@ -3545,7 +3722,7 @@ async function startHttpMcpServer(options = {}) {
|
|
|
3545
3722
|
if (devNoAuth && !isLoopbackHost(host)) {
|
|
3546
3723
|
throw new McpRuntimeError("HTTP_DEV_NO_AUTH_UNSAFE_HOST", "--dev-no-auth is only allowed with localhost or 127.0.0.1.");
|
|
3547
3724
|
}
|
|
3548
|
-
const authToken = devNoAuth ? void 0 : env
|
|
3725
|
+
const authToken = devNoAuth ? void 0 : envValue(env, authTokenEnv);
|
|
3549
3726
|
if (!devNoAuth && !authToken) {
|
|
3550
3727
|
throw new McpRuntimeError("HTTP_AUTH_TOKEN_MISSING", `${authTokenEnv} is not set. HTTP MCP requires bearer auth by default.`);
|
|
3551
3728
|
}
|
|
@@ -3569,11 +3746,11 @@ async function startHttpMcpServer(options = {}) {
|
|
|
3569
3746
|
});
|
|
3570
3747
|
});
|
|
3571
3748
|
try {
|
|
3572
|
-
await new Promise((
|
|
3749
|
+
await new Promise((resolve2, reject) => {
|
|
3573
3750
|
server.once("error", reject);
|
|
3574
3751
|
server.listen(port, host, () => {
|
|
3575
3752
|
server.off("error", reject);
|
|
3576
|
-
|
|
3753
|
+
resolve2();
|
|
3577
3754
|
});
|
|
3578
3755
|
});
|
|
3579
3756
|
} catch (error) {
|
|
@@ -3611,7 +3788,7 @@ async function startStreamableHttpMcpServer(options = {}) {
|
|
|
3611
3788
|
if (devNoAuth && !isLoopbackHost(host)) {
|
|
3612
3789
|
throw new McpRuntimeError("HTTP_DEV_NO_AUTH_UNSAFE_HOST", "--dev-no-auth is only allowed with localhost or 127.0.0.1.");
|
|
3613
3790
|
}
|
|
3614
|
-
const authToken = devNoAuth ? void 0 : env
|
|
3791
|
+
const authToken = devNoAuth ? void 0 : envValue(env, authTokenEnv);
|
|
3615
3792
|
if (!devNoAuth && !authToken) {
|
|
3616
3793
|
throw new McpRuntimeError("HTTP_AUTH_TOKEN_MISSING", `${authTokenEnv} is not set. Streamable HTTP MCP requires bearer auth by default.`);
|
|
3617
3794
|
}
|
|
@@ -3638,11 +3815,11 @@ async function startStreamableHttpMcpServer(options = {}) {
|
|
|
3638
3815
|
});
|
|
3639
3816
|
});
|
|
3640
3817
|
try {
|
|
3641
|
-
await new Promise((
|
|
3818
|
+
await new Promise((resolve2, reject) => {
|
|
3642
3819
|
server.once("error", reject);
|
|
3643
3820
|
server.listen(port, host, () => {
|
|
3644
3821
|
server.off("error", reject);
|
|
3645
|
-
|
|
3822
|
+
resolve2();
|
|
3646
3823
|
});
|
|
3647
3824
|
});
|
|
3648
3825
|
} catch (error) {
|
|
@@ -3966,20 +4143,20 @@ function isLoopbackHost(host) {
|
|
|
3966
4143
|
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
3967
4144
|
}
|
|
3968
4145
|
async function closeHttpServer(server, runtime) {
|
|
3969
|
-
await new Promise((
|
|
4146
|
+
await new Promise((resolve2, reject) => {
|
|
3970
4147
|
server.close((error) => {
|
|
3971
4148
|
if (error) reject(error);
|
|
3972
|
-
else
|
|
4149
|
+
else resolve2();
|
|
3973
4150
|
});
|
|
3974
4151
|
}).finally(() => {
|
|
3975
4152
|
runtime.close();
|
|
3976
4153
|
});
|
|
3977
4154
|
}
|
|
3978
4155
|
async function closeStreamableHttpServer(server, sessions) {
|
|
3979
|
-
await new Promise((
|
|
4156
|
+
await new Promise((resolve2, reject) => {
|
|
3980
4157
|
server.close((error) => {
|
|
3981
4158
|
if (error) reject(error);
|
|
3982
|
-
else
|
|
4159
|
+
else resolve2();
|
|
3983
4160
|
});
|
|
3984
4161
|
}).finally(() => closeStreamableSessions(sessions));
|
|
3985
4162
|
}
|
|
@@ -4023,8 +4200,8 @@ function listedLocalCapabilities(config) {
|
|
|
4023
4200
|
}
|
|
4024
4201
|
function createCloudClient(config, env) {
|
|
4025
4202
|
const cloud2 = requireCloudConfig(config);
|
|
4026
|
-
const baseUrl = env
|
|
4027
|
-
const runnerToken = env
|
|
4203
|
+
const baseUrl = envValue(env, cloud2.base_url_env);
|
|
4204
|
+
const runnerToken = envValue(env, cloud2.runner_token_env);
|
|
4028
4205
|
if (!baseUrl) throw new McpRuntimeError("CLOUD_BASE_URL_MISSING", `${cloud2.base_url_env} is not set.`);
|
|
4029
4206
|
if (!runnerToken) throw new McpRuntimeError("CLOUD_RUNNER_TOKEN_MISSING", `${cloud2.runner_token_env} is not set.`);
|
|
4030
4207
|
return new ControlPlaneClient({
|
|
@@ -4106,6 +4283,9 @@ async function callConfiguredTool(input) {
|
|
|
4106
4283
|
if (capability.kind === "proposal" && input.config.mode === "read_only") {
|
|
4107
4284
|
throw new McpRuntimeError("PROPOSALS_DISABLED", "This runner is in read_only mode; proposal tools are disabled.");
|
|
4108
4285
|
}
|
|
4286
|
+
if (capability.kind === "proposal" && input.config.mode === "review") {
|
|
4287
|
+
assertProposalWritebackResolvable(input.config, capability);
|
|
4288
|
+
}
|
|
4109
4289
|
const source = input.config.sources?.[capability.source];
|
|
4110
4290
|
if (!source) throw new McpRuntimeError("SOURCE_NOT_FOUND", `Unknown source: ${capability.source}`);
|
|
4111
4291
|
const context = resolveTrustedContext(input.config, input.env, capability);
|
|
@@ -4275,7 +4455,7 @@ function resultEnvelopeFromLegacy(legacy, capability, canonicalName) {
|
|
|
4275
4455
|
const targetType = typeof target2?.type === "string" ? target2.type : capability?.target.table ?? "object";
|
|
4276
4456
|
const targetId = target2?.id !== void 0 ? String(target2.id) : "unknown";
|
|
4277
4457
|
const executor = writebackExecutorName(legacy.writeback);
|
|
4278
|
-
const writebackMode = executor && executor !== "sql_update" && executor !== "trusted_worker_required" ? "app_handler" : "direct_update";
|
|
4458
|
+
const writebackMode = executor === "read_only" || executor === "none" ? "proposal_only" : executor && executor !== "sql_update" && executor !== "trusted_worker_required" ? "app_handler" : "direct_update";
|
|
4279
4459
|
return {
|
|
4280
4460
|
ok: true,
|
|
4281
4461
|
summary: `Created proposal ${proposalId} for ${targetType} ${targetId}. Source database changed: no.`,
|
|
@@ -4330,6 +4510,44 @@ function writebackExecutorName(value) {
|
|
|
4330
4510
|
if (!isRecord4(value)) return void 0;
|
|
4331
4511
|
return typeof value.executor === "string" ? value.executor : typeof value.mode === "string" ? value.mode : void 0;
|
|
4332
4512
|
}
|
|
4513
|
+
function capabilityWritebackMode2(capability) {
|
|
4514
|
+
const mode = capability.writeback?.mode;
|
|
4515
|
+
if (mode === "direct_sql" || mode === "app_handler" || mode === "cloud_worker" || mode === "none") return mode;
|
|
4516
|
+
if (capability.executor && capability.executor !== "sql_update") return "app_handler";
|
|
4517
|
+
return "direct_sql";
|
|
4518
|
+
}
|
|
4519
|
+
function capabilityWritebackExecutor(capability) {
|
|
4520
|
+
return capability.writeback?.executor ?? capability.executor;
|
|
4521
|
+
}
|
|
4522
|
+
function assertProposalWritebackResolvable(config, capability) {
|
|
4523
|
+
if (capability.kind !== "proposal") return;
|
|
4524
|
+
const mode = capabilityWritebackMode2(capability);
|
|
4525
|
+
if (mode === "none" || mode === "cloud_worker") return;
|
|
4526
|
+
if (mode === "direct_sql") {
|
|
4527
|
+
const source = config.sources?.[capability.source];
|
|
4528
|
+
if (!source) {
|
|
4529
|
+
throw new McpRuntimeError("WRITEBACK_UNRESOLVED", `capability ${capability.name} declares DIRECT SQL writeback but source ${capability.source} is not configured.`);
|
|
4530
|
+
}
|
|
4531
|
+
if (source.read_only === true) {
|
|
4532
|
+
throw new McpRuntimeError("WRITEBACK_UNRESOLVED", `capability ${capability.name} declares DIRECT SQL writeback but source ${capability.source} is read-only.`);
|
|
4533
|
+
}
|
|
4534
|
+
if (!source.write_url_env) {
|
|
4535
|
+
throw new McpRuntimeError("WRITEBACK_UNRESOLVED", `capability ${capability.name} declares DIRECT SQL writeback but source ${capability.source} has no write_url_env.`);
|
|
4536
|
+
}
|
|
4537
|
+
return;
|
|
4538
|
+
}
|
|
4539
|
+
const executorName = capabilityWritebackExecutor(capability);
|
|
4540
|
+
if (!executorName || executorName === "sql_update") {
|
|
4541
|
+
throw new McpRuntimeError("WRITEBACK_UNRESOLVED", `capability ${capability.name} declares HANDLER writeback but has no executor name.`);
|
|
4542
|
+
}
|
|
4543
|
+
const executor = config.executors?.[executorName];
|
|
4544
|
+
if (!isRecord4(executor)) {
|
|
4545
|
+
throw new McpRuntimeError("WRITEBACK_UNRESOLVED", `capability ${capability.name} declares HANDLER writeback but executor ${executorName} is not configured.`);
|
|
4546
|
+
}
|
|
4547
|
+
if (executor.type !== "http_handler" && executor.type !== "command_handler") {
|
|
4548
|
+
throw new McpRuntimeError("WRITEBACK_UNRESOLVED", `capability ${capability.name} declares HANDLER writeback but executor ${executorName} is not an app-owned handler.`);
|
|
4549
|
+
}
|
|
4550
|
+
}
|
|
4333
4551
|
function evidenceHandle(bundleId) {
|
|
4334
4552
|
return {
|
|
4335
4553
|
bundle_id: bundleId,
|
|
@@ -4389,6 +4607,9 @@ function buildChangeSet(input) {
|
|
|
4389
4607
|
enforcePatchGuards(input.capability, before, patch);
|
|
4390
4608
|
const after = { ...before, ...patch };
|
|
4391
4609
|
const guard = expectedVersionGuard(input.capability, before);
|
|
4610
|
+
const writebackMode = capabilityWritebackMode2(input.capability);
|
|
4611
|
+
const changeSetWritebackMode = writebackMode === "none" ? "read_only" : "trusted_worker_required";
|
|
4612
|
+
const writebackExecutor = writebackMode === "none" ? "none" : writebackMode === "cloud_worker" ? "cloud_worker" : writebackMode === "direct_sql" ? "sql_update" : capabilityWritebackExecutor(input.capability);
|
|
4392
4613
|
const proposalCore = {
|
|
4393
4614
|
schema_version: protocolVersions.changeSet,
|
|
4394
4615
|
proposal_id: stableId("wrp", {
|
|
@@ -4436,8 +4657,8 @@ function buildChangeSet(input) {
|
|
|
4436
4657
|
},
|
|
4437
4658
|
writeback: {
|
|
4438
4659
|
status: "not_applied",
|
|
4439
|
-
mode:
|
|
4440
|
-
executor:
|
|
4660
|
+
mode: changeSetWritebackMode,
|
|
4661
|
+
executor: writebackExecutor
|
|
4441
4662
|
},
|
|
4442
4663
|
source_database_mutated: false,
|
|
4443
4664
|
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -4460,7 +4681,7 @@ async function readCurrentRow(input) {
|
|
|
4460
4681
|
return readMysqlRow(input);
|
|
4461
4682
|
}
|
|
4462
4683
|
async function readPostgresRow(input) {
|
|
4463
|
-
const connectionString = input.env
|
|
4684
|
+
const connectionString = envValue(input.env, input.source.read_url_env);
|
|
4464
4685
|
if (!connectionString) throw new McpRuntimeError("SOURCE_CREDENTIAL_MISSING", `${input.source.read_url_env} is not set.`);
|
|
4465
4686
|
const pool = new Pool({ connectionString });
|
|
4466
4687
|
const client = await pool.connect();
|
|
@@ -4482,7 +4703,7 @@ async function readPostgresRow(input) {
|
|
|
4482
4703
|
}
|
|
4483
4704
|
}
|
|
4484
4705
|
async function readMysqlRow(input) {
|
|
4485
|
-
const uri = input.env
|
|
4706
|
+
const uri = envValue(input.env, input.source.read_url_env);
|
|
4486
4707
|
if (!uri) throw new McpRuntimeError("SOURCE_CREDENTIAL_MISSING", `${input.source.read_url_env} is not set.`);
|
|
4487
4708
|
const connection = await mysql.createConnection({ uri, dateStrings: true });
|
|
4488
4709
|
try {
|
|
@@ -4537,8 +4758,8 @@ function resolveTrustedContext(config, env, capability) {
|
|
|
4537
4758
|
if (provider === "environment") {
|
|
4538
4759
|
const tenantEnv = String(values.tenant_id_env ?? "SYNAPSOR_TENANT_ID");
|
|
4539
4760
|
const principalEnv = String(values.principal_env ?? "SYNAPSOR_PRINCIPAL");
|
|
4540
|
-
const tenant = env
|
|
4541
|
-
const principal = env
|
|
4761
|
+
const tenant = envValue(env, tenantEnv);
|
|
4762
|
+
const principal = envValue(env, principalEnv);
|
|
4542
4763
|
if (!tenant || !principal) throw new McpRuntimeError("TRUSTED_BINDING_MISSING", `${tenantEnv} and ${principalEnv} must be set.`);
|
|
4543
4764
|
return { tenant_id: tenant, principal, provenance: "environment" };
|
|
4544
4765
|
}
|
|
@@ -4551,8 +4772,19 @@ function resolveTrustedContext(config, env, capability) {
|
|
|
4551
4772
|
throw new McpRuntimeError("TRUSTED_CONTEXT_UNSUPPORTED", `${provider} trusted context is not available in local stdio mode.`);
|
|
4552
4773
|
}
|
|
4553
4774
|
function valueFromEnvOrLiteral(envName, literal, env) {
|
|
4554
|
-
if (typeof envName === "string"
|
|
4555
|
-
|
|
4775
|
+
if (typeof envName === "string") {
|
|
4776
|
+
const value2 = envValue(env, envName);
|
|
4777
|
+
if (value2) return value2;
|
|
4778
|
+
}
|
|
4779
|
+
if (typeof literal !== "string") return void 0;
|
|
4780
|
+
const value = literal.trim();
|
|
4781
|
+
return value.length > 0 ? value : void 0;
|
|
4782
|
+
}
|
|
4783
|
+
function envValue(env, name) {
|
|
4784
|
+
const value = env[name];
|
|
4785
|
+
if (value === void 0) return void 0;
|
|
4786
|
+
const trimmed = value.trim();
|
|
4787
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
4556
4788
|
}
|
|
4557
4789
|
function validateToolArgs(capability, args) {
|
|
4558
4790
|
for (const [name, spec] of Object.entries(capability.args)) {
|
|
@@ -6421,7 +6653,7 @@ async function startPolling(config, adapters2, signal) {
|
|
|
6421
6653
|
} catch (error) {
|
|
6422
6654
|
logger.error("runner loop failed", { error: error instanceof Error ? error.message : String(error) });
|
|
6423
6655
|
}
|
|
6424
|
-
await new Promise((
|
|
6656
|
+
await new Promise((resolve2) => setTimeout(resolve2, config.pollIntervalMs));
|
|
6425
6657
|
}
|
|
6426
6658
|
}
|
|
6427
6659
|
|
|
@@ -6453,7 +6685,7 @@ function parseAgentDsl(source) {
|
|
|
6453
6685
|
}
|
|
6454
6686
|
return { contexts, capabilities, workflows };
|
|
6455
6687
|
}
|
|
6456
|
-
function
|
|
6688
|
+
function compileAgentDslWithWarnings(source) {
|
|
6457
6689
|
const ast = parseAgentDsl(source);
|
|
6458
6690
|
const contexts = ast.contexts.map((context) => ({
|
|
6459
6691
|
name: context.name,
|
|
@@ -6465,6 +6697,8 @@ function compileAgentDsl(source) {
|
|
|
6465
6697
|
const spec = {
|
|
6466
6698
|
name: capability.name,
|
|
6467
6699
|
kind: capability.kind,
|
|
6700
|
+
...capability.description ? { description: capability.description } : {},
|
|
6701
|
+
...capability.returnsHint ? { returns_hint: capability.returnsHint } : {},
|
|
6468
6702
|
context: capability.context,
|
|
6469
6703
|
...capability.source ? { source: capability.source } : {},
|
|
6470
6704
|
subject: {
|
|
@@ -6474,7 +6708,7 @@ function compileAgentDsl(source) {
|
|
|
6474
6708
|
...capability.tenantKey ? { tenant_key: capability.tenantKey } : {},
|
|
6475
6709
|
...capability.conflictKey ? { conflict_key: capability.conflictKey } : {}
|
|
6476
6710
|
},
|
|
6477
|
-
args: capability.args,
|
|
6711
|
+
args: specArgsFromDsl(capability.args),
|
|
6478
6712
|
...capability.lookup ? { lookup: { id_from_arg: capability.lookup.arg } } : {},
|
|
6479
6713
|
visible_fields: capability.visibleFields,
|
|
6480
6714
|
...capability.keptOutFields.length ? { kept_out_fields: capability.keptOutFields } : {},
|
|
@@ -6486,6 +6720,8 @@ function compileAgentDsl(source) {
|
|
|
6486
6720
|
action: capability.proposal.action,
|
|
6487
6721
|
allowed_fields: capability.proposal.allowedFields,
|
|
6488
6722
|
patch: capability.proposal.patch,
|
|
6723
|
+
...capability.proposal.numericBounds ? { numeric_bounds: capability.proposal.numericBounds } : {},
|
|
6724
|
+
...capability.proposal.transitionGuards ? { transition_guards: capability.proposal.transitionGuards } : {},
|
|
6489
6725
|
conflict_guard: capability.conflictKey ? { column: capability.conflictKey } : { weak_guard_ack: true },
|
|
6490
6726
|
approval: { mode: "human", required_role: capability.proposal.approvalRole ?? "local_reviewer" },
|
|
6491
6727
|
writeback: {
|
|
@@ -6512,12 +6748,12 @@ function compileAgentDsl(source) {
|
|
|
6512
6748
|
...workflows.length ? { workflows } : {}
|
|
6513
6749
|
};
|
|
6514
6750
|
assertValidContract(contract);
|
|
6515
|
-
return normalizeContract(contract);
|
|
6751
|
+
return { contract: normalizeContract(contract), warnings: collectDslWarnings(ast) };
|
|
6516
6752
|
}
|
|
6517
6753
|
function validateAgentDsl(source) {
|
|
6518
6754
|
try {
|
|
6519
|
-
|
|
6520
|
-
return { ok: true, errors: [], warnings:
|
|
6755
|
+
const result = compileAgentDslWithWarnings(source);
|
|
6756
|
+
return { ok: true, errors: [], warnings: result.warnings };
|
|
6521
6757
|
} catch (error) {
|
|
6522
6758
|
if (error instanceof AgentDslError) {
|
|
6523
6759
|
return {
|
|
@@ -6602,6 +6838,7 @@ function parseContextBlock(block) {
|
|
|
6602
6838
|
function parseCapabilityBlock(block) {
|
|
6603
6839
|
const capability = {
|
|
6604
6840
|
name: block.name,
|
|
6841
|
+
line: block.line,
|
|
6605
6842
|
kind: "read",
|
|
6606
6843
|
context: "",
|
|
6607
6844
|
schema: "",
|
|
@@ -6612,6 +6849,16 @@ function parseCapabilityBlock(block) {
|
|
|
6612
6849
|
keptOutFields: []
|
|
6613
6850
|
};
|
|
6614
6851
|
for (const item of block.body) {
|
|
6852
|
+
const description = item.text.match(/^DESCRIPTION\s+'(.*)'$/i);
|
|
6853
|
+
if (description) {
|
|
6854
|
+
capability.description = description[1] ?? "";
|
|
6855
|
+
continue;
|
|
6856
|
+
}
|
|
6857
|
+
const returnsHint = item.text.match(/^RETURNS\s+HINT\s+'(.*)'$/i);
|
|
6858
|
+
if (returnsHint) {
|
|
6859
|
+
capability.returnsHint = returnsHint[1] ?? "";
|
|
6860
|
+
continue;
|
|
6861
|
+
}
|
|
6615
6862
|
const context = item.text.match(/^USING\s+CONTEXT\s+([A-Za-z_][A-Za-z0-9_.]*)$/i);
|
|
6616
6863
|
if (context?.[1]) {
|
|
6617
6864
|
capability.context = context[1];
|
|
@@ -6643,13 +6890,9 @@ function parseCapabilityBlock(block) {
|
|
|
6643
6890
|
capability.conflictKey = conflict3[1];
|
|
6644
6891
|
continue;
|
|
6645
6892
|
}
|
|
6646
|
-
const arg = item.text.match(/^ARG\s+([A-Za-z_][A-Za-z0-9_]*)\s+(STRING|TEXT|NUMBER|BOOLEAN|BOOL)
|
|
6893
|
+
const arg = item.text.match(/^ARG\s+([A-Za-z_][A-Za-z0-9_]*)\s+(STRING|TEXT|NUMBER|BOOLEAN|BOOL)\b(.*)$/i);
|
|
6647
6894
|
if (arg?.[1] && arg[2]) {
|
|
6648
|
-
capability.args[arg[1]] =
|
|
6649
|
-
type: normalizeArgType(arg[2]),
|
|
6650
|
-
...item.text.match(/\sREQUIRED(?:\s|$)/i) ? { required: true } : {},
|
|
6651
|
-
...arg[3] ? { max_length: Number(arg[3]) } : {}
|
|
6652
|
-
};
|
|
6895
|
+
capability.args[arg[1]] = parseArgSpec(arg[1], arg[2], arg[3] ?? "", item.line);
|
|
6653
6896
|
continue;
|
|
6654
6897
|
}
|
|
6655
6898
|
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);
|
|
@@ -6699,6 +6942,30 @@ function parseCapabilityBlock(block) {
|
|
|
6699
6942
|
capability.proposal.allowedFields.push(patch[1]);
|
|
6700
6943
|
continue;
|
|
6701
6944
|
}
|
|
6945
|
+
const bound = item.text.match(/^BOUND\s+([A-Za-z_][A-Za-z0-9_]*)\s+(-?\d+(?:\.\d+)?)?\s*\.\.\s*(-?\d+(?:\.\d+)?)?$/i);
|
|
6946
|
+
if (bound?.[1]) {
|
|
6947
|
+
ensureProposal(capability, item);
|
|
6948
|
+
const minimum = bound[2] !== void 0 ? Number(bound[2]) : void 0;
|
|
6949
|
+
const maximum = bound[3] !== void 0 ? Number(bound[3]) : void 0;
|
|
6950
|
+
if (minimum === void 0 && maximum === void 0)
|
|
6951
|
+
throw dslError(item.line, 1, "BOUND_RANGE_REQUIRED", "BOUND requires minimum, maximum, or both, such as 1..2500");
|
|
6952
|
+
capability.proposal.numericBounds ??= {};
|
|
6953
|
+
capability.proposal.numericBounds[bound[1]] = {
|
|
6954
|
+
...minimum !== void 0 ? { minimum } : {},
|
|
6955
|
+
...maximum !== void 0 ? { maximum } : {}
|
|
6956
|
+
};
|
|
6957
|
+
continue;
|
|
6958
|
+
}
|
|
6959
|
+
const transition = item.text.match(/^TRANSITION\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s+FROM\s+([A-Za-z_][A-Za-z0-9_]*))?\s+ALLOW\s+(.+)$/i);
|
|
6960
|
+
if (transition?.[1] && transition[3]) {
|
|
6961
|
+
ensureProposal(capability, item);
|
|
6962
|
+
capability.proposal.transitionGuards ??= {};
|
|
6963
|
+
capability.proposal.transitionGuards[transition[1]] = {
|
|
6964
|
+
...transition[2] ? { from_column: transition[2] } : {},
|
|
6965
|
+
allowed: parseTransitionAllowed(transition[3], item.line)
|
|
6966
|
+
};
|
|
6967
|
+
continue;
|
|
6968
|
+
}
|
|
6702
6969
|
const approval = item.text.match(/^APPROVAL\s+ROLE\s+([A-Za-z_][A-Za-z0-9_.-]*)$/i);
|
|
6703
6970
|
if (approval?.[1]) {
|
|
6704
6971
|
ensureProposal(capability, item);
|
|
@@ -6731,6 +6998,40 @@ function parseCapabilityBlock(block) {
|
|
|
6731
6998
|
throw dslError(block.line, 1, "PROPOSAL_PATCH_REQUIRED", `${block.name} proposal requires at least one PATCH line`);
|
|
6732
6999
|
return capability;
|
|
6733
7000
|
}
|
|
7001
|
+
function specArgsFromDsl(args) {
|
|
7002
|
+
return Object.fromEntries(Object.entries(args).map(([name, arg]) => {
|
|
7003
|
+
const { line: _line, ...spec } = arg;
|
|
7004
|
+
return [name, spec];
|
|
7005
|
+
}));
|
|
7006
|
+
}
|
|
7007
|
+
function collectDslWarnings(ast) {
|
|
7008
|
+
const warnings = [];
|
|
7009
|
+
for (const capability of ast.capabilities) {
|
|
7010
|
+
if (capability.kind !== "proposal" || !capability.proposal)
|
|
7011
|
+
continue;
|
|
7012
|
+
const line = capability.line ?? 1;
|
|
7013
|
+
if (!capability.description) {
|
|
7014
|
+
warnings.push({ line, column: 1, code: "DESCRIPTION_RECOMMENDED", message: `${capability.name} is a proposal capability without DESCRIPTION.` });
|
|
7015
|
+
}
|
|
7016
|
+
if (!capability.returnsHint) {
|
|
7017
|
+
warnings.push({ line, column: 1, code: "RETURNS_HINT_RECOMMENDED", message: `${capability.name} is a proposal capability without RETURNS HINT.` });
|
|
7018
|
+
}
|
|
7019
|
+
for (const [column, binding] of Object.entries(capability.proposal.patch)) {
|
|
7020
|
+
if (!binding.from_arg)
|
|
7021
|
+
continue;
|
|
7022
|
+
const arg = capability.args[binding.from_arg];
|
|
7023
|
+
if (arg?.type === "number" && arg.minimum === void 0 && arg.maximum === void 0 && capability.proposal.numericBounds?.[column] === void 0) {
|
|
7024
|
+
warnings.push({
|
|
7025
|
+
line: arg.line ?? line,
|
|
7026
|
+
column: 1,
|
|
7027
|
+
code: "NUMERIC_PATCH_BOUND_RECOMMENDED",
|
|
7028
|
+
message: `${capability.name} patches ${column} from numeric arg ${binding.from_arg} without ARG MIN/MAX or BOUND ${column}.`
|
|
7029
|
+
});
|
|
7030
|
+
}
|
|
7031
|
+
}
|
|
7032
|
+
}
|
|
7033
|
+
return warnings;
|
|
7034
|
+
}
|
|
6734
7035
|
function parseWorkflowBlock(block) {
|
|
6735
7036
|
const workflow = { name: block.name, context: "", allowedCapabilities: [] };
|
|
6736
7037
|
for (const item of block.body) {
|
|
@@ -6788,6 +7089,76 @@ function parsePatchBinding(raw) {
|
|
|
6788
7089
|
return { fixed: quoted[1] ?? "" };
|
|
6789
7090
|
return { fixed: trimmed };
|
|
6790
7091
|
}
|
|
7092
|
+
function parseArgSpec(name, rawType, rawOptions, line) {
|
|
7093
|
+
const type = normalizeArgType(rawType);
|
|
7094
|
+
let rest = rawOptions.trim();
|
|
7095
|
+
let description;
|
|
7096
|
+
const descriptionMatch = rest.match(/\bDESCRIPTION\s+'([^']*)'/i);
|
|
7097
|
+
if (descriptionMatch) {
|
|
7098
|
+
description = descriptionMatch[1] ?? "";
|
|
7099
|
+
rest = `${rest.slice(0, descriptionMatch.index)} ${rest.slice((descriptionMatch.index ?? 0) + descriptionMatch[0].length)}`.trim();
|
|
7100
|
+
}
|
|
7101
|
+
const required = /\bREQUIRED\b/i.test(rest);
|
|
7102
|
+
rest = rest.replace(/\bREQUIRED\b/ig, " ").trim();
|
|
7103
|
+
const maxLengthMatch = rest.match(/\bMAX\s+LENGTH\s+(\d+)\b/i);
|
|
7104
|
+
const maxLength = maxLengthMatch?.[1] ? Number(maxLengthMatch[1]) : void 0;
|
|
7105
|
+
if (maxLengthMatch)
|
|
7106
|
+
rest = `${rest.slice(0, maxLengthMatch.index)} ${rest.slice((maxLengthMatch.index ?? 0) + maxLengthMatch[0].length)}`.trim();
|
|
7107
|
+
const minMatch = rest.match(/\bMIN\s+(-?\d+(?:\.\d+)?)\b/i);
|
|
7108
|
+
const minimum = minMatch?.[1] ? Number(minMatch[1]) : void 0;
|
|
7109
|
+
if (minMatch)
|
|
7110
|
+
rest = `${rest.slice(0, minMatch.index)} ${rest.slice((minMatch.index ?? 0) + minMatch[0].length)}`.trim();
|
|
7111
|
+
const maxMatch = rest.match(/\bMAX\s+(-?\d+(?:\.\d+)?)\b/i);
|
|
7112
|
+
const maximumOrLegacyLength = maxMatch?.[1] ? Number(maxMatch[1]) : void 0;
|
|
7113
|
+
if (maxMatch)
|
|
7114
|
+
rest = `${rest.slice(0, maxMatch.index)} ${rest.slice((maxMatch.index ?? 0) + maxMatch[0].length)}`.trim();
|
|
7115
|
+
if (rest.trim().length > 0) {
|
|
7116
|
+
throw dslError(line, 1, "ARG_OPTIONS_UNSUPPORTED", `unsupported ARG options for ${name}: ${rest.trim()}`);
|
|
7117
|
+
}
|
|
7118
|
+
if (type !== "number" && minimum !== void 0) {
|
|
7119
|
+
throw dslError(line, 1, "ARG_MIN_REQUIRES_NUMBER", `ARG ${name} uses MIN, but MIN is only valid for NUMBER args`);
|
|
7120
|
+
}
|
|
7121
|
+
if (type === "boolean" && (maxLength !== void 0 || maximumOrLegacyLength !== void 0)) {
|
|
7122
|
+
throw dslError(line, 1, "ARG_MAX_INVALID_FOR_BOOLEAN", `ARG ${name} cannot use MAX or MAX LENGTH because BOOLEAN has no numeric or length bound`);
|
|
7123
|
+
}
|
|
7124
|
+
if (type === "number" && maxLength !== void 0) {
|
|
7125
|
+
throw dslError(line, 1, "ARG_MAX_LENGTH_REQUIRES_TEXT", `ARG ${name} uses MAX LENGTH, but MAX LENGTH is only valid for STRING/TEXT args`);
|
|
7126
|
+
}
|
|
7127
|
+
return {
|
|
7128
|
+
type,
|
|
7129
|
+
line,
|
|
7130
|
+
...required ? { required: true } : {},
|
|
7131
|
+
...description !== void 0 ? { description } : {},
|
|
7132
|
+
...type === "number" && minimum !== void 0 ? { minimum } : {},
|
|
7133
|
+
...type === "number" && maximumOrLegacyLength !== void 0 ? { maximum: maximumOrLegacyLength } : {},
|
|
7134
|
+
...type !== "number" && maxLength !== void 0 ? { max_length: maxLength } : {},
|
|
7135
|
+
...type !== "number" && maxLength === void 0 && maximumOrLegacyLength !== void 0 ? { max_length: maximumOrLegacyLength } : {}
|
|
7136
|
+
};
|
|
7137
|
+
}
|
|
7138
|
+
function parseTransitionAllowed(raw, line) {
|
|
7139
|
+
const allowed = {};
|
|
7140
|
+
for (const item of raw.split(",").map((part) => part.trim()).filter(Boolean)) {
|
|
7141
|
+
const match = item.match(/^(.+?)\s*->\s*(.+)$/);
|
|
7142
|
+
if (!match?.[1] || !match[2]) {
|
|
7143
|
+
throw dslError(line, 1, "TRANSITION_RULE_INVALID", `transition rule must use from -> to syntax: ${item}`);
|
|
7144
|
+
}
|
|
7145
|
+
const from = parseStateValue(match[1]);
|
|
7146
|
+
const toValues = match[2].split("|").map(parseStateValue).filter(Boolean);
|
|
7147
|
+
if (!from || toValues.length === 0) {
|
|
7148
|
+
throw dslError(line, 1, "TRANSITION_RULE_INVALID", `transition rule must name non-empty states: ${item}`);
|
|
7149
|
+
}
|
|
7150
|
+
allowed[from] = toValues;
|
|
7151
|
+
}
|
|
7152
|
+
if (Object.keys(allowed).length === 0) {
|
|
7153
|
+
throw dslError(line, 1, "TRANSITION_ALLOWED_REQUIRED", "TRANSITION requires at least one from -> to rule");
|
|
7154
|
+
}
|
|
7155
|
+
return allowed;
|
|
7156
|
+
}
|
|
7157
|
+
function parseStateValue(raw) {
|
|
7158
|
+
const trimmed = raw.trim();
|
|
7159
|
+
const quoted = trimmed.match(/^'(.*)'$/);
|
|
7160
|
+
return (quoted?.[1] ?? trimmed).trim();
|
|
7161
|
+
}
|
|
6791
7162
|
function normalizeBindingSource(source) {
|
|
6792
7163
|
const normalized = source.toUpperCase();
|
|
6793
7164
|
if (normalized === "ENV")
|
|
@@ -6857,11 +7228,11 @@ async function startLocalUiServer(options = {}) {
|
|
|
6857
7228
|
});
|
|
6858
7229
|
}
|
|
6859
7230
|
});
|
|
6860
|
-
await new Promise((
|
|
7231
|
+
await new Promise((resolve2, reject) => {
|
|
6861
7232
|
server.once("error", reject);
|
|
6862
7233
|
server.listen(options.port ?? 0, host, () => {
|
|
6863
7234
|
server.off("error", reject);
|
|
6864
|
-
|
|
7235
|
+
resolve2();
|
|
6865
7236
|
});
|
|
6866
7237
|
});
|
|
6867
7238
|
const address = server.address();
|
|
@@ -6874,10 +7245,10 @@ async function startLocalUiServer(options = {}) {
|
|
|
6874
7245
|
port,
|
|
6875
7246
|
token,
|
|
6876
7247
|
csrfToken,
|
|
6877
|
-
close: () => new Promise((
|
|
7248
|
+
close: () => new Promise((resolve2, reject) => {
|
|
6878
7249
|
server.close((error) => {
|
|
6879
7250
|
if (error) reject(error);
|
|
6880
|
-
else
|
|
7251
|
+
else resolve2();
|
|
6881
7252
|
});
|
|
6882
7253
|
})
|
|
6883
7254
|
};
|
|
@@ -8720,7 +9091,7 @@ async function writeGeneratedSmokeInputFile(lookupArg, objectId, force) {
|
|
|
8720
9091
|
`, force);
|
|
8721
9092
|
}
|
|
8722
9093
|
async function maybeRunGeneratedSmokeCall(input) {
|
|
8723
|
-
const required = uniqueStrings([input.readUrlEnv, input.tenantEnv, input.principalEnv]).filter((envName) => !input.env
|
|
9094
|
+
const required = uniqueStrings([input.readUrlEnv, input.tenantEnv, input.principalEnv]).filter((envName) => !envValue2(input.env, envName));
|
|
8724
9095
|
if (required.length > 0) {
|
|
8725
9096
|
return [
|
|
8726
9097
|
"Smoke call not run yet.",
|
|
@@ -9194,12 +9565,15 @@ async function dslValidate(args) {
|
|
|
9194
9565
|
const target2 = firstPositional(args);
|
|
9195
9566
|
if (!target2) throw new Error("dsl validate requires <contract.synapsor>");
|
|
9196
9567
|
const source = await fs3.readFile(target2, "utf8");
|
|
9568
|
+
const strict = args.includes("--strict");
|
|
9197
9569
|
const result = validateAgentDsl(source);
|
|
9198
9570
|
if (args.includes("--json")) {
|
|
9199
9571
|
process2.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
9200
9572
|
`);
|
|
9201
9573
|
} else if (result.ok) {
|
|
9202
9574
|
process2.stdout.write(`dsl valid: ${target2}
|
|
9575
|
+
`);
|
|
9576
|
+
for (const warning of result.warnings) process2.stdout.write(`warning ${warning.line}:${warning.column} ${warning.code}: ${warning.message}
|
|
9203
9577
|
`);
|
|
9204
9578
|
} else {
|
|
9205
9579
|
process2.stdout.write(`dsl invalid: ${target2}
|
|
@@ -9207,13 +9581,22 @@ async function dslValidate(args) {
|
|
|
9207
9581
|
for (const error of result.errors) process2.stdout.write(`error ${error.line}:${error.column} ${error.code}: ${error.message}
|
|
9208
9582
|
`);
|
|
9209
9583
|
}
|
|
9210
|
-
return result.ok ? 0 : 1;
|
|
9584
|
+
return result.ok && (!strict || result.warnings.length === 0) ? 0 : 1;
|
|
9211
9585
|
}
|
|
9212
9586
|
async function dslCompile(args) {
|
|
9213
9587
|
const target2 = firstPositional(args);
|
|
9214
9588
|
if (!target2) throw new Error("dsl compile requires <contract.synapsor>");
|
|
9215
9589
|
const source = await fs3.readFile(target2, "utf8");
|
|
9216
|
-
const
|
|
9590
|
+
const strict = args.includes("--strict");
|
|
9591
|
+
const result = compileAgentDslWithWarnings(source);
|
|
9592
|
+
if (strict && result.warnings.length > 0) {
|
|
9593
|
+
process2.stdout.write(`dsl warnings treated as errors: ${target2}
|
|
9594
|
+
`);
|
|
9595
|
+
for (const warning of result.warnings) process2.stdout.write(`warning ${warning.line}:${warning.column} ${warning.code}: ${warning.message}
|
|
9596
|
+
`);
|
|
9597
|
+
return 1;
|
|
9598
|
+
}
|
|
9599
|
+
const contract = result.contract;
|
|
9217
9600
|
const output = outputArg(args);
|
|
9218
9601
|
const text = `${JSON.stringify(contract, null, 2)}
|
|
9219
9602
|
`;
|
|
@@ -9224,6 +9607,8 @@ async function dslCompile(args) {
|
|
|
9224
9607
|
} else {
|
|
9225
9608
|
process2.stdout.write(text);
|
|
9226
9609
|
}
|
|
9610
|
+
for (const warning of result.warnings) process2.stderr.write(`warning ${warning.line}:${warning.column} ${warning.code}: ${warning.message}
|
|
9611
|
+
`);
|
|
9227
9612
|
return 0;
|
|
9228
9613
|
}
|
|
9229
9614
|
async function contractValidate(args) {
|
|
@@ -9353,8 +9738,7 @@ function bundleReadme(contract) {
|
|
|
9353
9738
|
}
|
|
9354
9739
|
async function configValidate(args) {
|
|
9355
9740
|
const configPath = optionalArg(args, "--config") ?? "synapsor.runner.json";
|
|
9356
|
-
const
|
|
9357
|
-
const result = validateRunnerCapabilityConfig(parsed);
|
|
9741
|
+
const result = await validateConfigFile(configPath);
|
|
9358
9742
|
if (args.includes("--json")) {
|
|
9359
9743
|
process2.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
9360
9744
|
`);
|
|
@@ -9375,6 +9759,30 @@ async function configValidate(args) {
|
|
|
9375
9759
|
}
|
|
9376
9760
|
return result.ok ? 0 : 1;
|
|
9377
9761
|
}
|
|
9762
|
+
async function validateConfigFile(configPath) {
|
|
9763
|
+
const parsed = JSON.parse(await fs3.readFile(configPath, "utf8"));
|
|
9764
|
+
const raw = validateRunnerCapabilityConfig(parsed);
|
|
9765
|
+
if (!raw.ok) return raw;
|
|
9766
|
+
try {
|
|
9767
|
+
const resolved = resolveRuntimeConfig(parsed, path3.dirname(path3.resolve(configPath)));
|
|
9768
|
+
const resolvedValidation = validateRunnerCapabilityConfig(resolved);
|
|
9769
|
+
return {
|
|
9770
|
+
ok: resolvedValidation.ok,
|
|
9771
|
+
errors: resolvedValidation.errors,
|
|
9772
|
+
warnings: [...raw.warnings, ...resolvedValidation.warnings]
|
|
9773
|
+
};
|
|
9774
|
+
} catch (error) {
|
|
9775
|
+
return {
|
|
9776
|
+
ok: false,
|
|
9777
|
+
errors: [{
|
|
9778
|
+
path: "$.contracts",
|
|
9779
|
+
code: "CONTRACT_RESOLUTION_FAILED",
|
|
9780
|
+
message: error instanceof Error ? error.message : String(error)
|
|
9781
|
+
}],
|
|
9782
|
+
warnings: raw.warnings
|
|
9783
|
+
};
|
|
9784
|
+
}
|
|
9785
|
+
}
|
|
9378
9786
|
async function configShow(args) {
|
|
9379
9787
|
const configPath = optionalArg(args, "--config") ?? "synapsor.runner.json";
|
|
9380
9788
|
const parsed = JSON.parse(await fs3.readFile(configPath, "utf8"));
|
|
@@ -9438,13 +9846,49 @@ function trustedContextsForDoctor(config) {
|
|
|
9438
9846
|
return contexts;
|
|
9439
9847
|
}
|
|
9440
9848
|
function envPresenceCheck(envName, message) {
|
|
9849
|
+
const value = envValue2(process2.env, envName);
|
|
9441
9850
|
return {
|
|
9442
9851
|
name: `env:${envName}`,
|
|
9443
|
-
ok: Boolean(
|
|
9444
|
-
level:
|
|
9445
|
-
message:
|
|
9852
|
+
ok: Boolean(value),
|
|
9853
|
+
level: value ? "pass" : "fail",
|
|
9854
|
+
message: value ? `${envName} is set.` : message
|
|
9446
9855
|
};
|
|
9447
9856
|
}
|
|
9857
|
+
function proposalWritebackResolutionDoctorCheck(config, capability) {
|
|
9858
|
+
const mode = capabilityWritebackMode2(capability);
|
|
9859
|
+
if (mode === "none") {
|
|
9860
|
+
return {
|
|
9861
|
+
name: `capability:${capability.name}:writeback-resolution`,
|
|
9862
|
+
ok: true,
|
|
9863
|
+
level: "pass",
|
|
9864
|
+
message: "Capability explicitly declares no local writeback; proposals are review records only."
|
|
9865
|
+
};
|
|
9866
|
+
}
|
|
9867
|
+
if (mode === "cloud_worker") {
|
|
9868
|
+
return {
|
|
9869
|
+
name: `capability:${capability.name}:writeback-resolution`,
|
|
9870
|
+
ok: true,
|
|
9871
|
+
level: "pass",
|
|
9872
|
+
message: "Capability declares cloud-worker writeback; local apply is intentionally unavailable."
|
|
9873
|
+
};
|
|
9874
|
+
}
|
|
9875
|
+
try {
|
|
9876
|
+
assertProposalWritebackResolvable(config, capability);
|
|
9877
|
+
return {
|
|
9878
|
+
name: `capability:${capability.name}:writeback-resolution`,
|
|
9879
|
+
ok: true,
|
|
9880
|
+
level: "pass",
|
|
9881
|
+
message: mode === "direct_sql" ? "Direct SQL writeback definition resolves to a source and writer env var name." : `App-owned handler writeback resolves to executor ${capabilityWritebackExecutor(capability)}.`
|
|
9882
|
+
};
|
|
9883
|
+
} catch (error) {
|
|
9884
|
+
return {
|
|
9885
|
+
name: `capability:${capability.name}:writeback-resolution`,
|
|
9886
|
+
ok: false,
|
|
9887
|
+
level: "fail",
|
|
9888
|
+
message: error instanceof Error ? error.message : String(error)
|
|
9889
|
+
};
|
|
9890
|
+
}
|
|
9891
|
+
}
|
|
9448
9892
|
async function httpHandlerReachabilityCheck(executorName, rawUrl, timeoutMs) {
|
|
9449
9893
|
try {
|
|
9450
9894
|
const url = new URL(rawUrl);
|
|
@@ -9494,7 +9938,7 @@ function safeReachabilityError(error) {
|
|
|
9494
9938
|
return "connection failed";
|
|
9495
9939
|
}
|
|
9496
9940
|
async function inspectConfiguredSource(input) {
|
|
9497
|
-
if (!process2.env
|
|
9941
|
+
if (!envValue2(process2.env, input.source.read_url_env)) return;
|
|
9498
9942
|
const capabilities = (input.config.capabilities ?? []).filter((capability) => capability.source === input.sourceName);
|
|
9499
9943
|
const schemas = Array.from(new Set(capabilities.map((capability) => capability.target.schema)));
|
|
9500
9944
|
for (const schema of schemas.length ? schemas : [void 0]) {
|
|
@@ -9837,15 +10281,15 @@ function firstRunConfigEnvChecks(config) {
|
|
|
9837
10281
|
String(values.tenant_id_env ?? "SYNAPSOR_TENANT_ID"),
|
|
9838
10282
|
String(values.principal_env ?? "SYNAPSOR_PRINCIPAL")
|
|
9839
10283
|
]) {
|
|
9840
|
-
checks.push(process2.env
|
|
10284
|
+
checks.push(envValue2(process2.env, envName) ? pass(`env-${envName}`, `${envName} is set for ${contextName}.`, "Trusted tenant/principal values must come from the launcher, not the model.", "No action needed.") : warn(`env-${envName}`, `${envName} is not set for ${contextName}.`, "Trusted tenant/principal values must come from the launcher, not the model.", `Set ${envName}, or use the generated .env.example as a template.`));
|
|
9841
10285
|
}
|
|
9842
10286
|
}
|
|
9843
10287
|
for (const [sourceName, source] of Object.entries(config.sources ?? {})) {
|
|
9844
|
-
checks.push(process2.env
|
|
10288
|
+
checks.push(envValue2(process2.env, source.read_url_env) ? pass(`env-${source.read_url_env}`, `${source.read_url_env} is set for ${sourceName}.`, "Configured capabilities need a read credential env var to inspect/propose against your DB.", "No action needed.") : warn(`env-${source.read_url_env}`, `${source.read_url_env} is not set for ${sourceName}.`, "Configured capabilities need a read credential env var to inspect/propose against your DB.", `Set ${source.read_url_env} before running doctor, tools preview, or mcp serve against your own database.`));
|
|
9845
10289
|
if (source.write_url_env) {
|
|
9846
|
-
checks.push(process2.env
|
|
9847
|
-
const readValue = process2.env
|
|
9848
|
-
const writeValue = process2.env
|
|
10290
|
+
checks.push(envValue2(process2.env, source.write_url_env) ? pass(`env-${source.write_url_env}`, `${source.write_url_env} is set for ${sourceName}.`, "Trusted writeback needs a separate writer credential outside the MCP client.", "No action needed.") : warn(`env-${source.write_url_env}`, `${source.write_url_env} is not set for ${sourceName}.`, "Trusted writeback needs a separate writer credential outside the MCP client.", `Set ${source.write_url_env} only when you are ready to apply an approved writeback job.`));
|
|
10291
|
+
const readValue = envValue2(process2.env, source.read_url_env);
|
|
10292
|
+
const writeValue = envValue2(process2.env, source.write_url_env);
|
|
9849
10293
|
if (readValue && writeValue && readValue === writeValue) {
|
|
9850
10294
|
checks.push(fail(`credential-split-${sourceName}`, `Read and write env vars resolve to the same credential for ${sourceName}.`, "Read/proposal authority and writeback authority must be separated.", "Use a read-only credential for MCP reads and a separate writer credential only for trusted apply."));
|
|
9851
10295
|
}
|
|
@@ -9900,11 +10344,11 @@ function fail(name, checked, why, fix) {
|
|
|
9900
10344
|
return { name, status: "fail", checked, why, fix };
|
|
9901
10345
|
}
|
|
9902
10346
|
async function isPortAvailable(port) {
|
|
9903
|
-
return new Promise((
|
|
10347
|
+
return new Promise((resolve2) => {
|
|
9904
10348
|
const server = net.createServer();
|
|
9905
|
-
server.once("error", () =>
|
|
10349
|
+
server.once("error", () => resolve2(false));
|
|
9906
10350
|
server.listen(port, "127.0.0.1", () => {
|
|
9907
|
-
server.close(() =>
|
|
10351
|
+
server.close(() => resolve2(true));
|
|
9908
10352
|
});
|
|
9909
10353
|
});
|
|
9910
10354
|
}
|
|
@@ -9924,9 +10368,10 @@ async function localDoctor(args) {
|
|
|
9924
10368
|
const allowSharedCredential = args.includes("--allow-shared-credential");
|
|
9925
10369
|
const checkHandlers = args.includes("--check-handlers");
|
|
9926
10370
|
const checkWriteback = args.includes("--check-writeback") || args.includes("--check-db");
|
|
9927
|
-
const
|
|
10371
|
+
const rawConfig = JSON.parse(await fs3.readFile(configPath, "utf8"));
|
|
10372
|
+
let parsed = rawConfig;
|
|
9928
10373
|
const checks = [];
|
|
9929
|
-
const validation =
|
|
10374
|
+
const validation = await validateConfigFile(configPath);
|
|
9930
10375
|
checks.push({
|
|
9931
10376
|
name: "config-valid",
|
|
9932
10377
|
ok: validation.ok,
|
|
@@ -9936,6 +10381,9 @@ async function localDoctor(args) {
|
|
|
9936
10381
|
for (const warning of validation.warnings) {
|
|
9937
10382
|
checks.push({ name: `config-warning:${warning.code}`, ok: true, level: "warn", message: warning.message });
|
|
9938
10383
|
}
|
|
10384
|
+
if (validation.ok) {
|
|
10385
|
+
parsed = await readRuntimeConfig(configPath);
|
|
10386
|
+
}
|
|
9939
10387
|
const contextsToCheck = trustedContextsForDoctor(parsed);
|
|
9940
10388
|
for (const [contextName, contextValues] of contextsToCheck) {
|
|
9941
10389
|
const tenantEnv = String(contextValues.tenant_id_env ?? "SYNAPSOR_TENANT_ID");
|
|
@@ -9945,14 +10393,19 @@ async function localDoctor(args) {
|
|
|
9945
10393
|
}
|
|
9946
10394
|
}
|
|
9947
10395
|
const sources = parsed.sources ?? {};
|
|
10396
|
+
if (parsed.mode === "review") {
|
|
10397
|
+
for (const capability of (parsed.capabilities ?? []).filter((item) => item.kind === "proposal")) {
|
|
10398
|
+
checks.push(proposalWritebackResolutionDoctorCheck(parsed, capability));
|
|
10399
|
+
}
|
|
10400
|
+
}
|
|
9948
10401
|
for (const [sourceName, source] of Object.entries(sources)) {
|
|
9949
10402
|
checks.push(envPresenceCheck(source.read_url_env, `${source.read_url_env} is required for ${sourceName} reads.`));
|
|
9950
10403
|
if (parsed.mode === "review") {
|
|
9951
10404
|
if (sourceNeedsSqlWriteback(parsed, sourceName)) {
|
|
9952
10405
|
if (source.write_url_env) {
|
|
9953
10406
|
checks.push(envPresenceCheck(source.write_url_env, `${source.write_url_env} is required for trusted writeback in review mode.`));
|
|
9954
|
-
const readValue = process2.env
|
|
9955
|
-
const writeValue = process2.env
|
|
10407
|
+
const readValue = envValue2(process2.env, source.read_url_env);
|
|
10408
|
+
const writeValue = envValue2(process2.env, source.write_url_env);
|
|
9956
10409
|
if (readValue && writeValue && readValue === writeValue) {
|
|
9957
10410
|
checks.push({
|
|
9958
10411
|
name: `source:${sourceName}:credential-separation`,
|
|
@@ -9966,7 +10419,7 @@ async function localDoctor(args) {
|
|
|
9966
10419
|
} else {
|
|
9967
10420
|
checks.push({ name: `source:${sourceName}:write-url-env`, ok: false, level: "fail", message: "SQL writeback proposal capabilities require write_url_env for trusted writeback." });
|
|
9968
10421
|
}
|
|
9969
|
-
const writeUrl = source.write_url_env ? process2.env
|
|
10422
|
+
const writeUrl = source.write_url_env ? envValue2(process2.env, source.write_url_env) : void 0;
|
|
9970
10423
|
if (checkWriteback && writeUrl) {
|
|
9971
10424
|
checks.push(...await directSqlWritebackDoctorChecks(parsed, sourceName, source, writeUrl));
|
|
9972
10425
|
} else if (checkWriteback) {
|
|
@@ -9994,7 +10447,7 @@ async function localDoctor(args) {
|
|
|
9994
10447
|
const urlEnv = String(executor.url_env ?? "");
|
|
9995
10448
|
if (urlEnv) {
|
|
9996
10449
|
checks.push(envPresenceCheck(urlEnv, `${urlEnv} is required for http_handler executor ${executorName}.`));
|
|
9997
|
-
const handlerUrl = process2.env
|
|
10450
|
+
const handlerUrl = envValue2(process2.env, urlEnv);
|
|
9998
10451
|
if (checkHandlers && handlerUrl) {
|
|
9999
10452
|
checks.push(await httpHandlerReachabilityCheck(executorName, handlerUrl, Number(executor.timeout_ms ?? 3e3)));
|
|
10000
10453
|
} else if (!checkHandlers) {
|
|
@@ -10108,7 +10561,7 @@ async function directSqlWritebackDoctorChecks(config, sourceName, source, writeU
|
|
|
10108
10561
|
function directSqlProposalCapabilities(config, sourceName) {
|
|
10109
10562
|
return (config.capabilities ?? []).filter((capability) => {
|
|
10110
10563
|
if (capability.kind !== "proposal" || capability.source !== sourceName) return false;
|
|
10111
|
-
return (capability
|
|
10564
|
+
return capabilityWritebackMode2(capability) === "direct_sql";
|
|
10112
10565
|
});
|
|
10113
10566
|
}
|
|
10114
10567
|
async function rollbackOnlyTargetProbe(engine, databaseUrl, capability) {
|
|
@@ -10299,7 +10752,7 @@ async function applyProposal(args, proposalId) {
|
|
|
10299
10752
|
const storePath = optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE ?? "./.synapsor/local.db";
|
|
10300
10753
|
const dryRun = args.includes("--dry-run") || process2.env.SYNAPSOR_DRY_RUN === "true";
|
|
10301
10754
|
const runnerId = optionalArg(args, "--runner") ?? process2.env.SYNAPSOR_RUNNER_ID ?? "local_runner";
|
|
10302
|
-
const config =
|
|
10755
|
+
const config = await readRuntimeConfig(configPath);
|
|
10303
10756
|
const resolvedProposalId = await resolveProposalId(proposalId, storePath);
|
|
10304
10757
|
const validation = validateRunnerCapabilityConfig(config);
|
|
10305
10758
|
if (!validation.ok) {
|
|
@@ -10316,6 +10769,9 @@ async function applyProposal(args, proposalId) {
|
|
|
10316
10769
|
const proposal = requireLocalProposal(store, resolvedProposalId);
|
|
10317
10770
|
const capability = findProposalCapability(config, proposal);
|
|
10318
10771
|
const executorName = proposalExecutorName(proposal, capability);
|
|
10772
|
+
if (executorName === "none" || executorName === "cloud_worker") {
|
|
10773
|
+
throw new Error(`proposal ${resolvedProposalId} is not locally applyable; capability ${capability.name} declares ${executorName === "none" ? "no local writeback" : "cloud-worker writeback"}.`);
|
|
10774
|
+
}
|
|
10319
10775
|
if (executorName === "sql_update") {
|
|
10320
10776
|
const job = store.createWritebackJobFromProposal(resolvedProposalId, {
|
|
10321
10777
|
project_id: optionalArg(args, "--project") ?? "local",
|
|
@@ -10373,16 +10829,19 @@ async function applySqlJob(job, configPath, storePath, dryRun, env = process2.en
|
|
|
10373
10829
|
return result;
|
|
10374
10830
|
}
|
|
10375
10831
|
async function resolveSqlWriteDatabaseUrl(job, configPath, env) {
|
|
10376
|
-
const config =
|
|
10832
|
+
const config = await readRuntimeConfig(configPath);
|
|
10377
10833
|
const source = config.sources?.[job.source_id];
|
|
10378
10834
|
const writeUrlEnv = source?.write_url_env;
|
|
10379
|
-
if (writeUrlEnv
|
|
10380
|
-
|
|
10835
|
+
if (writeUrlEnv) {
|
|
10836
|
+
const value = envValue2(env, writeUrlEnv);
|
|
10837
|
+
if (value) return value;
|
|
10838
|
+
}
|
|
10839
|
+
return envValue2(env, "SYNAPSOR_DATABASE_URL") || "";
|
|
10381
10840
|
}
|
|
10382
10841
|
function sourceNeedsSqlWriteback(config, sourceName) {
|
|
10383
10842
|
return (config.capabilities ?? []).some((capability) => {
|
|
10384
10843
|
if (capability.kind !== "proposal" || capability.source !== sourceName) return false;
|
|
10385
|
-
return (capability
|
|
10844
|
+
return capabilityWritebackMode2(capability) === "direct_sql";
|
|
10386
10845
|
});
|
|
10387
10846
|
}
|
|
10388
10847
|
function findProposalCapability(config, proposal) {
|
|
@@ -10401,8 +10860,12 @@ function findProposalCapability(config, proposal) {
|
|
|
10401
10860
|
return capability;
|
|
10402
10861
|
}
|
|
10403
10862
|
function proposalExecutorName(proposal, capability) {
|
|
10863
|
+
const mode = capabilityWritebackMode2(capability);
|
|
10864
|
+
if (mode === "none") return "none";
|
|
10865
|
+
if (mode === "cloud_worker") return "cloud_worker";
|
|
10866
|
+
if (mode === "direct_sql") return "sql_update";
|
|
10404
10867
|
const writeback2 = proposal.change_set.writeback;
|
|
10405
|
-
return capability
|
|
10868
|
+
return capabilityWritebackExecutor(capability) ?? (typeof writeback2.executor === "string" ? writeback2.executor : void 0) ?? "sql_update";
|
|
10406
10869
|
}
|
|
10407
10870
|
function executorConfig(config, executorName) {
|
|
10408
10871
|
const raw = config.executors?.[executorName];
|
|
@@ -10427,7 +10890,7 @@ async function applyHttpHandlerProposal(input) {
|
|
|
10427
10890
|
executor: input.executorName,
|
|
10428
10891
|
request: prepared.request
|
|
10429
10892
|
});
|
|
10430
|
-
const url = input.env
|
|
10893
|
+
const url = envValue2(input.env, input.executor.url_env);
|
|
10431
10894
|
if (!url) throw new Error(`${input.executor.url_env} is not set`);
|
|
10432
10895
|
const headers = {
|
|
10433
10896
|
accept: "application/json",
|
|
@@ -10435,7 +10898,7 @@ async function applyHttpHandlerProposal(input) {
|
|
|
10435
10898
|
"idempotency-key": prepared.request.idempotency_key
|
|
10436
10899
|
};
|
|
10437
10900
|
if (input.executor.auth) {
|
|
10438
|
-
const token = input.env
|
|
10901
|
+
const token = envValue2(input.env, input.executor.auth.token_env);
|
|
10439
10902
|
if (!token) throw new Error(`${input.executor.auth.token_env} is not set`);
|
|
10440
10903
|
headers.authorization = `Bearer ${token}`;
|
|
10441
10904
|
}
|
|
@@ -10450,7 +10913,7 @@ async function applyHttpHandlerProposal(input) {
|
|
|
10450
10913
|
headers["x-synapsor-issued-at"] = issuedAt;
|
|
10451
10914
|
headers["x-synapsor-proposal-id"] = prepared.proposal.proposal_id;
|
|
10452
10915
|
if (input.executor.signing_secret_env) {
|
|
10453
|
-
const signingSecret = input.env
|
|
10916
|
+
const signingSecret = envValue2(input.env, input.executor.signing_secret_env);
|
|
10454
10917
|
if (!signingSecret) throw new Error(`${input.executor.signing_secret_env} is not set`);
|
|
10455
10918
|
headers["x-synapsor-signature"] = signHandlerRequestBody(requestBody, signingSecret);
|
|
10456
10919
|
}
|
|
@@ -10488,7 +10951,7 @@ async function applyCommandHandlerProposal(input) {
|
|
|
10488
10951
|
executor: input.executorName,
|
|
10489
10952
|
request: prepared.request
|
|
10490
10953
|
});
|
|
10491
|
-
const commandText = input.env
|
|
10954
|
+
const commandText = envValue2(input.env, input.executor.command_env);
|
|
10492
10955
|
if (!commandText) throw new Error(`${input.executor.command_env} is not set`);
|
|
10493
10956
|
const [command, ...commandArgs] = splitCommand(commandText);
|
|
10494
10957
|
if (!command) throw new Error(`${input.executor.command_env} did not contain a command`);
|
|
@@ -10738,7 +11201,7 @@ function safeHandlerDetails(value) {
|
|
|
10738
11201
|
return redactConfig(value);
|
|
10739
11202
|
}
|
|
10740
11203
|
async function runCommandHandler(command, args, request, timeoutMs) {
|
|
10741
|
-
return new Promise((
|
|
11204
|
+
return new Promise((resolve2) => {
|
|
10742
11205
|
const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"], env: process2.env });
|
|
10743
11206
|
let stdout = "";
|
|
10744
11207
|
let stderr = "";
|
|
@@ -10747,7 +11210,7 @@ async function runCommandHandler(command, args, request, timeoutMs) {
|
|
|
10747
11210
|
if (settled) return;
|
|
10748
11211
|
settled = true;
|
|
10749
11212
|
clearTimeout(timeout);
|
|
10750
|
-
|
|
11213
|
+
resolve2(body);
|
|
10751
11214
|
};
|
|
10752
11215
|
const timeout = setTimeout(() => {
|
|
10753
11216
|
child.kill("SIGTERM");
|
|
@@ -10772,7 +11235,7 @@ async function runCommandHandler(command, args, request, timeoutMs) {
|
|
|
10772
11235
|
});
|
|
10773
11236
|
}
|
|
10774
11237
|
async function verifyLocalWritebackAuthority(job, configPath, storePath) {
|
|
10775
|
-
const config =
|
|
11238
|
+
const config = await readRuntimeConfig(configPath);
|
|
10776
11239
|
const validation = validateRunnerCapabilityConfig(config);
|
|
10777
11240
|
if (!validation.ok) {
|
|
10778
11241
|
throw new Error(`cannot apply writeback with invalid local config: ${validation.errors.map((error) => error.code).join(", ")}`);
|
|
@@ -10790,7 +11253,7 @@ async function verifyLocalWritebackAuthority(job, configPath, storePath) {
|
|
|
10790
11253
|
if (Date.parse(String(job.lease_expires_at)) < Date.now()) {
|
|
10791
11254
|
throw new Error("writeback job lease has expired");
|
|
10792
11255
|
}
|
|
10793
|
-
const proposalCapabilities = (config.capabilities ?? []).filter((capability) => capability.kind === "proposal" && capability.source === job.source_id);
|
|
11256
|
+
const proposalCapabilities = (config.capabilities ?? []).filter((capability) => capability.kind === "proposal" && capability.source === job.source_id && capabilityWritebackMode2(capability) === "direct_sql");
|
|
10794
11257
|
const matching = proposalCapabilities.find((capability) => capabilityMatchesJob(capability, job));
|
|
10795
11258
|
if (!matching) {
|
|
10796
11259
|
throw new Error("writeback job does not match any reviewed proposal capability in local config");
|
|
@@ -10979,7 +11442,7 @@ function formatReviewModeUp(input) {
|
|
|
10979
11442
|
} else {
|
|
10980
11443
|
lines.push(
|
|
10981
11444
|
` Streamable HTTP endpoint: http://${input.host}:${input.port}/mcp`,
|
|
10982
|
-
` Auth token env: ${input.authTokenEnv} (${process2.env
|
|
11445
|
+
` Auth token env: ${input.authTokenEnv} (${envValue2(process2.env, input.authTokenEnv) ? "set" : "missing"})`,
|
|
10983
11446
|
input.serveRequested ? input.dryRun ? " Status: dry run only; server not started." : " Status: starting after this checklist." : ` Start command: ${cliCommandName()} up --serve --config ${input.configPath} --store ${input.storePath} --port ${input.port} --auth-token-env ${input.authTokenEnv} --alias-mode ${input.aliasMode}`
|
|
10984
11447
|
);
|
|
10985
11448
|
}
|
|
@@ -10998,12 +11461,19 @@ function formatUpWritebackLines(config) {
|
|
|
10998
11461
|
const proposals2 = (config.capabilities ?? []).filter((capability) => capability.kind === "proposal");
|
|
10999
11462
|
if (proposals2.length === 0) return [" - no proposal capabilities; this config is read-only from Runner's perspective"];
|
|
11000
11463
|
return proposals2.map((capability) => {
|
|
11001
|
-
const
|
|
11002
|
-
if (
|
|
11464
|
+
const mode = capabilityWritebackMode2(capability);
|
|
11465
|
+
if (mode === "none") {
|
|
11466
|
+
return ` - ${capability.name}: proposal-only; no local writeback`;
|
|
11467
|
+
}
|
|
11468
|
+
if (mode === "cloud_worker") {
|
|
11469
|
+
return ` - ${capability.name}: cloud-worker writeback; local apply disabled`;
|
|
11470
|
+
}
|
|
11471
|
+
if (mode === "direct_sql") {
|
|
11003
11472
|
const source = config.sources?.[capability.source];
|
|
11004
11473
|
const envName = source?.write_url_env ?? "SYNAPSOR_DATABASE_URL";
|
|
11005
|
-
return ` - ${capability.name}: direct guarded one-row UPDATE via ${envName} (${process2.env
|
|
11474
|
+
return ` - ${capability.name}: direct guarded one-row UPDATE via ${envName} (${envValue2(process2.env, envName) ? "set" : "missing"})`;
|
|
11006
11475
|
}
|
|
11476
|
+
const executorName = capabilityWritebackExecutor(capability) ?? "missing_executor";
|
|
11007
11477
|
const executor = config.executors?.[executorName];
|
|
11008
11478
|
return ` - ${capability.name}: app-owned ${String(executor?.type ?? "executor")} ${executorName}`;
|
|
11009
11479
|
});
|
|
@@ -11018,14 +11488,14 @@ function formatUpHandlerLines(config) {
|
|
|
11018
11488
|
const tokenEnv = typeof auth?.token_env === "string" ? auth.token_env : void 0;
|
|
11019
11489
|
const signingSecretEnv = typeof executor.signing_secret_env === "string" ? executor.signing_secret_env : void 0;
|
|
11020
11490
|
lines.push(` - ${name}: http_handler`);
|
|
11021
|
-
if (urlEnv) lines.push(` url env: ${urlEnv} (${process2.env
|
|
11022
|
-
if (tokenEnv) lines.push(` bearer token env: ${tokenEnv} (${process2.env
|
|
11023
|
-
if (signingSecretEnv) lines.push(` signing secret env: ${signingSecretEnv} (${process2.env
|
|
11491
|
+
if (urlEnv) lines.push(` url env: ${urlEnv} (${envValue2(process2.env, urlEnv) ? "set" : "missing"})`);
|
|
11492
|
+
if (tokenEnv) lines.push(` bearer token env: ${tokenEnv} (${envValue2(process2.env, tokenEnv) ? "set" : "missing"})`);
|
|
11493
|
+
if (signingSecretEnv) lines.push(` signing secret env: ${signingSecretEnv} (${envValue2(process2.env, signingSecretEnv) ? "set" : "missing"})`);
|
|
11024
11494
|
if (!signingSecretEnv) lines.push(" signing secret env: not configured (recommended unless loopback-only)");
|
|
11025
11495
|
} else if (executor.type === "command_handler") {
|
|
11026
11496
|
const commandEnv = typeof executor.command_env === "string" ? executor.command_env : "";
|
|
11027
11497
|
lines.push(` - ${name}: command_handler`);
|
|
11028
|
-
if (commandEnv) lines.push(` command env: ${commandEnv} (${process2.env
|
|
11498
|
+
if (commandEnv) lines.push(` command env: ${commandEnv} (${envValue2(process2.env, commandEnv) ? "set" : "missing"})`);
|
|
11029
11499
|
}
|
|
11030
11500
|
}
|
|
11031
11501
|
return lines;
|
|
@@ -11069,19 +11539,33 @@ async function cloudPush(args) {
|
|
|
11069
11539
|
if (!target2) throw new Error("cloud push requires <synapsor.contract.json>");
|
|
11070
11540
|
const parsed = JSON.parse(await fs3.readFile(target2, "utf8"));
|
|
11071
11541
|
const contract = normalizeContract(parsed);
|
|
11542
|
+
const workspace = (optionalArg(args, "--workspace") ?? optionalArg(args, "--project") ?? process2.env.SYNAPSOR_WORKSPACE_ID ?? process2.env.SYNAPSOR_PROJECT_ID ?? "").trim();
|
|
11543
|
+
const name = (optionalArg(args, "--name") ?? contract.metadata?.name ?? "").trim();
|
|
11544
|
+
const description = (optionalArg(args, "--description") ?? contract.metadata?.description ?? "").trim();
|
|
11545
|
+
const idempotencyKey = optionalArg(args, "--idempotency-key");
|
|
11072
11546
|
const payload = {
|
|
11073
11547
|
schema_version: "synapsor.cloud-contract-push.v0.1",
|
|
11074
11548
|
contract,
|
|
11075
11549
|
summary: contractSummary(contract),
|
|
11076
|
-
workspace
|
|
11077
|
-
name
|
|
11550
|
+
workspace,
|
|
11551
|
+
name,
|
|
11552
|
+
description,
|
|
11553
|
+
source: "runner",
|
|
11554
|
+
source_versions: {
|
|
11555
|
+
"@synapsor/runner": process2.env.npm_package_version ?? "unknown"
|
|
11556
|
+
},
|
|
11557
|
+
activate: args.includes("--activate"),
|
|
11558
|
+
idempotency_key: idempotencyKey,
|
|
11078
11559
|
pushed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
11079
11560
|
};
|
|
11080
11561
|
const dryRun = args.includes("--dry-run");
|
|
11081
|
-
|
|
11562
|
+
const json = args.includes("--json");
|
|
11563
|
+
if (dryRun && json) {
|
|
11082
11564
|
process2.stdout.write(`${JSON.stringify({ ok: dryRun, dry_run: dryRun, payload }, null, 2)}
|
|
11083
11565
|
`);
|
|
11084
|
-
|
|
11566
|
+
return 0;
|
|
11567
|
+
}
|
|
11568
|
+
if (dryRun) {
|
|
11085
11569
|
process2.stdout.write("Synapsor Cloud contract push preview\n");
|
|
11086
11570
|
process2.stdout.write(`Contract: ${target2}
|
|
11087
11571
|
`);
|
|
@@ -11095,17 +11579,48 @@ async function cloudPush(args) {
|
|
|
11095
11579
|
`);
|
|
11096
11580
|
process2.stdout.write(`Kept-out fields: ${payload.summary.kept_out_fields}
|
|
11097
11581
|
`);
|
|
11098
|
-
|
|
11099
|
-
if (dryRun) {
|
|
11100
|
-
if (!args.includes("--json")) process2.stdout.write("Dry run only. No Cloud upload attempted.\n");
|
|
11582
|
+
process2.stdout.write("Dry run only. No Cloud upload attempted.\n");
|
|
11101
11583
|
return 0;
|
|
11102
11584
|
}
|
|
11103
|
-
const apiUrl = optionalArg(args, "--api-url") ?? process2.env.SYNAPSOR_CLOUD_BASE_URL;
|
|
11104
|
-
const token = optionalArg(args, "--token") ?? process2.env.SYNAPSOR_CLOUD_TOKEN ?? process2.env.SYNAPSOR_RUNNER_TOKEN;
|
|
11585
|
+
const apiUrl = (optionalArg(args, "--api-url") ?? process2.env.SYNAPSOR_CLOUD_BASE_URL ?? "").trim();
|
|
11586
|
+
const token = (optionalArg(args, "--token") ?? process2.env.SYNAPSOR_CLOUD_TOKEN ?? process2.env.SYNAPSOR_RUNNER_TOKEN ?? "").trim();
|
|
11587
|
+
if (!workspace) {
|
|
11588
|
+
throw new Error("cloud push upload requires --workspace <project_id> or SYNAPSOR_WORKSPACE_ID/SYNAPSOR_PROJECT_ID.");
|
|
11589
|
+
}
|
|
11105
11590
|
if (!apiUrl || !token) {
|
|
11106
|
-
throw new Error("cloud push upload requires --
|
|
11591
|
+
throw new Error("cloud push upload requires --api-url plus --token/SYNAPSOR_CLOUD_TOKEN. Use --dry-run for local validation without a network call.");
|
|
11592
|
+
}
|
|
11593
|
+
const response = await postCloudContractPush({
|
|
11594
|
+
apiUrl,
|
|
11595
|
+
token,
|
|
11596
|
+
workspace,
|
|
11597
|
+
payload,
|
|
11598
|
+
idempotencyKey
|
|
11599
|
+
});
|
|
11600
|
+
if (json) {
|
|
11601
|
+
process2.stdout.write(`${JSON.stringify(response, null, 2)}
|
|
11602
|
+
`);
|
|
11603
|
+
return 0;
|
|
11107
11604
|
}
|
|
11108
|
-
|
|
11605
|
+
const contractId = cloudStringField(response, "contract_id") || cloudStringField(response.contract, "contract_id");
|
|
11606
|
+
const versionId = cloudStringField(response, "contract_version_id") || cloudStringField(response.version, "contract_version_id");
|
|
11607
|
+
const digest = cloudStringField(response, "digest") || cloudStringField(response.version, "digest");
|
|
11608
|
+
const status = cloudStringField(response, "status") || cloudStringField(response.version, "status") || "stored";
|
|
11609
|
+
process2.stdout.write("Synapsor Cloud contract push complete\n");
|
|
11610
|
+
process2.stdout.write(`Workspace: ${workspace}
|
|
11611
|
+
`);
|
|
11612
|
+
if (contractId) process2.stdout.write(`Contract id: ${contractId}
|
|
11613
|
+
`);
|
|
11614
|
+
if (versionId) process2.stdout.write(`Version id: ${versionId}
|
|
11615
|
+
`);
|
|
11616
|
+
if (digest) process2.stdout.write(`Digest: ${digest}
|
|
11617
|
+
`);
|
|
11618
|
+
process2.stdout.write(`Status: ${status}
|
|
11619
|
+
`);
|
|
11620
|
+
const registryUrl = cloudStringField(response, "registry_url");
|
|
11621
|
+
if (registryUrl) process2.stdout.write(`Registry: ${registryUrl}
|
|
11622
|
+
`);
|
|
11623
|
+
return 0;
|
|
11109
11624
|
}
|
|
11110
11625
|
function contractSummary(contract) {
|
|
11111
11626
|
return {
|
|
@@ -11116,6 +11631,81 @@ function contractSummary(contract) {
|
|
|
11116
11631
|
kept_out_fields: contract.capabilities.reduce((count, capability) => count + (capability.kept_out_fields?.length ?? 0), 0)
|
|
11117
11632
|
};
|
|
11118
11633
|
}
|
|
11634
|
+
async function postCloudContractPush(input) {
|
|
11635
|
+
let endpoint;
|
|
11636
|
+
try {
|
|
11637
|
+
endpoint = new URL(`/v1/control/projects/${encodeURIComponent(input.workspace)}/agent-contracts`, input.apiUrl.endsWith("/") ? input.apiUrl : `${input.apiUrl}/`);
|
|
11638
|
+
} catch {
|
|
11639
|
+
throw new Error("cloud push upload failed: --api-url/SYNAPSOR_CLOUD_BASE_URL is not a valid URL.");
|
|
11640
|
+
}
|
|
11641
|
+
const headers = {
|
|
11642
|
+
accept: "application/json",
|
|
11643
|
+
authorization: `Bearer ${input.token}`,
|
|
11644
|
+
"content-type": "application/json",
|
|
11645
|
+
"user-agent": "synapsor-runner-cloud-push"
|
|
11646
|
+
};
|
|
11647
|
+
if (input.idempotencyKey) headers["idempotency-key"] = input.idempotencyKey;
|
|
11648
|
+
const controller = new AbortController();
|
|
11649
|
+
const timeout = setTimeout(() => controller.abort(), 15e3);
|
|
11650
|
+
let response;
|
|
11651
|
+
try {
|
|
11652
|
+
response = await fetch(endpoint, {
|
|
11653
|
+
method: "POST",
|
|
11654
|
+
headers,
|
|
11655
|
+
body: JSON.stringify(input.payload),
|
|
11656
|
+
signal: controller.signal
|
|
11657
|
+
});
|
|
11658
|
+
} catch (error) {
|
|
11659
|
+
const reason = error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError") ? "request timed out" : "network request failed";
|
|
11660
|
+
throw new Error(`cloud push upload failed: ${reason}. Check --api-url/SYNAPSOR_CLOUD_BASE_URL and network connectivity.`);
|
|
11661
|
+
} finally {
|
|
11662
|
+
clearTimeout(timeout);
|
|
11663
|
+
}
|
|
11664
|
+
const text = await response.text();
|
|
11665
|
+
const body = parseCloudResponseJson(text);
|
|
11666
|
+
if (!response.ok || body.ok === false) {
|
|
11667
|
+
throw new Error(cloudPushErrorMessage(response.status, body));
|
|
11668
|
+
}
|
|
11669
|
+
return body;
|
|
11670
|
+
}
|
|
11671
|
+
function parseCloudResponseJson(text) {
|
|
11672
|
+
if (!text.trim()) return {};
|
|
11673
|
+
try {
|
|
11674
|
+
const parsed = JSON.parse(text);
|
|
11675
|
+
return isRecord7(parsed) ? parsed : { value: parsed };
|
|
11676
|
+
} catch {
|
|
11677
|
+
return { raw: text.slice(0, 500) };
|
|
11678
|
+
}
|
|
11679
|
+
}
|
|
11680
|
+
function cloudPushErrorMessage(status, body) {
|
|
11681
|
+
const code = typeof body.error === "string" && body.error ? body.error : `http_${status}`;
|
|
11682
|
+
const detail = cloudValidationDetail(body);
|
|
11683
|
+
if (status === 401) return `cloud push upload failed: token is missing, invalid, or expired (${code}).`;
|
|
11684
|
+
if (status === 403) return `cloud push upload failed: token does not have permission to write this workspace (${code}).`;
|
|
11685
|
+
if (status === 404) return `cloud push upload failed: Cloud API URL or workspace was not found (${code}).`;
|
|
11686
|
+
if (status === 409) return `cloud push upload failed: registry conflict (${code}).${detail}`;
|
|
11687
|
+
if (status === 422) return `cloud push upload failed: Cloud rejected the contract (${code}).${detail}`;
|
|
11688
|
+
if (status >= 500) return `cloud push upload failed: Cloud returned HTTP ${status} (${code}).`;
|
|
11689
|
+
return `cloud push upload failed: Cloud returned HTTP ${status} (${code}).${detail}`;
|
|
11690
|
+
}
|
|
11691
|
+
function cloudValidationDetail(body) {
|
|
11692
|
+
const errors = Array.isArray(body.errors) ? body.errors : [];
|
|
11693
|
+
if (!errors.length) {
|
|
11694
|
+
const message = typeof body.message === "string" ? body.message : "";
|
|
11695
|
+
return message ? ` ${message}` : "";
|
|
11696
|
+
}
|
|
11697
|
+
const rendered = errors.slice(0, 3).map((issue) => {
|
|
11698
|
+
if (!isRecord7(issue)) return String(issue);
|
|
11699
|
+
const path4 = typeof issue.path === "string" ? issue.path : "$";
|
|
11700
|
+
const code = typeof issue.code === "string" ? issue.code : "validation_error";
|
|
11701
|
+
const message = typeof issue.message === "string" ? issue.message : "";
|
|
11702
|
+
return `${path4} ${code}${message ? `: ${message}` : ""}`;
|
|
11703
|
+
}).join("; ");
|
|
11704
|
+
return ` ${rendered}`;
|
|
11705
|
+
}
|
|
11706
|
+
function cloudStringField(value, key) {
|
|
11707
|
+
return isRecord7(value) && typeof value[key] === "string" ? value[key] : "";
|
|
11708
|
+
}
|
|
11119
11709
|
async function cloudConnect(args) {
|
|
11120
11710
|
const configPath = optionalArg(args, "--config") ?? process2.env.SYNAPSOR_MCP_CONFIG ?? "synapsor.cloud.json";
|
|
11121
11711
|
const parsed = JSON.parse(await fs3.readFile(configPath, "utf8"));
|
|
@@ -11126,8 +11716,8 @@ async function cloudConnect(args) {
|
|
|
11126
11716
|
}
|
|
11127
11717
|
const baseUrlEnv = parsed.cloud.base_url_env || "SYNAPSOR_CLOUD_BASE_URL";
|
|
11128
11718
|
const tokenEnv = parsed.cloud.runner_token_env || "SYNAPSOR_RUNNER_TOKEN";
|
|
11129
|
-
const baseUrl = process2.env
|
|
11130
|
-
const runnerToken = process2.env
|
|
11719
|
+
const baseUrl = envValue2(process2.env, baseUrlEnv);
|
|
11720
|
+
const runnerToken = envValue2(process2.env, tokenEnv);
|
|
11131
11721
|
const missing = [baseUrl ? "" : baseUrlEnv, runnerToken ? "" : tokenEnv].filter(Boolean);
|
|
11132
11722
|
if (missing.length > 0) {
|
|
11133
11723
|
process2.stdout.write(`missing environment variables: ${missing.join(", ")}
|
|
@@ -11303,7 +11893,7 @@ async function writebackDoctor(args) {
|
|
|
11303
11893
|
let ok = true;
|
|
11304
11894
|
for (const [sourceName, source] of sqlSources) {
|
|
11305
11895
|
const writeEnv = source.write_url_env;
|
|
11306
|
-
const writeUrl = writeEnv ? process2.env
|
|
11896
|
+
const writeUrl = writeEnv ? envValue2(process2.env, writeEnv) : void 0;
|
|
11307
11897
|
lines.push(`Source: ${sourceName}`);
|
|
11308
11898
|
lines.push(` engine: ${source.engine}`);
|
|
11309
11899
|
lines.push(` writer env: ${writeEnv ?? "(missing write_url_env)"}`);
|
|
@@ -11929,13 +12519,13 @@ async function mcpServeHttp(args) {
|
|
|
11929
12519
|
throw error;
|
|
11930
12520
|
}
|
|
11931
12521
|
process2.stderr.write("Press Ctrl+C to stop.\n");
|
|
11932
|
-
await new Promise((
|
|
12522
|
+
await new Promise((resolve2) => {
|
|
11933
12523
|
const stop = async () => {
|
|
11934
12524
|
process2.off("SIGINT", stop);
|
|
11935
12525
|
process2.off("SIGTERM", stop);
|
|
11936
12526
|
await server.close();
|
|
11937
12527
|
await releaseLease();
|
|
11938
|
-
|
|
12528
|
+
resolve2();
|
|
11939
12529
|
};
|
|
11940
12530
|
process2.once("SIGINT", stop);
|
|
11941
12531
|
process2.once("SIGTERM", stop);
|
|
@@ -11977,13 +12567,13 @@ async function mcpServeStreamableHttp(args) {
|
|
|
11977
12567
|
throw error;
|
|
11978
12568
|
}
|
|
11979
12569
|
process2.stderr.write("Press Ctrl+C to stop.\n");
|
|
11980
|
-
await new Promise((
|
|
12570
|
+
await new Promise((resolve2) => {
|
|
11981
12571
|
const stop = async () => {
|
|
11982
12572
|
process2.off("SIGINT", stop);
|
|
11983
12573
|
process2.off("SIGTERM", stop);
|
|
11984
12574
|
await server.close();
|
|
11985
12575
|
await releaseLease();
|
|
11986
|
-
|
|
12576
|
+
resolve2();
|
|
11987
12577
|
};
|
|
11988
12578
|
process2.once("SIGINT", stop);
|
|
11989
12579
|
process2.once("SIGTERM", stop);
|
|
@@ -12989,7 +13579,7 @@ function isRunnerConfigLike(value) {
|
|
|
12989
13579
|
}
|
|
12990
13580
|
async function fetchRemoteMcpTools(target2, args, timeoutMs) {
|
|
12991
13581
|
const bearerEnv = optionalArg(args, "--bearer-env") ?? "SYNAPSOR_MCP_AUDIT_BEARER";
|
|
12992
|
-
const bearer = process2.env
|
|
13582
|
+
const bearer = envValue2(process2.env, bearerEnv);
|
|
12993
13583
|
const controller = new AbortController();
|
|
12994
13584
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
12995
13585
|
try {
|
|
@@ -13015,7 +13605,7 @@ async function fetchRemoteMcpTools(target2, args, timeoutMs) {
|
|
|
13015
13605
|
async function fetchStdioMcpTools(commandText, timeoutMs) {
|
|
13016
13606
|
const [command, ...commandArgs] = splitCommand(commandText);
|
|
13017
13607
|
if (!command) throw new Error("mcp audit stdio target requires a command");
|
|
13018
|
-
return new Promise((
|
|
13608
|
+
return new Promise((resolve2, reject) => {
|
|
13019
13609
|
const child = spawn(command, commandArgs, {
|
|
13020
13610
|
env: process2.env,
|
|
13021
13611
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -13053,7 +13643,7 @@ async function fetchStdioMcpTools(commandText, timeoutMs) {
|
|
|
13053
13643
|
reject(new Error(`mcp audit stdio tools/list response not found${stderr ? `: ${stderr.slice(0, 240)}` : ""}`));
|
|
13054
13644
|
return;
|
|
13055
13645
|
}
|
|
13056
|
-
|
|
13646
|
+
resolve2(response);
|
|
13057
13647
|
});
|
|
13058
13648
|
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-11-25", capabilities: {}, clientInfo: { name: "synapsor-mcp-audit", version: "0.1.0" } } })}
|
|
13059
13649
|
`);
|
|
@@ -13152,12 +13742,12 @@ async function ui(args) {
|
|
|
13152
13742
|
process2.stdout.write("Opening the local review UI in your browser when a desktop opener is available.\n");
|
|
13153
13743
|
}
|
|
13154
13744
|
process2.stdout.write("Approval and rejection actions require the per-run local session plus CSRF token. Press Ctrl+C to stop.\n");
|
|
13155
|
-
await new Promise((
|
|
13745
|
+
await new Promise((resolve2) => {
|
|
13156
13746
|
const stop = async () => {
|
|
13157
13747
|
process2.off("SIGINT", stop);
|
|
13158
13748
|
process2.off("SIGTERM", stop);
|
|
13159
13749
|
await server.close();
|
|
13160
|
-
|
|
13750
|
+
resolve2();
|
|
13161
13751
|
};
|
|
13162
13752
|
process2.once("SIGINT", stop);
|
|
13163
13753
|
process2.once("SIGTERM", stop);
|
|
@@ -13670,7 +14260,7 @@ async function eventsTail(args) {
|
|
|
13670
14260
|
}
|
|
13671
14261
|
const seen = /* @__PURE__ */ new Set();
|
|
13672
14262
|
await printOnce(seen);
|
|
13673
|
-
await new Promise((
|
|
14263
|
+
await new Promise((resolve2) => {
|
|
13674
14264
|
const timer = setInterval(() => {
|
|
13675
14265
|
void printOnce(seen).catch((error) => {
|
|
13676
14266
|
process2.stderr.write(`events tail error: ${safeErrorMessage(error)}
|
|
@@ -13681,7 +14271,7 @@ async function eventsTail(args) {
|
|
|
13681
14271
|
clearInterval(timer);
|
|
13682
14272
|
process2.off("SIGINT", stop);
|
|
13683
14273
|
process2.off("SIGTERM", stop);
|
|
13684
|
-
|
|
14274
|
+
resolve2();
|
|
13685
14275
|
};
|
|
13686
14276
|
process2.once("SIGINT", stop);
|
|
13687
14277
|
process2.once("SIGTERM", stop);
|
|
@@ -13690,7 +14280,7 @@ async function eventsTail(args) {
|
|
|
13690
14280
|
}
|
|
13691
14281
|
async function eventsWebhook(args) {
|
|
13692
14282
|
assertKnownOptions(args, eventWebhookAllowedOptions, "events webhook");
|
|
13693
|
-
const url = optionalArg(args, "--url") ??
|
|
14283
|
+
const url = optionalArg(args, "--url") ?? envValue2(optionalArg(args, "--url-env"));
|
|
13694
14284
|
if (!url) throw new Error("events webhook requires --url <https://...> or --url-env <ENV>");
|
|
13695
14285
|
const endpoint = new URL(url);
|
|
13696
14286
|
if (!["http:", "https:"].includes(endpoint.protocol)) throw new Error("events webhook URL must use http or https");
|
|
@@ -13704,7 +14294,7 @@ async function eventsWebhook(args) {
|
|
|
13704
14294
|
const sinceEventId = optionalPositiveIntegerArg(args, "--since-event-id");
|
|
13705
14295
|
if (!Number.isFinite(intervalMs) || intervalMs < 250) throw new Error("--interval-ms must be at least 250");
|
|
13706
14296
|
if (!Number.isFinite(timeoutMs) || timeoutMs < 250) throw new Error("--timeout-ms must be at least 250");
|
|
13707
|
-
const token =
|
|
14297
|
+
const token = envValue2(optionalArg(args, "--auth-token-env"));
|
|
13708
14298
|
const filters = eventFiltersFromArgs(args);
|
|
13709
14299
|
const seen = /* @__PURE__ */ new Set();
|
|
13710
14300
|
const pushOnce = async () => {
|
|
@@ -13739,7 +14329,7 @@ async function eventsWebhook(args) {
|
|
|
13739
14329
|
`);
|
|
13740
14330
|
}
|
|
13741
14331
|
if (!follow) return 0;
|
|
13742
|
-
await new Promise((
|
|
14332
|
+
await new Promise((resolve2) => {
|
|
13743
14333
|
const timer = setInterval(() => {
|
|
13744
14334
|
void pushOnce().catch((error) => {
|
|
13745
14335
|
process2.stderr.write(`events webhook error: ${safeErrorMessage(error)}
|
|
@@ -13750,7 +14340,7 @@ async function eventsWebhook(args) {
|
|
|
13750
14340
|
clearInterval(timer);
|
|
13751
14341
|
process2.off("SIGINT", stop);
|
|
13752
14342
|
process2.off("SIGTERM", stop);
|
|
13753
|
-
|
|
14343
|
+
resolve2();
|
|
13754
14344
|
};
|
|
13755
14345
|
process2.once("SIGINT", stop);
|
|
13756
14346
|
process2.once("SIGTERM", stop);
|
|
@@ -14124,9 +14714,19 @@ function optionalPositiveIntegerArg(args, flag) {
|
|
|
14124
14714
|
if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`${flag} must be a non-negative integer`);
|
|
14125
14715
|
return parsed;
|
|
14126
14716
|
}
|
|
14127
|
-
function
|
|
14128
|
-
if (
|
|
14129
|
-
|
|
14717
|
+
function envValue2(first, second) {
|
|
14718
|
+
if (typeof first === "string" || first === void 0) {
|
|
14719
|
+
if (!first) return void 0;
|
|
14720
|
+
return trimmedEnvValue(process2.env, first);
|
|
14721
|
+
}
|
|
14722
|
+
if (!second) return void 0;
|
|
14723
|
+
return trimmedEnvValue(first, second);
|
|
14724
|
+
}
|
|
14725
|
+
function trimmedEnvValue(env, name) {
|
|
14726
|
+
const value = env[name];
|
|
14727
|
+
if (value === void 0) return void 0;
|
|
14728
|
+
const trimmed = value.trim();
|
|
14729
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
14130
14730
|
}
|
|
14131
14731
|
function linkedProposalFilter(args, store, options = {}) {
|
|
14132
14732
|
const noLinkedProposal = "__synapsor_no_linked_proposal__";
|
|
@@ -14346,11 +14946,11 @@ async function waitForReferenceDemoDatabase() {
|
|
|
14346
14946
|
stdio: "pipe"
|
|
14347
14947
|
});
|
|
14348
14948
|
if (result.status === 0) {
|
|
14349
|
-
await new Promise((
|
|
14949
|
+
await new Promise((resolve2) => setTimeout(resolve2, 500));
|
|
14350
14950
|
return;
|
|
14351
14951
|
}
|
|
14352
14952
|
last = result.stderr || result.stdout || `exit ${result.status}`;
|
|
14353
|
-
await new Promise((
|
|
14953
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e3));
|
|
14354
14954
|
}
|
|
14355
14955
|
throw new Error(`demo database did not become ready: ${last}`);
|
|
14356
14956
|
}
|
|
@@ -14416,6 +15016,7 @@ function firstPositional(args) {
|
|
|
14416
15016
|
const flagsWithValues = /* @__PURE__ */ new Set([
|
|
14417
15017
|
"--allowed-columns",
|
|
14418
15018
|
"--approval-role",
|
|
15019
|
+
"--api-url",
|
|
14419
15020
|
"--actor",
|
|
14420
15021
|
"--action",
|
|
14421
15022
|
"--auth-token-env",
|
|
@@ -14426,6 +15027,7 @@ function firstPositional(args) {
|
|
|
14426
15027
|
"--conflict-column",
|
|
14427
15028
|
"--cors-origin",
|
|
14428
15029
|
"--database-url-env",
|
|
15030
|
+
"--description",
|
|
14429
15031
|
"--destination",
|
|
14430
15032
|
"--engine",
|
|
14431
15033
|
"--evidence",
|
|
@@ -14481,10 +15083,12 @@ function firstPositional(args) {
|
|
|
14481
15083
|
"--tenant-env",
|
|
14482
15084
|
"--tenant-key",
|
|
14483
15085
|
"--timeout-ms",
|
|
15086
|
+
"--token",
|
|
14484
15087
|
"--to",
|
|
14485
15088
|
"--transition-guard",
|
|
14486
15089
|
"--url",
|
|
14487
15090
|
"--visible-columns",
|
|
15091
|
+
"--workspace",
|
|
14488
15092
|
"--writeback-job",
|
|
14489
15093
|
"--write-url-env"
|
|
14490
15094
|
]);
|