@synapsor/runner 0.1.5 → 0.1.7
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 +17 -0
- package/README.md +7 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/runner.mjs +339 -106
- package/docs/capability-authoring.md +29 -1
- 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/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@
|
|
|
4
4
|
|
|
5
5
|
No unreleased changes yet.
|
|
6
6
|
|
|
7
|
+
## 0.1.7
|
|
8
|
+
|
|
9
|
+
### Contract Writeback Resolution
|
|
10
|
+
|
|
11
|
+
- Fixes contract-authored proposal capabilities loaded through `contracts: []`
|
|
12
|
+
so `apply` resolves the same reviewed capability catalog used by serve,
|
|
13
|
+
tools, propose, and doctor.
|
|
14
|
+
- Rejects duplicate capability names across embedded runner config and
|
|
15
|
+
referenced contracts instead of silently shadowing a safety contract.
|
|
16
|
+
- Preserves canonical contract writeback modes, including direct SQL,
|
|
17
|
+
app-owned handler, cloud-worker, and proposal-only/no-local-writeback
|
|
18
|
+
semantics.
|
|
19
|
+
- Fails broken applyable writeback definitions at propose/doctor time before a
|
|
20
|
+
human approves a proposal.
|
|
21
|
+
- Creates local store parent directories automatically and trims env-derived
|
|
22
|
+
URLs, tokens, and trusted context values before use.
|
|
23
|
+
|
|
7
24
|
## 0.1.5
|
|
8
25
|
|
|
9
26
|
### Contract Authoring Front Door
|
package/README.md
CHANGED
|
@@ -277,9 +277,16 @@ synapsor-runner dsl compile ./contract.synapsor --out ./synapsor.contract.json
|
|
|
277
277
|
synapsor-runner contract validate ./synapsor.contract.json
|
|
278
278
|
synapsor-runner contract bundle ./synapsor.contract.json --out ./synapsor-runner-bundle
|
|
279
279
|
synapsor-runner cloud push ./synapsor.contract.json --dry-run
|
|
280
|
+
cd ./synapsor-runner-bundle
|
|
281
|
+
cp .env.example .env # fill in and export your read-only database values
|
|
280
282
|
synapsor-runner mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
281
283
|
```
|
|
282
284
|
|
|
285
|
+
The `contract bundle` step generates `synapsor.runner.json` (with env-var
|
|
286
|
+
placeholders) inside the bundle directory, which is why `mcp serve` runs from
|
|
287
|
+
there. The server keeps stdout clean for MCP protocol frames and prints its
|
|
288
|
+
ready line on stderr.
|
|
289
|
+
|
|
283
290
|
Your `synapsor.runner.json` supplies local wiring: database env var names,
|
|
284
291
|
SQLite store path, MCP transport settings, and local debug options. The
|
|
285
292
|
contract supplies the portable meaning: contexts, capabilities, evidence,
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAYA,OAAO,
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAYA,OAAO,EAAoN,KAAK,WAAW,EAA2F,MAAM,6BAA6B,CAAC;AAoB1W,OAAO,EAA6G,KAAK,YAAY,EAAwB,MAAM,2BAA2B,CAAC;AAE/L,OAAO,EAOL,KAAK,gBAAgB,EAEtB,MAAM,mCAAmC,CAAC;AA6U3C,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAqD1D;AA+DD,KAAK,SAAS,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAE9E,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,GAAE;IACP,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;CACvC,GACL,OAAO,CAAC,MAAM,CAAC,CA2TjB;AAyrED,wBAAsB,0BAA0B,CAAC,GAAG,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAS/H"}
|
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) {
|
|
@@ -889,7 +897,21 @@ function validateCapabilities(value, sources, contexts, executors, mode, strict,
|
|
|
889
897
|
const sourceNames = isRecord(sources) ? new Set(Object.keys(sources)) : /* @__PURE__ */ new Set();
|
|
890
898
|
const contextNames = isRecord(contexts) ? new Set(Object.keys(contexts)) : /* @__PURE__ */ new Set();
|
|
891
899
|
const executorNames = isRecord(executors) ? new Set(Object.keys(executors)) : /* @__PURE__ */ new Set();
|
|
900
|
+
const capabilityNames = /* @__PURE__ */ new Map();
|
|
892
901
|
value.forEach((capability, index) => validateCapability(capability, index, sourceNames, contextNames, executorNames, strict, errors, warnings));
|
|
902
|
+
value.forEach((capability, index) => {
|
|
903
|
+
if (!isRecord(capability) || !isQualifiedName(capability.name)) return;
|
|
904
|
+
const previous = capabilityNames.get(capability.name);
|
|
905
|
+
if (previous !== void 0) {
|
|
906
|
+
errors.push({
|
|
907
|
+
path: `$.capabilities[${index}].name`,
|
|
908
|
+
code: "DUPLICATE_CAPABILITY_NAME",
|
|
909
|
+
message: `Capability ${capability.name} is already defined at $.capabilities[${previous}]. Capability names must be unique.`
|
|
910
|
+
});
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
capabilityNames.set(capability.name, index);
|
|
914
|
+
});
|
|
893
915
|
}
|
|
894
916
|
function validateCapability(value, index, sourceNames, contextNames, executorNames, strict, errors, warnings) {
|
|
895
917
|
const path4 = `$.capabilities[${index}]`;
|
|
@@ -923,6 +945,7 @@ function validateCapability(value, index, sourceNames, contextNames, executorNam
|
|
|
923
945
|
errors.push({ path: `${path4}.executor`, code: "UNKNOWN_EXECUTOR", message: "Capability executor must be sql_update or reference a configured executor." });
|
|
924
946
|
}
|
|
925
947
|
}
|
|
948
|
+
validateCapabilityWriteback(value, path4, executorNames, strict, errors);
|
|
926
949
|
validateTarget(value.target, `${path4}.target`, strict, errors, warnings);
|
|
927
950
|
validateArgs(value.args, `${path4}.args`, strict, errors);
|
|
928
951
|
validateLookup(value.lookup, `${path4}.lookup`, strict, errors);
|
|
@@ -932,6 +955,40 @@ function validateCapability(value, index, sourceNames, contextNames, executorNam
|
|
|
932
955
|
}
|
|
933
956
|
if (value.kind === "proposal") {
|
|
934
957
|
validateProposalCapability(value, path4, strict, errors);
|
|
958
|
+
} else if (value.writeback !== void 0) {
|
|
959
|
+
errors.push({ path: `${path4}.writeback`, code: "WRITEBACK_ONLY_FOR_PROPOSAL", message: "writeback is only valid on proposal capabilities." });
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
function validateCapabilityWriteback(capability, path4, executorNames, strict, errors) {
|
|
963
|
+
if (capability.writeback === void 0) return;
|
|
964
|
+
if (!isRecord(capability.writeback)) {
|
|
965
|
+
errors.push({ path: `${path4}.writeback`, code: "WRITEBACK_NOT_OBJECT", message: "writeback must be an object." });
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (strict) checkUnknownKeys(capability.writeback, WRITEBACK_KEYS, `${path4}.writeback`, errors);
|
|
969
|
+
if (!WRITEBACK_MODES.has(String(capability.writeback.mode))) {
|
|
970
|
+
errors.push({ path: `${path4}.writeback.mode`, code: "INVALID_WRITEBACK_MODE", message: "writeback.mode must be direct_sql, app_handler, cloud_worker, or none." });
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
const mode = String(capability.writeback.mode);
|
|
974
|
+
const executor = isNonEmptyString(capability.writeback.executor) ? capability.writeback.executor : isNonEmptyString(capability.executor) ? capability.executor : void 0;
|
|
975
|
+
if (mode === "direct_sql") {
|
|
976
|
+
if (executor !== void 0 && executor !== "sql_update") {
|
|
977
|
+
errors.push({ path: `${path4}.writeback.executor`, code: "WRITEBACK_EXECUTOR_MISMATCH", message: "direct_sql writeback cannot name an app-owned executor." });
|
|
978
|
+
}
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
if (mode === "none") {
|
|
982
|
+
if (executor !== void 0) {
|
|
983
|
+
errors.push({ path: `${path4}.writeback.executor`, code: "WRITEBACK_EXECUTOR_MISMATCH", message: "WRITEBACK NONE must not name an executor." });
|
|
984
|
+
}
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
if (mode === "cloud_worker") return;
|
|
988
|
+
if (!executor || executor === "sql_update") {
|
|
989
|
+
errors.push({ path: `${path4}.writeback.executor`, code: "WRITEBACK_EXECUTOR_REQUIRED", message: "app_handler writeback must name a configured executor." });
|
|
990
|
+
} else if (!executorNames.has(executor)) {
|
|
991
|
+
errors.push({ path: `${path4}.writeback.executor`, code: "UNKNOWN_EXECUTOR", message: "app_handler writeback executor must reference a configured executor." });
|
|
935
992
|
}
|
|
936
993
|
}
|
|
937
994
|
function validateTarget(value, path4, strict, errors, warnings) {
|
|
@@ -1250,6 +1307,8 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
|
1250
1307
|
|
|
1251
1308
|
// packages/proposal-store/src/index.ts
|
|
1252
1309
|
import { DatabaseSync } from "node:sqlite";
|
|
1310
|
+
import { mkdirSync } from "node:fs";
|
|
1311
|
+
import { dirname, resolve } from "node:path";
|
|
1253
1312
|
var ProposalStoreError = class extends Error {
|
|
1254
1313
|
constructor(code, message) {
|
|
1255
1314
|
super(message);
|
|
@@ -1263,6 +1322,9 @@ var ProposalStore = class {
|
|
|
1263
1322
|
path;
|
|
1264
1323
|
constructor(path4 = ":memory:") {
|
|
1265
1324
|
this.path = path4;
|
|
1325
|
+
if (path4 !== ":memory:") {
|
|
1326
|
+
mkdirSync(dirname(resolve(path4)), { recursive: true });
|
|
1327
|
+
}
|
|
1266
1328
|
this.db = new DatabaseSync(path4);
|
|
1267
1329
|
this.migrate();
|
|
1268
1330
|
}
|
|
@@ -1952,6 +2014,10 @@ var ProposalStore = class {
|
|
|
1952
2014
|
if (changeSet.writeback.mode !== "trusted_worker_required") {
|
|
1953
2015
|
throw new ProposalStoreError("WRITEBACK_NOT_REQUIRED", `proposal ${proposalId} uses ${changeSet.writeback.mode}`);
|
|
1954
2016
|
}
|
|
2017
|
+
const writebackExecutor = changeSet.writeback.executor;
|
|
2018
|
+
if (typeof writebackExecutor === "string" && writebackExecutor !== "sql_update" && writebackExecutor !== "trusted_worker_required") {
|
|
2019
|
+
throw new ProposalStoreError("WRITEBACK_NOT_DIRECT_SQL", `proposal ${proposalId} uses app-owned or non-local writeback executor ${writebackExecutor}`);
|
|
2020
|
+
}
|
|
1955
2021
|
if (changeSet.source.kind !== "external_postgres" && changeSet.source.kind !== "external_mysql") {
|
|
1956
2022
|
throw new ProposalStoreError("WRITEBACK_TARGET_NOT_EXTERNAL", `proposal ${proposalId} targets ${changeSet.source.kind}`);
|
|
1957
2023
|
}
|
|
@@ -2755,7 +2821,7 @@ var PROPOSAL_KEYS = /* @__PURE__ */ new Set(["action", "allowed_fields", "patch"
|
|
|
2755
2821
|
var PATCH_KEYS = /* @__PURE__ */ new Set(["fixed", "from_arg"]);
|
|
2756
2822
|
var CONFLICT_GUARD_KEYS2 = /* @__PURE__ */ new Set(["column", "weak_guard_ack"]);
|
|
2757
2823
|
var APPROVAL_KEYS2 = /* @__PURE__ */ new Set(["mode", "required_role"]);
|
|
2758
|
-
var
|
|
2824
|
+
var WRITEBACK_KEYS2 = /* @__PURE__ */ new Set(["mode", "executor", "idempotency_key"]);
|
|
2759
2825
|
var WORKFLOW_KEYS = /* @__PURE__ */ new Set(["name", "description", "context", "allowed_capabilities", "required_evidence", "approval", "settlement", "replay"]);
|
|
2760
2826
|
var POLICY_KEYS = /* @__PURE__ */ new Set(["name", "kind", "mode", "rules"]);
|
|
2761
2827
|
var EVIDENCE_RECORD_KEYS = /* @__PURE__ */ new Set(["handle", "capability", "query_fingerprint", "items"]);
|
|
@@ -3057,7 +3123,7 @@ function validateProposalAction(value, path4, errors) {
|
|
|
3057
3123
|
if (value.approval !== void 0)
|
|
3058
3124
|
validateNestedObject(value.approval, APPROVAL_KEYS2, `${path4}.approval`, errors);
|
|
3059
3125
|
if (value.writeback !== void 0) {
|
|
3060
|
-
validateNestedObject(value.writeback,
|
|
3126
|
+
validateNestedObject(value.writeback, WRITEBACK_KEYS2, `${path4}.writeback`, errors);
|
|
3061
3127
|
if (isRecord3(value.writeback) && !["direct_sql", "app_handler", "cloud_worker", "none"].includes(String(value.writeback.mode)))
|
|
3062
3128
|
errors.push({ path: `${path4}.writeback.mode`, code: "INVALID_WRITEBACK_MODE", message: "writeback.mode must be direct_sql, app_handler, cloud_worker, or none." });
|
|
3063
3129
|
}
|
|
@@ -3296,20 +3362,31 @@ function loadRuntimeConfigFromFile(configPath = process.env.SYNAPSOR_MCP_CONFIG
|
|
|
3296
3362
|
}
|
|
3297
3363
|
function resolveRuntimeConfig(config, baseDir = process.cwd()) {
|
|
3298
3364
|
if (!Array.isArray(config.contracts) || config.contracts.length === 0) return config;
|
|
3365
|
+
const seenCapabilities = /* @__PURE__ */ new Map();
|
|
3366
|
+
for (const [index, capability] of (config.capabilities ?? []).entries()) {
|
|
3367
|
+
rememberCapabilityName(seenCapabilities, capability.name, `embedded capabilities[${index}]`);
|
|
3368
|
+
}
|
|
3299
3369
|
const resolved = {
|
|
3300
3370
|
...config,
|
|
3301
3371
|
contexts: { ...config.contexts ?? {} },
|
|
3302
3372
|
capabilities: [...config.capabilities ?? []]
|
|
3303
3373
|
};
|
|
3304
|
-
for (const contractPath of config.contracts) {
|
|
3374
|
+
for (const [contractIndex, contractPath] of config.contracts.entries()) {
|
|
3305
3375
|
const fullPath = path.resolve(baseDir, contractPath);
|
|
3306
3376
|
const contract = normalizeContract(JSON.parse(fs.readFileSync(fullPath, "utf8")));
|
|
3307
|
-
mergeContractIntoRuntimeConfig(resolved, contract);
|
|
3377
|
+
mergeContractIntoRuntimeConfig(resolved, contract, `contracts[${contractIndex}] ${contractPath}`, seenCapabilities);
|
|
3308
3378
|
}
|
|
3309
3379
|
delete resolved.contracts;
|
|
3310
3380
|
return resolved;
|
|
3311
3381
|
}
|
|
3312
|
-
function
|
|
3382
|
+
function rememberCapabilityName(seen, name, origin) {
|
|
3383
|
+
const previous = seen.get(name);
|
|
3384
|
+
if (previous) {
|
|
3385
|
+
throw new Error(`Duplicate capability ${name}: ${origin} conflicts with ${previous}. Capability names must be unique across embedded runner config and referenced contracts.`);
|
|
3386
|
+
}
|
|
3387
|
+
seen.set(name, origin);
|
|
3388
|
+
}
|
|
3389
|
+
function mergeContractIntoRuntimeConfig(config, contract, origin, seenCapabilities) {
|
|
3313
3390
|
const resources = new Map((contract.resources ?? []).map((resource) => [resource.name, resource]));
|
|
3314
3391
|
for (const context of contract.contexts) {
|
|
3315
3392
|
if (!config.contexts) config.contexts = {};
|
|
@@ -3320,7 +3397,8 @@ function mergeContractIntoRuntimeConfig(config, contract) {
|
|
|
3320
3397
|
if (context) config.trusted_context = runtimeContextFromSpec(context);
|
|
3321
3398
|
}
|
|
3322
3399
|
if (!config.capabilities) config.capabilities = [];
|
|
3323
|
-
for (const capability of contract.capabilities) {
|
|
3400
|
+
for (const [capabilityIndex, capability] of contract.capabilities.entries()) {
|
|
3401
|
+
rememberCapabilityName(seenCapabilities, capability.name, `${origin} capabilities[${capabilityIndex}]`);
|
|
3324
3402
|
config.capabilities.push(runtimeCapabilityFromSpec(capability, resources, config));
|
|
3325
3403
|
}
|
|
3326
3404
|
}
|
|
@@ -3364,7 +3442,11 @@ function runtimeCapabilityFromSpec(capability, resources, config) {
|
|
|
3364
3442
|
runtime.allowed_columns = capability.proposal.allowed_fields;
|
|
3365
3443
|
runtime.conflict_guard = capability.proposal.conflict_guard;
|
|
3366
3444
|
runtime.approval = capability.proposal.approval;
|
|
3367
|
-
|
|
3445
|
+
runtime.writeback = {
|
|
3446
|
+
mode: capability.proposal.writeback?.mode ?? "direct_sql",
|
|
3447
|
+
...capability.proposal.writeback?.executor ? { executor: capability.proposal.writeback.executor } : {}
|
|
3448
|
+
};
|
|
3449
|
+
if (capability.proposal.writeback?.executor) {
|
|
3368
3450
|
runtime.executor = capability.proposal.writeback.executor;
|
|
3369
3451
|
}
|
|
3370
3452
|
}
|
|
@@ -3521,11 +3603,12 @@ async function serveStdio(options = {}) {
|
|
|
3521
3603
|
const server = createSynapsorMcpServer(runtime, { toolNameStyle: options.toolNameStyle });
|
|
3522
3604
|
const transport = new StdioServerTransport();
|
|
3523
3605
|
await server.connect(transport);
|
|
3524
|
-
|
|
3606
|
+
process.stderr.write("synapsor-runner MCP stdio server ready. Waiting for an MCP client on stdio; logs stay on stderr.\n");
|
|
3607
|
+
await new Promise((resolve2) => {
|
|
3525
3608
|
const previousOnClose = transport.onclose;
|
|
3526
3609
|
const close = () => {
|
|
3527
3610
|
runtime.close();
|
|
3528
|
-
|
|
3611
|
+
resolve2();
|
|
3529
3612
|
};
|
|
3530
3613
|
transport.onclose = () => {
|
|
3531
3614
|
previousOnClose?.();
|
|
@@ -3544,7 +3627,7 @@ async function startHttpMcpServer(options = {}) {
|
|
|
3544
3627
|
if (devNoAuth && !isLoopbackHost(host)) {
|
|
3545
3628
|
throw new McpRuntimeError("HTTP_DEV_NO_AUTH_UNSAFE_HOST", "--dev-no-auth is only allowed with localhost or 127.0.0.1.");
|
|
3546
3629
|
}
|
|
3547
|
-
const authToken = devNoAuth ? void 0 : env
|
|
3630
|
+
const authToken = devNoAuth ? void 0 : envValue(env, authTokenEnv);
|
|
3548
3631
|
if (!devNoAuth && !authToken) {
|
|
3549
3632
|
throw new McpRuntimeError("HTTP_AUTH_TOKEN_MISSING", `${authTokenEnv} is not set. HTTP MCP requires bearer auth by default.`);
|
|
3550
3633
|
}
|
|
@@ -3568,11 +3651,11 @@ async function startHttpMcpServer(options = {}) {
|
|
|
3568
3651
|
});
|
|
3569
3652
|
});
|
|
3570
3653
|
try {
|
|
3571
|
-
await new Promise((
|
|
3654
|
+
await new Promise((resolve2, reject) => {
|
|
3572
3655
|
server.once("error", reject);
|
|
3573
3656
|
server.listen(port, host, () => {
|
|
3574
3657
|
server.off("error", reject);
|
|
3575
|
-
|
|
3658
|
+
resolve2();
|
|
3576
3659
|
});
|
|
3577
3660
|
});
|
|
3578
3661
|
} catch (error) {
|
|
@@ -3610,7 +3693,7 @@ async function startStreamableHttpMcpServer(options = {}) {
|
|
|
3610
3693
|
if (devNoAuth && !isLoopbackHost(host)) {
|
|
3611
3694
|
throw new McpRuntimeError("HTTP_DEV_NO_AUTH_UNSAFE_HOST", "--dev-no-auth is only allowed with localhost or 127.0.0.1.");
|
|
3612
3695
|
}
|
|
3613
|
-
const authToken = devNoAuth ? void 0 : env
|
|
3696
|
+
const authToken = devNoAuth ? void 0 : envValue(env, authTokenEnv);
|
|
3614
3697
|
if (!devNoAuth && !authToken) {
|
|
3615
3698
|
throw new McpRuntimeError("HTTP_AUTH_TOKEN_MISSING", `${authTokenEnv} is not set. Streamable HTTP MCP requires bearer auth by default.`);
|
|
3616
3699
|
}
|
|
@@ -3637,11 +3720,11 @@ async function startStreamableHttpMcpServer(options = {}) {
|
|
|
3637
3720
|
});
|
|
3638
3721
|
});
|
|
3639
3722
|
try {
|
|
3640
|
-
await new Promise((
|
|
3723
|
+
await new Promise((resolve2, reject) => {
|
|
3641
3724
|
server.once("error", reject);
|
|
3642
3725
|
server.listen(port, host, () => {
|
|
3643
3726
|
server.off("error", reject);
|
|
3644
|
-
|
|
3727
|
+
resolve2();
|
|
3645
3728
|
});
|
|
3646
3729
|
});
|
|
3647
3730
|
} catch (error) {
|
|
@@ -3965,20 +4048,20 @@ function isLoopbackHost(host) {
|
|
|
3965
4048
|
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
3966
4049
|
}
|
|
3967
4050
|
async function closeHttpServer(server, runtime) {
|
|
3968
|
-
await new Promise((
|
|
4051
|
+
await new Promise((resolve2, reject) => {
|
|
3969
4052
|
server.close((error) => {
|
|
3970
4053
|
if (error) reject(error);
|
|
3971
|
-
else
|
|
4054
|
+
else resolve2();
|
|
3972
4055
|
});
|
|
3973
4056
|
}).finally(() => {
|
|
3974
4057
|
runtime.close();
|
|
3975
4058
|
});
|
|
3976
4059
|
}
|
|
3977
4060
|
async function closeStreamableHttpServer(server, sessions) {
|
|
3978
|
-
await new Promise((
|
|
4061
|
+
await new Promise((resolve2, reject) => {
|
|
3979
4062
|
server.close((error) => {
|
|
3980
4063
|
if (error) reject(error);
|
|
3981
|
-
else
|
|
4064
|
+
else resolve2();
|
|
3982
4065
|
});
|
|
3983
4066
|
}).finally(() => closeStreamableSessions(sessions));
|
|
3984
4067
|
}
|
|
@@ -4022,8 +4105,8 @@ function listedLocalCapabilities(config) {
|
|
|
4022
4105
|
}
|
|
4023
4106
|
function createCloudClient(config, env) {
|
|
4024
4107
|
const cloud2 = requireCloudConfig(config);
|
|
4025
|
-
const baseUrl = env
|
|
4026
|
-
const runnerToken = env
|
|
4108
|
+
const baseUrl = envValue(env, cloud2.base_url_env);
|
|
4109
|
+
const runnerToken = envValue(env, cloud2.runner_token_env);
|
|
4027
4110
|
if (!baseUrl) throw new McpRuntimeError("CLOUD_BASE_URL_MISSING", `${cloud2.base_url_env} is not set.`);
|
|
4028
4111
|
if (!runnerToken) throw new McpRuntimeError("CLOUD_RUNNER_TOKEN_MISSING", `${cloud2.runner_token_env} is not set.`);
|
|
4029
4112
|
return new ControlPlaneClient({
|
|
@@ -4105,6 +4188,9 @@ async function callConfiguredTool(input) {
|
|
|
4105
4188
|
if (capability.kind === "proposal" && input.config.mode === "read_only") {
|
|
4106
4189
|
throw new McpRuntimeError("PROPOSALS_DISABLED", "This runner is in read_only mode; proposal tools are disabled.");
|
|
4107
4190
|
}
|
|
4191
|
+
if (capability.kind === "proposal" && input.config.mode === "review") {
|
|
4192
|
+
assertProposalWritebackResolvable(input.config, capability);
|
|
4193
|
+
}
|
|
4108
4194
|
const source = input.config.sources?.[capability.source];
|
|
4109
4195
|
if (!source) throw new McpRuntimeError("SOURCE_NOT_FOUND", `Unknown source: ${capability.source}`);
|
|
4110
4196
|
const context = resolveTrustedContext(input.config, input.env, capability);
|
|
@@ -4274,7 +4360,7 @@ function resultEnvelopeFromLegacy(legacy, capability, canonicalName) {
|
|
|
4274
4360
|
const targetType = typeof target2?.type === "string" ? target2.type : capability?.target.table ?? "object";
|
|
4275
4361
|
const targetId = target2?.id !== void 0 ? String(target2.id) : "unknown";
|
|
4276
4362
|
const executor = writebackExecutorName(legacy.writeback);
|
|
4277
|
-
const writebackMode = executor && executor !== "sql_update" && executor !== "trusted_worker_required" ? "app_handler" : "direct_update";
|
|
4363
|
+
const writebackMode = executor === "read_only" || executor === "none" ? "proposal_only" : executor && executor !== "sql_update" && executor !== "trusted_worker_required" ? "app_handler" : "direct_update";
|
|
4278
4364
|
return {
|
|
4279
4365
|
ok: true,
|
|
4280
4366
|
summary: `Created proposal ${proposalId} for ${targetType} ${targetId}. Source database changed: no.`,
|
|
@@ -4329,6 +4415,44 @@ function writebackExecutorName(value) {
|
|
|
4329
4415
|
if (!isRecord4(value)) return void 0;
|
|
4330
4416
|
return typeof value.executor === "string" ? value.executor : typeof value.mode === "string" ? value.mode : void 0;
|
|
4331
4417
|
}
|
|
4418
|
+
function capabilityWritebackMode2(capability) {
|
|
4419
|
+
const mode = capability.writeback?.mode;
|
|
4420
|
+
if (mode === "direct_sql" || mode === "app_handler" || mode === "cloud_worker" || mode === "none") return mode;
|
|
4421
|
+
if (capability.executor && capability.executor !== "sql_update") return "app_handler";
|
|
4422
|
+
return "direct_sql";
|
|
4423
|
+
}
|
|
4424
|
+
function capabilityWritebackExecutor(capability) {
|
|
4425
|
+
return capability.writeback?.executor ?? capability.executor;
|
|
4426
|
+
}
|
|
4427
|
+
function assertProposalWritebackResolvable(config, capability) {
|
|
4428
|
+
if (capability.kind !== "proposal") return;
|
|
4429
|
+
const mode = capabilityWritebackMode2(capability);
|
|
4430
|
+
if (mode === "none" || mode === "cloud_worker") return;
|
|
4431
|
+
if (mode === "direct_sql") {
|
|
4432
|
+
const source = config.sources?.[capability.source];
|
|
4433
|
+
if (!source) {
|
|
4434
|
+
throw new McpRuntimeError("WRITEBACK_UNRESOLVED", `capability ${capability.name} declares DIRECT SQL writeback but source ${capability.source} is not configured.`);
|
|
4435
|
+
}
|
|
4436
|
+
if (source.read_only === true) {
|
|
4437
|
+
throw new McpRuntimeError("WRITEBACK_UNRESOLVED", `capability ${capability.name} declares DIRECT SQL writeback but source ${capability.source} is read-only.`);
|
|
4438
|
+
}
|
|
4439
|
+
if (!source.write_url_env) {
|
|
4440
|
+
throw new McpRuntimeError("WRITEBACK_UNRESOLVED", `capability ${capability.name} declares DIRECT SQL writeback but source ${capability.source} has no write_url_env.`);
|
|
4441
|
+
}
|
|
4442
|
+
return;
|
|
4443
|
+
}
|
|
4444
|
+
const executorName = capabilityWritebackExecutor(capability);
|
|
4445
|
+
if (!executorName || executorName === "sql_update") {
|
|
4446
|
+
throw new McpRuntimeError("WRITEBACK_UNRESOLVED", `capability ${capability.name} declares HANDLER writeback but has no executor name.`);
|
|
4447
|
+
}
|
|
4448
|
+
const executor = config.executors?.[executorName];
|
|
4449
|
+
if (!isRecord4(executor)) {
|
|
4450
|
+
throw new McpRuntimeError("WRITEBACK_UNRESOLVED", `capability ${capability.name} declares HANDLER writeback but executor ${executorName} is not configured.`);
|
|
4451
|
+
}
|
|
4452
|
+
if (executor.type !== "http_handler" && executor.type !== "command_handler") {
|
|
4453
|
+
throw new McpRuntimeError("WRITEBACK_UNRESOLVED", `capability ${capability.name} declares HANDLER writeback but executor ${executorName} is not an app-owned handler.`);
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4332
4456
|
function evidenceHandle(bundleId) {
|
|
4333
4457
|
return {
|
|
4334
4458
|
bundle_id: bundleId,
|
|
@@ -4388,6 +4512,9 @@ function buildChangeSet(input) {
|
|
|
4388
4512
|
enforcePatchGuards(input.capability, before, patch);
|
|
4389
4513
|
const after = { ...before, ...patch };
|
|
4390
4514
|
const guard = expectedVersionGuard(input.capability, before);
|
|
4515
|
+
const writebackMode = capabilityWritebackMode2(input.capability);
|
|
4516
|
+
const changeSetWritebackMode = writebackMode === "none" ? "read_only" : "trusted_worker_required";
|
|
4517
|
+
const writebackExecutor = writebackMode === "none" ? "none" : writebackMode === "cloud_worker" ? "cloud_worker" : writebackMode === "direct_sql" ? "sql_update" : capabilityWritebackExecutor(input.capability);
|
|
4391
4518
|
const proposalCore = {
|
|
4392
4519
|
schema_version: protocolVersions.changeSet,
|
|
4393
4520
|
proposal_id: stableId("wrp", {
|
|
@@ -4435,8 +4562,8 @@ function buildChangeSet(input) {
|
|
|
4435
4562
|
},
|
|
4436
4563
|
writeback: {
|
|
4437
4564
|
status: "not_applied",
|
|
4438
|
-
mode:
|
|
4439
|
-
executor:
|
|
4565
|
+
mode: changeSetWritebackMode,
|
|
4566
|
+
executor: writebackExecutor
|
|
4440
4567
|
},
|
|
4441
4568
|
source_database_mutated: false,
|
|
4442
4569
|
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -4459,7 +4586,7 @@ async function readCurrentRow(input) {
|
|
|
4459
4586
|
return readMysqlRow(input);
|
|
4460
4587
|
}
|
|
4461
4588
|
async function readPostgresRow(input) {
|
|
4462
|
-
const connectionString = input.env
|
|
4589
|
+
const connectionString = envValue(input.env, input.source.read_url_env);
|
|
4463
4590
|
if (!connectionString) throw new McpRuntimeError("SOURCE_CREDENTIAL_MISSING", `${input.source.read_url_env} is not set.`);
|
|
4464
4591
|
const pool = new Pool({ connectionString });
|
|
4465
4592
|
const client = await pool.connect();
|
|
@@ -4481,7 +4608,7 @@ async function readPostgresRow(input) {
|
|
|
4481
4608
|
}
|
|
4482
4609
|
}
|
|
4483
4610
|
async function readMysqlRow(input) {
|
|
4484
|
-
const uri = input.env
|
|
4611
|
+
const uri = envValue(input.env, input.source.read_url_env);
|
|
4485
4612
|
if (!uri) throw new McpRuntimeError("SOURCE_CREDENTIAL_MISSING", `${input.source.read_url_env} is not set.`);
|
|
4486
4613
|
const connection = await mysql.createConnection({ uri, dateStrings: true });
|
|
4487
4614
|
try {
|
|
@@ -4536,8 +4663,8 @@ function resolveTrustedContext(config, env, capability) {
|
|
|
4536
4663
|
if (provider === "environment") {
|
|
4537
4664
|
const tenantEnv = String(values.tenant_id_env ?? "SYNAPSOR_TENANT_ID");
|
|
4538
4665
|
const principalEnv = String(values.principal_env ?? "SYNAPSOR_PRINCIPAL");
|
|
4539
|
-
const tenant = env
|
|
4540
|
-
const principal = env
|
|
4666
|
+
const tenant = envValue(env, tenantEnv);
|
|
4667
|
+
const principal = envValue(env, principalEnv);
|
|
4541
4668
|
if (!tenant || !principal) throw new McpRuntimeError("TRUSTED_BINDING_MISSING", `${tenantEnv} and ${principalEnv} must be set.`);
|
|
4542
4669
|
return { tenant_id: tenant, principal, provenance: "environment" };
|
|
4543
4670
|
}
|
|
@@ -4550,8 +4677,19 @@ function resolveTrustedContext(config, env, capability) {
|
|
|
4550
4677
|
throw new McpRuntimeError("TRUSTED_CONTEXT_UNSUPPORTED", `${provider} trusted context is not available in local stdio mode.`);
|
|
4551
4678
|
}
|
|
4552
4679
|
function valueFromEnvOrLiteral(envName, literal, env) {
|
|
4553
|
-
if (typeof envName === "string"
|
|
4554
|
-
|
|
4680
|
+
if (typeof envName === "string") {
|
|
4681
|
+
const value2 = envValue(env, envName);
|
|
4682
|
+
if (value2) return value2;
|
|
4683
|
+
}
|
|
4684
|
+
if (typeof literal !== "string") return void 0;
|
|
4685
|
+
const value = literal.trim();
|
|
4686
|
+
return value.length > 0 ? value : void 0;
|
|
4687
|
+
}
|
|
4688
|
+
function envValue(env, name) {
|
|
4689
|
+
const value = env[name];
|
|
4690
|
+
if (value === void 0) return void 0;
|
|
4691
|
+
const trimmed = value.trim();
|
|
4692
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
4555
4693
|
}
|
|
4556
4694
|
function validateToolArgs(capability, args) {
|
|
4557
4695
|
for (const [name, spec] of Object.entries(capability.args)) {
|
|
@@ -6420,7 +6558,7 @@ async function startPolling(config, adapters2, signal) {
|
|
|
6420
6558
|
} catch (error) {
|
|
6421
6559
|
logger.error("runner loop failed", { error: error instanceof Error ? error.message : String(error) });
|
|
6422
6560
|
}
|
|
6423
|
-
await new Promise((
|
|
6561
|
+
await new Promise((resolve2) => setTimeout(resolve2, config.pollIntervalMs));
|
|
6424
6562
|
}
|
|
6425
6563
|
}
|
|
6426
6564
|
|
|
@@ -6856,11 +6994,11 @@ async function startLocalUiServer(options = {}) {
|
|
|
6856
6994
|
});
|
|
6857
6995
|
}
|
|
6858
6996
|
});
|
|
6859
|
-
await new Promise((
|
|
6997
|
+
await new Promise((resolve2, reject) => {
|
|
6860
6998
|
server.once("error", reject);
|
|
6861
6999
|
server.listen(options.port ?? 0, host, () => {
|
|
6862
7000
|
server.off("error", reject);
|
|
6863
|
-
|
|
7001
|
+
resolve2();
|
|
6864
7002
|
});
|
|
6865
7003
|
});
|
|
6866
7004
|
const address = server.address();
|
|
@@ -6873,10 +7011,10 @@ async function startLocalUiServer(options = {}) {
|
|
|
6873
7011
|
port,
|
|
6874
7012
|
token,
|
|
6875
7013
|
csrfToken,
|
|
6876
|
-
close: () => new Promise((
|
|
7014
|
+
close: () => new Promise((resolve2, reject) => {
|
|
6877
7015
|
server.close((error) => {
|
|
6878
7016
|
if (error) reject(error);
|
|
6879
|
-
else
|
|
7017
|
+
else resolve2();
|
|
6880
7018
|
});
|
|
6881
7019
|
})
|
|
6882
7020
|
};
|
|
@@ -8719,7 +8857,7 @@ async function writeGeneratedSmokeInputFile(lookupArg, objectId, force) {
|
|
|
8719
8857
|
`, force);
|
|
8720
8858
|
}
|
|
8721
8859
|
async function maybeRunGeneratedSmokeCall(input) {
|
|
8722
|
-
const required = uniqueStrings([input.readUrlEnv, input.tenantEnv, input.principalEnv]).filter((envName) => !input.env
|
|
8860
|
+
const required = uniqueStrings([input.readUrlEnv, input.tenantEnv, input.principalEnv]).filter((envName) => !envValue2(input.env, envName));
|
|
8723
8861
|
if (required.length > 0) {
|
|
8724
8862
|
return [
|
|
8725
8863
|
"Smoke call not run yet.",
|
|
@@ -9352,8 +9490,7 @@ function bundleReadme(contract) {
|
|
|
9352
9490
|
}
|
|
9353
9491
|
async function configValidate(args) {
|
|
9354
9492
|
const configPath = optionalArg(args, "--config") ?? "synapsor.runner.json";
|
|
9355
|
-
const
|
|
9356
|
-
const result = validateRunnerCapabilityConfig(parsed);
|
|
9493
|
+
const result = await validateConfigFile(configPath);
|
|
9357
9494
|
if (args.includes("--json")) {
|
|
9358
9495
|
process2.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
9359
9496
|
`);
|
|
@@ -9374,6 +9511,30 @@ async function configValidate(args) {
|
|
|
9374
9511
|
}
|
|
9375
9512
|
return result.ok ? 0 : 1;
|
|
9376
9513
|
}
|
|
9514
|
+
async function validateConfigFile(configPath) {
|
|
9515
|
+
const parsed = JSON.parse(await fs3.readFile(configPath, "utf8"));
|
|
9516
|
+
const raw = validateRunnerCapabilityConfig(parsed);
|
|
9517
|
+
if (!raw.ok) return raw;
|
|
9518
|
+
try {
|
|
9519
|
+
const resolved = resolveRuntimeConfig(parsed, path3.dirname(path3.resolve(configPath)));
|
|
9520
|
+
const resolvedValidation = validateRunnerCapabilityConfig(resolved);
|
|
9521
|
+
return {
|
|
9522
|
+
ok: resolvedValidation.ok,
|
|
9523
|
+
errors: resolvedValidation.errors,
|
|
9524
|
+
warnings: [...raw.warnings, ...resolvedValidation.warnings]
|
|
9525
|
+
};
|
|
9526
|
+
} catch (error) {
|
|
9527
|
+
return {
|
|
9528
|
+
ok: false,
|
|
9529
|
+
errors: [{
|
|
9530
|
+
path: "$.contracts",
|
|
9531
|
+
code: "CONTRACT_RESOLUTION_FAILED",
|
|
9532
|
+
message: error instanceof Error ? error.message : String(error)
|
|
9533
|
+
}],
|
|
9534
|
+
warnings: raw.warnings
|
|
9535
|
+
};
|
|
9536
|
+
}
|
|
9537
|
+
}
|
|
9377
9538
|
async function configShow(args) {
|
|
9378
9539
|
const configPath = optionalArg(args, "--config") ?? "synapsor.runner.json";
|
|
9379
9540
|
const parsed = JSON.parse(await fs3.readFile(configPath, "utf8"));
|
|
@@ -9437,13 +9598,49 @@ function trustedContextsForDoctor(config) {
|
|
|
9437
9598
|
return contexts;
|
|
9438
9599
|
}
|
|
9439
9600
|
function envPresenceCheck(envName, message) {
|
|
9601
|
+
const value = envValue2(process2.env, envName);
|
|
9440
9602
|
return {
|
|
9441
9603
|
name: `env:${envName}`,
|
|
9442
|
-
ok: Boolean(
|
|
9443
|
-
level:
|
|
9444
|
-
message:
|
|
9604
|
+
ok: Boolean(value),
|
|
9605
|
+
level: value ? "pass" : "fail",
|
|
9606
|
+
message: value ? `${envName} is set.` : message
|
|
9445
9607
|
};
|
|
9446
9608
|
}
|
|
9609
|
+
function proposalWritebackResolutionDoctorCheck(config, capability) {
|
|
9610
|
+
const mode = capabilityWritebackMode2(capability);
|
|
9611
|
+
if (mode === "none") {
|
|
9612
|
+
return {
|
|
9613
|
+
name: `capability:${capability.name}:writeback-resolution`,
|
|
9614
|
+
ok: true,
|
|
9615
|
+
level: "pass",
|
|
9616
|
+
message: "Capability explicitly declares no local writeback; proposals are review records only."
|
|
9617
|
+
};
|
|
9618
|
+
}
|
|
9619
|
+
if (mode === "cloud_worker") {
|
|
9620
|
+
return {
|
|
9621
|
+
name: `capability:${capability.name}:writeback-resolution`,
|
|
9622
|
+
ok: true,
|
|
9623
|
+
level: "pass",
|
|
9624
|
+
message: "Capability declares cloud-worker writeback; local apply is intentionally unavailable."
|
|
9625
|
+
};
|
|
9626
|
+
}
|
|
9627
|
+
try {
|
|
9628
|
+
assertProposalWritebackResolvable(config, capability);
|
|
9629
|
+
return {
|
|
9630
|
+
name: `capability:${capability.name}:writeback-resolution`,
|
|
9631
|
+
ok: true,
|
|
9632
|
+
level: "pass",
|
|
9633
|
+
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)}.`
|
|
9634
|
+
};
|
|
9635
|
+
} catch (error) {
|
|
9636
|
+
return {
|
|
9637
|
+
name: `capability:${capability.name}:writeback-resolution`,
|
|
9638
|
+
ok: false,
|
|
9639
|
+
level: "fail",
|
|
9640
|
+
message: error instanceof Error ? error.message : String(error)
|
|
9641
|
+
};
|
|
9642
|
+
}
|
|
9643
|
+
}
|
|
9447
9644
|
async function httpHandlerReachabilityCheck(executorName, rawUrl, timeoutMs) {
|
|
9448
9645
|
try {
|
|
9449
9646
|
const url = new URL(rawUrl);
|
|
@@ -9493,7 +9690,7 @@ function safeReachabilityError(error) {
|
|
|
9493
9690
|
return "connection failed";
|
|
9494
9691
|
}
|
|
9495
9692
|
async function inspectConfiguredSource(input) {
|
|
9496
|
-
if (!process2.env
|
|
9693
|
+
if (!envValue2(process2.env, input.source.read_url_env)) return;
|
|
9497
9694
|
const capabilities = (input.config.capabilities ?? []).filter((capability) => capability.source === input.sourceName);
|
|
9498
9695
|
const schemas = Array.from(new Set(capabilities.map((capability) => capability.target.schema)));
|
|
9499
9696
|
for (const schema of schemas.length ? schemas : [void 0]) {
|
|
@@ -9836,15 +10033,15 @@ function firstRunConfigEnvChecks(config) {
|
|
|
9836
10033
|
String(values.tenant_id_env ?? "SYNAPSOR_TENANT_ID"),
|
|
9837
10034
|
String(values.principal_env ?? "SYNAPSOR_PRINCIPAL")
|
|
9838
10035
|
]) {
|
|
9839
|
-
checks.push(process2.env
|
|
10036
|
+
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.`));
|
|
9840
10037
|
}
|
|
9841
10038
|
}
|
|
9842
10039
|
for (const [sourceName, source] of Object.entries(config.sources ?? {})) {
|
|
9843
|
-
checks.push(process2.env
|
|
10040
|
+
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.`));
|
|
9844
10041
|
if (source.write_url_env) {
|
|
9845
|
-
checks.push(process2.env
|
|
9846
|
-
const readValue = process2.env
|
|
9847
|
-
const writeValue = process2.env
|
|
10042
|
+
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.`));
|
|
10043
|
+
const readValue = envValue2(process2.env, source.read_url_env);
|
|
10044
|
+
const writeValue = envValue2(process2.env, source.write_url_env);
|
|
9848
10045
|
if (readValue && writeValue && readValue === writeValue) {
|
|
9849
10046
|
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."));
|
|
9850
10047
|
}
|
|
@@ -9899,11 +10096,11 @@ function fail(name, checked, why, fix) {
|
|
|
9899
10096
|
return { name, status: "fail", checked, why, fix };
|
|
9900
10097
|
}
|
|
9901
10098
|
async function isPortAvailable(port) {
|
|
9902
|
-
return new Promise((
|
|
10099
|
+
return new Promise((resolve2) => {
|
|
9903
10100
|
const server = net.createServer();
|
|
9904
|
-
server.once("error", () =>
|
|
10101
|
+
server.once("error", () => resolve2(false));
|
|
9905
10102
|
server.listen(port, "127.0.0.1", () => {
|
|
9906
|
-
server.close(() =>
|
|
10103
|
+
server.close(() => resolve2(true));
|
|
9907
10104
|
});
|
|
9908
10105
|
});
|
|
9909
10106
|
}
|
|
@@ -9923,9 +10120,10 @@ async function localDoctor(args) {
|
|
|
9923
10120
|
const allowSharedCredential = args.includes("--allow-shared-credential");
|
|
9924
10121
|
const checkHandlers = args.includes("--check-handlers");
|
|
9925
10122
|
const checkWriteback = args.includes("--check-writeback") || args.includes("--check-db");
|
|
9926
|
-
const
|
|
10123
|
+
const rawConfig = JSON.parse(await fs3.readFile(configPath, "utf8"));
|
|
10124
|
+
let parsed = rawConfig;
|
|
9927
10125
|
const checks = [];
|
|
9928
|
-
const validation =
|
|
10126
|
+
const validation = await validateConfigFile(configPath);
|
|
9929
10127
|
checks.push({
|
|
9930
10128
|
name: "config-valid",
|
|
9931
10129
|
ok: validation.ok,
|
|
@@ -9935,6 +10133,9 @@ async function localDoctor(args) {
|
|
|
9935
10133
|
for (const warning of validation.warnings) {
|
|
9936
10134
|
checks.push({ name: `config-warning:${warning.code}`, ok: true, level: "warn", message: warning.message });
|
|
9937
10135
|
}
|
|
10136
|
+
if (validation.ok) {
|
|
10137
|
+
parsed = await readRuntimeConfig(configPath);
|
|
10138
|
+
}
|
|
9938
10139
|
const contextsToCheck = trustedContextsForDoctor(parsed);
|
|
9939
10140
|
for (const [contextName, contextValues] of contextsToCheck) {
|
|
9940
10141
|
const tenantEnv = String(contextValues.tenant_id_env ?? "SYNAPSOR_TENANT_ID");
|
|
@@ -9944,14 +10145,19 @@ async function localDoctor(args) {
|
|
|
9944
10145
|
}
|
|
9945
10146
|
}
|
|
9946
10147
|
const sources = parsed.sources ?? {};
|
|
10148
|
+
if (parsed.mode === "review") {
|
|
10149
|
+
for (const capability of (parsed.capabilities ?? []).filter((item) => item.kind === "proposal")) {
|
|
10150
|
+
checks.push(proposalWritebackResolutionDoctorCheck(parsed, capability));
|
|
10151
|
+
}
|
|
10152
|
+
}
|
|
9947
10153
|
for (const [sourceName, source] of Object.entries(sources)) {
|
|
9948
10154
|
checks.push(envPresenceCheck(source.read_url_env, `${source.read_url_env} is required for ${sourceName} reads.`));
|
|
9949
10155
|
if (parsed.mode === "review") {
|
|
9950
10156
|
if (sourceNeedsSqlWriteback(parsed, sourceName)) {
|
|
9951
10157
|
if (source.write_url_env) {
|
|
9952
10158
|
checks.push(envPresenceCheck(source.write_url_env, `${source.write_url_env} is required for trusted writeback in review mode.`));
|
|
9953
|
-
const readValue = process2.env
|
|
9954
|
-
const writeValue = process2.env
|
|
10159
|
+
const readValue = envValue2(process2.env, source.read_url_env);
|
|
10160
|
+
const writeValue = envValue2(process2.env, source.write_url_env);
|
|
9955
10161
|
if (readValue && writeValue && readValue === writeValue) {
|
|
9956
10162
|
checks.push({
|
|
9957
10163
|
name: `source:${sourceName}:credential-separation`,
|
|
@@ -9965,7 +10171,7 @@ async function localDoctor(args) {
|
|
|
9965
10171
|
} else {
|
|
9966
10172
|
checks.push({ name: `source:${sourceName}:write-url-env`, ok: false, level: "fail", message: "SQL writeback proposal capabilities require write_url_env for trusted writeback." });
|
|
9967
10173
|
}
|
|
9968
|
-
const writeUrl = source.write_url_env ? process2.env
|
|
10174
|
+
const writeUrl = source.write_url_env ? envValue2(process2.env, source.write_url_env) : void 0;
|
|
9969
10175
|
if (checkWriteback && writeUrl) {
|
|
9970
10176
|
checks.push(...await directSqlWritebackDoctorChecks(parsed, sourceName, source, writeUrl));
|
|
9971
10177
|
} else if (checkWriteback) {
|
|
@@ -9993,7 +10199,7 @@ async function localDoctor(args) {
|
|
|
9993
10199
|
const urlEnv = String(executor.url_env ?? "");
|
|
9994
10200
|
if (urlEnv) {
|
|
9995
10201
|
checks.push(envPresenceCheck(urlEnv, `${urlEnv} is required for http_handler executor ${executorName}.`));
|
|
9996
|
-
const handlerUrl = process2.env
|
|
10202
|
+
const handlerUrl = envValue2(process2.env, urlEnv);
|
|
9997
10203
|
if (checkHandlers && handlerUrl) {
|
|
9998
10204
|
checks.push(await httpHandlerReachabilityCheck(executorName, handlerUrl, Number(executor.timeout_ms ?? 3e3)));
|
|
9999
10205
|
} else if (!checkHandlers) {
|
|
@@ -10107,7 +10313,7 @@ async function directSqlWritebackDoctorChecks(config, sourceName, source, writeU
|
|
|
10107
10313
|
function directSqlProposalCapabilities(config, sourceName) {
|
|
10108
10314
|
return (config.capabilities ?? []).filter((capability) => {
|
|
10109
10315
|
if (capability.kind !== "proposal" || capability.source !== sourceName) return false;
|
|
10110
|
-
return (capability
|
|
10316
|
+
return capabilityWritebackMode2(capability) === "direct_sql";
|
|
10111
10317
|
});
|
|
10112
10318
|
}
|
|
10113
10319
|
async function rollbackOnlyTargetProbe(engine, databaseUrl, capability) {
|
|
@@ -10298,7 +10504,7 @@ async function applyProposal(args, proposalId) {
|
|
|
10298
10504
|
const storePath = optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE ?? "./.synapsor/local.db";
|
|
10299
10505
|
const dryRun = args.includes("--dry-run") || process2.env.SYNAPSOR_DRY_RUN === "true";
|
|
10300
10506
|
const runnerId = optionalArg(args, "--runner") ?? process2.env.SYNAPSOR_RUNNER_ID ?? "local_runner";
|
|
10301
|
-
const config =
|
|
10507
|
+
const config = await readRuntimeConfig(configPath);
|
|
10302
10508
|
const resolvedProposalId = await resolveProposalId(proposalId, storePath);
|
|
10303
10509
|
const validation = validateRunnerCapabilityConfig(config);
|
|
10304
10510
|
if (!validation.ok) {
|
|
@@ -10315,6 +10521,9 @@ async function applyProposal(args, proposalId) {
|
|
|
10315
10521
|
const proposal = requireLocalProposal(store, resolvedProposalId);
|
|
10316
10522
|
const capability = findProposalCapability(config, proposal);
|
|
10317
10523
|
const executorName = proposalExecutorName(proposal, capability);
|
|
10524
|
+
if (executorName === "none" || executorName === "cloud_worker") {
|
|
10525
|
+
throw new Error(`proposal ${resolvedProposalId} is not locally applyable; capability ${capability.name} declares ${executorName === "none" ? "no local writeback" : "cloud-worker writeback"}.`);
|
|
10526
|
+
}
|
|
10318
10527
|
if (executorName === "sql_update") {
|
|
10319
10528
|
const job = store.createWritebackJobFromProposal(resolvedProposalId, {
|
|
10320
10529
|
project_id: optionalArg(args, "--project") ?? "local",
|
|
@@ -10372,16 +10581,19 @@ async function applySqlJob(job, configPath, storePath, dryRun, env = process2.en
|
|
|
10372
10581
|
return result;
|
|
10373
10582
|
}
|
|
10374
10583
|
async function resolveSqlWriteDatabaseUrl(job, configPath, env) {
|
|
10375
|
-
const config =
|
|
10584
|
+
const config = await readRuntimeConfig(configPath);
|
|
10376
10585
|
const source = config.sources?.[job.source_id];
|
|
10377
10586
|
const writeUrlEnv = source?.write_url_env;
|
|
10378
|
-
if (writeUrlEnv
|
|
10379
|
-
|
|
10587
|
+
if (writeUrlEnv) {
|
|
10588
|
+
const value = envValue2(env, writeUrlEnv);
|
|
10589
|
+
if (value) return value;
|
|
10590
|
+
}
|
|
10591
|
+
return envValue2(env, "SYNAPSOR_DATABASE_URL") || "";
|
|
10380
10592
|
}
|
|
10381
10593
|
function sourceNeedsSqlWriteback(config, sourceName) {
|
|
10382
10594
|
return (config.capabilities ?? []).some((capability) => {
|
|
10383
10595
|
if (capability.kind !== "proposal" || capability.source !== sourceName) return false;
|
|
10384
|
-
return (capability
|
|
10596
|
+
return capabilityWritebackMode2(capability) === "direct_sql";
|
|
10385
10597
|
});
|
|
10386
10598
|
}
|
|
10387
10599
|
function findProposalCapability(config, proposal) {
|
|
@@ -10400,8 +10612,12 @@ function findProposalCapability(config, proposal) {
|
|
|
10400
10612
|
return capability;
|
|
10401
10613
|
}
|
|
10402
10614
|
function proposalExecutorName(proposal, capability) {
|
|
10615
|
+
const mode = capabilityWritebackMode2(capability);
|
|
10616
|
+
if (mode === "none") return "none";
|
|
10617
|
+
if (mode === "cloud_worker") return "cloud_worker";
|
|
10618
|
+
if (mode === "direct_sql") return "sql_update";
|
|
10403
10619
|
const writeback2 = proposal.change_set.writeback;
|
|
10404
|
-
return capability
|
|
10620
|
+
return capabilityWritebackExecutor(capability) ?? (typeof writeback2.executor === "string" ? writeback2.executor : void 0) ?? "sql_update";
|
|
10405
10621
|
}
|
|
10406
10622
|
function executorConfig(config, executorName) {
|
|
10407
10623
|
const raw = config.executors?.[executorName];
|
|
@@ -10426,7 +10642,7 @@ async function applyHttpHandlerProposal(input) {
|
|
|
10426
10642
|
executor: input.executorName,
|
|
10427
10643
|
request: prepared.request
|
|
10428
10644
|
});
|
|
10429
|
-
const url = input.env
|
|
10645
|
+
const url = envValue2(input.env, input.executor.url_env);
|
|
10430
10646
|
if (!url) throw new Error(`${input.executor.url_env} is not set`);
|
|
10431
10647
|
const headers = {
|
|
10432
10648
|
accept: "application/json",
|
|
@@ -10434,7 +10650,7 @@ async function applyHttpHandlerProposal(input) {
|
|
|
10434
10650
|
"idempotency-key": prepared.request.idempotency_key
|
|
10435
10651
|
};
|
|
10436
10652
|
if (input.executor.auth) {
|
|
10437
|
-
const token = input.env
|
|
10653
|
+
const token = envValue2(input.env, input.executor.auth.token_env);
|
|
10438
10654
|
if (!token) throw new Error(`${input.executor.auth.token_env} is not set`);
|
|
10439
10655
|
headers.authorization = `Bearer ${token}`;
|
|
10440
10656
|
}
|
|
@@ -10449,7 +10665,7 @@ async function applyHttpHandlerProposal(input) {
|
|
|
10449
10665
|
headers["x-synapsor-issued-at"] = issuedAt;
|
|
10450
10666
|
headers["x-synapsor-proposal-id"] = prepared.proposal.proposal_id;
|
|
10451
10667
|
if (input.executor.signing_secret_env) {
|
|
10452
|
-
const signingSecret = input.env
|
|
10668
|
+
const signingSecret = envValue2(input.env, input.executor.signing_secret_env);
|
|
10453
10669
|
if (!signingSecret) throw new Error(`${input.executor.signing_secret_env} is not set`);
|
|
10454
10670
|
headers["x-synapsor-signature"] = signHandlerRequestBody(requestBody, signingSecret);
|
|
10455
10671
|
}
|
|
@@ -10487,7 +10703,7 @@ async function applyCommandHandlerProposal(input) {
|
|
|
10487
10703
|
executor: input.executorName,
|
|
10488
10704
|
request: prepared.request
|
|
10489
10705
|
});
|
|
10490
|
-
const commandText = input.env
|
|
10706
|
+
const commandText = envValue2(input.env, input.executor.command_env);
|
|
10491
10707
|
if (!commandText) throw new Error(`${input.executor.command_env} is not set`);
|
|
10492
10708
|
const [command, ...commandArgs] = splitCommand(commandText);
|
|
10493
10709
|
if (!command) throw new Error(`${input.executor.command_env} did not contain a command`);
|
|
@@ -10737,7 +10953,7 @@ function safeHandlerDetails(value) {
|
|
|
10737
10953
|
return redactConfig(value);
|
|
10738
10954
|
}
|
|
10739
10955
|
async function runCommandHandler(command, args, request, timeoutMs) {
|
|
10740
|
-
return new Promise((
|
|
10956
|
+
return new Promise((resolve2) => {
|
|
10741
10957
|
const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"], env: process2.env });
|
|
10742
10958
|
let stdout = "";
|
|
10743
10959
|
let stderr = "";
|
|
@@ -10746,7 +10962,7 @@ async function runCommandHandler(command, args, request, timeoutMs) {
|
|
|
10746
10962
|
if (settled) return;
|
|
10747
10963
|
settled = true;
|
|
10748
10964
|
clearTimeout(timeout);
|
|
10749
|
-
|
|
10965
|
+
resolve2(body);
|
|
10750
10966
|
};
|
|
10751
10967
|
const timeout = setTimeout(() => {
|
|
10752
10968
|
child.kill("SIGTERM");
|
|
@@ -10771,7 +10987,7 @@ async function runCommandHandler(command, args, request, timeoutMs) {
|
|
|
10771
10987
|
});
|
|
10772
10988
|
}
|
|
10773
10989
|
async function verifyLocalWritebackAuthority(job, configPath, storePath) {
|
|
10774
|
-
const config =
|
|
10990
|
+
const config = await readRuntimeConfig(configPath);
|
|
10775
10991
|
const validation = validateRunnerCapabilityConfig(config);
|
|
10776
10992
|
if (!validation.ok) {
|
|
10777
10993
|
throw new Error(`cannot apply writeback with invalid local config: ${validation.errors.map((error) => error.code).join(", ")}`);
|
|
@@ -10789,7 +11005,7 @@ async function verifyLocalWritebackAuthority(job, configPath, storePath) {
|
|
|
10789
11005
|
if (Date.parse(String(job.lease_expires_at)) < Date.now()) {
|
|
10790
11006
|
throw new Error("writeback job lease has expired");
|
|
10791
11007
|
}
|
|
10792
|
-
const proposalCapabilities = (config.capabilities ?? []).filter((capability) => capability.kind === "proposal" && capability.source === job.source_id);
|
|
11008
|
+
const proposalCapabilities = (config.capabilities ?? []).filter((capability) => capability.kind === "proposal" && capability.source === job.source_id && capabilityWritebackMode2(capability) === "direct_sql");
|
|
10793
11009
|
const matching = proposalCapabilities.find((capability) => capabilityMatchesJob(capability, job));
|
|
10794
11010
|
if (!matching) {
|
|
10795
11011
|
throw new Error("writeback job does not match any reviewed proposal capability in local config");
|
|
@@ -10978,7 +11194,7 @@ function formatReviewModeUp(input) {
|
|
|
10978
11194
|
} else {
|
|
10979
11195
|
lines.push(
|
|
10980
11196
|
` Streamable HTTP endpoint: http://${input.host}:${input.port}/mcp`,
|
|
10981
|
-
` Auth token env: ${input.authTokenEnv} (${process2.env
|
|
11197
|
+
` Auth token env: ${input.authTokenEnv} (${envValue2(process2.env, input.authTokenEnv) ? "set" : "missing"})`,
|
|
10982
11198
|
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}`
|
|
10983
11199
|
);
|
|
10984
11200
|
}
|
|
@@ -10997,12 +11213,19 @@ function formatUpWritebackLines(config) {
|
|
|
10997
11213
|
const proposals2 = (config.capabilities ?? []).filter((capability) => capability.kind === "proposal");
|
|
10998
11214
|
if (proposals2.length === 0) return [" - no proposal capabilities; this config is read-only from Runner's perspective"];
|
|
10999
11215
|
return proposals2.map((capability) => {
|
|
11000
|
-
const
|
|
11001
|
-
if (
|
|
11216
|
+
const mode = capabilityWritebackMode2(capability);
|
|
11217
|
+
if (mode === "none") {
|
|
11218
|
+
return ` - ${capability.name}: proposal-only; no local writeback`;
|
|
11219
|
+
}
|
|
11220
|
+
if (mode === "cloud_worker") {
|
|
11221
|
+
return ` - ${capability.name}: cloud-worker writeback; local apply disabled`;
|
|
11222
|
+
}
|
|
11223
|
+
if (mode === "direct_sql") {
|
|
11002
11224
|
const source = config.sources?.[capability.source];
|
|
11003
11225
|
const envName = source?.write_url_env ?? "SYNAPSOR_DATABASE_URL";
|
|
11004
|
-
return ` - ${capability.name}: direct guarded one-row UPDATE via ${envName} (${process2.env
|
|
11226
|
+
return ` - ${capability.name}: direct guarded one-row UPDATE via ${envName} (${envValue2(process2.env, envName) ? "set" : "missing"})`;
|
|
11005
11227
|
}
|
|
11228
|
+
const executorName = capabilityWritebackExecutor(capability) ?? "missing_executor";
|
|
11006
11229
|
const executor = config.executors?.[executorName];
|
|
11007
11230
|
return ` - ${capability.name}: app-owned ${String(executor?.type ?? "executor")} ${executorName}`;
|
|
11008
11231
|
});
|
|
@@ -11017,14 +11240,14 @@ function formatUpHandlerLines(config) {
|
|
|
11017
11240
|
const tokenEnv = typeof auth?.token_env === "string" ? auth.token_env : void 0;
|
|
11018
11241
|
const signingSecretEnv = typeof executor.signing_secret_env === "string" ? executor.signing_secret_env : void 0;
|
|
11019
11242
|
lines.push(` - ${name}: http_handler`);
|
|
11020
|
-
if (urlEnv) lines.push(` url env: ${urlEnv} (${process2.env
|
|
11021
|
-
if (tokenEnv) lines.push(` bearer token env: ${tokenEnv} (${process2.env
|
|
11022
|
-
if (signingSecretEnv) lines.push(` signing secret env: ${signingSecretEnv} (${process2.env
|
|
11243
|
+
if (urlEnv) lines.push(` url env: ${urlEnv} (${envValue2(process2.env, urlEnv) ? "set" : "missing"})`);
|
|
11244
|
+
if (tokenEnv) lines.push(` bearer token env: ${tokenEnv} (${envValue2(process2.env, tokenEnv) ? "set" : "missing"})`);
|
|
11245
|
+
if (signingSecretEnv) lines.push(` signing secret env: ${signingSecretEnv} (${envValue2(process2.env, signingSecretEnv) ? "set" : "missing"})`);
|
|
11023
11246
|
if (!signingSecretEnv) lines.push(" signing secret env: not configured (recommended unless loopback-only)");
|
|
11024
11247
|
} else if (executor.type === "command_handler") {
|
|
11025
11248
|
const commandEnv = typeof executor.command_env === "string" ? executor.command_env : "";
|
|
11026
11249
|
lines.push(` - ${name}: command_handler`);
|
|
11027
|
-
if (commandEnv) lines.push(` command env: ${commandEnv} (${process2.env
|
|
11250
|
+
if (commandEnv) lines.push(` command env: ${commandEnv} (${envValue2(process2.env, commandEnv) ? "set" : "missing"})`);
|
|
11028
11251
|
}
|
|
11029
11252
|
}
|
|
11030
11253
|
return lines;
|
|
@@ -11125,8 +11348,8 @@ async function cloudConnect(args) {
|
|
|
11125
11348
|
}
|
|
11126
11349
|
const baseUrlEnv = parsed.cloud.base_url_env || "SYNAPSOR_CLOUD_BASE_URL";
|
|
11127
11350
|
const tokenEnv = parsed.cloud.runner_token_env || "SYNAPSOR_RUNNER_TOKEN";
|
|
11128
|
-
const baseUrl = process2.env
|
|
11129
|
-
const runnerToken = process2.env
|
|
11351
|
+
const baseUrl = envValue2(process2.env, baseUrlEnv);
|
|
11352
|
+
const runnerToken = envValue2(process2.env, tokenEnv);
|
|
11130
11353
|
const missing = [baseUrl ? "" : baseUrlEnv, runnerToken ? "" : tokenEnv].filter(Boolean);
|
|
11131
11354
|
if (missing.length > 0) {
|
|
11132
11355
|
process2.stdout.write(`missing environment variables: ${missing.join(", ")}
|
|
@@ -11302,7 +11525,7 @@ async function writebackDoctor(args) {
|
|
|
11302
11525
|
let ok = true;
|
|
11303
11526
|
for (const [sourceName, source] of sqlSources) {
|
|
11304
11527
|
const writeEnv = source.write_url_env;
|
|
11305
|
-
const writeUrl = writeEnv ? process2.env
|
|
11528
|
+
const writeUrl = writeEnv ? envValue2(process2.env, writeEnv) : void 0;
|
|
11306
11529
|
lines.push(`Source: ${sourceName}`);
|
|
11307
11530
|
lines.push(` engine: ${source.engine}`);
|
|
11308
11531
|
lines.push(` writer env: ${writeEnv ?? "(missing write_url_env)"}`);
|
|
@@ -11928,13 +12151,13 @@ async function mcpServeHttp(args) {
|
|
|
11928
12151
|
throw error;
|
|
11929
12152
|
}
|
|
11930
12153
|
process2.stderr.write("Press Ctrl+C to stop.\n");
|
|
11931
|
-
await new Promise((
|
|
12154
|
+
await new Promise((resolve2) => {
|
|
11932
12155
|
const stop = async () => {
|
|
11933
12156
|
process2.off("SIGINT", stop);
|
|
11934
12157
|
process2.off("SIGTERM", stop);
|
|
11935
12158
|
await server.close();
|
|
11936
12159
|
await releaseLease();
|
|
11937
|
-
|
|
12160
|
+
resolve2();
|
|
11938
12161
|
};
|
|
11939
12162
|
process2.once("SIGINT", stop);
|
|
11940
12163
|
process2.once("SIGTERM", stop);
|
|
@@ -11976,13 +12199,13 @@ async function mcpServeStreamableHttp(args) {
|
|
|
11976
12199
|
throw error;
|
|
11977
12200
|
}
|
|
11978
12201
|
process2.stderr.write("Press Ctrl+C to stop.\n");
|
|
11979
|
-
await new Promise((
|
|
12202
|
+
await new Promise((resolve2) => {
|
|
11980
12203
|
const stop = async () => {
|
|
11981
12204
|
process2.off("SIGINT", stop);
|
|
11982
12205
|
process2.off("SIGTERM", stop);
|
|
11983
12206
|
await server.close();
|
|
11984
12207
|
await releaseLease();
|
|
11985
|
-
|
|
12208
|
+
resolve2();
|
|
11986
12209
|
};
|
|
11987
12210
|
process2.once("SIGINT", stop);
|
|
11988
12211
|
process2.once("SIGTERM", stop);
|
|
@@ -12988,7 +13211,7 @@ function isRunnerConfigLike(value) {
|
|
|
12988
13211
|
}
|
|
12989
13212
|
async function fetchRemoteMcpTools(target2, args, timeoutMs) {
|
|
12990
13213
|
const bearerEnv = optionalArg(args, "--bearer-env") ?? "SYNAPSOR_MCP_AUDIT_BEARER";
|
|
12991
|
-
const bearer = process2.env
|
|
13214
|
+
const bearer = envValue2(process2.env, bearerEnv);
|
|
12992
13215
|
const controller = new AbortController();
|
|
12993
13216
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
12994
13217
|
try {
|
|
@@ -13014,7 +13237,7 @@ async function fetchRemoteMcpTools(target2, args, timeoutMs) {
|
|
|
13014
13237
|
async function fetchStdioMcpTools(commandText, timeoutMs) {
|
|
13015
13238
|
const [command, ...commandArgs] = splitCommand(commandText);
|
|
13016
13239
|
if (!command) throw new Error("mcp audit stdio target requires a command");
|
|
13017
|
-
return new Promise((
|
|
13240
|
+
return new Promise((resolve2, reject) => {
|
|
13018
13241
|
const child = spawn(command, commandArgs, {
|
|
13019
13242
|
env: process2.env,
|
|
13020
13243
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -13052,7 +13275,7 @@ async function fetchStdioMcpTools(commandText, timeoutMs) {
|
|
|
13052
13275
|
reject(new Error(`mcp audit stdio tools/list response not found${stderr ? `: ${stderr.slice(0, 240)}` : ""}`));
|
|
13053
13276
|
return;
|
|
13054
13277
|
}
|
|
13055
|
-
|
|
13278
|
+
resolve2(response);
|
|
13056
13279
|
});
|
|
13057
13280
|
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" } } })}
|
|
13058
13281
|
`);
|
|
@@ -13151,12 +13374,12 @@ async function ui(args) {
|
|
|
13151
13374
|
process2.stdout.write("Opening the local review UI in your browser when a desktop opener is available.\n");
|
|
13152
13375
|
}
|
|
13153
13376
|
process2.stdout.write("Approval and rejection actions require the per-run local session plus CSRF token. Press Ctrl+C to stop.\n");
|
|
13154
|
-
await new Promise((
|
|
13377
|
+
await new Promise((resolve2) => {
|
|
13155
13378
|
const stop = async () => {
|
|
13156
13379
|
process2.off("SIGINT", stop);
|
|
13157
13380
|
process2.off("SIGTERM", stop);
|
|
13158
13381
|
await server.close();
|
|
13159
|
-
|
|
13382
|
+
resolve2();
|
|
13160
13383
|
};
|
|
13161
13384
|
process2.once("SIGINT", stop);
|
|
13162
13385
|
process2.once("SIGTERM", stop);
|
|
@@ -13669,7 +13892,7 @@ async function eventsTail(args) {
|
|
|
13669
13892
|
}
|
|
13670
13893
|
const seen = /* @__PURE__ */ new Set();
|
|
13671
13894
|
await printOnce(seen);
|
|
13672
|
-
await new Promise((
|
|
13895
|
+
await new Promise((resolve2) => {
|
|
13673
13896
|
const timer = setInterval(() => {
|
|
13674
13897
|
void printOnce(seen).catch((error) => {
|
|
13675
13898
|
process2.stderr.write(`events tail error: ${safeErrorMessage(error)}
|
|
@@ -13680,7 +13903,7 @@ async function eventsTail(args) {
|
|
|
13680
13903
|
clearInterval(timer);
|
|
13681
13904
|
process2.off("SIGINT", stop);
|
|
13682
13905
|
process2.off("SIGTERM", stop);
|
|
13683
|
-
|
|
13906
|
+
resolve2();
|
|
13684
13907
|
};
|
|
13685
13908
|
process2.once("SIGINT", stop);
|
|
13686
13909
|
process2.once("SIGTERM", stop);
|
|
@@ -13689,7 +13912,7 @@ async function eventsTail(args) {
|
|
|
13689
13912
|
}
|
|
13690
13913
|
async function eventsWebhook(args) {
|
|
13691
13914
|
assertKnownOptions(args, eventWebhookAllowedOptions, "events webhook");
|
|
13692
|
-
const url = optionalArg(args, "--url") ??
|
|
13915
|
+
const url = optionalArg(args, "--url") ?? envValue2(optionalArg(args, "--url-env"));
|
|
13693
13916
|
if (!url) throw new Error("events webhook requires --url <https://...> or --url-env <ENV>");
|
|
13694
13917
|
const endpoint = new URL(url);
|
|
13695
13918
|
if (!["http:", "https:"].includes(endpoint.protocol)) throw new Error("events webhook URL must use http or https");
|
|
@@ -13703,7 +13926,7 @@ async function eventsWebhook(args) {
|
|
|
13703
13926
|
const sinceEventId = optionalPositiveIntegerArg(args, "--since-event-id");
|
|
13704
13927
|
if (!Number.isFinite(intervalMs) || intervalMs < 250) throw new Error("--interval-ms must be at least 250");
|
|
13705
13928
|
if (!Number.isFinite(timeoutMs) || timeoutMs < 250) throw new Error("--timeout-ms must be at least 250");
|
|
13706
|
-
const token =
|
|
13929
|
+
const token = envValue2(optionalArg(args, "--auth-token-env"));
|
|
13707
13930
|
const filters = eventFiltersFromArgs(args);
|
|
13708
13931
|
const seen = /* @__PURE__ */ new Set();
|
|
13709
13932
|
const pushOnce = async () => {
|
|
@@ -13738,7 +13961,7 @@ async function eventsWebhook(args) {
|
|
|
13738
13961
|
`);
|
|
13739
13962
|
}
|
|
13740
13963
|
if (!follow) return 0;
|
|
13741
|
-
await new Promise((
|
|
13964
|
+
await new Promise((resolve2) => {
|
|
13742
13965
|
const timer = setInterval(() => {
|
|
13743
13966
|
void pushOnce().catch((error) => {
|
|
13744
13967
|
process2.stderr.write(`events webhook error: ${safeErrorMessage(error)}
|
|
@@ -13749,7 +13972,7 @@ async function eventsWebhook(args) {
|
|
|
13749
13972
|
clearInterval(timer);
|
|
13750
13973
|
process2.off("SIGINT", stop);
|
|
13751
13974
|
process2.off("SIGTERM", stop);
|
|
13752
|
-
|
|
13975
|
+
resolve2();
|
|
13753
13976
|
};
|
|
13754
13977
|
process2.once("SIGINT", stop);
|
|
13755
13978
|
process2.once("SIGTERM", stop);
|
|
@@ -14123,9 +14346,19 @@ function optionalPositiveIntegerArg(args, flag) {
|
|
|
14123
14346
|
if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`${flag} must be a non-negative integer`);
|
|
14124
14347
|
return parsed;
|
|
14125
14348
|
}
|
|
14126
|
-
function
|
|
14127
|
-
if (
|
|
14128
|
-
|
|
14349
|
+
function envValue2(first, second) {
|
|
14350
|
+
if (typeof first === "string" || first === void 0) {
|
|
14351
|
+
if (!first) return void 0;
|
|
14352
|
+
return trimmedEnvValue(process2.env, first);
|
|
14353
|
+
}
|
|
14354
|
+
if (!second) return void 0;
|
|
14355
|
+
return trimmedEnvValue(first, second);
|
|
14356
|
+
}
|
|
14357
|
+
function trimmedEnvValue(env, name) {
|
|
14358
|
+
const value = env[name];
|
|
14359
|
+
if (value === void 0) return void 0;
|
|
14360
|
+
const trimmed = value.trim();
|
|
14361
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
14129
14362
|
}
|
|
14130
14363
|
function linkedProposalFilter(args, store, options = {}) {
|
|
14131
14364
|
const noLinkedProposal = "__synapsor_no_linked_proposal__";
|
|
@@ -14345,11 +14578,11 @@ async function waitForReferenceDemoDatabase() {
|
|
|
14345
14578
|
stdio: "pipe"
|
|
14346
14579
|
});
|
|
14347
14580
|
if (result.status === 0) {
|
|
14348
|
-
await new Promise((
|
|
14581
|
+
await new Promise((resolve2) => setTimeout(resolve2, 500));
|
|
14349
14582
|
return;
|
|
14350
14583
|
}
|
|
14351
14584
|
last = result.stderr || result.stdout || `exit ${result.status}`;
|
|
14352
|
-
await new Promise((
|
|
14585
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e3));
|
|
14353
14586
|
}
|
|
14354
14587
|
throw new Error(`demo database did not become ready: ${last}`);
|
|
14355
14588
|
}
|
|
@@ -300,6 +300,34 @@ Use an app-owned executor when an approved proposal needs richer business work:
|
|
|
300
300
|
creating a credit row, inserting an outbox event, updating multiple app tables,
|
|
301
301
|
or calling your own service.
|
|
302
302
|
|
|
303
|
+
In DSL, keep the reviewed business contract portable and name the executor:
|
|
304
|
+
|
|
305
|
+
```sql
|
|
306
|
+
CREATE CAPABILITY billing.propose_plan_credit
|
|
307
|
+
CONTEXT trusted_operator
|
|
308
|
+
SOURCE local_postgres
|
|
309
|
+
ON public.invoices
|
|
310
|
+
PRIMARY KEY id
|
|
311
|
+
TENANT KEY tenant_id
|
|
312
|
+
ARG invoice_id STRING REQUIRED
|
|
313
|
+
ARG amount_cents NUMBER REQUIRED
|
|
314
|
+
ARG reason STRING REQUIRED
|
|
315
|
+
LOOKUP invoice_id
|
|
316
|
+
VISIBLE id, tenant_id, customer_id, status, credit_requested_cents, credit_reason, updated_at
|
|
317
|
+
PROPOSE ACTION billing.grant_plan_credit
|
|
318
|
+
ALLOW WRITE credit_requested_cents, credit_reason
|
|
319
|
+
PATCH credit_requested_cents = ARG amount_cents
|
|
320
|
+
PATCH credit_reason = ARG reason
|
|
321
|
+
CONFLICT GUARD updated_at
|
|
322
|
+
APPROVAL ROLE billing_lead
|
|
323
|
+
WRITEBACK APP HANDLER EXECUTOR billing_handler
|
|
324
|
+
END
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
`billing_handler` is contract content. Its URL, bearer-token env var, signing
|
|
328
|
+
secret env var, and timeout stay in `synapsor.runner.json` so credentials never
|
|
329
|
+
enter the portable contract:
|
|
330
|
+
|
|
303
331
|
```json
|
|
304
332
|
"executors": {
|
|
305
333
|
"billing_handler": {
|
|
@@ -316,7 +344,7 @@ or calling your own service.
|
|
|
316
344
|
}
|
|
317
345
|
```
|
|
318
346
|
|
|
319
|
-
|
|
347
|
+
For legacy embedded JSON capabilities, reference the same executor directly:
|
|
320
348
|
|
|
321
349
|
```json
|
|
322
350
|
{
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synapsor/runner",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Stop giving AI agents execute_sql; expose reviewed Postgres/MySQL MCP actions with proposals, approval, writeback, and replay.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -52,11 +52,6 @@
|
|
|
52
52
|
"engines": {
|
|
53
53
|
"node": ">=22.5.0"
|
|
54
54
|
},
|
|
55
|
-
"scripts": {
|
|
56
|
-
"build": "tsc -b && node ../../scripts/build-runner-package.mjs",
|
|
57
|
-
"prepack": "cd ../.. && corepack pnpm build:runner-package",
|
|
58
|
-
"test": "vitest run"
|
|
59
|
-
},
|
|
60
55
|
"dependencies": {
|
|
61
56
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
62
57
|
"mysql2": "^3.11.0",
|
|
@@ -77,5 +72,9 @@
|
|
|
77
72
|
},
|
|
78
73
|
"publishConfig": {
|
|
79
74
|
"access": "public"
|
|
75
|
+
},
|
|
76
|
+
"scripts": {
|
|
77
|
+
"build": "tsc -b && node ../../scripts/build-runner-package.mjs",
|
|
78
|
+
"test": "vitest run"
|
|
80
79
|
}
|
|
81
|
-
}
|
|
80
|
+
}
|