@synapsor/runner 0.1.0-alpha.10 → 0.1.0-alpha.13
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/README.md +203 -21
- package/dist/cli.d.ts.map +1 -1
- package/dist/runner.mjs +1103 -115
- package/docs/README.md +38 -0
- package/docs/app-owned-executors.md +26 -0
- package/docs/capability-authoring.md +265 -0
- package/docs/cloud-mode.md +24 -0
- package/docs/current-scope.md +24 -0
- package/docs/dependency-license-inventory.md +35 -0
- package/docs/doctor.md +98 -0
- package/docs/handler-helper.md +200 -0
- package/docs/http-mcp.md +35 -1
- package/docs/licensing.md +36 -0
- package/docs/local-mode.md +13 -2
- package/docs/mcp-client-setup.md +39 -0
- package/docs/openai-agents-sdk.md +57 -0
- package/docs/release-notes.md +76 -2
- package/docs/release-policy.md +86 -0
- package/docs/result-envelope-v2.md +148 -0
- package/docs/rfcs/001-result-envelope-v2.md +143 -0
- package/docs/rfcs/002-app-owned-handler-helper.md +161 -0
- package/docs/rfcs/003-integrator-feedback-teardown.md +97 -0
- package/docs/store-lifecycle.md +83 -0
- package/docs/use-your-own-database.md +18 -0
- package/docs/writeback-executors.md +29 -0
- package/examples/app-owned-writeback/README.md +1 -0
- package/examples/mcp-postgres-billing-app-handler/README.md +86 -0
- package/examples/mcp-postgres-billing-app-handler/app-handler.mjs +125 -0
- package/examples/mcp-postgres-billing-app-handler/docker-compose.yml +13 -0
- package/examples/mcp-postgres-billing-app-handler/schema.sql +59 -0
- package/examples/mcp-postgres-billing-app-handler/scripts/run-demo.sh +100 -0
- package/examples/mcp-postgres-billing-app-handler/seed.sql +39 -0
- package/examples/mcp-postgres-billing-app-handler/synapsor-handler.mjs +437 -0
- package/examples/mcp-postgres-billing-app-handler/synapsor.runner.json +158 -0
- package/examples/openai-agents-http/README.md +10 -2
- package/examples/openai-agents-stdio/README.md +8 -4
- package/examples/openai-agents-stdio/agent.py +2 -0
- package/fixtures/benchmark/mcp-efficiency.json +53 -0
- package/fixtures/benchmark/mcp-efficiency.txt +25 -0
- package/fixtures/protocol/MANIFEST.json +54 -0
- package/fixtures/protocol/change-set.late-fee-waiver.v1.json +72 -0
- package/fixtures/protocol/execution-receipt.applied.v1.json +14 -0
- package/fixtures/protocol/execution-receipt.conflict.v1.json +15 -0
- package/fixtures/protocol/runner-registration.v1.json +22 -0
- package/fixtures/protocol/writeback-job.late-fee-waiver.v1.json +44 -0
- package/package.json +4 -1
- package/schemas/change-set.v1.schema.json +140 -0
- package/schemas/execution-receipt.v1.schema.json +34 -0
- package/schemas/onboarding-selection.v1.schema.json +125 -0
- package/schemas/runner-registration.v1.schema.json +48 -0
- package/schemas/synapsor.app-handler-receipt.v1.json +39 -0
- package/schemas/synapsor.app-handler-request.v1.json +119 -0
- package/schemas/synapsor.runner.schema.json +412 -0
- package/schemas/writeback-job.v1.schema.json +121 -0
package/dist/runner.mjs
CHANGED
|
@@ -444,7 +444,7 @@ function sleep(ms) {
|
|
|
444
444
|
}
|
|
445
445
|
|
|
446
446
|
// packages/config/src/index.ts
|
|
447
|
-
var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["version", "mode", "storage", "sources", "trusted_context", "contexts", "executors", "capabilities", "cloud", "strict"]);
|
|
447
|
+
var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["version", "mode", "storage", "sources", "trusted_context", "contexts", "executors", "capabilities", "cloud", "strict", "result_format"]);
|
|
448
448
|
var STORAGE_KEYS = /* @__PURE__ */ new Set(["sqlite_path"]);
|
|
449
449
|
var CLOUD_KEYS = /* @__PURE__ */ new Set(["base_url_env", "runner_token_env", "runner_id", "runner_version", "project_id", "adapter_id", "source_id", "engines", "capabilities", "session"]);
|
|
450
450
|
var SOURCE_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -456,11 +456,13 @@ var SOURCE_KEYS = /* @__PURE__ */ new Set([
|
|
|
456
456
|
]);
|
|
457
457
|
var TRUSTED_CONTEXT_KEYS = /* @__PURE__ */ new Set(["provider", "values"]);
|
|
458
458
|
var CONTEXT_KEYS = TRUSTED_CONTEXT_KEYS;
|
|
459
|
-
var EXECUTOR_KEYS = /* @__PURE__ */ new Set(["type", "url_env", "method", "auth", "timeout_ms", "command_env"]);
|
|
459
|
+
var EXECUTOR_KEYS = /* @__PURE__ */ new Set(["type", "url_env", "method", "auth", "signing_secret_env", "timeout_ms", "command_env"]);
|
|
460
460
|
var EXECUTOR_AUTH_KEYS = /* @__PURE__ */ new Set(["type", "token_env"]);
|
|
461
461
|
var CAPABILITY_KEYS = /* @__PURE__ */ new Set([
|
|
462
462
|
"name",
|
|
463
463
|
"kind",
|
|
464
|
+
"description",
|
|
465
|
+
"returns_hint",
|
|
464
466
|
"source",
|
|
465
467
|
"context",
|
|
466
468
|
"executor",
|
|
@@ -480,7 +482,7 @@ var CAPABILITY_KEYS = /* @__PURE__ */ new Set([
|
|
|
480
482
|
]);
|
|
481
483
|
var TARGET_KEYS = /* @__PURE__ */ new Set(["schema", "table", "primary_key", "tenant_key", "single_tenant_dev"]);
|
|
482
484
|
var LOOKUP_KEYS = /* @__PURE__ */ new Set(["id_from_arg"]);
|
|
483
|
-
var ARG_KEYS = /* @__PURE__ */ new Set(["type", "required", "max_length", "minimum", "maximum", "enum"]);
|
|
485
|
+
var ARG_KEYS = /* @__PURE__ */ new Set(["type", "description", "required", "max_length", "minimum", "maximum", "enum"]);
|
|
484
486
|
var PATCH_BINDING_KEYS = /* @__PURE__ */ new Set(["fixed", "from_arg"]);
|
|
485
487
|
var NUMERIC_BOUND_KEYS = /* @__PURE__ */ new Set(["minimum", "maximum"]);
|
|
486
488
|
var TRANSITION_GUARD_KEYS = /* @__PURE__ */ new Set(["from_column", "allowed"]);
|
|
@@ -549,6 +551,9 @@ function validateRunnerCapabilityConfig(input) {
|
|
|
549
551
|
if (input.version !== 1) {
|
|
550
552
|
errors.push({ path: "$.version", code: "UNSUPPORTED_CONFIG_VERSION", message: "Runner config version must be 1." });
|
|
551
553
|
}
|
|
554
|
+
if (input.result_format !== void 0 && input.result_format !== 1 && input.result_format !== 2) {
|
|
555
|
+
errors.push({ path: "$.result_format", code: "INVALID_RESULT_FORMAT", message: "result_format must be 1 or 2." });
|
|
556
|
+
}
|
|
552
557
|
if (!isRunnerMode(input.mode)) {
|
|
553
558
|
errors.push({ path: "$.mode", code: "INVALID_MODE", message: "mode must be read_only, shadow, review, or cloud." });
|
|
554
559
|
}
|
|
@@ -797,6 +802,9 @@ function validateExecutors(value, mode, strict, errors) {
|
|
|
797
802
|
errors.push({ path: `${path4}.method`, code: "INVALID_HANDLER_METHOD", message: "http_handler.method must be POST, PUT, or PATCH." });
|
|
798
803
|
}
|
|
799
804
|
validateExecutorAuth(executor.auth, `${path4}.auth`, strict, errors);
|
|
805
|
+
if (executor.signing_secret_env !== void 0 && !isEnvName(executor.signing_secret_env)) {
|
|
806
|
+
errors.push({ path: `${path4}.signing_secret_env`, code: "HANDLER_SIGNING_SECRET_ENV_INVALID", message: "http_handler.signing_secret_env must name an environment variable containing the HMAC signing secret." });
|
|
807
|
+
}
|
|
800
808
|
if (executor.timeout_ms !== void 0 && !isPositiveInteger(executor.timeout_ms)) {
|
|
801
809
|
errors.push({ path: `${path4}.timeout_ms`, code: "INVALID_HANDLER_TIMEOUT", message: "http_handler.timeout_ms must be a positive integer." });
|
|
802
810
|
}
|
|
@@ -852,6 +860,12 @@ function validateCapability(value, index, sourceNames, contextNames, executorNam
|
|
|
852
860
|
if (!isCapabilityKind(value.kind)) {
|
|
853
861
|
errors.push({ path: `${path4}.kind`, code: "INVALID_CAPABILITY_KIND", message: "kind must be read or proposal." });
|
|
854
862
|
}
|
|
863
|
+
if (value.description !== void 0 && !isNonEmptyString(value.description)) {
|
|
864
|
+
errors.push({ path: `${path4}.description`, code: "INVALID_CAPABILITY_DESCRIPTION", message: "description must be a non-empty string when provided." });
|
|
865
|
+
}
|
|
866
|
+
if (value.returns_hint !== void 0 && !isNonEmptyString(value.returns_hint)) {
|
|
867
|
+
errors.push({ path: `${path4}.returns_hint`, code: "INVALID_RETURNS_HINT", message: "returns_hint must be a non-empty string when provided." });
|
|
868
|
+
}
|
|
855
869
|
if (!isNonEmptyString(value.source) || !sourceNames.has(value.source)) {
|
|
856
870
|
errors.push({ path: `${path4}.source`, code: "UNKNOWN_SOURCE", message: "Capability source must reference a configured source." });
|
|
857
871
|
}
|
|
@@ -926,6 +940,9 @@ function validateArgs(value, path4, strict, errors) {
|
|
|
926
940
|
if (!["string", "number", "boolean"].includes(String(arg.type))) {
|
|
927
941
|
errors.push({ path: `${argPath}.type`, code: "INVALID_ARG_TYPE", message: "Argument type must be string, number, or boolean." });
|
|
928
942
|
}
|
|
943
|
+
if (arg.description !== void 0 && !isNonEmptyString(arg.description)) {
|
|
944
|
+
errors.push({ path: `${argPath}.description`, code: "INVALID_ARG_DESCRIPTION", message: "Argument description must be a non-empty string when provided." });
|
|
945
|
+
}
|
|
929
946
|
if (arg.max_length !== void 0 && !isPositiveInteger(arg.max_length)) {
|
|
930
947
|
errors.push({ path: `${argPath}.max_length`, code: "INVALID_MAX_LENGTH", message: "max_length must be a positive integer." });
|
|
931
948
|
}
|
|
@@ -2073,6 +2090,11 @@ var ProposalStore = class {
|
|
|
2073
2090
|
const rows = this.db.prepare("SELECT * FROM proposal_events WHERE proposal_id = ? ORDER BY event_id ASC").all(proposalId);
|
|
2074
2091
|
return rows.map(rowToEvent).filter((event) => event !== void 0);
|
|
2075
2092
|
}
|
|
2093
|
+
listEvents(filters = {}) {
|
|
2094
|
+
const query = buildEventQuery(filters);
|
|
2095
|
+
const rows = this.db.prepare(query.sql).all(...query.params);
|
|
2096
|
+
return rows.map(rowToEvent).filter((event) => event !== void 0);
|
|
2097
|
+
}
|
|
2076
2098
|
receipts(proposalId) {
|
|
2077
2099
|
const rows = this.db.prepare("SELECT * FROM writeback_receipts WHERE proposal_id = ? ORDER BY receipt_id ASC").all(proposalId);
|
|
2078
2100
|
return rows.map(rowToReceipt).filter((receipt) => receipt !== void 0);
|
|
@@ -2387,6 +2409,15 @@ function buildReceiptQuery(filters) {
|
|
|
2387
2409
|
addTimeRange(clauses, params, "created_at", filters.from, filters.to);
|
|
2388
2410
|
return finishQuery("SELECT * FROM writeback_receipts", clauses, params, filters.limit);
|
|
2389
2411
|
}
|
|
2412
|
+
function buildEventQuery(filters) {
|
|
2413
|
+
const clauses = [];
|
|
2414
|
+
const params = [];
|
|
2415
|
+
addEqual(clauses, params, "proposal_id", filters.proposal);
|
|
2416
|
+
addEqual(clauses, params, "kind", filters.kind);
|
|
2417
|
+
addEqual(clauses, params, "actor", filters.actor);
|
|
2418
|
+
addTimeRange(clauses, params, "created_at", filters.from, filters.to);
|
|
2419
|
+
return finishQuery("SELECT * FROM proposal_events", clauses, params, filters.limit);
|
|
2420
|
+
}
|
|
2390
2421
|
function addEqual(clauses, params, column, value) {
|
|
2391
2422
|
if (!value) return;
|
|
2392
2423
|
clauses.push(`${column} = ?`);
|
|
@@ -2670,74 +2701,118 @@ function loadRuntimeConfigFromFile(configPath = process.env.SYNAPSOR_MCP_CONFIG
|
|
|
2670
2701
|
function createMcpRuntime(config, options = {}) {
|
|
2671
2702
|
assertValidRunnerCapabilityConfig(config);
|
|
2672
2703
|
const env = options.env ?? process.env;
|
|
2673
|
-
const
|
|
2704
|
+
const storePath = options.storePath ?? config.storage?.sqlite_path ?? "./.synapsor/local.db";
|
|
2705
|
+
const ownsStore = !options.store;
|
|
2706
|
+
const store = options.store ?? new ProposalStore(storePath);
|
|
2674
2707
|
const readRow = options.readRow ?? readCurrentRow;
|
|
2675
2708
|
const cloudClient = options.controlPlaneClient ?? (config.mode === "cloud" ? createCloudClient(config, env) : void 0);
|
|
2676
2709
|
const cloudTools = options.cloudTools ?? [];
|
|
2710
|
+
const resultFormat = options.resultFormat ?? config.result_format ?? 1;
|
|
2711
|
+
const assertStoreAvailable = () => {
|
|
2712
|
+
if (ownsStore) assertPersistentStoreAvailable(storePath);
|
|
2713
|
+
};
|
|
2677
2714
|
return {
|
|
2678
2715
|
config,
|
|
2679
2716
|
store,
|
|
2680
2717
|
listTools: () => config.mode === "cloud" ? cloudTools : listedLocalCapabilities(config).map((capability) => toolMetadata(capability)),
|
|
2681
|
-
callTool: async (name, args) =>
|
|
2682
|
-
|
|
2718
|
+
callTool: async (name, args) => {
|
|
2719
|
+
if (resultFormat === 2) {
|
|
2720
|
+
try {
|
|
2721
|
+
assertStoreAvailable();
|
|
2722
|
+
return await callConfiguredToolV2({ config, env, store, readRow, cloudClient, name, args });
|
|
2723
|
+
} catch (error) {
|
|
2724
|
+
const capability = config.mode === "cloud" ? void 0 : localCapabilities(config).find((item) => item.name === name);
|
|
2725
|
+
return errorEnvelopeFromError(error, capability, name);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
assertStoreAvailable();
|
|
2729
|
+
return callConfiguredTool({ config, env, store, readRow, cloudClient, name, args });
|
|
2730
|
+
},
|
|
2731
|
+
readResource: (uri) => {
|
|
2732
|
+
assertStoreAvailable();
|
|
2733
|
+
return readLocalResource(store, uri);
|
|
2734
|
+
},
|
|
2683
2735
|
close: () => {
|
|
2684
2736
|
if (!options.store) store.close();
|
|
2685
2737
|
}
|
|
2686
2738
|
};
|
|
2687
2739
|
}
|
|
2688
|
-
function
|
|
2740
|
+
function assertPersistentStoreAvailable(storePath) {
|
|
2741
|
+
if (storePath === ":memory:") return;
|
|
2742
|
+
if (fs.existsSync(storePath)) return;
|
|
2743
|
+
throw new McpRuntimeError(
|
|
2744
|
+
"LOCAL_STORE_UNAVAILABLE",
|
|
2745
|
+
"The local Synapsor store is temporarily unavailable. Restart the runner or recreate the store before retrying."
|
|
2746
|
+
);
|
|
2747
|
+
}
|
|
2748
|
+
function createSynapsorMcpServer(runtime, options = {}) {
|
|
2689
2749
|
const server = new McpServer(
|
|
2690
|
-
{ name: "synapsor-runner", version: "0.1.0-alpha.
|
|
2750
|
+
{ name: "synapsor-runner", version: "0.1.0-alpha.13" },
|
|
2691
2751
|
{ capabilities: { tools: {}, resources: {} } }
|
|
2692
2752
|
);
|
|
2753
|
+
const toolNameStyle = options.toolNameStyle ?? "canonical";
|
|
2693
2754
|
if (runtime.config.mode === "cloud") {
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2755
|
+
const tools2 = runtime.listTools();
|
|
2756
|
+
const exposedNames = toolNameExposureMap(tools2.map((tool) => tool.name), toolNameStyle);
|
|
2757
|
+
for (const tool of tools2) {
|
|
2758
|
+
for (const exposedName of exposedNames.get(tool.name) ?? [tool.name]) {
|
|
2759
|
+
server.registerTool(
|
|
2760
|
+
exposedName,
|
|
2761
|
+
{
|
|
2762
|
+
title: tool.title,
|
|
2763
|
+
description: toolDescriptionWithCanonical(tool.description, tool.name, exposedName),
|
|
2764
|
+
inputSchema: zodInputShapeFromJsonSchema(tool.input_schema),
|
|
2765
|
+
annotations: {
|
|
2766
|
+
readOnlyHint: Boolean(tool.annotations.readOnlyHint),
|
|
2767
|
+
destructiveHint: false,
|
|
2768
|
+
idempotentHint: Boolean(tool.annotations.idempotentHint),
|
|
2769
|
+
openWorldHint: false
|
|
2770
|
+
},
|
|
2771
|
+
_meta: {
|
|
2772
|
+
...tool.annotations,
|
|
2773
|
+
"synapsor.cloud_delegated": true,
|
|
2774
|
+
"synapsor.canonical_tool_name": tool.name,
|
|
2775
|
+
"synapsor.exposed_tool_name": exposedName,
|
|
2776
|
+
"synapsor.tool_name_style": toolNameStyle,
|
|
2777
|
+
"synapsor.raw_sql_exposed": false,
|
|
2778
|
+
"synapsor.approval_tool": false
|
|
2779
|
+
}
|
|
2706
2780
|
},
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
"synapsor.raw_sql_exposed": false,
|
|
2711
|
-
"synapsor.approval_tool": false
|
|
2712
|
-
}
|
|
2713
|
-
},
|
|
2714
|
-
async (args) => toolCallResult(runtime, tool.name, args)
|
|
2715
|
-
);
|
|
2781
|
+
async (args) => toolCallResult(runtime, tool.name, args)
|
|
2782
|
+
);
|
|
2783
|
+
}
|
|
2716
2784
|
}
|
|
2717
2785
|
} else {
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2786
|
+
const capabilities = listedLocalCapabilities(runtime.config);
|
|
2787
|
+
const exposedNames = toolNameExposureMap(capabilities.map((capability) => capability.name), toolNameStyle);
|
|
2788
|
+
for (const capability of capabilities) {
|
|
2789
|
+
for (const exposedName of exposedNames.get(capability.name) ?? [capability.name]) {
|
|
2790
|
+
server.registerTool(
|
|
2791
|
+
exposedName,
|
|
2792
|
+
{
|
|
2793
|
+
title: capability.name,
|
|
2794
|
+
description: capabilityDescription(capability, exposedName),
|
|
2795
|
+
inputSchema: zodInputShape(capability),
|
|
2796
|
+
annotations: {
|
|
2797
|
+
readOnlyHint: capability.kind === "read",
|
|
2798
|
+
destructiveHint: false,
|
|
2799
|
+
idempotentHint: capability.kind === "read",
|
|
2800
|
+
openWorldHint: false
|
|
2801
|
+
},
|
|
2802
|
+
_meta: {
|
|
2803
|
+
"synapsor.kind": capability.kind,
|
|
2804
|
+
"synapsor.source": capability.source,
|
|
2805
|
+
"synapsor.target": `${capability.target.schema}.${capability.target.table}`,
|
|
2806
|
+
"synapsor.canonical_tool_name": capability.name,
|
|
2807
|
+
"synapsor.exposed_tool_name": exposedName,
|
|
2808
|
+
"synapsor.tool_name_style": toolNameStyle,
|
|
2809
|
+
"synapsor.raw_sql_exposed": false,
|
|
2810
|
+
"synapsor.approval_tool": false
|
|
2811
|
+
}
|
|
2730
2812
|
},
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
"synapsor.target": `${capability.target.schema}.${capability.target.table}`,
|
|
2735
|
-
"synapsor.raw_sql_exposed": false,
|
|
2736
|
-
"synapsor.approval_tool": false
|
|
2737
|
-
}
|
|
2738
|
-
},
|
|
2739
|
-
async (args) => toolCallResult(runtime, capability.name, args)
|
|
2740
|
-
);
|
|
2813
|
+
async (args) => toolCallResult(runtime, capability.name, args)
|
|
2814
|
+
);
|
|
2815
|
+
}
|
|
2741
2816
|
}
|
|
2742
2817
|
}
|
|
2743
2818
|
server.registerResource(
|
|
@@ -2763,8 +2838,8 @@ function createSynapsorMcpServer(runtime) {
|
|
|
2763
2838
|
async function serveStdio(options = {}) {
|
|
2764
2839
|
const config = options.config ?? loadRuntimeConfigFromFile(options.configPath);
|
|
2765
2840
|
const cloudTools = config.mode === "cloud" ? await fetchCloudToolMetadata(config, process.env) : void 0;
|
|
2766
|
-
const runtime = createMcpRuntime(config, { storePath: options.storePath, cloudTools });
|
|
2767
|
-
const server = createSynapsorMcpServer(runtime);
|
|
2841
|
+
const runtime = createMcpRuntime(config, { storePath: options.storePath, resultFormat: options.resultFormat, cloudTools });
|
|
2842
|
+
const server = createSynapsorMcpServer(runtime, { toolNameStyle: options.toolNameStyle });
|
|
2768
2843
|
const transport = new StdioServerTransport();
|
|
2769
2844
|
await server.connect(transport);
|
|
2770
2845
|
await new Promise((resolve) => {
|
|
@@ -2799,6 +2874,7 @@ async function startHttpMcpServer(options = {}) {
|
|
|
2799
2874
|
const runtime = createMcpRuntime(config, {
|
|
2800
2875
|
env,
|
|
2801
2876
|
storePath: options.storePath,
|
|
2877
|
+
resultFormat: options.resultFormat,
|
|
2802
2878
|
readRow: options.readRow,
|
|
2803
2879
|
cloudTools
|
|
2804
2880
|
});
|
|
@@ -2872,6 +2948,8 @@ async function startStreamableHttpMcpServer(options = {}) {
|
|
|
2872
2948
|
readRow: options.readRow,
|
|
2873
2949
|
cloudTools,
|
|
2874
2950
|
env,
|
|
2951
|
+
toolNameStyle: options.toolNameStyle,
|
|
2952
|
+
resultFormat: options.resultFormat,
|
|
2875
2953
|
authToken,
|
|
2876
2954
|
devNoAuth,
|
|
2877
2955
|
corsOrigin: options.corsOrigin,
|
|
@@ -2914,7 +2992,7 @@ async function startStreamableHttpMcpServer(options = {}) {
|
|
|
2914
2992
|
};
|
|
2915
2993
|
}
|
|
2916
2994
|
async function handleStreamableHttpMcpRequest(input) {
|
|
2917
|
-
const { request, response, config, storePath, readRow, cloudTools, env, authToken, devNoAuth, corsOrigin, sessions, openSessions } = input;
|
|
2995
|
+
const { request, response, config, storePath, readRow, cloudTools, env, toolNameStyle, resultFormat, authToken, devNoAuth, corsOrigin, sessions, openSessions } = input;
|
|
2918
2996
|
try {
|
|
2919
2997
|
setCorsHeaders(response, corsOrigin);
|
|
2920
2998
|
if (request.method === "OPTIONS" && corsOrigin) {
|
|
@@ -2976,13 +3054,13 @@ async function handleStreamableHttpMcpRequest(input) {
|
|
|
2976
3054
|
}
|
|
2977
3055
|
}
|
|
2978
3056
|
});
|
|
2979
|
-
const runtime = createMcpRuntime(config, { env, storePath, readRow, cloudTools });
|
|
3057
|
+
const runtime = createMcpRuntime(config, { env, storePath, resultFormat, readRow, cloudTools });
|
|
2980
3058
|
session = { transport, runtime };
|
|
2981
3059
|
openSessions.add(session);
|
|
2982
3060
|
transport.onclose = () => {
|
|
2983
3061
|
if (session) disposeStreamableSession(session, sessions, openSessions);
|
|
2984
3062
|
};
|
|
2985
|
-
await createSynapsorMcpServer(runtime).connect(transport);
|
|
3063
|
+
await createSynapsorMcpServer(runtime, { toolNameStyle }).connect(transport);
|
|
2986
3064
|
await transport.handleRequest(request, response, parsedBody);
|
|
2987
3065
|
} catch (error) {
|
|
2988
3066
|
const message = sanitizeHttpError(error, authToken);
|
|
@@ -3103,6 +3181,50 @@ function requestIdFromPayload(payload) {
|
|
|
3103
3181
|
}
|
|
3104
3182
|
return isRecord3(payload) ? payload.id ?? null : null;
|
|
3105
3183
|
}
|
|
3184
|
+
function openaiToolNameAlias(canonicalName) {
|
|
3185
|
+
const sanitized = canonicalName.replace(/[^A-Za-z0-9_-]+/g, "__").replace(/_{3,}/g, "__").replace(/^_+|_+$/g, "");
|
|
3186
|
+
const base = sanitized.length > 0 ? sanitized : `tool_${shortToolHash(canonicalName)}`;
|
|
3187
|
+
if (base.length <= 64) return base;
|
|
3188
|
+
const suffix = shortToolHash(canonicalName);
|
|
3189
|
+
return `${base.slice(0, Math.max(1, 63 - suffix.length)).replace(/_+$/g, "")}_${suffix}`;
|
|
3190
|
+
}
|
|
3191
|
+
function toolNameExposures(canonicalNames, style) {
|
|
3192
|
+
const exposedNames = toolNameExposureMap(canonicalNames, style);
|
|
3193
|
+
return canonicalNames.flatMap((canonicalName) => {
|
|
3194
|
+
return (exposedNames.get(canonicalName) ?? [canonicalName]).map((exposedName) => ({
|
|
3195
|
+
canonicalName,
|
|
3196
|
+
exposedName,
|
|
3197
|
+
isAlias: exposedName !== canonicalName,
|
|
3198
|
+
style
|
|
3199
|
+
}));
|
|
3200
|
+
});
|
|
3201
|
+
}
|
|
3202
|
+
function toolNameExposureMap(canonicalNames, style) {
|
|
3203
|
+
const exposedByCanonical = /* @__PURE__ */ new Map();
|
|
3204
|
+
const canonicalByExposed = /* @__PURE__ */ new Map();
|
|
3205
|
+
if (style === "both") {
|
|
3206
|
+
for (const canonical of canonicalNames) canonicalByExposed.set(canonical, canonical);
|
|
3207
|
+
}
|
|
3208
|
+
for (const canonical of canonicalNames) {
|
|
3209
|
+
const names = /* @__PURE__ */ new Set();
|
|
3210
|
+
if (style === "canonical" || style === "both") names.add(canonical);
|
|
3211
|
+
if (style === "openai" || style === "both") {
|
|
3212
|
+
let alias = openaiToolNameAlias(canonical);
|
|
3213
|
+
const existing = canonicalByExposed.get(alias);
|
|
3214
|
+
if (existing && existing !== canonical) {
|
|
3215
|
+
const suffix = shortToolHash(canonical);
|
|
3216
|
+
alias = `${alias.slice(0, Math.max(1, 63 - suffix.length)).replace(/_+$/g, "")}_${suffix}`;
|
|
3217
|
+
}
|
|
3218
|
+
canonicalByExposed.set(alias, canonical);
|
|
3219
|
+
names.add(alias);
|
|
3220
|
+
}
|
|
3221
|
+
exposedByCanonical.set(canonical, [...names]);
|
|
3222
|
+
}
|
|
3223
|
+
return exposedByCanonical;
|
|
3224
|
+
}
|
|
3225
|
+
function shortToolHash(value) {
|
|
3226
|
+
return crypto.createHash("sha256").update(value).digest("hex").slice(0, 8);
|
|
3227
|
+
}
|
|
3106
3228
|
function setCorsHeaders(response, corsOrigin) {
|
|
3107
3229
|
if (corsOrigin) {
|
|
3108
3230
|
response.setHeader("access-control-allow-origin", corsOrigin);
|
|
@@ -3270,6 +3392,11 @@ function cloudToolMetadata(tool) {
|
|
|
3270
3392
|
}
|
|
3271
3393
|
};
|
|
3272
3394
|
}
|
|
3395
|
+
function toolDescriptionWithCanonical(description, canonicalName, exposedName) {
|
|
3396
|
+
if (!exposedName || exposedName === canonicalName) return description;
|
|
3397
|
+
return `Canonical Synapsor capability: ${canonicalName}.
|
|
3398
|
+
${description}`;
|
|
3399
|
+
}
|
|
3273
3400
|
function zodInputShapeFromJsonSchema(schema) {
|
|
3274
3401
|
const properties = isRecord3(schema.properties) ? schema.properties : {};
|
|
3275
3402
|
const required = Array.isArray(schema.required) ? new Set(schema.required.map(String)) : /* @__PURE__ */ new Set();
|
|
@@ -3447,6 +3574,135 @@ async function callConfiguredTool(input) {
|
|
|
3447
3574
|
source_database_mutated: false
|
|
3448
3575
|
};
|
|
3449
3576
|
}
|
|
3577
|
+
async function callConfiguredToolV2(input) {
|
|
3578
|
+
const capability = input.config.mode === "cloud" ? void 0 : localCapabilities(input.config).find((item) => item.name === input.name);
|
|
3579
|
+
try {
|
|
3580
|
+
const legacy = await callConfiguredTool(input);
|
|
3581
|
+
return resultEnvelopeFromLegacy(legacy, capability, input.name);
|
|
3582
|
+
} catch (error) {
|
|
3583
|
+
return errorEnvelopeFromError(error, capability, input.name);
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
function resultEnvelopeFromLegacy(legacy, capability, canonicalName) {
|
|
3587
|
+
const action = typeof legacy.action === "string" ? legacy.action : canonicalName;
|
|
3588
|
+
const kind = capability?.kind ?? (typeof legacy.proposal_id === "string" ? "proposal" : "read");
|
|
3589
|
+
const evidenceBundleId = typeof legacy.evidence_bundle_id === "string" ? legacy.evidence_bundle_id : void 0;
|
|
3590
|
+
const sourceChanged = Boolean(legacy.source_database_changed ?? legacy.source_database_mutated ?? false);
|
|
3591
|
+
const context = isRecord3(legacy.trusted_context) ? legacy.trusted_context : void 0;
|
|
3592
|
+
const target2 = isRecord3(legacy.target) ? legacy.target : void 0;
|
|
3593
|
+
if (kind === "proposal") {
|
|
3594
|
+
const proposalId = typeof legacy.proposal_id === "string" ? legacy.proposal_id : "wrp_unknown";
|
|
3595
|
+
const targetType = typeof target2?.type === "string" ? target2.type : capability?.target.table ?? "object";
|
|
3596
|
+
const targetId = target2?.id !== void 0 ? String(target2.id) : "unknown";
|
|
3597
|
+
const executor = writebackExecutorName(legacy.writeback);
|
|
3598
|
+
const writebackMode = executor && executor !== "sql_update" && executor !== "trusted_worker_required" ? "app_handler" : "direct_update";
|
|
3599
|
+
return {
|
|
3600
|
+
ok: true,
|
|
3601
|
+
summary: `Created proposal ${proposalId} for ${targetType} ${targetId}. Source database changed: no.`,
|
|
3602
|
+
action,
|
|
3603
|
+
kind,
|
|
3604
|
+
data: null,
|
|
3605
|
+
proposal: {
|
|
3606
|
+
id: proposalId,
|
|
3607
|
+
state: typeof legacy.status === "string" ? legacy.status : "review_required",
|
|
3608
|
+
target: `${targetType}:${targetId}`,
|
|
3609
|
+
diff: isRecord3(legacy.diff) ? legacy.diff : {},
|
|
3610
|
+
approval_required: legacy.approval_required !== false,
|
|
3611
|
+
writeback: {
|
|
3612
|
+
mode: writebackMode,
|
|
3613
|
+
applied: false
|
|
3614
|
+
},
|
|
3615
|
+
next: "A human must approve outside this model-facing tool surface; nothing is committed yet."
|
|
3616
|
+
},
|
|
3617
|
+
error: null,
|
|
3618
|
+
evidence: evidenceBundleId ? evidenceHandle(evidenceBundleId) : null,
|
|
3619
|
+
source_database_changed: sourceChanged,
|
|
3620
|
+
_meta: {
|
|
3621
|
+
tenant_id: typeof target2?.tenant_id === "string" ? target2.tenant_id : void 0,
|
|
3622
|
+
principal: typeof context?.principal === "string" ? context.principal : void 0,
|
|
3623
|
+
provenance: typeof context?.provenance === "string" ? context.provenance : void 0,
|
|
3624
|
+
canonical_capability: action
|
|
3625
|
+
}
|
|
3626
|
+
};
|
|
3627
|
+
}
|
|
3628
|
+
const businessObject = isRecord3(legacy.business_object) ? legacy.business_object : void 0;
|
|
3629
|
+
const objectType = typeof businessObject?.type === "string" ? businessObject.type : capability?.target.table ?? "record";
|
|
3630
|
+
const objectId = businessObject?.id !== void 0 ? String(businessObject.id) : String(legacy.action ?? action);
|
|
3631
|
+
return {
|
|
3632
|
+
ok: true,
|
|
3633
|
+
summary: `Read ${objectType} ${objectId} through ${action}. Source database changed: no.`,
|
|
3634
|
+
action,
|
|
3635
|
+
kind: "read",
|
|
3636
|
+
data: isRecord3(legacy.data) ? legacy.data : {},
|
|
3637
|
+
proposal: null,
|
|
3638
|
+
error: null,
|
|
3639
|
+
evidence: evidenceBundleId ? evidenceHandle(evidenceBundleId) : null,
|
|
3640
|
+
source_database_changed: sourceChanged,
|
|
3641
|
+
_meta: {
|
|
3642
|
+
tenant_id: typeof context?.tenant_id === "string" ? context.tenant_id : void 0,
|
|
3643
|
+
principal: typeof context?.principal === "string" ? context.principal : void 0,
|
|
3644
|
+
provenance: typeof context?.provenance === "string" ? context.provenance : void 0,
|
|
3645
|
+
canonical_capability: action
|
|
3646
|
+
}
|
|
3647
|
+
};
|
|
3648
|
+
}
|
|
3649
|
+
function writebackExecutorName(value) {
|
|
3650
|
+
if (!isRecord3(value)) return void 0;
|
|
3651
|
+
return typeof value.executor === "string" ? value.executor : typeof value.mode === "string" ? value.mode : void 0;
|
|
3652
|
+
}
|
|
3653
|
+
function evidenceHandle(bundleId) {
|
|
3654
|
+
return {
|
|
3655
|
+
bundle_id: bundleId,
|
|
3656
|
+
note: "audit/replay handle; you do not need to act on it during this turn"
|
|
3657
|
+
};
|
|
3658
|
+
}
|
|
3659
|
+
function errorEnvelopeFromError(error, capability, canonicalName) {
|
|
3660
|
+
const safe = safeToolError(error);
|
|
3661
|
+
const action = capability?.name ?? canonicalName;
|
|
3662
|
+
return {
|
|
3663
|
+
ok: false,
|
|
3664
|
+
summary: safe.message,
|
|
3665
|
+
action,
|
|
3666
|
+
kind: capability?.kind ?? "read",
|
|
3667
|
+
data: null,
|
|
3668
|
+
proposal: null,
|
|
3669
|
+
error: safe,
|
|
3670
|
+
evidence: null,
|
|
3671
|
+
source_database_changed: false,
|
|
3672
|
+
_meta: {
|
|
3673
|
+
canonical_capability: action
|
|
3674
|
+
}
|
|
3675
|
+
};
|
|
3676
|
+
}
|
|
3677
|
+
function safeToolError(error) {
|
|
3678
|
+
const runtimeCode = error instanceof McpRuntimeError ? error.code : void 0;
|
|
3679
|
+
if (runtimeCode === "ROW_NOT_FOUND") {
|
|
3680
|
+
return { code: "NOT_FOUND_IN_TENANT", message: "No authorized row was found in the trusted tenant scope.", retryable: false };
|
|
3681
|
+
}
|
|
3682
|
+
if (runtimeCode === "MCP_TOOL_NOT_FOUND") {
|
|
3683
|
+
return { code: "CAPABILITY_NOT_FOUND", message: "The requested Synapsor capability is not available.", retryable: false };
|
|
3684
|
+
}
|
|
3685
|
+
if (runtimeCode === "PROPOSALS_DISABLED") {
|
|
3686
|
+
return { code: "APPROVAL_REQUIRED", message: "Proposal tools are disabled for this runner mode.", retryable: false };
|
|
3687
|
+
}
|
|
3688
|
+
if (runtimeCode && (runtimeCode.startsWith("ARGUMENT_") || runtimeCode === "LOOKUP_ARG_MISSING" || runtimeCode === "MODEL_CANNOT_OVERRIDE_BINDING" || runtimeCode === "TRUSTED_BINDING_MISSING" || runtimeCode === "TRUSTED_CONTEXT_MISSING")) {
|
|
3689
|
+
return { code: "INVALID_ARGUMENT", message: "The tool input or trusted context binding is invalid.", retryable: false };
|
|
3690
|
+
}
|
|
3691
|
+
if (runtimeCode && (runtimeCode.startsWith("PATCH_") || runtimeCode === "CONFLICT_GUARD_MISSING")) {
|
|
3692
|
+
return { code: "POLICY_VIOLATION", message: "The requested change is outside the reviewed capability policy.", retryable: false };
|
|
3693
|
+
}
|
|
3694
|
+
if (runtimeCode === "LOCAL_STORE_UNAVAILABLE") {
|
|
3695
|
+
return { code: "TEMPORARILY_UNAVAILABLE", message: "The local runner store is temporarily unavailable. Restart the runner or recreate the store before retrying.", retryable: true };
|
|
3696
|
+
}
|
|
3697
|
+
if (runtimeCode === "SOURCE_CREDENTIAL_MISSING" || looksLikeInfraError(error)) {
|
|
3698
|
+
return { code: "TEMPORARILY_UNAVAILABLE", message: "The database is temporarily unavailable. Retry later.", retryable: true };
|
|
3699
|
+
}
|
|
3700
|
+
return { code: "INTERNAL", message: "The capability failed safely. Check the local runner logs for details.", retryable: false };
|
|
3701
|
+
}
|
|
3702
|
+
function looksLikeInfraError(error) {
|
|
3703
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
3704
|
+
return /\b(ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|timeout|connect|connection|database|authentication|certificate)\b/i.test(message);
|
|
3705
|
+
}
|
|
3450
3706
|
function buildChangeSet(input) {
|
|
3451
3707
|
const patch = buildPatch(input.capability, input.args);
|
|
3452
3708
|
const before = scalarRecord(input.currentRow);
|
|
@@ -3648,7 +3904,7 @@ function zodInputShape(capability) {
|
|
|
3648
3904
|
if (spec.type === "number" && spec.maximum !== void 0) schema = schema.max(spec.maximum);
|
|
3649
3905
|
if (spec.enum && spec.enum.length > 0) schema = schema.refine((value) => spec.enum?.includes(value), "value is not allowlisted");
|
|
3650
3906
|
if (spec.required === false) schema = schema.optional();
|
|
3651
|
-
shape[name] = schema.describe(`${name} business argument`);
|
|
3907
|
+
shape[name] = schema.describe(spec.description ?? `${name} business argument`);
|
|
3652
3908
|
}
|
|
3653
3909
|
return shape;
|
|
3654
3910
|
}
|
|
@@ -3661,6 +3917,7 @@ function toolMetadata(capability) {
|
|
|
3661
3917
|
input_schema: Object.fromEntries(Object.entries(capability.args).map(([name, spec]) => [name, {
|
|
3662
3918
|
type: spec.type,
|
|
3663
3919
|
required: spec.required !== false,
|
|
3920
|
+
...spec.description !== void 0 ? { description: spec.description } : {},
|
|
3664
3921
|
...spec.max_length !== void 0 ? { max_length: spec.max_length } : {},
|
|
3665
3922
|
...spec.minimum !== void 0 ? { minimum: spec.minimum } : {},
|
|
3666
3923
|
...spec.maximum !== void 0 ? { maximum: spec.maximum } : {},
|
|
@@ -3676,11 +3933,23 @@ function toolMetadata(capability) {
|
|
|
3676
3933
|
}
|
|
3677
3934
|
};
|
|
3678
3935
|
}
|
|
3679
|
-
function capabilityDescription(capability) {
|
|
3680
|
-
|
|
3681
|
-
|
|
3936
|
+
function capabilityDescription(capability, exposedName) {
|
|
3937
|
+
const lines = [];
|
|
3938
|
+
if (exposedName && exposedName !== capability.name) {
|
|
3939
|
+
lines.push(`Canonical Synapsor capability: ${capability.name}.`);
|
|
3940
|
+
}
|
|
3941
|
+
if (capability.description) {
|
|
3942
|
+
lines.push(capability.description);
|
|
3943
|
+
} else if (capability.kind === "read") {
|
|
3944
|
+
lines.push(`Read ${capability.target.schema}.${capability.target.table} through a reviewed Synapsor capability with trusted tenant context and evidence.`);
|
|
3945
|
+
} else {
|
|
3946
|
+
lines.push(`Create an evidence-backed Synapsor proposal for ${capability.target.schema}.${capability.target.table}; the source database is not mutated by this tool.`);
|
|
3947
|
+
}
|
|
3948
|
+
if (capability.returns_hint) {
|
|
3949
|
+
lines.push(capability.returns_hint);
|
|
3682
3950
|
}
|
|
3683
|
-
|
|
3951
|
+
lines.push("Evidence handles are audit/replay handles; the model does not need to call them during this turn.");
|
|
3952
|
+
return lines.join("\n");
|
|
3684
3953
|
}
|
|
3685
3954
|
function buildPatch(capability, args) {
|
|
3686
3955
|
if (!capability.patch) throw new McpRuntimeError("PATCH_REQUIRED", "Proposal capability has no patch mapping.");
|
|
@@ -3797,6 +4066,9 @@ function isRecord3(value) {
|
|
|
3797
4066
|
}
|
|
3798
4067
|
function toolErrorPayload(error) {
|
|
3799
4068
|
if (error instanceof McpRuntimeError) {
|
|
4069
|
+
if (error.code === "LOCAL_STORE_UNAVAILABLE") {
|
|
4070
|
+
return { ok: false, code: "TEMPORARILY_UNAVAILABLE", error: "The local runner store is temporarily unavailable. Restart the runner or recreate the store before retrying." };
|
|
4071
|
+
}
|
|
3800
4072
|
return { ok: false, code: error.code, error: error.message };
|
|
3801
4073
|
}
|
|
3802
4074
|
return { ok: false, code: "MCP_TOOL_FAILED", error: error instanceof Error ? error.message : String(error) };
|
|
@@ -4659,6 +4931,7 @@ function normalizedWriteback(spec) {
|
|
|
4659
4931
|
if (executor === "http_handler") {
|
|
4660
4932
|
const urlEnv = spec.writeback?.handler_url_env ?? "SYNAPSOR_APP_WRITEBACK_URL";
|
|
4661
4933
|
const tokenEnv = spec.writeback?.handler_token_env;
|
|
4934
|
+
const signingSecretEnv = spec.writeback?.handler_signing_secret_env;
|
|
4662
4935
|
return {
|
|
4663
4936
|
executor,
|
|
4664
4937
|
executorName,
|
|
@@ -4668,12 +4941,14 @@ function normalizedWriteback(spec) {
|
|
|
4668
4941
|
url_env: urlEnv,
|
|
4669
4942
|
method: "POST",
|
|
4670
4943
|
...tokenEnv ? { auth: { type: "bearer_env", token_env: tokenEnv } } : {},
|
|
4944
|
+
...signingSecretEnv ? { signing_secret_env: signingSecretEnv } : {},
|
|
4671
4945
|
timeout_ms: spec.writeback?.timeout_ms ?? 5e3
|
|
4672
4946
|
}
|
|
4673
4947
|
},
|
|
4674
4948
|
extraEnv: [
|
|
4675
4949
|
{ name: urlEnv, value: "http://127.0.0.1:8787/synapsor/writeback", comment: "App-owned writeback handler endpoint." },
|
|
4676
|
-
...tokenEnv ? [{ name: tokenEnv, value: "<handler-bearer-token>", comment: "Optional handler bearer token." }] : []
|
|
4950
|
+
...tokenEnv ? [{ name: tokenEnv, value: "<handler-bearer-token>", comment: "Optional handler bearer token." }] : [],
|
|
4951
|
+
...signingSecretEnv ? [{ name: signingSecretEnv, value: "<handler-hmac-signing-secret>", comment: "Optional HMAC signing secret for Runner-to-handler requests." }] : []
|
|
4677
4952
|
]
|
|
4678
4953
|
};
|
|
4679
4954
|
}
|
|
@@ -4811,6 +5086,7 @@ function validateSelectionSpec(spec) {
|
|
|
4811
5086
|
spec.write_url_env,
|
|
4812
5087
|
spec.writeback?.handler_url_env,
|
|
4813
5088
|
spec.writeback?.handler_token_env,
|
|
5089
|
+
spec.writeback?.handler_signing_secret_env,
|
|
4814
5090
|
spec.writeback?.handler_command_env,
|
|
4815
5091
|
spec.trusted_context?.tenant_id_env,
|
|
4816
5092
|
spec.trusted_context?.principal_env
|
|
@@ -6525,6 +6801,7 @@ ${cliCommandName()} --help
|
|
|
6525
6801
|
if (command === "query-audit") return queryAudit(rest);
|
|
6526
6802
|
if (command === "receipts") return receipts(rest);
|
|
6527
6803
|
if (command === "activity") return activity(rest);
|
|
6804
|
+
if (command === "events") return events(rest);
|
|
6528
6805
|
if (command === "store") return storeCommand(rest);
|
|
6529
6806
|
if (command === "shadow") return shadow(rest);
|
|
6530
6807
|
if (command === "ui") return ui(rest);
|
|
@@ -6725,11 +7002,13 @@ async function runInitWizard(args, options = {}) {
|
|
|
6725
7002
|
} else if (writebackPath === "http_handler") {
|
|
6726
7003
|
const urlEnv = await askEnvName(ask, "App-owned HTTP handler URL env var", optionalArg(args, "--handler-url-env") ?? "SYNAPSOR_APP_WRITEBACK_URL");
|
|
6727
7004
|
const tokenEnv = await askOptionalEnvName(ask, "Optional HTTP handler bearer-token env var", optionalArg(args, "--handler-token-env") ?? "");
|
|
7005
|
+
const signingSecretEnv = await askOptionalEnvName(ask, "Optional HTTP handler HMAC signing-secret env var", optionalArg(args, "--handler-signing-secret-env") ?? "");
|
|
6728
7006
|
writeback2 = {
|
|
6729
7007
|
executor: "http_handler",
|
|
6730
7008
|
executor_name: optionalArg(args, "--executor-name"),
|
|
6731
7009
|
handler_url_env: urlEnv,
|
|
6732
7010
|
...tokenEnv ? { handler_token_env: tokenEnv } : {},
|
|
7011
|
+
...signingSecretEnv ? { handler_signing_secret_env: signingSecretEnv } : {},
|
|
6733
7012
|
timeout_ms: positiveIntegerOption(args, "--handler-timeout-ms")
|
|
6734
7013
|
};
|
|
6735
7014
|
} else {
|
|
@@ -7032,6 +7311,7 @@ function writebackSpecFromArgs(args) {
|
|
|
7032
7311
|
executor_name: optionalArg(args, "--executor-name"),
|
|
7033
7312
|
handler_url_env: optionalArg(args, "--handler-url-env") ?? "SYNAPSOR_APP_WRITEBACK_URL",
|
|
7034
7313
|
...optionalArg(args, "--handler-token-env") ? { handler_token_env: optionalArg(args, "--handler-token-env") } : {},
|
|
7314
|
+
...optionalArg(args, "--handler-signing-secret-env") ? { handler_signing_secret_env: optionalArg(args, "--handler-signing-secret-env") } : {},
|
|
7035
7315
|
timeout_ms: positiveIntegerOption(args, "--handler-timeout-ms")
|
|
7036
7316
|
};
|
|
7037
7317
|
}
|
|
@@ -7470,6 +7750,54 @@ function envPresenceCheck(envName, message) {
|
|
|
7470
7750
|
message: process2.env[envName] ? `${envName} is set.` : message
|
|
7471
7751
|
};
|
|
7472
7752
|
}
|
|
7753
|
+
async function httpHandlerReachabilityCheck(executorName, rawUrl, timeoutMs) {
|
|
7754
|
+
try {
|
|
7755
|
+
const url = new URL(rawUrl);
|
|
7756
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
7757
|
+
return {
|
|
7758
|
+
name: `executor:${executorName}:handler-reachability`,
|
|
7759
|
+
ok: false,
|
|
7760
|
+
level: "fail",
|
|
7761
|
+
message: "HTTP handler URL must use http or https."
|
|
7762
|
+
};
|
|
7763
|
+
}
|
|
7764
|
+
} catch {
|
|
7765
|
+
return {
|
|
7766
|
+
name: `executor:${executorName}:handler-reachability`,
|
|
7767
|
+
ok: false,
|
|
7768
|
+
level: "fail",
|
|
7769
|
+
message: "HTTP handler URL env value is not a valid URL."
|
|
7770
|
+
};
|
|
7771
|
+
}
|
|
7772
|
+
const controller = new AbortController();
|
|
7773
|
+
const timeout = setTimeout(() => controller.abort(), Math.max(1, Math.min(timeoutMs || 3e3, 1e4)));
|
|
7774
|
+
try {
|
|
7775
|
+
const response = await fetch(rawUrl, {
|
|
7776
|
+
method: "OPTIONS",
|
|
7777
|
+
headers: { accept: "application/json" },
|
|
7778
|
+
signal: controller.signal
|
|
7779
|
+
});
|
|
7780
|
+
return {
|
|
7781
|
+
name: `executor:${executorName}:handler-reachability`,
|
|
7782
|
+
ok: true,
|
|
7783
|
+
level: "pass",
|
|
7784
|
+
message: `HTTP handler endpoint responded with HTTP ${response.status}; network path is reachable. This is not an apply/writeback probe.`
|
|
7785
|
+
};
|
|
7786
|
+
} catch (error) {
|
|
7787
|
+
return {
|
|
7788
|
+
name: `executor:${executorName}:handler-reachability`,
|
|
7789
|
+
ok: false,
|
|
7790
|
+
level: "fail",
|
|
7791
|
+
message: `HTTP handler endpoint did not respond to the reachability probe (${safeReachabilityError(error)}).`
|
|
7792
|
+
};
|
|
7793
|
+
} finally {
|
|
7794
|
+
clearTimeout(timeout);
|
|
7795
|
+
}
|
|
7796
|
+
}
|
|
7797
|
+
function safeReachabilityError(error) {
|
|
7798
|
+
if (error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError")) return "timeout";
|
|
7799
|
+
return "connection failed";
|
|
7800
|
+
}
|
|
7473
7801
|
async function inspectConfiguredSource(input) {
|
|
7474
7802
|
if (!process2.env[input.source.read_url_env]) return;
|
|
7475
7803
|
const capabilities = (input.config.capabilities ?? []).filter((capability) => capability.source === input.sourceName);
|
|
@@ -7899,6 +8227,8 @@ function formatFirstRunDoctor(report) {
|
|
|
7899
8227
|
async function localDoctor(args) {
|
|
7900
8228
|
const configPath = optionalArg(args, "--config") ?? "synapsor.runner.json";
|
|
7901
8229
|
const allowSharedCredential = args.includes("--allow-shared-credential");
|
|
8230
|
+
const checkHandlers = args.includes("--check-handlers");
|
|
8231
|
+
const checkWriteback = args.includes("--check-writeback") || args.includes("--check-db");
|
|
7902
8232
|
const parsed = JSON.parse(await fs3.readFile(configPath, "utf8"));
|
|
7903
8233
|
const checks = [];
|
|
7904
8234
|
const validation = validateRunnerCapabilityConfig(parsed);
|
|
@@ -7941,6 +8271,24 @@ async function localDoctor(args) {
|
|
|
7941
8271
|
} else {
|
|
7942
8272
|
checks.push({ name: `source:${sourceName}:write-url-env`, ok: false, level: "fail", message: "SQL writeback proposal capabilities require write_url_env for trusted writeback." });
|
|
7943
8273
|
}
|
|
8274
|
+
const writeUrl = source.write_url_env ? process2.env[source.write_url_env] : void 0;
|
|
8275
|
+
if (checkWriteback && writeUrl) {
|
|
8276
|
+
checks.push(...await directSqlWritebackDoctorChecks(parsed, sourceName, source, writeUrl));
|
|
8277
|
+
} else if (checkWriteback) {
|
|
8278
|
+
checks.push({
|
|
8279
|
+
name: `source:${sourceName}:writeback-probe`,
|
|
8280
|
+
ok: false,
|
|
8281
|
+
level: "fail",
|
|
8282
|
+
message: "Direct SQL writeback probe skipped because the writer env var is missing."
|
|
8283
|
+
});
|
|
8284
|
+
} else {
|
|
8285
|
+
checks.push({
|
|
8286
|
+
name: `source:${sourceName}:writeback-probe`,
|
|
8287
|
+
ok: true,
|
|
8288
|
+
level: "warn",
|
|
8289
|
+
message: `Direct SQL writeback was not probed. Rerun doctor with --check-writeback to verify writer connectivity, receipt-table permissions, and rollback-only target-table access.`
|
|
8290
|
+
});
|
|
8291
|
+
}
|
|
7944
8292
|
}
|
|
7945
8293
|
}
|
|
7946
8294
|
await inspectConfiguredSource({ config: parsed, sourceName, source, checks });
|
|
@@ -7949,10 +8297,34 @@ async function localDoctor(args) {
|
|
|
7949
8297
|
if (!isRecord6(executor)) continue;
|
|
7950
8298
|
if (executor.type === "http_handler") {
|
|
7951
8299
|
const urlEnv = String(executor.url_env ?? "");
|
|
7952
|
-
if (urlEnv)
|
|
8300
|
+
if (urlEnv) {
|
|
8301
|
+
checks.push(envPresenceCheck(urlEnv, `${urlEnv} is required for http_handler executor ${executorName}.`));
|
|
8302
|
+
const handlerUrl = process2.env[urlEnv];
|
|
8303
|
+
if (checkHandlers && handlerUrl) {
|
|
8304
|
+
checks.push(await httpHandlerReachabilityCheck(executorName, handlerUrl, Number(executor.timeout_ms ?? 3e3)));
|
|
8305
|
+
} else if (!checkHandlers) {
|
|
8306
|
+
checks.push({
|
|
8307
|
+
name: `executor:${executorName}:handler-reachability`,
|
|
8308
|
+
ok: true,
|
|
8309
|
+
level: "warn",
|
|
8310
|
+
message: `Handler reachability was not probed for ${executorName}. Rerun doctor with --check-handlers to verify the network path without applying a proposal.`
|
|
8311
|
+
});
|
|
8312
|
+
}
|
|
8313
|
+
}
|
|
7953
8314
|
const auth = isRecord6(executor.auth) ? executor.auth : void 0;
|
|
7954
8315
|
const tokenEnv = typeof auth?.token_env === "string" ? auth.token_env : void 0;
|
|
7955
8316
|
if (tokenEnv) checks.push(envPresenceCheck(tokenEnv, `${tokenEnv} is required for http_handler executor ${executorName} bearer auth.`));
|
|
8317
|
+
const signingSecretEnv = typeof executor.signing_secret_env === "string" ? executor.signing_secret_env : void 0;
|
|
8318
|
+
if (signingSecretEnv) {
|
|
8319
|
+
checks.push(envPresenceCheck(signingSecretEnv, `${signingSecretEnv} is required to sign http_handler requests for executor ${executorName}.`));
|
|
8320
|
+
} else {
|
|
8321
|
+
checks.push({
|
|
8322
|
+
name: `executor:${executorName}:handler-signing`,
|
|
8323
|
+
ok: true,
|
|
8324
|
+
level: "warn",
|
|
8325
|
+
message: `No signing_secret_env is configured for http_handler executor ${executorName}. HMAC signing is recommended unless the handler is loopback-only and protected by another trusted boundary.`
|
|
8326
|
+
});
|
|
8327
|
+
}
|
|
7956
8328
|
}
|
|
7957
8329
|
if (executor.type === "command_handler") {
|
|
7958
8330
|
const commandEnv = String(executor.command_env ?? "");
|
|
@@ -7989,6 +8361,155 @@ async function localDoctor(args) {
|
|
|
7989
8361
|
}
|
|
7990
8362
|
return report.ok ? 0 : 1;
|
|
7991
8363
|
}
|
|
8364
|
+
async function directSqlWritebackDoctorChecks(config, sourceName, source, writeUrl) {
|
|
8365
|
+
const checks = [];
|
|
8366
|
+
try {
|
|
8367
|
+
const result = await adapters[source.engine].doctor({
|
|
8368
|
+
controlPlaneUrl: "local",
|
|
8369
|
+
runnerToken: "local",
|
|
8370
|
+
runnerId: "doctor",
|
|
8371
|
+
sourceId: sourceName,
|
|
8372
|
+
databaseUrl: writeUrl,
|
|
8373
|
+
engine: source.engine,
|
|
8374
|
+
pollIntervalMs: 0,
|
|
8375
|
+
logLevel: "error",
|
|
8376
|
+
dryRun: true,
|
|
8377
|
+
stateDir: "./state"
|
|
8378
|
+
});
|
|
8379
|
+
checks.push({
|
|
8380
|
+
name: `source:${sourceName}:receipt-table-probe`,
|
|
8381
|
+
ok: result.ok,
|
|
8382
|
+
level: result.ok ? "pass" : "fail",
|
|
8383
|
+
message: result.ok ? "Writer credential can reach the database and the receipt-table rollback probe succeeded." : `Writer receipt-table probe failed (${safeDatabaseProbeError(result.details)}). ${receiptTableGuidance(source.engine)}`
|
|
8384
|
+
});
|
|
8385
|
+
} catch (error) {
|
|
8386
|
+
checks.push({
|
|
8387
|
+
name: `source:${sourceName}:receipt-table-probe`,
|
|
8388
|
+
ok: false,
|
|
8389
|
+
level: "fail",
|
|
8390
|
+
message: `Writer receipt-table probe failed (${safeDatabaseProbeError(error)}). ${receiptTableGuidance(source.engine)}`
|
|
8391
|
+
});
|
|
8392
|
+
}
|
|
8393
|
+
for (const capability of directSqlProposalCapabilities(config, sourceName)) {
|
|
8394
|
+
try {
|
|
8395
|
+
await rollbackOnlyTargetProbe(source.engine, writeUrl, capability);
|
|
8396
|
+
checks.push({
|
|
8397
|
+
name: `capability:${capability.name}:writeback-target-probe`,
|
|
8398
|
+
ok: true,
|
|
8399
|
+
level: "pass",
|
|
8400
|
+
message: `Rollback-only writer probe reached ${capability.target.schema}.${capability.target.table} and verified configured write columns without mutating business rows.`
|
|
8401
|
+
});
|
|
8402
|
+
} catch (error) {
|
|
8403
|
+
checks.push({
|
|
8404
|
+
name: `capability:${capability.name}:writeback-target-probe`,
|
|
8405
|
+
ok: false,
|
|
8406
|
+
level: "fail",
|
|
8407
|
+
message: `Rollback-only writer probe failed for configured target ${capability.target.schema}.${capability.target.table} (${safeDatabaseProbeError(error)}). Verify writer SELECT/UPDATE on the target table and configured columns.`
|
|
8408
|
+
});
|
|
8409
|
+
}
|
|
8410
|
+
}
|
|
8411
|
+
return checks;
|
|
8412
|
+
}
|
|
8413
|
+
function directSqlProposalCapabilities(config, sourceName) {
|
|
8414
|
+
return (config.capabilities ?? []).filter((capability) => {
|
|
8415
|
+
if (capability.kind !== "proposal" || capability.source !== sourceName) return false;
|
|
8416
|
+
return (capability.executor ?? "sql_update") === "sql_update";
|
|
8417
|
+
});
|
|
8418
|
+
}
|
|
8419
|
+
async function rollbackOnlyTargetProbe(engine, databaseUrl, capability) {
|
|
8420
|
+
if (engine === "postgres") {
|
|
8421
|
+
await rollbackOnlyPostgresTargetProbe(databaseUrl, capability);
|
|
8422
|
+
return;
|
|
8423
|
+
}
|
|
8424
|
+
await rollbackOnlyMysqlTargetProbe(databaseUrl, capability);
|
|
8425
|
+
}
|
|
8426
|
+
async function rollbackOnlyPostgresTargetProbe(databaseUrl, capability) {
|
|
8427
|
+
const pg = await dynamicImportModule("pg");
|
|
8428
|
+
const pool = new pg.Pool({ connectionString: databaseUrl });
|
|
8429
|
+
const client = await pool.connect();
|
|
8430
|
+
try {
|
|
8431
|
+
await client.query("BEGIN");
|
|
8432
|
+
try {
|
|
8433
|
+
const table = `${quotePostgresIdentifier2(capability.target.schema)}.${quotePostgresIdentifier2(capability.target.table)}`;
|
|
8434
|
+
const columns = proposalProbeColumns(capability).map(quotePostgresIdentifier2).join(", ");
|
|
8435
|
+
await client.query(`SELECT ${columns} FROM ${table} WHERE false FOR UPDATE`);
|
|
8436
|
+
for (const column of proposalUpdateProbeColumns(capability)) {
|
|
8437
|
+
const quoted = quotePostgresIdentifier2(column);
|
|
8438
|
+
await client.query(`UPDATE ${table} SET ${quoted} = ${quoted} WHERE false`);
|
|
8439
|
+
}
|
|
8440
|
+
await client.query("ROLLBACK");
|
|
8441
|
+
} catch (error) {
|
|
8442
|
+
await client.query("ROLLBACK").catch(() => void 0);
|
|
8443
|
+
throw error;
|
|
8444
|
+
}
|
|
8445
|
+
} finally {
|
|
8446
|
+
client.release();
|
|
8447
|
+
await pool.end();
|
|
8448
|
+
}
|
|
8449
|
+
}
|
|
8450
|
+
async function rollbackOnlyMysqlTargetProbe(databaseUrl, capability) {
|
|
8451
|
+
const mysql4 = await dynamicImportModule("mysql2/promise");
|
|
8452
|
+
const connection = await mysql4.createConnection({ uri: databaseUrl, dateStrings: true });
|
|
8453
|
+
try {
|
|
8454
|
+
await connection.beginTransaction();
|
|
8455
|
+
try {
|
|
8456
|
+
const table = `${quoteMysqlIdentifier2(capability.target.schema)}.${quoteMysqlIdentifier2(capability.target.table)}`;
|
|
8457
|
+
const columns = proposalProbeColumns(capability).map(quoteMysqlIdentifier2).join(", ");
|
|
8458
|
+
await connection.query(`SELECT ${columns} FROM ${table} WHERE 1 = 0 FOR UPDATE`);
|
|
8459
|
+
for (const column of proposalUpdateProbeColumns(capability)) {
|
|
8460
|
+
const quoted = quoteMysqlIdentifier2(column);
|
|
8461
|
+
await connection.query(`UPDATE ${table} SET ${quoted} = ${quoted} WHERE 1 = 0`);
|
|
8462
|
+
}
|
|
8463
|
+
await connection.rollback();
|
|
8464
|
+
} catch (error) {
|
|
8465
|
+
await connection.rollback().catch(() => void 0);
|
|
8466
|
+
throw error;
|
|
8467
|
+
}
|
|
8468
|
+
} finally {
|
|
8469
|
+
await connection.end();
|
|
8470
|
+
}
|
|
8471
|
+
}
|
|
8472
|
+
async function dynamicImportModule(specifier) {
|
|
8473
|
+
const importer = new Function("specifier", "return import(specifier)");
|
|
8474
|
+
return importer(specifier);
|
|
8475
|
+
}
|
|
8476
|
+
function proposalProbeColumns(capability) {
|
|
8477
|
+
const columns = /* @__PURE__ */ new Set();
|
|
8478
|
+
columns.add(capability.target.primary_key);
|
|
8479
|
+
if (capability.target.tenant_key) columns.add(capability.target.tenant_key);
|
|
8480
|
+
if (capability.conflict_guard?.column) columns.add(capability.conflict_guard.column);
|
|
8481
|
+
for (const column of capability.visible_columns ?? []) columns.add(column);
|
|
8482
|
+
for (const column of proposalUpdateProbeColumns(capability)) columns.add(column);
|
|
8483
|
+
return [...columns];
|
|
8484
|
+
}
|
|
8485
|
+
function proposalUpdateProbeColumns(capability) {
|
|
8486
|
+
const columns = /* @__PURE__ */ new Set();
|
|
8487
|
+
for (const column of capability.allowed_columns ?? []) columns.add(column);
|
|
8488
|
+
for (const column of Object.keys(capability.patch ?? {})) columns.add(column);
|
|
8489
|
+
return [...columns];
|
|
8490
|
+
}
|
|
8491
|
+
function quotePostgresIdentifier2(value) {
|
|
8492
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
8493
|
+
}
|
|
8494
|
+
function quoteMysqlIdentifier2(value) {
|
|
8495
|
+
return `\`${value.replace(/`/g, "``")}\``;
|
|
8496
|
+
}
|
|
8497
|
+
function safeDatabaseProbeError(error) {
|
|
8498
|
+
const raw = typeof error === "string" ? error : error instanceof Error ? error.message : JSON.stringify(error ?? {});
|
|
8499
|
+
const message = raw.toLowerCase();
|
|
8500
|
+
if (/permission|denied|not authorized|insufficient|42501|er_tableaccess_denied|er_dbaccess_denied/.test(message)) return "permission denied";
|
|
8501
|
+
if (/authentication|password|28p01|access denied for user|invalid authorization/.test(message)) return "authentication failed";
|
|
8502
|
+
if (/timeout|timed out|etimedout/.test(message)) return "timeout";
|
|
8503
|
+
if (/econnrefused|enotfound|eai_again|network|connection terminated|connection failed/.test(message)) return "connection failed";
|
|
8504
|
+
if (/does not exist|unknown database|no such table|undefined_table|er_no_such_table|42p01/.test(message)) return "configured object not found";
|
|
8505
|
+
return "database probe failed";
|
|
8506
|
+
}
|
|
8507
|
+
function receiptTableGuidance(engine) {
|
|
8508
|
+
if (engine === "postgres") {
|
|
8509
|
+
return `Pre-create the receipt table with "${cliCommandName()} writeback migration --engine postgres --schema synapsor" and grant it with "${cliCommandName()} writeback grants --engine postgres --schema synapsor --writer-role <writer_role>", or use an app-owned handler executor.`;
|
|
8510
|
+
}
|
|
8511
|
+
return `Pre-create the receipt table with "${cliCommandName()} writeback migration --engine mysql --schema <database_name>" and grant it with "${cliCommandName()} writeback grants --engine mysql --schema <database_name> --writer-role \\"'<writer>'@'%'\\"", or use an app-owned handler executor.`;
|
|
8512
|
+
}
|
|
7992
8513
|
async function localDoctorStoreStats(storePath) {
|
|
7993
8514
|
if (!storePath || storePath === ":memory:") return { path: storePath ?? "not configured", exists: storePath === ":memory:" };
|
|
7994
8515
|
if (!await fileExists(storePath)) return { path: storePath, exists: false };
|
|
@@ -8174,6 +8695,9 @@ function executorConfig(config, executorName) {
|
|
|
8174
8695
|
if (raw.type === "sql_update") return { type: "sql_update" };
|
|
8175
8696
|
throw new Error(`executor ${executorName} has unsupported type`);
|
|
8176
8697
|
}
|
|
8698
|
+
function signHandlerRequestBody(body, secret) {
|
|
8699
|
+
return `sha256=${crypto5.createHmac("sha256", secret).update(body).digest("hex")}`;
|
|
8700
|
+
}
|
|
8177
8701
|
async function applyHttpHandlerProposal(input) {
|
|
8178
8702
|
const duplicate = duplicateHandlerReceipt(input.store, input.proposalId);
|
|
8179
8703
|
if (duplicate) return alreadyAppliedReceipt(duplicate.receipt, input.runnerId);
|
|
@@ -8198,6 +8722,21 @@ async function applyHttpHandlerProposal(input) {
|
|
|
8198
8722
|
if (!token) throw new Error(`${input.executor.auth.token_env} is not set`);
|
|
8199
8723
|
headers.authorization = `Bearer ${token}`;
|
|
8200
8724
|
}
|
|
8725
|
+
const issuedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8726
|
+
const requestBody = JSON.stringify({
|
|
8727
|
+
protocol_version: "1.0",
|
|
8728
|
+
...prepared.request,
|
|
8729
|
+
issued_at: issuedAt,
|
|
8730
|
+
executor: input.executorName,
|
|
8731
|
+
dry_run: input.dryRun
|
|
8732
|
+
});
|
|
8733
|
+
headers["x-synapsor-issued-at"] = issuedAt;
|
|
8734
|
+
headers["x-synapsor-proposal-id"] = prepared.proposal.proposal_id;
|
|
8735
|
+
if (input.executor.signing_secret_env) {
|
|
8736
|
+
const signingSecret = input.env[input.executor.signing_secret_env];
|
|
8737
|
+
if (!signingSecret) throw new Error(`${input.executor.signing_secret_env} is not set`);
|
|
8738
|
+
headers["x-synapsor-signature"] = signHandlerRequestBody(requestBody, signingSecret);
|
|
8739
|
+
}
|
|
8201
8740
|
const controller = new AbortController();
|
|
8202
8741
|
const timeout = setTimeout(() => controller.abort(), Math.max(1, input.executor.timeout_ms ?? 5e3));
|
|
8203
8742
|
let receipt;
|
|
@@ -8205,7 +8744,7 @@ async function applyHttpHandlerProposal(input) {
|
|
|
8205
8744
|
const response = await fetch(url, {
|
|
8206
8745
|
method: input.executor.method ?? "POST",
|
|
8207
8746
|
headers,
|
|
8208
|
-
body:
|
|
8747
|
+
body: requestBody,
|
|
8209
8748
|
signal: controller.signal
|
|
8210
8749
|
});
|
|
8211
8750
|
const text = await response.text();
|
|
@@ -8633,7 +9172,7 @@ async function cloudConnect(args) {
|
|
|
8633
9172
|
return 1;
|
|
8634
9173
|
}
|
|
8635
9174
|
const runnerId = String(parsed.cloud.runner_id || process2.env.SYNAPSOR_RUNNER_ID || "synapsor_runner_local").trim();
|
|
8636
|
-
const runnerVersion = String(parsed.cloud.runner_version || process2.env.npm_package_version || "0.1.0-alpha.
|
|
9175
|
+
const runnerVersion = String(parsed.cloud.runner_version || process2.env.npm_package_version || "0.1.0-alpha.13").trim();
|
|
8637
9176
|
const engines = normalizeEngines(parsed.cloud.engines);
|
|
8638
9177
|
const capabilities = normalizeCapabilities(parsed.cloud.capabilities);
|
|
8639
9178
|
const client = new ControlPlaneClient({
|
|
@@ -8687,6 +9226,7 @@ async function mcp(args) {
|
|
|
8687
9226
|
if (subcommand === "serve-streamable-http") return mcpServeStreamableHttp(rest);
|
|
8688
9227
|
if (subcommand === "audit") return mcpAudit(rest);
|
|
8689
9228
|
if (subcommand === "config") return mcpConfig(rest);
|
|
9229
|
+
if (subcommand === "client-config") return mcpConfigure(rest);
|
|
8690
9230
|
if (subcommand === "configure") return mcpConfigure(rest);
|
|
8691
9231
|
if (subcommand === "smoke") return mcpSmoke(rest);
|
|
8692
9232
|
usage(["mcp"]);
|
|
@@ -8695,6 +9235,7 @@ async function mcp(args) {
|
|
|
8695
9235
|
async function tools(args) {
|
|
8696
9236
|
const [subcommand, ...rest] = args;
|
|
8697
9237
|
if (subcommand === "preview") return toolsPreview(rest);
|
|
9238
|
+
if (subcommand === "list") return toolsPreview(rest);
|
|
8698
9239
|
usage(["tools"]);
|
|
8699
9240
|
return 2;
|
|
8700
9241
|
}
|
|
@@ -9345,44 +9886,74 @@ function quickDemoChangeSet() {
|
|
|
9345
9886
|
};
|
|
9346
9887
|
}
|
|
9347
9888
|
async function mcpServe(args) {
|
|
9889
|
+
const transport = optionalArg(args, "--transport") ?? "stdio";
|
|
9890
|
+
if (transport === "streamable-http") return mcpServeStreamableHttp(args);
|
|
9891
|
+
if (transport === "http" || transport === "json-rpc-http" || transport === "jsonrpc-http") return mcpServeHttp(args);
|
|
9892
|
+
if (transport !== "stdio") throw new Error("--transport must be stdio, streamable-http, or http");
|
|
9348
9893
|
const configPath = optionalArg(args, "--config") ?? process2.env.SYNAPSOR_MCP_CONFIG;
|
|
9349
9894
|
const readOnly = args.includes("--read-only");
|
|
9350
9895
|
const config = readOnly ? { ...await readRuntimeConfig(configPath ?? defaultConfigPath), mode: "read_only" } : void 0;
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
|
|
9896
|
+
const toolNameStyle = toolNameStyleOption(args);
|
|
9897
|
+
const resultFormat = resultFormatOption(args);
|
|
9898
|
+
const storePath = optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE;
|
|
9899
|
+
const releaseLease = await writeStoreLease(storePath, "mcp", "stdio", args.includes("--allow-concurrent-store"));
|
|
9900
|
+
try {
|
|
9901
|
+
await serveStdio({
|
|
9902
|
+
configPath,
|
|
9903
|
+
storePath,
|
|
9904
|
+
config,
|
|
9905
|
+
toolNameStyle,
|
|
9906
|
+
resultFormat
|
|
9907
|
+
});
|
|
9908
|
+
return 0;
|
|
9909
|
+
} finally {
|
|
9910
|
+
await releaseLease();
|
|
9911
|
+
}
|
|
9357
9912
|
}
|
|
9358
9913
|
async function mcpServeHttp(args) {
|
|
9914
|
+
process2.stderr.write([
|
|
9915
|
+
"Warning: mcp serve-http is a legacy JSON-RPC bridge, not spec MCP Streamable HTTP.",
|
|
9916
|
+
`For OpenAI Agents SDK or standard HTTP MCP clients, use: ${cliCommandName()} mcp serve --transport streamable-http`,
|
|
9917
|
+
""
|
|
9918
|
+
].join("\n"));
|
|
9359
9919
|
const configPath = optionalArg(args, "--config") ?? process2.env.SYNAPSOR_MCP_CONFIG;
|
|
9360
9920
|
const readOnly = args.includes("--read-only");
|
|
9361
9921
|
const config = readOnly ? { ...await readRuntimeConfig(configPath ?? defaultConfigPath), mode: "read_only" } : void 0;
|
|
9362
9922
|
const host = optionalArg(args, "--host") ?? "127.0.0.1";
|
|
9363
9923
|
const port = Number(optionalArg(args, "--port") ?? "8765");
|
|
9924
|
+
const resultFormat = resultFormatOption(args);
|
|
9364
9925
|
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
9365
9926
|
throw new Error("--port must be an integer from 1 to 65535");
|
|
9366
9927
|
}
|
|
9367
9928
|
if (host === "0.0.0.0") {
|
|
9368
9929
|
process2.stderr.write("Warning: binding Synapsor Runner HTTP MCP to 0.0.0.0 exposes model-facing tools on the network. Use TLS, private networking, authentication, and rate limits.\n");
|
|
9369
9930
|
}
|
|
9370
|
-
const
|
|
9371
|
-
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9931
|
+
const storePath = optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE;
|
|
9932
|
+
const releaseLease = await writeStoreLease(storePath, "mcp", "legacy-jsonrpc", args.includes("--allow-concurrent-store"));
|
|
9933
|
+
let server;
|
|
9934
|
+
try {
|
|
9935
|
+
server = await startHttpMcpServer({
|
|
9936
|
+
configPath,
|
|
9937
|
+
config,
|
|
9938
|
+
storePath,
|
|
9939
|
+
host,
|
|
9940
|
+
port,
|
|
9941
|
+
authTokenEnv: optionalArg(args, "--auth-token-env") ?? "SYNAPSOR_RUNNER_HTTP_TOKEN",
|
|
9942
|
+
devNoAuth: args.includes("--dev-no-auth"),
|
|
9943
|
+
corsOrigin: optionalArg(args, "--cors-origin"),
|
|
9944
|
+
resultFormat
|
|
9945
|
+
});
|
|
9946
|
+
} catch (error) {
|
|
9947
|
+
await releaseLease();
|
|
9948
|
+
throw error;
|
|
9949
|
+
}
|
|
9380
9950
|
process2.stderr.write("Press Ctrl+C to stop.\n");
|
|
9381
9951
|
await new Promise((resolve) => {
|
|
9382
9952
|
const stop = async () => {
|
|
9383
9953
|
process2.off("SIGINT", stop);
|
|
9384
9954
|
process2.off("SIGTERM", stop);
|
|
9385
9955
|
await server.close();
|
|
9956
|
+
await releaseLease();
|
|
9386
9957
|
resolve();
|
|
9387
9958
|
};
|
|
9388
9959
|
process2.once("SIGINT", stop);
|
|
@@ -9394,6 +9965,8 @@ async function mcpServeStreamableHttp(args) {
|
|
|
9394
9965
|
const configPath = optionalArg(args, "--config") ?? process2.env.SYNAPSOR_MCP_CONFIG;
|
|
9395
9966
|
const readOnly = args.includes("--read-only");
|
|
9396
9967
|
const config = readOnly ? { ...await readRuntimeConfig(configPath ?? defaultConfigPath), mode: "read_only" } : void 0;
|
|
9968
|
+
const toolNameStyle = toolNameStyleOption(args);
|
|
9969
|
+
const resultFormat = resultFormatOption(args);
|
|
9397
9970
|
const host = optionalArg(args, "--host") ?? "127.0.0.1";
|
|
9398
9971
|
const port = Number(optionalArg(args, "--port") ?? "8766");
|
|
9399
9972
|
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
@@ -9402,22 +9975,33 @@ async function mcpServeStreamableHttp(args) {
|
|
|
9402
9975
|
if (host === "0.0.0.0") {
|
|
9403
9976
|
process2.stderr.write("Warning: binding Synapsor Runner Streamable HTTP MCP to 0.0.0.0 exposes model-facing tools on the network. Use TLS, private networking, authentication, and rate limits.\n");
|
|
9404
9977
|
}
|
|
9405
|
-
const
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9978
|
+
const storePath = optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE;
|
|
9979
|
+
const releaseLease = await writeStoreLease(storePath, "mcp", "streamable-http", args.includes("--allow-concurrent-store"));
|
|
9980
|
+
let server;
|
|
9981
|
+
try {
|
|
9982
|
+
server = await startStreamableHttpMcpServer({
|
|
9983
|
+
configPath,
|
|
9984
|
+
config,
|
|
9985
|
+
storePath,
|
|
9986
|
+
host,
|
|
9987
|
+
port,
|
|
9988
|
+
toolNameStyle,
|
|
9989
|
+
authTokenEnv: optionalArg(args, "--auth-token-env") ?? "SYNAPSOR_RUNNER_HTTP_TOKEN",
|
|
9990
|
+
devNoAuth: args.includes("--dev-no-auth"),
|
|
9991
|
+
corsOrigin: optionalArg(args, "--cors-origin"),
|
|
9992
|
+
resultFormat
|
|
9993
|
+
});
|
|
9994
|
+
} catch (error) {
|
|
9995
|
+
await releaseLease();
|
|
9996
|
+
throw error;
|
|
9997
|
+
}
|
|
9415
9998
|
process2.stderr.write("Press Ctrl+C to stop.\n");
|
|
9416
9999
|
await new Promise((resolve) => {
|
|
9417
10000
|
const stop = async () => {
|
|
9418
10001
|
process2.off("SIGINT", stop);
|
|
9419
10002
|
process2.off("SIGTERM", stop);
|
|
9420
10003
|
await server.close();
|
|
10004
|
+
await releaseLease();
|
|
9421
10005
|
resolve();
|
|
9422
10006
|
};
|
|
9423
10007
|
process2.once("SIGINT", stop);
|
|
@@ -9425,6 +10009,101 @@ async function mcpServeStreamableHttp(args) {
|
|
|
9425
10009
|
});
|
|
9426
10010
|
return 0;
|
|
9427
10011
|
}
|
|
10012
|
+
async function writeStoreLease(storePath, mode, transport, allowConcurrent) {
|
|
10013
|
+
const resolved = resolveStorePathForLease(storePath);
|
|
10014
|
+
if (!resolved) return async () => void 0;
|
|
10015
|
+
await assertNoActiveStoreLease(resolved, allowConcurrent, "serve");
|
|
10016
|
+
const leasePath = storeLeasePath(resolved);
|
|
10017
|
+
const lease = {
|
|
10018
|
+
pid: process2.pid,
|
|
10019
|
+
mode,
|
|
10020
|
+
transport,
|
|
10021
|
+
store_path: resolved,
|
|
10022
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
10023
|
+
};
|
|
10024
|
+
await fs3.mkdir(path3.dirname(resolved), { recursive: true });
|
|
10025
|
+
await fs3.writeFile(leasePath, `${JSON.stringify(lease, null, 2)}
|
|
10026
|
+
`, "utf8");
|
|
10027
|
+
return async () => {
|
|
10028
|
+
const current = await readStoreLease(resolved);
|
|
10029
|
+
if (current?.pid === process2.pid && current.transport === transport) {
|
|
10030
|
+
await fs3.rm(leasePath, { force: true });
|
|
10031
|
+
}
|
|
10032
|
+
};
|
|
10033
|
+
}
|
|
10034
|
+
async function assertNoActiveStoreLease(storePath, force, operation) {
|
|
10035
|
+
const resolved = resolveStorePathForLease(storePath);
|
|
10036
|
+
if (!resolved) return;
|
|
10037
|
+
const lease = await readStoreLease(resolved);
|
|
10038
|
+
if (!lease) return;
|
|
10039
|
+
if (!pidIsActive(lease.pid)) {
|
|
10040
|
+
await fs3.rm(storeLeasePath(resolved), { force: true });
|
|
10041
|
+
return;
|
|
10042
|
+
}
|
|
10043
|
+
const message = `Local store appears active for ${lease.mode}/${lease.transport} (pid ${lease.pid}, started ${lease.started_at}). Refusing ${operation}. Stop the server or rerun with --force if you have verified it is safe.`;
|
|
10044
|
+
if (!force) throw new Error(message);
|
|
10045
|
+
process2.stderr.write(`Warning: ${message}
|
|
10046
|
+
`);
|
|
10047
|
+
}
|
|
10048
|
+
function resolveStorePathForLease(storePath) {
|
|
10049
|
+
const value = storePath ?? process2.env.SYNAPSOR_LOCAL_STORE ?? "./.synapsor/local.db";
|
|
10050
|
+
if (value === ":memory:") return void 0;
|
|
10051
|
+
return path3.resolve(value);
|
|
10052
|
+
}
|
|
10053
|
+
function storeLeasePath(resolvedStorePath) {
|
|
10054
|
+
return `${resolvedStorePath}.lease.json`;
|
|
10055
|
+
}
|
|
10056
|
+
async function readStoreLease(storePath) {
|
|
10057
|
+
const resolved = resolveStorePathForLease(storePath);
|
|
10058
|
+
if (!resolved) return void 0;
|
|
10059
|
+
try {
|
|
10060
|
+
const parsed = JSON.parse(await fs3.readFile(storeLeasePath(resolved), "utf8"));
|
|
10061
|
+
if (typeof parsed.pid !== "number" || typeof parsed.mode !== "string" || typeof parsed.transport !== "string" || typeof parsed.started_at !== "string") {
|
|
10062
|
+
return void 0;
|
|
10063
|
+
}
|
|
10064
|
+
return {
|
|
10065
|
+
pid: parsed.pid,
|
|
10066
|
+
mode: parsed.mode,
|
|
10067
|
+
transport: parsed.transport,
|
|
10068
|
+
store_path: typeof parsed.store_path === "string" ? parsed.store_path : resolved,
|
|
10069
|
+
started_at: parsed.started_at
|
|
10070
|
+
};
|
|
10071
|
+
} catch (error) {
|
|
10072
|
+
if (error.code === "ENOENT") return void 0;
|
|
10073
|
+
return void 0;
|
|
10074
|
+
}
|
|
10075
|
+
}
|
|
10076
|
+
function pidIsActive(pid) {
|
|
10077
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
10078
|
+
try {
|
|
10079
|
+
process2.kill(pid, 0);
|
|
10080
|
+
return true;
|
|
10081
|
+
} catch (error) {
|
|
10082
|
+
return error.code === "EPERM";
|
|
10083
|
+
}
|
|
10084
|
+
}
|
|
10085
|
+
function toolNameStyleOption(args) {
|
|
10086
|
+
const requestedStyle = optionalArg(args, "--tool-name-style");
|
|
10087
|
+
const requestedAliasMode = optionalArg(args, "--alias-mode");
|
|
10088
|
+
if (requestedStyle && requestedAliasMode && requestedStyle !== requestedAliasMode) {
|
|
10089
|
+
throw new Error("--tool-name-style and --alias-mode must match when both are provided");
|
|
10090
|
+
}
|
|
10091
|
+
const requested = requestedAliasMode ?? requestedStyle;
|
|
10092
|
+
if (args.includes("--openai-tool-aliases")) {
|
|
10093
|
+
if (requested && requested !== "openai") throw new Error("--openai-tool-aliases cannot be combined with a non-openai alias mode");
|
|
10094
|
+
return "openai";
|
|
10095
|
+
}
|
|
10096
|
+
if (!requested) return "canonical";
|
|
10097
|
+
if (requested === "canonical" || requested === "openai" || requested === "both") return requested;
|
|
10098
|
+
throw new Error("--alias-mode must be canonical, openai, or both");
|
|
10099
|
+
}
|
|
10100
|
+
function resultFormatOption(args) {
|
|
10101
|
+
const requested = optionalArg(args, "--result-format");
|
|
10102
|
+
if (!requested) return void 0;
|
|
10103
|
+
if (requested === "1" || requested === "v1") return 1;
|
|
10104
|
+
if (requested === "2" || requested === "v2") return 2;
|
|
10105
|
+
throw new Error("--result-format must be v1, 1, v2, or 2");
|
|
10106
|
+
}
|
|
9428
10107
|
async function mcpAudit(args) {
|
|
9429
10108
|
const format = optionalArg(args, "--format") ?? (args.includes("--json") ? "json" : "text");
|
|
9430
10109
|
if (!["text", "json", "markdown"].includes(format)) {
|
|
@@ -9567,21 +10246,33 @@ function formatProposeResult(capabilityName, result, storePath) {
|
|
|
9567
10246
|
`;
|
|
9568
10247
|
}
|
|
9569
10248
|
async function mcpConfigure(args) {
|
|
9570
|
-
const client = optionalArg(args, "--client");
|
|
9571
|
-
if (!client) throw new Error("mcp configure requires --client generic-stdio|claude-desktop|cursor|vscode");
|
|
10249
|
+
const client = normalizeMcpClientName(optionalArg(args, "--client"));
|
|
10250
|
+
if (!client) throw new Error("mcp configure requires --client generic-stdio|claude|claude-desktop|cursor|vscode|openai-agents");
|
|
9572
10251
|
const useAbsolutePaths = args.includes("--absolute-paths");
|
|
9573
10252
|
const rawConfigPath = optionalArg(args, "--config") ?? "./synapsor.runner.json";
|
|
9574
10253
|
const rawStorePath = optionalArg(args, "--store") ?? "./.synapsor/local.db";
|
|
9575
10254
|
const configPath = useAbsolutePaths ? path3.resolve(rawConfigPath) : rawConfigPath;
|
|
9576
10255
|
const storePath = useAbsolutePaths ? path3.resolve(rawStorePath) : rawStorePath;
|
|
10256
|
+
const transport = mcpClientConfigTransport(args, client);
|
|
10257
|
+
const aliasMode = mcpClientConfigAliasMode(args, client);
|
|
10258
|
+
const includeInstructions = args.includes("--include-instructions");
|
|
10259
|
+
const host = optionalArg(args, "--host") ?? "127.0.0.1";
|
|
10260
|
+
const port = Number(optionalArg(args, "--port") ?? "8766");
|
|
10261
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
10262
|
+
throw new Error("--port must be an integer from 1 to 65535");
|
|
10263
|
+
}
|
|
10264
|
+
const authTokenEnv = optionalArg(args, "--auth-token-env") ?? "SYNAPSOR_RUNNER_HTTP_TOKEN";
|
|
9577
10265
|
if (!await fileExists(rawConfigPath)) {
|
|
9578
10266
|
process2.stderr.write(`Warning: config path does not exist yet: ${rawConfigPath}
|
|
9579
10267
|
`);
|
|
9580
10268
|
}
|
|
9581
|
-
if (!path3.isAbsolute(configPath) || !path3.isAbsolute(storePath)) {
|
|
10269
|
+
if (transport === "stdio" && (!path3.isAbsolute(configPath) || !path3.isAbsolute(storePath))) {
|
|
9582
10270
|
process2.stderr.write("Warning: relative paths are resolved by the MCP client working directory. Use --absolute-paths if the client runs from another directory.\n");
|
|
9583
10271
|
}
|
|
9584
|
-
const snippet = mcpClientSnippet(client, configPath, storePath);
|
|
10272
|
+
const snippet = mcpClientSnippet(client, configPath, storePath, { transport, aliasMode, host, port, authTokenEnv });
|
|
10273
|
+
if (includeInstructions) {
|
|
10274
|
+
snippet.agent_instructions = mcpAgentInstructions(client, aliasMode);
|
|
10275
|
+
}
|
|
9585
10276
|
if (args.includes("--write")) {
|
|
9586
10277
|
const destination = optionalArg(args, "--destination");
|
|
9587
10278
|
if (!destination) throw new Error("mcp configure --write requires --destination <path>");
|
|
@@ -9599,20 +10290,124 @@ async function mcpConfigure(args) {
|
|
|
9599
10290
|
async function mcpConfig(args) {
|
|
9600
10291
|
const [client, ...rest] = args;
|
|
9601
10292
|
if (!client || client.startsWith("--")) return mcpConfigure(["--client", "claude-desktop", ...args]);
|
|
9602
|
-
return mcpConfigure(["--client", client, ...rest]);
|
|
10293
|
+
return mcpConfigure(["--client", normalizeMcpClientName(client) ?? client, ...rest]);
|
|
10294
|
+
}
|
|
10295
|
+
function normalizeMcpClientName(client) {
|
|
10296
|
+
if (client === "claude") return "claude-desktop";
|
|
10297
|
+
return client;
|
|
10298
|
+
}
|
|
10299
|
+
function mcpClientConfigTransport(args, client) {
|
|
10300
|
+
const requested = optionalArg(args, "--transport") ?? (client === "openai-agents" ? "streamable-http" : "stdio");
|
|
10301
|
+
if (requested === "stdio" || requested === "streamable-http") return requested;
|
|
10302
|
+
if (requested === "http" || requested === "json-rpc-http" || requested === "jsonrpc-http") {
|
|
10303
|
+
throw new Error("mcp config uses stdio or streamable-http. The lightweight JSON-RPC HTTP bridge is not a standard MCP client transport.");
|
|
10304
|
+
}
|
|
10305
|
+
throw new Error("--transport must be stdio or streamable-http");
|
|
10306
|
+
}
|
|
10307
|
+
function mcpClientConfigAliasMode(args, client) {
|
|
10308
|
+
const requested = optionalArg(args, "--alias-mode");
|
|
10309
|
+
const aliasMode = requested ?? (args.includes("--openai-tool-aliases") ? "openai" : client === "openai-agents" ? "openai" : "canonical");
|
|
10310
|
+
if (aliasMode === "canonical" || aliasMode === "openai" || aliasMode === "both") return aliasMode;
|
|
10311
|
+
throw new Error("--alias-mode must be canonical, openai, or both");
|
|
9603
10312
|
}
|
|
9604
|
-
function
|
|
10313
|
+
function serveArgsForClient(configPath, storePath, options) {
|
|
10314
|
+
const args = options.transport === "streamable-http" ? [
|
|
10315
|
+
"mcp",
|
|
10316
|
+
"serve-streamable-http",
|
|
10317
|
+
"--config",
|
|
10318
|
+
configPath,
|
|
10319
|
+
"--store",
|
|
10320
|
+
storePath,
|
|
10321
|
+
"--host",
|
|
10322
|
+
options.host,
|
|
10323
|
+
"--port",
|
|
10324
|
+
String(options.port),
|
|
10325
|
+
"--auth-token-env",
|
|
10326
|
+
options.authTokenEnv
|
|
10327
|
+
] : ["mcp", "serve", "--config", configPath, "--store", storePath];
|
|
10328
|
+
if (options.aliasMode !== "canonical") args.push("--alias-mode", options.aliasMode);
|
|
10329
|
+
return args;
|
|
10330
|
+
}
|
|
10331
|
+
function mcpClientSnippet(client, configPath, storePath, options) {
|
|
9605
10332
|
const command = cliCommandName();
|
|
9606
|
-
const args =
|
|
10333
|
+
const args = serveArgsForClient(configPath, storePath, options);
|
|
9607
10334
|
if (client === "generic" || client === "generic-stdio") return { command, args };
|
|
9608
10335
|
if (client === "claude-desktop" || client === "cursor") {
|
|
10336
|
+
if (options.transport !== "stdio") throw new Error(`${client} config output currently supports stdio. Use --transport stdio.`);
|
|
9609
10337
|
return { mcpServers: { synapsor: { command, args } } };
|
|
9610
10338
|
}
|
|
9611
10339
|
if (client === "vscode") {
|
|
10340
|
+
if (options.transport !== "stdio") throw new Error("vscode config output currently supports stdio. Use --transport stdio.");
|
|
9612
10341
|
return { servers: { synapsor: { type: "stdio", command, args } } };
|
|
9613
10342
|
}
|
|
10343
|
+
if (client === "openai-agents") {
|
|
10344
|
+
if (options.transport !== "streamable-http") throw new Error("openai-agents config output uses Streamable HTTP. Use --transport streamable-http.");
|
|
10345
|
+
const url = `http://${options.host}:${options.port}/mcp`;
|
|
10346
|
+
return {
|
|
10347
|
+
transport: "streamable-http",
|
|
10348
|
+
start_server: {
|
|
10349
|
+
command,
|
|
10350
|
+
args,
|
|
10351
|
+
env: {
|
|
10352
|
+
[options.authTokenEnv]: "<set-a-random-local-token>"
|
|
10353
|
+
}
|
|
10354
|
+
},
|
|
10355
|
+
openai_agents_sdk: {
|
|
10356
|
+
package: "openai-agents",
|
|
10357
|
+
url,
|
|
10358
|
+
headers_from_env: {
|
|
10359
|
+
Authorization: `Bearer $${options.authTokenEnv}`
|
|
10360
|
+
},
|
|
10361
|
+
python: [
|
|
10362
|
+
"import os",
|
|
10363
|
+
"from agents.mcp import MCPServerStreamableHttp",
|
|
10364
|
+
"",
|
|
10365
|
+
"synapsor_mcp = MCPServerStreamableHttp(",
|
|
10366
|
+
` params={`,
|
|
10367
|
+
` "url": "${url}",`,
|
|
10368
|
+
` "headers": {"Authorization": f"Bearer {os.environ['${options.authTokenEnv}']}"},`,
|
|
10369
|
+
" }",
|
|
10370
|
+
")"
|
|
10371
|
+
].join("\n")
|
|
10372
|
+
},
|
|
10373
|
+
tool_names: {
|
|
10374
|
+
canonical: "billing.inspect_invoice",
|
|
10375
|
+
model_visible_with_alias_mode_openai: "billing__inspect_invoice",
|
|
10376
|
+
alias_mode: options.aliasMode
|
|
10377
|
+
},
|
|
10378
|
+
notes: [
|
|
10379
|
+
"Start the local Streamable HTTP MCP server before creating the OpenAI Agents SDK server.",
|
|
10380
|
+
"OpenAI-facing configs should use --alias-mode openai because OpenAI function names cannot contain dots.",
|
|
10381
|
+
"Runner maps aliases back to canonical Synapsor capability names and includes the canonical name in MCP tool metadata.",
|
|
10382
|
+
"This config contains no database URLs, write credentials, API keys, or bearer token values."
|
|
10383
|
+
]
|
|
10384
|
+
};
|
|
10385
|
+
}
|
|
9614
10386
|
throw new Error(`unsupported MCP client: ${client}`);
|
|
9615
10387
|
}
|
|
10388
|
+
function mcpAgentInstructions(client, aliasMode) {
|
|
10389
|
+
const toolNameNote = aliasMode === "openai" ? "OpenAI-facing tool names may use aliases such as billing__inspect_invoice. Treat the canonical Synapsor capability name in tool metadata/results as the audit name." : "Use the model-visible Synapsor tool names exactly as listed by the MCP client.";
|
|
10390
|
+
return {
|
|
10391
|
+
target_client: client,
|
|
10392
|
+
alias_mode: aliasMode,
|
|
10393
|
+
recommended_system_prompt: [
|
|
10394
|
+
"Use Synapsor Runner tools in a propose-first pattern.",
|
|
10395
|
+
"Inspect relevant records, policy rows, and other evidence before proposing a change.",
|
|
10396
|
+
"Do not claim a database change was committed unless a result says source_database_changed: true.",
|
|
10397
|
+
"Proposal tools create reviewable proposals only; they do not commit writes.",
|
|
10398
|
+
"You cannot approve, apply, commit, or write back through model-facing MCP tools.",
|
|
10399
|
+
"On VERSION_CONFLICT, re-inspect the record before proposing again.",
|
|
10400
|
+
"Evidence handles are audit/replay handles; you do not need to call them during the turn.",
|
|
10401
|
+
toolNameNote
|
|
10402
|
+
].join(" "),
|
|
10403
|
+
checklist: [
|
|
10404
|
+
"Inspect evidence before proposing.",
|
|
10405
|
+
"Use trusted session scope; never ask the user/model for tenant or principal values.",
|
|
10406
|
+
"Report proposal ids and source_database_changed exactly from the tool result.",
|
|
10407
|
+
"If ok is false, follow error.code. On TEMPORARILY_UNAVAILABLE, retry later. On NOT_FOUND_IN_TENANT, do not infer cross-tenant existence."
|
|
10408
|
+
]
|
|
10409
|
+
};
|
|
10410
|
+
}
|
|
9616
10411
|
async function mcpSmoke(args) {
|
|
9617
10412
|
const boundary = await inspectMcpToolBoundary(args);
|
|
9618
10413
|
if (args.includes("--json")) {
|
|
@@ -9659,7 +10454,9 @@ async function toolsPreview(args) {
|
|
|
9659
10454
|
ok: boundary.ok,
|
|
9660
10455
|
config_path: boundary.configPath,
|
|
9661
10456
|
store_path: boundary.storePath,
|
|
10457
|
+
alias_mode: boundary.aliasMode,
|
|
9662
10458
|
exposed_to_mcp: boundary.names,
|
|
10459
|
+
alias_mappings: boundary.exposures,
|
|
9663
10460
|
not_exposed_to_mcp: defaultBlockedToolSurface(),
|
|
9664
10461
|
checks: boundary.checks
|
|
9665
10462
|
}, null, 2)}
|
|
@@ -9672,6 +10469,7 @@ async function toolsPreview(args) {
|
|
|
9672
10469
|
async function inspectMcpToolBoundary(args) {
|
|
9673
10470
|
const configPath = optionalArg(args, "--config") ?? "./synapsor.runner.json";
|
|
9674
10471
|
const storePath = optionalArg(args, "--store") ?? "./.synapsor/local.db";
|
|
10472
|
+
const aliasMode = args.includes("--aliases") && !optionalArg(args, "--alias-mode") && !optionalArg(args, "--tool-name-style") ? "both" : toolNameStyleOption(args);
|
|
9675
10473
|
if (!await fileExists(configPath)) {
|
|
9676
10474
|
throw new Error(`MCP tool preview could not find ${configPath}.
|
|
9677
10475
|
|
|
@@ -9685,7 +10483,8 @@ Run ${cliCommandName()} onboard db --from-env DATABASE_URL, or pass --config <pa
|
|
|
9685
10483
|
const runtime = createMcpRuntime(parsed, { storePath });
|
|
9686
10484
|
try {
|
|
9687
10485
|
const tools2 = runtime.listTools();
|
|
9688
|
-
const
|
|
10486
|
+
const exposures = toolNameExposures(tools2.map((tool) => tool.name), aliasMode);
|
|
10487
|
+
const names = exposures.map((item) => item.exposedName);
|
|
9689
10488
|
const serialized = JSON.stringify(tools2);
|
|
9690
10489
|
const checks = [
|
|
9691
10490
|
{ name: "semantic tools present", ok: names.length > 0, detail: names.join(", ") || "none" },
|
|
@@ -9696,7 +10495,7 @@ Run ${cliCommandName()} onboard db --from-env DATABASE_URL, or pass --config <pa
|
|
|
9696
10495
|
{ name: "write credentials absent", ok: !/(password|secret|bearer|private[_-]?key|token)/i.test(serialized), detail: "MCP tools do not include write credentials" }
|
|
9697
10496
|
];
|
|
9698
10497
|
const ok = checks.every((check) => check.ok);
|
|
9699
|
-
return { ok, configPath, storePath, names, checks };
|
|
10498
|
+
return { ok, configPath, storePath, aliasMode, names, exposures, checks };
|
|
9700
10499
|
} finally {
|
|
9701
10500
|
runtime.close();
|
|
9702
10501
|
}
|
|
@@ -9713,13 +10512,15 @@ function defaultBlockedToolSurface() {
|
|
|
9713
10512
|
];
|
|
9714
10513
|
}
|
|
9715
10514
|
function formatToolsPreview(input) {
|
|
10515
|
+
const exposedLines = input.exposures.length > 0 ? input.exposures.map((item) => item.isAlias ? ` - ${item.exposedName} -> ${item.canonicalName}` : ` - ${item.exposedName}`) : [" - (none)"];
|
|
9716
10516
|
const lines = [
|
|
9717
10517
|
`Synapsor tools preview: ${input.ok ? "ok" : "failed"}`,
|
|
9718
10518
|
`Config: ${input.configPath}`,
|
|
9719
10519
|
`Store: ${input.storePath}`,
|
|
10520
|
+
`Alias mode: ${input.aliasMode}`,
|
|
9720
10521
|
"",
|
|
9721
10522
|
"Exposed to MCP:",
|
|
9722
|
-
...
|
|
10523
|
+
...exposedLines,
|
|
9723
10524
|
"",
|
|
9724
10525
|
"Not exposed to MCP:",
|
|
9725
10526
|
...defaultBlockedToolSurface().map((name) => ` - ${name}`),
|
|
@@ -10322,11 +11123,18 @@ async function activity(args) {
|
|
|
10322
11123
|
usage(["activity"]);
|
|
10323
11124
|
return 2;
|
|
10324
11125
|
}
|
|
11126
|
+
async function events(args) {
|
|
11127
|
+
const [subcommand, ...rest] = args;
|
|
11128
|
+
if (subcommand === "tail") return eventsTail(rest);
|
|
11129
|
+
usage(["events"]);
|
|
11130
|
+
return 2;
|
|
11131
|
+
}
|
|
10325
11132
|
async function storeCommand(args) {
|
|
10326
11133
|
const [subcommand, ...rest] = args;
|
|
10327
11134
|
if (subcommand === "stats") return storeStats(rest);
|
|
10328
11135
|
if (subcommand === "vacuum") return storeVacuum(rest);
|
|
10329
11136
|
if (subcommand === "prune") return storePrune(rest);
|
|
11137
|
+
if (subcommand === "reset") return storeReset(rest);
|
|
10330
11138
|
usage(["store"]);
|
|
10331
11139
|
return 2;
|
|
10332
11140
|
}
|
|
@@ -10842,6 +11650,56 @@ async function activitySearch(args) {
|
|
|
10842
11650
|
store.close();
|
|
10843
11651
|
}
|
|
10844
11652
|
}
|
|
11653
|
+
async function eventsTail(args) {
|
|
11654
|
+
assertKnownOptions(args, eventTailAllowedOptions, "events tail");
|
|
11655
|
+
const follow = args.includes("--follow");
|
|
11656
|
+
if (follow && args.includes("--json")) throw new Error("events tail --follow does not support --json yet");
|
|
11657
|
+
const storePath = optionalArg(args, "--store");
|
|
11658
|
+
const intervalMs = Number(optionalArg(args, "--interval-ms") ?? "1000");
|
|
11659
|
+
if (!Number.isFinite(intervalMs) || intervalMs < 250) throw new Error("--interval-ms must be at least 250");
|
|
11660
|
+
const filters = eventFiltersFromArgs(args);
|
|
11661
|
+
const printOnce = async (seen2) => {
|
|
11662
|
+
const store = await openLocalStore(["--store", storePath ?? "./.synapsor/local.db"]);
|
|
11663
|
+
try {
|
|
11664
|
+
const rows = store.listEvents(filters).sort((left, right) => left.event_id - right.event_id).filter((event) => !seen2?.has(event.event_id));
|
|
11665
|
+
if (seen2) rows.forEach((event) => seen2.add(event.event_id));
|
|
11666
|
+
if (args.includes("--json")) {
|
|
11667
|
+
process2.stdout.write(`${JSON.stringify({ events: rows }, null, 2)}
|
|
11668
|
+
`);
|
|
11669
|
+
} else if (rows.length === 0 && !follow) {
|
|
11670
|
+
process2.stdout.write("No local events found.\n");
|
|
11671
|
+
} else {
|
|
11672
|
+
for (const event of rows) process2.stdout.write(formatEventLine(event, showDetails(args)));
|
|
11673
|
+
}
|
|
11674
|
+
return rows.length;
|
|
11675
|
+
} finally {
|
|
11676
|
+
store.close();
|
|
11677
|
+
}
|
|
11678
|
+
};
|
|
11679
|
+
if (!follow) {
|
|
11680
|
+
await printOnce();
|
|
11681
|
+
return 0;
|
|
11682
|
+
}
|
|
11683
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11684
|
+
await printOnce(seen);
|
|
11685
|
+
await new Promise((resolve) => {
|
|
11686
|
+
const timer = setInterval(() => {
|
|
11687
|
+
void printOnce(seen).catch((error) => {
|
|
11688
|
+
process2.stderr.write(`events tail error: ${safeErrorMessage(error)}
|
|
11689
|
+
`);
|
|
11690
|
+
});
|
|
11691
|
+
}, intervalMs);
|
|
11692
|
+
const stop = () => {
|
|
11693
|
+
clearInterval(timer);
|
|
11694
|
+
process2.off("SIGINT", stop);
|
|
11695
|
+
process2.off("SIGTERM", stop);
|
|
11696
|
+
resolve();
|
|
11697
|
+
};
|
|
11698
|
+
process2.once("SIGINT", stop);
|
|
11699
|
+
process2.once("SIGTERM", stop);
|
|
11700
|
+
});
|
|
11701
|
+
return 0;
|
|
11702
|
+
}
|
|
10845
11703
|
async function storeStats(args) {
|
|
10846
11704
|
assertKnownOptions(args, storeStatsAllowedOptions, "store stats");
|
|
10847
11705
|
const store = await openLocalStore(args);
|
|
@@ -10879,6 +11737,7 @@ async function storePrune(args) {
|
|
|
10879
11737
|
if (args.includes("--yes") && args.includes("--dry-run")) throw new Error("store prune accepts either --dry-run or --yes, not both");
|
|
10880
11738
|
const cutoff = cutoffFromOlderThan(olderThan);
|
|
10881
11739
|
const dryRun = !args.includes("--yes");
|
|
11740
|
+
if (!dryRun) await assertNoActiveStoreLease(optionalArg(args, "--store"), args.includes("--force"), "store prune");
|
|
10882
11741
|
const store = await openLocalStore(args);
|
|
10883
11742
|
try {
|
|
10884
11743
|
const result = store.pruneBefore(cutoff, { dryRun });
|
|
@@ -10890,6 +11749,36 @@ async function storePrune(args) {
|
|
|
10890
11749
|
store.close();
|
|
10891
11750
|
}
|
|
10892
11751
|
}
|
|
11752
|
+
async function storeReset(args) {
|
|
11753
|
+
assertKnownOptions(args, storeResetAllowedOptions, "store reset");
|
|
11754
|
+
const storePath = optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE ?? "./.synapsor/local.db";
|
|
11755
|
+
if (storePath === ":memory:") throw new Error("store reset does not apply to :memory: stores");
|
|
11756
|
+
if (!args.includes("--yes")) {
|
|
11757
|
+
throw new Error("store reset is destructive for the local ledger. Rerun with --yes after backing up anything you need.");
|
|
11758
|
+
}
|
|
11759
|
+
await assertNoActiveStoreLease(storePath, args.includes("--force"), "store reset");
|
|
11760
|
+
const resolved = path3.resolve(storePath);
|
|
11761
|
+
const candidates = [resolved, `${resolved}-wal`, `${resolved}-shm`, storeLeasePath(resolved)];
|
|
11762
|
+
const removed = [];
|
|
11763
|
+
for (const candidate of candidates) {
|
|
11764
|
+
try {
|
|
11765
|
+
await fs3.rm(candidate, { force: true });
|
|
11766
|
+
removed.push(candidate);
|
|
11767
|
+
} catch (error) {
|
|
11768
|
+
if (error.code !== "ENOENT") throw error;
|
|
11769
|
+
}
|
|
11770
|
+
}
|
|
11771
|
+
const result = {
|
|
11772
|
+
ok: true,
|
|
11773
|
+
store: resolved,
|
|
11774
|
+
removed,
|
|
11775
|
+
source_database_changed: false
|
|
11776
|
+
};
|
|
11777
|
+
if (args.includes("--json")) process2.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
11778
|
+
`);
|
|
11779
|
+
else process2.stdout.write(formatStoreReset(result));
|
|
11780
|
+
return 0;
|
|
11781
|
+
}
|
|
10893
11782
|
var commonReadOptions = /* @__PURE__ */ new Set(["--store", "--json", "--details", "--debug"]);
|
|
10894
11783
|
var showAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions]);
|
|
10895
11784
|
var exportAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions, "--output", "--out", "--format", "--evidence", "--audit"]);
|
|
@@ -10949,6 +11838,17 @@ var receiptListAllowedOptions = /* @__PURE__ */ new Set([
|
|
|
10949
11838
|
"--to",
|
|
10950
11839
|
"--limit"
|
|
10951
11840
|
]);
|
|
11841
|
+
var eventTailAllowedOptions = /* @__PURE__ */ new Set([
|
|
11842
|
+
...commonReadOptions,
|
|
11843
|
+
"--proposal",
|
|
11844
|
+
"--kind",
|
|
11845
|
+
"--actor",
|
|
11846
|
+
"--from",
|
|
11847
|
+
"--to",
|
|
11848
|
+
"--limit",
|
|
11849
|
+
"--follow",
|
|
11850
|
+
"--interval-ms"
|
|
11851
|
+
]);
|
|
10952
11852
|
var replayShowAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions, "--proposal", "--replay", "--evidence"]);
|
|
10953
11853
|
var replayExportAllowedOptions = /* @__PURE__ */ new Set([...replayShowAllowedOptions, "--output", "--out", "--format"]);
|
|
10954
11854
|
var replayListAllowedOptions = /* @__PURE__ */ new Set([
|
|
@@ -10991,7 +11891,8 @@ var activitySearchAllowedOptions = /* @__PURE__ */ new Set([
|
|
|
10991
11891
|
]);
|
|
10992
11892
|
var storeStatsAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions]);
|
|
10993
11893
|
var storeVacuumAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions]);
|
|
10994
|
-
var storePruneAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions, "--older-than", "--dry-run", "--yes"]);
|
|
11894
|
+
var storePruneAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions, "--older-than", "--dry-run", "--yes", "--force"]);
|
|
11895
|
+
var storeResetAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions, "--yes", "--force"]);
|
|
10995
11896
|
function assertKnownOptions(args, allowed, commandName) {
|
|
10996
11897
|
for (const arg of args) {
|
|
10997
11898
|
if (!arg.startsWith("--")) continue;
|
|
@@ -11140,6 +12041,16 @@ function receiptFiltersFromActivityArgs(args, store) {
|
|
|
11140
12041
|
limit: limitFromArgs(args)
|
|
11141
12042
|
};
|
|
11142
12043
|
}
|
|
12044
|
+
function eventFiltersFromArgs(args) {
|
|
12045
|
+
return {
|
|
12046
|
+
proposal: optionalArg(args, "--proposal"),
|
|
12047
|
+
kind: optionalArg(args, "--kind"),
|
|
12048
|
+
actor: optionalArg(args, "--actor"),
|
|
12049
|
+
from: optionalArg(args, "--from"),
|
|
12050
|
+
to: optionalArg(args, "--to"),
|
|
12051
|
+
limit: limitFromArgs(args)
|
|
12052
|
+
};
|
|
12053
|
+
}
|
|
11143
12054
|
function linkedProposalFilter(args, store, options = {}) {
|
|
11144
12055
|
const noLinkedProposal = "__synapsor_no_linked_proposal__";
|
|
11145
12056
|
const replay2 = optionalArg(args, "--replay");
|
|
@@ -11397,7 +12308,7 @@ function databaseInputFromArgs(args) {
|
|
|
11397
12308
|
if (inlineUrl && !isDatabaseUrl(inlineUrl)) {
|
|
11398
12309
|
throw new Error("--from must be a postgres://, postgresql://, or mysql:// URL.");
|
|
11399
12310
|
}
|
|
11400
|
-
const fromEnv = optionalArg(args, "--from-env") ?? optionalArg(args, "--database-url-env");
|
|
12311
|
+
const fromEnv = optionalArg(args, "--from-env") ?? optionalArg(args, "--url-env") ?? optionalArg(args, "--database-url-env");
|
|
11401
12312
|
const configDatabaseUrlEnv = fromEnv ?? "SYNAPSOR_DATABASE_READ_URL";
|
|
11402
12313
|
if (inlineUrl) {
|
|
11403
12314
|
return {
|
|
@@ -11443,6 +12354,7 @@ function firstPositional(args) {
|
|
|
11443
12354
|
"--format",
|
|
11444
12355
|
"--from",
|
|
11445
12356
|
"--from-env",
|
|
12357
|
+
"--url-env",
|
|
11446
12358
|
"--host",
|
|
11447
12359
|
"--idempotency-key",
|
|
11448
12360
|
"--input",
|
|
@@ -11633,11 +12545,11 @@ function formatProposalDetail(proposal, storedEvidenceItemCount) {
|
|
|
11633
12545
|
...formatChangeLines(proposal)
|
|
11634
12546
|
].join("\n") + "\n";
|
|
11635
12547
|
}
|
|
11636
|
-
function formatProposalEventDetail(
|
|
11637
|
-
if (
|
|
12548
|
+
function formatProposalEventDetail(events2) {
|
|
12549
|
+
if (events2.length === 0) return "Events:\n none\n";
|
|
11638
12550
|
return [
|
|
11639
12551
|
"Events:",
|
|
11640
|
-
...
|
|
12552
|
+
...events2.map((event) => ` event ${event.event_id}: ${event.kind} by ${event.actor} at ${event.created_at}`)
|
|
11641
12553
|
].join("\n") + "\n";
|
|
11642
12554
|
}
|
|
11643
12555
|
function formatProposalDebug(proposal, storePath) {
|
|
@@ -12099,6 +13011,22 @@ function formatActivityNext(items, storeSuffix) {
|
|
|
12099
13011
|
return `${lines.join("\n")}
|
|
12100
13012
|
`;
|
|
12101
13013
|
}
|
|
13014
|
+
function formatEventLine(event, details = false) {
|
|
13015
|
+
const lines = [
|
|
13016
|
+
`${event.created_at} ${event.kind}`,
|
|
13017
|
+
` proposal: ${event.proposal_id}`,
|
|
13018
|
+
` actor: ${event.actor}`
|
|
13019
|
+
];
|
|
13020
|
+
if (details && Object.keys(event.payload).length > 0) {
|
|
13021
|
+
lines.push(` payload: ${JSON.stringify(event.payload)}`);
|
|
13022
|
+
}
|
|
13023
|
+
lines.push("");
|
|
13024
|
+
return `${lines.join("\n")}
|
|
13025
|
+
`;
|
|
13026
|
+
}
|
|
13027
|
+
function safeErrorMessage(error) {
|
|
13028
|
+
return error instanceof Error ? error.message : String(error);
|
|
13029
|
+
}
|
|
12102
13030
|
function formatStoreStats(stats) {
|
|
12103
13031
|
return [
|
|
12104
13032
|
`Local store: ${stats.path}`,
|
|
@@ -12130,6 +13058,18 @@ function formatStorePrune(result) {
|
|
|
12130
13058
|
return `${lines.join("\n")}
|
|
12131
13059
|
`;
|
|
12132
13060
|
}
|
|
13061
|
+
function formatStoreReset(result) {
|
|
13062
|
+
const lines = [
|
|
13063
|
+
"Local store reset complete",
|
|
13064
|
+
`Store: ${result.store}`,
|
|
13065
|
+
`Source database changed: ${result.source_database_changed ? "yes" : "no"}`,
|
|
13066
|
+
"",
|
|
13067
|
+
"Removed:",
|
|
13068
|
+
...result.removed.length ? result.removed.map((entry) => ` - ${entry}`) : [" - no local store files were present"]
|
|
13069
|
+
];
|
|
13070
|
+
return `${lines.join("\n")}
|
|
13071
|
+
`;
|
|
13072
|
+
}
|
|
12133
13073
|
function cutoffFromOlderThan(value) {
|
|
12134
13074
|
const match = value.match(/^(\d+)([smhd])$/i);
|
|
12135
13075
|
if (!match) throw new Error("--older-than must use a duration such as 30d, 12h, 90m, or 0d");
|
|
@@ -12371,7 +13311,7 @@ function starterCloudConfig() {
|
|
|
12371
13311
|
base_url_env: "SYNAPSOR_CLOUD_BASE_URL",
|
|
12372
13312
|
runner_token_env: "SYNAPSOR_RUNNER_TOKEN",
|
|
12373
13313
|
runner_id: "synapsor_runner_local",
|
|
12374
|
-
runner_version: "0.1.0-alpha.
|
|
13314
|
+
runner_version: "0.1.0-alpha.13",
|
|
12375
13315
|
project_id: "token_scope",
|
|
12376
13316
|
adapter_id: "mcp.your_adapter",
|
|
12377
13317
|
source_id: "src_replace_me",
|
|
@@ -12423,6 +13363,7 @@ function isKnownTopLevelCommand(command) {
|
|
|
12423
13363
|
"query-audit",
|
|
12424
13364
|
"receipts",
|
|
12425
13365
|
"activity",
|
|
13366
|
+
"events",
|
|
12426
13367
|
"store",
|
|
12427
13368
|
"shadow",
|
|
12428
13369
|
"ui"
|
|
@@ -12451,6 +13392,7 @@ Commands:
|
|
|
12451
13392
|
mcp Serve safe semantic tools over MCP
|
|
12452
13393
|
onboard One-command own-database setup
|
|
12453
13394
|
smoke Test generated tool calls before wiring an MCP client
|
|
13395
|
+
tools List model-facing MCP tools and aliases
|
|
12454
13396
|
writeback Print direct SQL writeback receipt DDL, grants, and checks
|
|
12455
13397
|
handler Create app-owned writeback handler templates
|
|
12456
13398
|
propose Create a local evidence-backed proposal
|
|
@@ -12460,6 +13402,7 @@ Commands:
|
|
|
12460
13402
|
query-audit Inspect local query audit records
|
|
12461
13403
|
receipts Inspect guarded writeback receipts
|
|
12462
13404
|
activity Search local evidence/replay ledger
|
|
13405
|
+
events Tail local proposal/writeback lifecycle events
|
|
12463
13406
|
store Inspect and maintain the local SQLite ledger
|
|
12464
13407
|
apply Apply an approved proposal with guarded writeback
|
|
12465
13408
|
replay Show what happened
|
|
@@ -12472,6 +13415,7 @@ Examples:
|
|
|
12472
13415
|
${cmd} inspect --from-env DATABASE_URL
|
|
12473
13416
|
${cmd} init --wizard --from-env DATABASE_URL
|
|
12474
13417
|
${cmd} smoke call --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
13418
|
+
${cmd} tools list --aliases --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
12475
13419
|
${cmd} handler template node-fastify --output ./synapsor-writeback-handler.mjs
|
|
12476
13420
|
${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
12477
13421
|
${cmd} propose billing.propose_late_fee_waiver --sample
|
|
@@ -12479,7 +13423,7 @@ Examples:
|
|
|
12479
13423
|
`,
|
|
12480
13424
|
start: `Usage:
|
|
12481
13425
|
${cmd} start --from-env DATABASE_URL [--schema public] [--mode read_only|shadow|review]
|
|
12482
|
-
${cmd} start --from-env DATABASE_URL --mode review --writeback http_handler --handler-url-env APP_WRITEBACK_URL
|
|
13426
|
+
${cmd} start --from-env DATABASE_URL --mode review --writeback http_handler --handler-url-env APP_WRITEBACK_URL [--handler-signing-secret-env APP_WRITEBACK_SIGNING_SECRET]
|
|
12483
13427
|
${cmd} start
|
|
12484
13428
|
|
|
12485
13429
|
With --from-env, run the guided own-database setup: inspect schema, choose one
|
|
@@ -12492,43 +13436,63 @@ so it is not confused with first-run onboarding.
|
|
|
12492
13436
|
`,
|
|
12493
13437
|
inspect: `Usage:
|
|
12494
13438
|
${cmd} inspect --from-env DATABASE_URL [--engine auto|postgres|mysql] [--schema public] [--json]
|
|
13439
|
+
${cmd} inspect --engine postgres --url-env DATABASE_URL
|
|
12495
13440
|
${cmd} inspect "<postgres-or-mysql-url>" [--engine auto|postgres|mysql] [--schema public] [--json]
|
|
12496
13441
|
|
|
12497
13442
|
Inspect schema metadata without mutating the database or printing credentials.
|
|
12498
13443
|
`,
|
|
12499
13444
|
init: `Usage:
|
|
12500
13445
|
${cmd} init --wizard --from-env DATABASE_URL [--mode read_only|review|shadow] [--out synapsor.runner.json]
|
|
13446
|
+
${cmd} init --engine postgres --url-env DATABASE_URL --mode review --table public.invoices
|
|
12501
13447
|
${cmd} init --inspection-json schema.json --table invoices --mode review --patch-from-arg waiver_reason=reason
|
|
12502
|
-
${cmd} init --inspection-json schema.json --table invoices --mode review --writeback http_handler --handler-url-env APP_WRITEBACK_URL
|
|
13448
|
+
${cmd} init --inspection-json schema.json --table invoices --mode review --writeback http_handler --handler-url-env APP_WRITEBACK_URL [--handler-signing-secret-env APP_WRITEBACK_SIGNING_SECRET]
|
|
12503
13449
|
|
|
12504
13450
|
Generate a reviewed Synapsor Runner contract. Defaults to read-only in the wizard.
|
|
12505
13451
|
Review mode writeback choices: sql_update, http_handler, command_handler.
|
|
12506
13452
|
`,
|
|
12507
13453
|
mcp: `Usage:
|
|
12508
13454
|
${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
13455
|
+
${cmd} mcp serve --transport streamable-http --config ./synapsor.runner.json --store ./.synapsor/local.db --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
|
|
12509
13456
|
${cmd} mcp serve-streamable-http --config ./synapsor.runner.json --store ./.synapsor/local.db --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
|
|
12510
13457
|
${cmd} mcp serve-http --config ./synapsor.runner.json --store ./.synapsor/local.db --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
|
|
12511
13458
|
${cmd} mcp config --absolute-paths --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
13459
|
+
${cmd} mcp client-config --client openai-agents --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
12512
13460
|
${cmd} mcp audit --example dangerous-db-mcp
|
|
12513
13461
|
${cmd} mcp audit ./tools-list.json
|
|
12514
13462
|
|
|
12515
13463
|
Use stdio for local MCP clients that launch the runner. Use Streamable HTTP for standard HTTP MCP clients. Use serve-http only when you explicitly want the lightweight JSON-RPC bridge.
|
|
12516
13464
|
MCP clients see semantic tools. They do not receive raw SQL, write credentials, approval tools, or commit tools.
|
|
13465
|
+
`,
|
|
13466
|
+
tools: `Usage:
|
|
13467
|
+
${cmd} tools list --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
13468
|
+
${cmd} tools list --aliases --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
13469
|
+
${cmd} tools preview --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
13470
|
+
|
|
13471
|
+
List the model-facing MCP tools generated from a reviewed Runner config.
|
|
13472
|
+
Use --aliases to show canonical Synapsor names and OpenAI-safe aliases.
|
|
13473
|
+
This command never prints database URLs or write credentials.
|
|
12517
13474
|
`,
|
|
12518
13475
|
"mcp serve": `Usage:
|
|
12519
|
-
${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db [--read-only] [--local]
|
|
13476
|
+
${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db [--transport stdio] [--read-only] [--local] [--alias-mode canonical|openai|both] [--result-format v1|v2]
|
|
13477
|
+
${cmd} mcp serve --transport streamable-http --config ./synapsor.runner.json --store ./.synapsor/local.db --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN [--result-format v2]
|
|
12520
13478
|
|
|
12521
13479
|
Start the stdio MCP server for local MCP clients such as Claude Desktop, Cursor, or local agent tools. Startup logs stay off stdout so the MCP protocol remains clean.
|
|
13480
|
+
Use --alias-mode openai, or --openai-tool-aliases, for clients that reject dotted tool names. Use --alias-mode both to expose canonical and alias names.
|
|
13481
|
+
Use --result-format v2 to return one stable ok/summary/data/proposal/error envelope from every tool call.
|
|
12522
13482
|
`,
|
|
12523
13483
|
"mcp serve-streamable-http": `Usage:
|
|
12524
13484
|
export SYNAPSOR_RUNNER_HTTP_TOKEN=...
|
|
12525
|
-
${cmd} mcp serve-streamable-http --config ./synapsor.runner.json --store ./.synapsor/local.db [--host 127.0.0.1] [--port 8766] [--auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN]
|
|
13485
|
+
${cmd} mcp serve-streamable-http --config ./synapsor.runner.json --store ./.synapsor/local.db [--host 127.0.0.1] [--port 8766] [--auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN] [--alias-mode canonical|openai|both] [--result-format v1|v2]
|
|
12526
13486
|
|
|
12527
13487
|
Start the spec-compatible MCP Streamable HTTP endpoint for clients and SDKs that support HTTP MCP.
|
|
12528
13488
|
Bearer auth is required by default.
|
|
12529
13489
|
|
|
12530
13490
|
Alpha scope:
|
|
12531
13491
|
- Supports MCP initialize/session behavior through the official MCP Streamable HTTP transport.
|
|
13492
|
+
- Use --alias-mode openai, or --openai-tool-aliases, for clients that reject dotted tool names.
|
|
13493
|
+
- Use --alias-mode both to expose canonical names and aliases.
|
|
13494
|
+
- Use --result-format v2 for the stable ok/summary/data/proposal/error envelope.
|
|
13495
|
+
- OpenAI aliases expose names such as billing__inspect_invoice while preserving the canonical Synapsor name in _meta.
|
|
12532
13496
|
- Use /mcp for the MCP endpoint and /healthz for service health.
|
|
12533
13497
|
- Sessions are in-memory. Restarting the runner clears active HTTP MCP sessions.
|
|
12534
13498
|
|
|
@@ -12541,7 +13505,7 @@ Security:
|
|
|
12541
13505
|
`,
|
|
12542
13506
|
"mcp serve-http": `Usage:
|
|
12543
13507
|
export SYNAPSOR_RUNNER_HTTP_TOKEN=...
|
|
12544
|
-
${cmd} mcp serve-http --config ./synapsor.runner.json --store ./.synapsor/local.db [--host 127.0.0.1] [--port 8765] [--auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN]
|
|
13508
|
+
${cmd} mcp serve-http --config ./synapsor.runner.json --store ./.synapsor/local.db [--host 127.0.0.1] [--port 8765] [--auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN] [--result-format v1|v2]
|
|
12545
13509
|
|
|
12546
13510
|
Start the lightweight HTTP JSON-RPC bridge for app/server deployments that want simple POST calls.
|
|
12547
13511
|
Bearer auth is required by default.
|
|
@@ -12557,9 +13521,20 @@ Security:
|
|
|
12557
13521
|
- Optional CORS: --cors-origin http://localhost:3000
|
|
12558
13522
|
`,
|
|
12559
13523
|
"mcp config": `Usage:
|
|
12560
|
-
${cmd} mcp config [claude-desktop|cursor|generic|vscode] [--absolute-paths] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
|
|
13524
|
+
${cmd} mcp config [claude-desktop|cursor|generic|vscode|openai-agents] [--absolute-paths] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
|
|
13525
|
+
${cmd} mcp client-config --client openai-agents [--transport streamable-http] [--port 8766] [--alias-mode openai] [--include-instructions] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
|
|
12561
13526
|
|
|
12562
13527
|
Print MCP client configuration that references the local runner command, not database URLs. Defaults to claude-desktop.
|
|
13528
|
+
OpenAI Agents SDK output uses Streamable HTTP and OpenAI-safe aliases by default.
|
|
13529
|
+
`,
|
|
13530
|
+
"mcp client-config": `Usage:
|
|
13531
|
+
${cmd} mcp client-config --client claude-desktop [--absolute-paths] [--include-instructions] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
|
|
13532
|
+
${cmd} mcp client-config --client cursor [--absolute-paths] [--include-instructions] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
|
|
13533
|
+
${cmd} mcp client-config --client openai-agents [--transport streamable-http] [--port 8766] [--alias-mode openai] [--include-instructions] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
|
|
13534
|
+
|
|
13535
|
+
Print MCP client configuration that references the local runner command, not database URLs.
|
|
13536
|
+
OpenAI Agents SDK output uses Streamable HTTP and OpenAI-safe aliases by default.
|
|
13537
|
+
Use --include-instructions to include the recommended propose-first agent prompt.
|
|
12563
13538
|
`,
|
|
12564
13539
|
smoke: `Usage:
|
|
12565
13540
|
${cmd} smoke call [capability-name] [--sample] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
|
|
@@ -12631,10 +13606,13 @@ Static MCP/database risk review only. This is not a security guarantee.
|
|
|
12631
13606
|
doctor: `Usage:
|
|
12632
13607
|
${cmd} doctor --config synapsor.runner.json
|
|
12633
13608
|
${cmd} doctor --config synapsor.runner.json --json
|
|
13609
|
+
${cmd} doctor --config synapsor.runner.json --check-handlers
|
|
13610
|
+
${cmd} doctor --config synapsor.runner.json --check-writeback
|
|
12634
13611
|
${cmd} doctor --config synapsor.runner.json --report --redact --output synapsor-doctor.md
|
|
12635
13612
|
${cmd} doctor --first-run
|
|
12636
13613
|
|
|
12637
|
-
Validate local config, environment bindings, semantic tool boundary, source metadata when reachable, and local store stats. Reports are redacted; do not paste secrets into issues.
|
|
13614
|
+
Validate local config, environment bindings, semantic tool boundary, source metadata when reachable, handler signing/reachability, direct SQL writeback readiness, and local store stats. Reports are redacted; do not paste secrets into issues.
|
|
13615
|
+
Use --check-writeback only after reviewing receipt-table DDL/grants; it connects with the trusted writer and can create the receipt table if the writer has permission.
|
|
12638
13616
|
`,
|
|
12639
13617
|
proposals: `Usage:
|
|
12640
13618
|
${cmd} proposals list [--tenant acme] [--capability billing.propose_late_fee_waiver] [--object invoice:INV-3001] [--status applied]
|
|
@@ -12701,14 +13679,24 @@ or an administrator must pre-create and grant it.
|
|
|
12701
13679
|
${cmd} activity search --capability billing.propose_late_fee_waiver --from 2026-06-01 --to 2026-06-23
|
|
12702
13680
|
|
|
12703
13681
|
Search the local SQLite evidence/replay ledger across proposals, evidence, query audit, receipts, and replay records.
|
|
13682
|
+
`,
|
|
13683
|
+
events: `Usage:
|
|
13684
|
+
${cmd} events tail --store ./.synapsor/local.db
|
|
13685
|
+
${cmd} events tail --proposal wrp_...
|
|
13686
|
+
${cmd} events tail --kind writeback_applied
|
|
13687
|
+
${cmd} events tail --follow --interval-ms 1000
|
|
13688
|
+
|
|
13689
|
+
Show local proposal/writeback lifecycle events such as proposal_created, proposal_approved, writeback_applied, writeback_conflict, and writeback_failed.
|
|
12704
13690
|
`,
|
|
12705
13691
|
store: `Usage:
|
|
12706
13692
|
${cmd} store stats --store ./.synapsor/local.db
|
|
12707
13693
|
${cmd} store vacuum --store ./.synapsor/local.db
|
|
12708
13694
|
${cmd} store prune --store ./.synapsor/local.db --older-than 30d --dry-run
|
|
12709
13695
|
${cmd} store prune --store ./.synapsor/local.db --older-than 30d --yes
|
|
13696
|
+
${cmd} store prune --store ./.synapsor/local.db --older-than 30d --yes --force
|
|
13697
|
+
${cmd} store reset --store ./.synapsor/local.db --yes
|
|
12710
13698
|
|
|
12711
|
-
Local store maintenance only. Prune defaults to dry-run and never
|
|
13699
|
+
Local store maintenance only. Prune defaults to dry-run and reset requires --yes. These commands never touch your source Postgres/MySQL database. Destructive operations refuse while an active server lease exists unless --force is provided.
|
|
12712
13700
|
`,
|
|
12713
13701
|
demo: `Usage:
|
|
12714
13702
|
${cmd} demo [--force]
|