@synapsor/runner 0.1.0-alpha.11 → 0.1.0-alpha.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -0
- package/README.md +169 -23
- package/dist/cli.d.ts.map +1 -1
- package/dist/runner.mjs +855 -66
- package/docs/README.md +21 -0
- package/docs/app-owned-executors.md +5 -0
- package/docs/capability-authoring.md +265 -0
- package/docs/doctor.md +98 -0
- package/docs/handler-helper.md +217 -0
- package/docs/local-mode.md +13 -2
- package/docs/release-notes.md +57 -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/writeback-executors.md +18 -0
- package/examples/app-owned-writeback/README.md +1 -0
- package/examples/mcp-postgres-billing-app-handler/README.md +7 -2
- package/examples/mcp-postgres-billing-app-handler/app-handler.mjs +77 -149
- package/examples/mcp-postgres-billing-app-handler/scripts/run-demo.sh +1 -0
- package/examples/mcp-postgres-billing-app-handler/synapsor-handler.mjs +437 -0
- package/examples/mcp-postgres-billing-app-handler/synapsor.runner.json +1 -0
- package/package.json +3 -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,24 +2701,53 @@ 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
|
}
|
|
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
|
+
}
|
|
2688
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.14" },
|
|
2691
2751
|
{ capabilities: { tools: {}, resources: {} } }
|
|
2692
2752
|
);
|
|
2693
2753
|
const toolNameStyle = options.toolNameStyle ?? "canonical";
|
|
@@ -2700,7 +2760,7 @@ function createSynapsorMcpServer(runtime, options = {}) {
|
|
|
2700
2760
|
exposedName,
|
|
2701
2761
|
{
|
|
2702
2762
|
title: tool.title,
|
|
2703
|
-
description: tool.description,
|
|
2763
|
+
description: toolDescriptionWithCanonical(tool.description, tool.name, exposedName),
|
|
2704
2764
|
inputSchema: zodInputShapeFromJsonSchema(tool.input_schema),
|
|
2705
2765
|
annotations: {
|
|
2706
2766
|
readOnlyHint: Boolean(tool.annotations.readOnlyHint),
|
|
@@ -2731,7 +2791,7 @@ function createSynapsorMcpServer(runtime, options = {}) {
|
|
|
2731
2791
|
exposedName,
|
|
2732
2792
|
{
|
|
2733
2793
|
title: capability.name,
|
|
2734
|
-
description: capabilityDescription(capability),
|
|
2794
|
+
description: capabilityDescription(capability, exposedName),
|
|
2735
2795
|
inputSchema: zodInputShape(capability),
|
|
2736
2796
|
annotations: {
|
|
2737
2797
|
readOnlyHint: capability.kind === "read",
|
|
@@ -2778,7 +2838,7 @@ function createSynapsorMcpServer(runtime, options = {}) {
|
|
|
2778
2838
|
async function serveStdio(options = {}) {
|
|
2779
2839
|
const config = options.config ?? loadRuntimeConfigFromFile(options.configPath);
|
|
2780
2840
|
const cloudTools = config.mode === "cloud" ? await fetchCloudToolMetadata(config, process.env) : void 0;
|
|
2781
|
-
const runtime = createMcpRuntime(config, { storePath: options.storePath, cloudTools });
|
|
2841
|
+
const runtime = createMcpRuntime(config, { storePath: options.storePath, resultFormat: options.resultFormat, cloudTools });
|
|
2782
2842
|
const server = createSynapsorMcpServer(runtime, { toolNameStyle: options.toolNameStyle });
|
|
2783
2843
|
const transport = new StdioServerTransport();
|
|
2784
2844
|
await server.connect(transport);
|
|
@@ -2814,6 +2874,7 @@ async function startHttpMcpServer(options = {}) {
|
|
|
2814
2874
|
const runtime = createMcpRuntime(config, {
|
|
2815
2875
|
env,
|
|
2816
2876
|
storePath: options.storePath,
|
|
2877
|
+
resultFormat: options.resultFormat,
|
|
2817
2878
|
readRow: options.readRow,
|
|
2818
2879
|
cloudTools
|
|
2819
2880
|
});
|
|
@@ -2888,6 +2949,7 @@ async function startStreamableHttpMcpServer(options = {}) {
|
|
|
2888
2949
|
cloudTools,
|
|
2889
2950
|
env,
|
|
2890
2951
|
toolNameStyle: options.toolNameStyle,
|
|
2952
|
+
resultFormat: options.resultFormat,
|
|
2891
2953
|
authToken,
|
|
2892
2954
|
devNoAuth,
|
|
2893
2955
|
corsOrigin: options.corsOrigin,
|
|
@@ -2930,7 +2992,7 @@ async function startStreamableHttpMcpServer(options = {}) {
|
|
|
2930
2992
|
};
|
|
2931
2993
|
}
|
|
2932
2994
|
async function handleStreamableHttpMcpRequest(input) {
|
|
2933
|
-
const { request, response, config, storePath, readRow, cloudTools, env, toolNameStyle, authToken, devNoAuth, corsOrigin, sessions, openSessions } = input;
|
|
2995
|
+
const { request, response, config, storePath, readRow, cloudTools, env, toolNameStyle, resultFormat, authToken, devNoAuth, corsOrigin, sessions, openSessions } = input;
|
|
2934
2996
|
try {
|
|
2935
2997
|
setCorsHeaders(response, corsOrigin);
|
|
2936
2998
|
if (request.method === "OPTIONS" && corsOrigin) {
|
|
@@ -2992,7 +3054,7 @@ async function handleStreamableHttpMcpRequest(input) {
|
|
|
2992
3054
|
}
|
|
2993
3055
|
}
|
|
2994
3056
|
});
|
|
2995
|
-
const runtime = createMcpRuntime(config, { env, storePath, readRow, cloudTools });
|
|
3057
|
+
const runtime = createMcpRuntime(config, { env, storePath, resultFormat, readRow, cloudTools });
|
|
2996
3058
|
session = { transport, runtime };
|
|
2997
3059
|
openSessions.add(session);
|
|
2998
3060
|
transport.onclose = () => {
|
|
@@ -3330,6 +3392,11 @@ function cloudToolMetadata(tool) {
|
|
|
3330
3392
|
}
|
|
3331
3393
|
};
|
|
3332
3394
|
}
|
|
3395
|
+
function toolDescriptionWithCanonical(description, canonicalName, exposedName) {
|
|
3396
|
+
if (!exposedName || exposedName === canonicalName) return description;
|
|
3397
|
+
return `Canonical Synapsor capability: ${canonicalName}.
|
|
3398
|
+
${description}`;
|
|
3399
|
+
}
|
|
3333
3400
|
function zodInputShapeFromJsonSchema(schema) {
|
|
3334
3401
|
const properties = isRecord3(schema.properties) ? schema.properties : {};
|
|
3335
3402
|
const required = Array.isArray(schema.required) ? new Set(schema.required.map(String)) : /* @__PURE__ */ new Set();
|
|
@@ -3507,6 +3574,135 @@ async function callConfiguredTool(input) {
|
|
|
3507
3574
|
source_database_mutated: false
|
|
3508
3575
|
};
|
|
3509
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
|
+
}
|
|
3510
3706
|
function buildChangeSet(input) {
|
|
3511
3707
|
const patch = buildPatch(input.capability, input.args);
|
|
3512
3708
|
const before = scalarRecord(input.currentRow);
|
|
@@ -3708,7 +3904,7 @@ function zodInputShape(capability) {
|
|
|
3708
3904
|
if (spec.type === "number" && spec.maximum !== void 0) schema = schema.max(spec.maximum);
|
|
3709
3905
|
if (spec.enum && spec.enum.length > 0) schema = schema.refine((value) => spec.enum?.includes(value), "value is not allowlisted");
|
|
3710
3906
|
if (spec.required === false) schema = schema.optional();
|
|
3711
|
-
shape[name] = schema.describe(`${name} business argument`);
|
|
3907
|
+
shape[name] = schema.describe(spec.description ?? `${name} business argument`);
|
|
3712
3908
|
}
|
|
3713
3909
|
return shape;
|
|
3714
3910
|
}
|
|
@@ -3721,6 +3917,7 @@ function toolMetadata(capability) {
|
|
|
3721
3917
|
input_schema: Object.fromEntries(Object.entries(capability.args).map(([name, spec]) => [name, {
|
|
3722
3918
|
type: spec.type,
|
|
3723
3919
|
required: spec.required !== false,
|
|
3920
|
+
...spec.description !== void 0 ? { description: spec.description } : {},
|
|
3724
3921
|
...spec.max_length !== void 0 ? { max_length: spec.max_length } : {},
|
|
3725
3922
|
...spec.minimum !== void 0 ? { minimum: spec.minimum } : {},
|
|
3726
3923
|
...spec.maximum !== void 0 ? { maximum: spec.maximum } : {},
|
|
@@ -3736,11 +3933,23 @@ function toolMetadata(capability) {
|
|
|
3736
3933
|
}
|
|
3737
3934
|
};
|
|
3738
3935
|
}
|
|
3739
|
-
function capabilityDescription(capability) {
|
|
3740
|
-
|
|
3741
|
-
|
|
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);
|
|
3742
3950
|
}
|
|
3743
|
-
|
|
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");
|
|
3744
3953
|
}
|
|
3745
3954
|
function buildPatch(capability, args) {
|
|
3746
3955
|
if (!capability.patch) throw new McpRuntimeError("PATCH_REQUIRED", "Proposal capability has no patch mapping.");
|
|
@@ -3857,6 +4066,9 @@ function isRecord3(value) {
|
|
|
3857
4066
|
}
|
|
3858
4067
|
function toolErrorPayload(error) {
|
|
3859
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
|
+
}
|
|
3860
4072
|
return { ok: false, code: error.code, error: error.message };
|
|
3861
4073
|
}
|
|
3862
4074
|
return { ok: false, code: "MCP_TOOL_FAILED", error: error instanceof Error ? error.message : String(error) };
|
|
@@ -4719,6 +4931,7 @@ function normalizedWriteback(spec) {
|
|
|
4719
4931
|
if (executor === "http_handler") {
|
|
4720
4932
|
const urlEnv = spec.writeback?.handler_url_env ?? "SYNAPSOR_APP_WRITEBACK_URL";
|
|
4721
4933
|
const tokenEnv = spec.writeback?.handler_token_env;
|
|
4934
|
+
const signingSecretEnv = spec.writeback?.handler_signing_secret_env;
|
|
4722
4935
|
return {
|
|
4723
4936
|
executor,
|
|
4724
4937
|
executorName,
|
|
@@ -4728,12 +4941,14 @@ function normalizedWriteback(spec) {
|
|
|
4728
4941
|
url_env: urlEnv,
|
|
4729
4942
|
method: "POST",
|
|
4730
4943
|
...tokenEnv ? { auth: { type: "bearer_env", token_env: tokenEnv } } : {},
|
|
4944
|
+
...signingSecretEnv ? { signing_secret_env: signingSecretEnv } : {},
|
|
4731
4945
|
timeout_ms: spec.writeback?.timeout_ms ?? 5e3
|
|
4732
4946
|
}
|
|
4733
4947
|
},
|
|
4734
4948
|
extraEnv: [
|
|
4735
4949
|
{ name: urlEnv, value: "http://127.0.0.1:8787/synapsor/writeback", comment: "App-owned writeback handler endpoint." },
|
|
4736
|
-
...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." }] : []
|
|
4737
4952
|
]
|
|
4738
4953
|
};
|
|
4739
4954
|
}
|
|
@@ -4871,6 +5086,7 @@ function validateSelectionSpec(spec) {
|
|
|
4871
5086
|
spec.write_url_env,
|
|
4872
5087
|
spec.writeback?.handler_url_env,
|
|
4873
5088
|
spec.writeback?.handler_token_env,
|
|
5089
|
+
spec.writeback?.handler_signing_secret_env,
|
|
4874
5090
|
spec.writeback?.handler_command_env,
|
|
4875
5091
|
spec.trusted_context?.tenant_id_env,
|
|
4876
5092
|
spec.trusted_context?.principal_env
|
|
@@ -6585,6 +6801,7 @@ ${cliCommandName()} --help
|
|
|
6585
6801
|
if (command === "query-audit") return queryAudit(rest);
|
|
6586
6802
|
if (command === "receipts") return receipts(rest);
|
|
6587
6803
|
if (command === "activity") return activity(rest);
|
|
6804
|
+
if (command === "events") return events(rest);
|
|
6588
6805
|
if (command === "store") return storeCommand(rest);
|
|
6589
6806
|
if (command === "shadow") return shadow(rest);
|
|
6590
6807
|
if (command === "ui") return ui(rest);
|
|
@@ -6785,11 +7002,13 @@ async function runInitWizard(args, options = {}) {
|
|
|
6785
7002
|
} else if (writebackPath === "http_handler") {
|
|
6786
7003
|
const urlEnv = await askEnvName(ask, "App-owned HTTP handler URL env var", optionalArg(args, "--handler-url-env") ?? "SYNAPSOR_APP_WRITEBACK_URL");
|
|
6787
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") ?? "");
|
|
6788
7006
|
writeback2 = {
|
|
6789
7007
|
executor: "http_handler",
|
|
6790
7008
|
executor_name: optionalArg(args, "--executor-name"),
|
|
6791
7009
|
handler_url_env: urlEnv,
|
|
6792
7010
|
...tokenEnv ? { handler_token_env: tokenEnv } : {},
|
|
7011
|
+
...signingSecretEnv ? { handler_signing_secret_env: signingSecretEnv } : {},
|
|
6793
7012
|
timeout_ms: positiveIntegerOption(args, "--handler-timeout-ms")
|
|
6794
7013
|
};
|
|
6795
7014
|
} else {
|
|
@@ -7092,6 +7311,7 @@ function writebackSpecFromArgs(args) {
|
|
|
7092
7311
|
executor_name: optionalArg(args, "--executor-name"),
|
|
7093
7312
|
handler_url_env: optionalArg(args, "--handler-url-env") ?? "SYNAPSOR_APP_WRITEBACK_URL",
|
|
7094
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") } : {},
|
|
7095
7315
|
timeout_ms: positiveIntegerOption(args, "--handler-timeout-ms")
|
|
7096
7316
|
};
|
|
7097
7317
|
}
|
|
@@ -7530,6 +7750,54 @@ function envPresenceCheck(envName, message) {
|
|
|
7530
7750
|
message: process2.env[envName] ? `${envName} is set.` : message
|
|
7531
7751
|
};
|
|
7532
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
|
+
}
|
|
7533
7801
|
async function inspectConfiguredSource(input) {
|
|
7534
7802
|
if (!process2.env[input.source.read_url_env]) return;
|
|
7535
7803
|
const capabilities = (input.config.capabilities ?? []).filter((capability) => capability.source === input.sourceName);
|
|
@@ -7959,6 +8227,8 @@ function formatFirstRunDoctor(report) {
|
|
|
7959
8227
|
async function localDoctor(args) {
|
|
7960
8228
|
const configPath = optionalArg(args, "--config") ?? "synapsor.runner.json";
|
|
7961
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");
|
|
7962
8232
|
const parsed = JSON.parse(await fs3.readFile(configPath, "utf8"));
|
|
7963
8233
|
const checks = [];
|
|
7964
8234
|
const validation = validateRunnerCapabilityConfig(parsed);
|
|
@@ -8001,6 +8271,24 @@ async function localDoctor(args) {
|
|
|
8001
8271
|
} else {
|
|
8002
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." });
|
|
8003
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
|
+
}
|
|
8004
8292
|
}
|
|
8005
8293
|
}
|
|
8006
8294
|
await inspectConfiguredSource({ config: parsed, sourceName, source, checks });
|
|
@@ -8009,10 +8297,34 @@ async function localDoctor(args) {
|
|
|
8009
8297
|
if (!isRecord6(executor)) continue;
|
|
8010
8298
|
if (executor.type === "http_handler") {
|
|
8011
8299
|
const urlEnv = String(executor.url_env ?? "");
|
|
8012
|
-
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
|
+
}
|
|
8013
8314
|
const auth = isRecord6(executor.auth) ? executor.auth : void 0;
|
|
8014
8315
|
const tokenEnv = typeof auth?.token_env === "string" ? auth.token_env : void 0;
|
|
8015
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
|
+
}
|
|
8016
8328
|
}
|
|
8017
8329
|
if (executor.type === "command_handler") {
|
|
8018
8330
|
const commandEnv = String(executor.command_env ?? "");
|
|
@@ -8049,6 +8361,155 @@ async function localDoctor(args) {
|
|
|
8049
8361
|
}
|
|
8050
8362
|
return report.ok ? 0 : 1;
|
|
8051
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
|
+
}
|
|
8052
8513
|
async function localDoctorStoreStats(storePath) {
|
|
8053
8514
|
if (!storePath || storePath === ":memory:") return { path: storePath ?? "not configured", exists: storePath === ":memory:" };
|
|
8054
8515
|
if (!await fileExists(storePath)) return { path: storePath, exists: false };
|
|
@@ -8234,6 +8695,9 @@ function executorConfig(config, executorName) {
|
|
|
8234
8695
|
if (raw.type === "sql_update") return { type: "sql_update" };
|
|
8235
8696
|
throw new Error(`executor ${executorName} has unsupported type`);
|
|
8236
8697
|
}
|
|
8698
|
+
function signHandlerRequestBody(body, secret) {
|
|
8699
|
+
return `sha256=${crypto5.createHmac("sha256", secret).update(body).digest("hex")}`;
|
|
8700
|
+
}
|
|
8237
8701
|
async function applyHttpHandlerProposal(input) {
|
|
8238
8702
|
const duplicate = duplicateHandlerReceipt(input.store, input.proposalId);
|
|
8239
8703
|
if (duplicate) return alreadyAppliedReceipt(duplicate.receipt, input.runnerId);
|
|
@@ -8258,6 +8722,21 @@ async function applyHttpHandlerProposal(input) {
|
|
|
8258
8722
|
if (!token) throw new Error(`${input.executor.auth.token_env} is not set`);
|
|
8259
8723
|
headers.authorization = `Bearer ${token}`;
|
|
8260
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
|
+
}
|
|
8261
8740
|
const controller = new AbortController();
|
|
8262
8741
|
const timeout = setTimeout(() => controller.abort(), Math.max(1, input.executor.timeout_ms ?? 5e3));
|
|
8263
8742
|
let receipt;
|
|
@@ -8265,7 +8744,7 @@ async function applyHttpHandlerProposal(input) {
|
|
|
8265
8744
|
const response = await fetch(url, {
|
|
8266
8745
|
method: input.executor.method ?? "POST",
|
|
8267
8746
|
headers,
|
|
8268
|
-
body:
|
|
8747
|
+
body: requestBody,
|
|
8269
8748
|
signal: controller.signal
|
|
8270
8749
|
});
|
|
8271
8750
|
const text = await response.text();
|
|
@@ -8693,7 +9172,7 @@ async function cloudConnect(args) {
|
|
|
8693
9172
|
return 1;
|
|
8694
9173
|
}
|
|
8695
9174
|
const runnerId = String(parsed.cloud.runner_id || process2.env.SYNAPSOR_RUNNER_ID || "synapsor_runner_local").trim();
|
|
8696
|
-
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.14").trim();
|
|
8697
9176
|
const engines = normalizeEngines(parsed.cloud.engines);
|
|
8698
9177
|
const capabilities = normalizeCapabilities(parsed.cloud.capabilities);
|
|
8699
9178
|
const client = new ControlPlaneClient({
|
|
@@ -8756,6 +9235,7 @@ async function mcp(args) {
|
|
|
8756
9235
|
async function tools(args) {
|
|
8757
9236
|
const [subcommand, ...rest] = args;
|
|
8758
9237
|
if (subcommand === "preview") return toolsPreview(rest);
|
|
9238
|
+
if (subcommand === "list") return toolsPreview(rest);
|
|
8759
9239
|
usage(["tools"]);
|
|
8760
9240
|
return 2;
|
|
8761
9241
|
}
|
|
@@ -9414,42 +9894,66 @@ async function mcpServe(args) {
|
|
|
9414
9894
|
const readOnly = args.includes("--read-only");
|
|
9415
9895
|
const config = readOnly ? { ...await readRuntimeConfig(configPath ?? defaultConfigPath), mode: "read_only" } : void 0;
|
|
9416
9896
|
const toolNameStyle = toolNameStyleOption(args);
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
|
|
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
|
+
}
|
|
9424
9912
|
}
|
|
9425
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"));
|
|
9426
9919
|
const configPath = optionalArg(args, "--config") ?? process2.env.SYNAPSOR_MCP_CONFIG;
|
|
9427
9920
|
const readOnly = args.includes("--read-only");
|
|
9428
9921
|
const config = readOnly ? { ...await readRuntimeConfig(configPath ?? defaultConfigPath), mode: "read_only" } : void 0;
|
|
9429
9922
|
const host = optionalArg(args, "--host") ?? "127.0.0.1";
|
|
9430
9923
|
const port = Number(optionalArg(args, "--port") ?? "8765");
|
|
9924
|
+
const resultFormat = resultFormatOption(args);
|
|
9431
9925
|
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
9432
9926
|
throw new Error("--port must be an integer from 1 to 65535");
|
|
9433
9927
|
}
|
|
9434
9928
|
if (host === "0.0.0.0") {
|
|
9435
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");
|
|
9436
9930
|
}
|
|
9437
|
-
const
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
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
|
+
}
|
|
9447
9950
|
process2.stderr.write("Press Ctrl+C to stop.\n");
|
|
9448
9951
|
await new Promise((resolve) => {
|
|
9449
9952
|
const stop = async () => {
|
|
9450
9953
|
process2.off("SIGINT", stop);
|
|
9451
9954
|
process2.off("SIGTERM", stop);
|
|
9452
9955
|
await server.close();
|
|
9956
|
+
await releaseLease();
|
|
9453
9957
|
resolve();
|
|
9454
9958
|
};
|
|
9455
9959
|
process2.once("SIGINT", stop);
|
|
@@ -9462,6 +9966,7 @@ async function mcpServeStreamableHttp(args) {
|
|
|
9462
9966
|
const readOnly = args.includes("--read-only");
|
|
9463
9967
|
const config = readOnly ? { ...await readRuntimeConfig(configPath ?? defaultConfigPath), mode: "read_only" } : void 0;
|
|
9464
9968
|
const toolNameStyle = toolNameStyleOption(args);
|
|
9969
|
+
const resultFormat = resultFormatOption(args);
|
|
9465
9970
|
const host = optionalArg(args, "--host") ?? "127.0.0.1";
|
|
9466
9971
|
const port = Number(optionalArg(args, "--port") ?? "8766");
|
|
9467
9972
|
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
@@ -9470,23 +9975,33 @@ async function mcpServeStreamableHttp(args) {
|
|
|
9470
9975
|
if (host === "0.0.0.0") {
|
|
9471
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");
|
|
9472
9977
|
}
|
|
9473
|
-
const
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
|
|
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
|
+
}
|
|
9484
9998
|
process2.stderr.write("Press Ctrl+C to stop.\n");
|
|
9485
9999
|
await new Promise((resolve) => {
|
|
9486
10000
|
const stop = async () => {
|
|
9487
10001
|
process2.off("SIGINT", stop);
|
|
9488
10002
|
process2.off("SIGTERM", stop);
|
|
9489
10003
|
await server.close();
|
|
10004
|
+
await releaseLease();
|
|
9490
10005
|
resolve();
|
|
9491
10006
|
};
|
|
9492
10007
|
process2.once("SIGINT", stop);
|
|
@@ -9494,6 +10009,79 @@ async function mcpServeStreamableHttp(args) {
|
|
|
9494
10009
|
});
|
|
9495
10010
|
return 0;
|
|
9496
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
|
+
}
|
|
9497
10085
|
function toolNameStyleOption(args) {
|
|
9498
10086
|
const requestedStyle = optionalArg(args, "--tool-name-style");
|
|
9499
10087
|
const requestedAliasMode = optionalArg(args, "--alias-mode");
|
|
@@ -9509,6 +10097,13 @@ function toolNameStyleOption(args) {
|
|
|
9509
10097
|
if (requested === "canonical" || requested === "openai" || requested === "both") return requested;
|
|
9510
10098
|
throw new Error("--alias-mode must be canonical, openai, or both");
|
|
9511
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
|
+
}
|
|
9512
10107
|
async function mcpAudit(args) {
|
|
9513
10108
|
const format = optionalArg(args, "--format") ?? (args.includes("--json") ? "json" : "text");
|
|
9514
10109
|
if (!["text", "json", "markdown"].includes(format)) {
|
|
@@ -9660,6 +10255,7 @@ async function mcpConfigure(args) {
|
|
|
9660
10255
|
const storePath = useAbsolutePaths ? path3.resolve(rawStorePath) : rawStorePath;
|
|
9661
10256
|
const transport = mcpClientConfigTransport(args, client);
|
|
9662
10257
|
const aliasMode = mcpClientConfigAliasMode(args, client);
|
|
10258
|
+
const includeInstructions = args.includes("--include-instructions");
|
|
9663
10259
|
const host = optionalArg(args, "--host") ?? "127.0.0.1";
|
|
9664
10260
|
const port = Number(optionalArg(args, "--port") ?? "8766");
|
|
9665
10261
|
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
@@ -9674,6 +10270,9 @@ async function mcpConfigure(args) {
|
|
|
9674
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");
|
|
9675
10271
|
}
|
|
9676
10272
|
const snippet = mcpClientSnippet(client, configPath, storePath, { transport, aliasMode, host, port, authTokenEnv });
|
|
10273
|
+
if (includeInstructions) {
|
|
10274
|
+
snippet.agent_instructions = mcpAgentInstructions(client, aliasMode);
|
|
10275
|
+
}
|
|
9677
10276
|
if (args.includes("--write")) {
|
|
9678
10277
|
const destination = optionalArg(args, "--destination");
|
|
9679
10278
|
if (!destination) throw new Error("mcp configure --write requires --destination <path>");
|
|
@@ -9786,6 +10385,29 @@ function mcpClientSnippet(client, configPath, storePath, options) {
|
|
|
9786
10385
|
}
|
|
9787
10386
|
throw new Error(`unsupported MCP client: ${client}`);
|
|
9788
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
|
+
}
|
|
9789
10411
|
async function mcpSmoke(args) {
|
|
9790
10412
|
const boundary = await inspectMcpToolBoundary(args);
|
|
9791
10413
|
if (args.includes("--json")) {
|
|
@@ -10501,11 +11123,18 @@ async function activity(args) {
|
|
|
10501
11123
|
usage(["activity"]);
|
|
10502
11124
|
return 2;
|
|
10503
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
|
+
}
|
|
10504
11132
|
async function storeCommand(args) {
|
|
10505
11133
|
const [subcommand, ...rest] = args;
|
|
10506
11134
|
if (subcommand === "stats") return storeStats(rest);
|
|
10507
11135
|
if (subcommand === "vacuum") return storeVacuum(rest);
|
|
10508
11136
|
if (subcommand === "prune") return storePrune(rest);
|
|
11137
|
+
if (subcommand === "reset") return storeReset(rest);
|
|
10509
11138
|
usage(["store"]);
|
|
10510
11139
|
return 2;
|
|
10511
11140
|
}
|
|
@@ -11021,6 +11650,56 @@ async function activitySearch(args) {
|
|
|
11021
11650
|
store.close();
|
|
11022
11651
|
}
|
|
11023
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
|
+
}
|
|
11024
11703
|
async function storeStats(args) {
|
|
11025
11704
|
assertKnownOptions(args, storeStatsAllowedOptions, "store stats");
|
|
11026
11705
|
const store = await openLocalStore(args);
|
|
@@ -11058,6 +11737,7 @@ async function storePrune(args) {
|
|
|
11058
11737
|
if (args.includes("--yes") && args.includes("--dry-run")) throw new Error("store prune accepts either --dry-run or --yes, not both");
|
|
11059
11738
|
const cutoff = cutoffFromOlderThan(olderThan);
|
|
11060
11739
|
const dryRun = !args.includes("--yes");
|
|
11740
|
+
if (!dryRun) await assertNoActiveStoreLease(optionalArg(args, "--store"), args.includes("--force"), "store prune");
|
|
11061
11741
|
const store = await openLocalStore(args);
|
|
11062
11742
|
try {
|
|
11063
11743
|
const result = store.pruneBefore(cutoff, { dryRun });
|
|
@@ -11069,6 +11749,36 @@ async function storePrune(args) {
|
|
|
11069
11749
|
store.close();
|
|
11070
11750
|
}
|
|
11071
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
|
+
}
|
|
11072
11782
|
var commonReadOptions = /* @__PURE__ */ new Set(["--store", "--json", "--details", "--debug"]);
|
|
11073
11783
|
var showAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions]);
|
|
11074
11784
|
var exportAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions, "--output", "--out", "--format", "--evidence", "--audit"]);
|
|
@@ -11128,6 +11838,17 @@ var receiptListAllowedOptions = /* @__PURE__ */ new Set([
|
|
|
11128
11838
|
"--to",
|
|
11129
11839
|
"--limit"
|
|
11130
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
|
+
]);
|
|
11131
11852
|
var replayShowAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions, "--proposal", "--replay", "--evidence"]);
|
|
11132
11853
|
var replayExportAllowedOptions = /* @__PURE__ */ new Set([...replayShowAllowedOptions, "--output", "--out", "--format"]);
|
|
11133
11854
|
var replayListAllowedOptions = /* @__PURE__ */ new Set([
|
|
@@ -11170,7 +11891,8 @@ var activitySearchAllowedOptions = /* @__PURE__ */ new Set([
|
|
|
11170
11891
|
]);
|
|
11171
11892
|
var storeStatsAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions]);
|
|
11172
11893
|
var storeVacuumAllowedOptions = /* @__PURE__ */ new Set([...commonReadOptions]);
|
|
11173
|
-
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"]);
|
|
11174
11896
|
function assertKnownOptions(args, allowed, commandName) {
|
|
11175
11897
|
for (const arg of args) {
|
|
11176
11898
|
if (!arg.startsWith("--")) continue;
|
|
@@ -11319,6 +12041,16 @@ function receiptFiltersFromActivityArgs(args, store) {
|
|
|
11319
12041
|
limit: limitFromArgs(args)
|
|
11320
12042
|
};
|
|
11321
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
|
+
}
|
|
11322
12054
|
function linkedProposalFilter(args, store, options = {}) {
|
|
11323
12055
|
const noLinkedProposal = "__synapsor_no_linked_proposal__";
|
|
11324
12056
|
const replay2 = optionalArg(args, "--replay");
|
|
@@ -11813,11 +12545,11 @@ function formatProposalDetail(proposal, storedEvidenceItemCount) {
|
|
|
11813
12545
|
...formatChangeLines(proposal)
|
|
11814
12546
|
].join("\n") + "\n";
|
|
11815
12547
|
}
|
|
11816
|
-
function formatProposalEventDetail(
|
|
11817
|
-
if (
|
|
12548
|
+
function formatProposalEventDetail(events2) {
|
|
12549
|
+
if (events2.length === 0) return "Events:\n none\n";
|
|
11818
12550
|
return [
|
|
11819
12551
|
"Events:",
|
|
11820
|
-
...
|
|
12552
|
+
...events2.map((event) => ` event ${event.event_id}: ${event.kind} by ${event.actor} at ${event.created_at}`)
|
|
11821
12553
|
].join("\n") + "\n";
|
|
11822
12554
|
}
|
|
11823
12555
|
function formatProposalDebug(proposal, storePath) {
|
|
@@ -12279,6 +13011,22 @@ function formatActivityNext(items, storeSuffix) {
|
|
|
12279
13011
|
return `${lines.join("\n")}
|
|
12280
13012
|
`;
|
|
12281
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
|
+
}
|
|
12282
13030
|
function formatStoreStats(stats) {
|
|
12283
13031
|
return [
|
|
12284
13032
|
`Local store: ${stats.path}`,
|
|
@@ -12310,6 +13058,18 @@ function formatStorePrune(result) {
|
|
|
12310
13058
|
return `${lines.join("\n")}
|
|
12311
13059
|
`;
|
|
12312
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
|
+
}
|
|
12313
13073
|
function cutoffFromOlderThan(value) {
|
|
12314
13074
|
const match = value.match(/^(\d+)([smhd])$/i);
|
|
12315
13075
|
if (!match) throw new Error("--older-than must use a duration such as 30d, 12h, 90m, or 0d");
|
|
@@ -12551,7 +13311,7 @@ function starterCloudConfig() {
|
|
|
12551
13311
|
base_url_env: "SYNAPSOR_CLOUD_BASE_URL",
|
|
12552
13312
|
runner_token_env: "SYNAPSOR_RUNNER_TOKEN",
|
|
12553
13313
|
runner_id: "synapsor_runner_local",
|
|
12554
|
-
runner_version: "0.1.0-alpha.
|
|
13314
|
+
runner_version: "0.1.0-alpha.14",
|
|
12555
13315
|
project_id: "token_scope",
|
|
12556
13316
|
adapter_id: "mcp.your_adapter",
|
|
12557
13317
|
source_id: "src_replace_me",
|
|
@@ -12603,6 +13363,7 @@ function isKnownTopLevelCommand(command) {
|
|
|
12603
13363
|
"query-audit",
|
|
12604
13364
|
"receipts",
|
|
12605
13365
|
"activity",
|
|
13366
|
+
"events",
|
|
12606
13367
|
"store",
|
|
12607
13368
|
"shadow",
|
|
12608
13369
|
"ui"
|
|
@@ -12631,6 +13392,7 @@ Commands:
|
|
|
12631
13392
|
mcp Serve safe semantic tools over MCP
|
|
12632
13393
|
onboard One-command own-database setup
|
|
12633
13394
|
smoke Test generated tool calls before wiring an MCP client
|
|
13395
|
+
tools List model-facing MCP tools and aliases
|
|
12634
13396
|
writeback Print direct SQL writeback receipt DDL, grants, and checks
|
|
12635
13397
|
handler Create app-owned writeback handler templates
|
|
12636
13398
|
propose Create a local evidence-backed proposal
|
|
@@ -12640,6 +13402,7 @@ Commands:
|
|
|
12640
13402
|
query-audit Inspect local query audit records
|
|
12641
13403
|
receipts Inspect guarded writeback receipts
|
|
12642
13404
|
activity Search local evidence/replay ledger
|
|
13405
|
+
events Tail local proposal/writeback lifecycle events
|
|
12643
13406
|
store Inspect and maintain the local SQLite ledger
|
|
12644
13407
|
apply Apply an approved proposal with guarded writeback
|
|
12645
13408
|
replay Show what happened
|
|
@@ -12652,6 +13415,7 @@ Examples:
|
|
|
12652
13415
|
${cmd} inspect --from-env DATABASE_URL
|
|
12653
13416
|
${cmd} init --wizard --from-env DATABASE_URL
|
|
12654
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
|
|
12655
13419
|
${cmd} handler template node-fastify --output ./synapsor-writeback-handler.mjs
|
|
12656
13420
|
${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
12657
13421
|
${cmd} propose billing.propose_late_fee_waiver --sample
|
|
@@ -12659,7 +13423,7 @@ Examples:
|
|
|
12659
13423
|
`,
|
|
12660
13424
|
start: `Usage:
|
|
12661
13425
|
${cmd} start --from-env DATABASE_URL [--schema public] [--mode read_only|shadow|review]
|
|
12662
|
-
${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]
|
|
12663
13427
|
${cmd} start
|
|
12664
13428
|
|
|
12665
13429
|
With --from-env, run the guided own-database setup: inspect schema, choose one
|
|
@@ -12681,7 +13445,7 @@ Inspect schema metadata without mutating the database or printing credentials.
|
|
|
12681
13445
|
${cmd} init --wizard --from-env DATABASE_URL [--mode read_only|review|shadow] [--out synapsor.runner.json]
|
|
12682
13446
|
${cmd} init --engine postgres --url-env DATABASE_URL --mode review --table public.invoices
|
|
12683
13447
|
${cmd} init --inspection-json schema.json --table invoices --mode review --patch-from-arg waiver_reason=reason
|
|
12684
|
-
${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]
|
|
12685
13449
|
|
|
12686
13450
|
Generate a reviewed Synapsor Runner contract. Defaults to read-only in the wizard.
|
|
12687
13451
|
Review mode writeback choices: sql_update, http_handler, command_handler.
|
|
@@ -12698,17 +13462,27 @@ Review mode writeback choices: sql_update, http_handler, command_handler.
|
|
|
12698
13462
|
|
|
12699
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.
|
|
12700
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.
|
|
12701
13474
|
`,
|
|
12702
13475
|
"mcp serve": `Usage:
|
|
12703
|
-
${cmd} mcp serve --config ./synapsor.runner.json --store ./.synapsor/local.db [--transport stdio] [--read-only] [--local] [--alias-mode canonical|openai|both]
|
|
12704
|
-
${cmd} mcp serve --transport streamable-http --config ./synapsor.runner.json --store ./.synapsor/local.db --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
|
|
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]
|
|
12705
13478
|
|
|
12706
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.
|
|
12707
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.
|
|
12708
13482
|
`,
|
|
12709
13483
|
"mcp serve-streamable-http": `Usage:
|
|
12710
13484
|
export SYNAPSOR_RUNNER_HTTP_TOKEN=...
|
|
12711
|
-
${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]
|
|
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]
|
|
12712
13486
|
|
|
12713
13487
|
Start the spec-compatible MCP Streamable HTTP endpoint for clients and SDKs that support HTTP MCP.
|
|
12714
13488
|
Bearer auth is required by default.
|
|
@@ -12717,6 +13491,7 @@ Alpha scope:
|
|
|
12717
13491
|
- Supports MCP initialize/session behavior through the official MCP Streamable HTTP transport.
|
|
12718
13492
|
- Use --alias-mode openai, or --openai-tool-aliases, for clients that reject dotted tool names.
|
|
12719
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.
|
|
12720
13495
|
- OpenAI aliases expose names such as billing__inspect_invoice while preserving the canonical Synapsor name in _meta.
|
|
12721
13496
|
- Use /mcp for the MCP endpoint and /healthz for service health.
|
|
12722
13497
|
- Sessions are in-memory. Restarting the runner clears active HTTP MCP sessions.
|
|
@@ -12730,7 +13505,7 @@ Security:
|
|
|
12730
13505
|
`,
|
|
12731
13506
|
"mcp serve-http": `Usage:
|
|
12732
13507
|
export SYNAPSOR_RUNNER_HTTP_TOKEN=...
|
|
12733
|
-
${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]
|
|
12734
13509
|
|
|
12735
13510
|
Start the lightweight HTTP JSON-RPC bridge for app/server deployments that want simple POST calls.
|
|
12736
13511
|
Bearer auth is required by default.
|
|
@@ -12747,18 +13522,19 @@ Security:
|
|
|
12747
13522
|
`,
|
|
12748
13523
|
"mcp config": `Usage:
|
|
12749
13524
|
${cmd} mcp config [claude-desktop|cursor|generic|vscode|openai-agents] [--absolute-paths] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
|
|
12750
|
-
${cmd} mcp client-config --client openai-agents [--transport streamable-http] [--port 8766] [--alias-mode openai] [--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]
|
|
12751
13526
|
|
|
12752
13527
|
Print MCP client configuration that references the local runner command, not database URLs. Defaults to claude-desktop.
|
|
12753
13528
|
OpenAI Agents SDK output uses Streamable HTTP and OpenAI-safe aliases by default.
|
|
12754
13529
|
`,
|
|
12755
13530
|
"mcp client-config": `Usage:
|
|
12756
|
-
${cmd} mcp client-config --client claude-desktop [--absolute-paths] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
|
|
12757
|
-
${cmd} mcp client-config --client cursor [--absolute-paths] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
|
|
12758
|
-
${cmd} mcp client-config --client openai-agents [--transport streamable-http] [--port 8766] [--alias-mode openai] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
|
|
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]
|
|
12759
13534
|
|
|
12760
13535
|
Print MCP client configuration that references the local runner command, not database URLs.
|
|
12761
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.
|
|
12762
13538
|
`,
|
|
12763
13539
|
smoke: `Usage:
|
|
12764
13540
|
${cmd} smoke call [capability-name] [--sample] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
|
|
@@ -12830,10 +13606,13 @@ Static MCP/database risk review only. This is not a security guarantee.
|
|
|
12830
13606
|
doctor: `Usage:
|
|
12831
13607
|
${cmd} doctor --config synapsor.runner.json
|
|
12832
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
|
|
12833
13611
|
${cmd} doctor --config synapsor.runner.json --report --redact --output synapsor-doctor.md
|
|
12834
13612
|
${cmd} doctor --first-run
|
|
12835
13613
|
|
|
12836
|
-
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.
|
|
12837
13616
|
`,
|
|
12838
13617
|
proposals: `Usage:
|
|
12839
13618
|
${cmd} proposals list [--tenant acme] [--capability billing.propose_late_fee_waiver] [--object invoice:INV-3001] [--status applied]
|
|
@@ -12900,14 +13679,24 @@ or an administrator must pre-create and grant it.
|
|
|
12900
13679
|
${cmd} activity search --capability billing.propose_late_fee_waiver --from 2026-06-01 --to 2026-06-23
|
|
12901
13680
|
|
|
12902
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.
|
|
12903
13690
|
`,
|
|
12904
13691
|
store: `Usage:
|
|
12905
13692
|
${cmd} store stats --store ./.synapsor/local.db
|
|
12906
13693
|
${cmd} store vacuum --store ./.synapsor/local.db
|
|
12907
13694
|
${cmd} store prune --store ./.synapsor/local.db --older-than 30d --dry-run
|
|
12908
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
|
|
12909
13698
|
|
|
12910
|
-
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.
|
|
12911
13700
|
`,
|
|
12912
13701
|
demo: `Usage:
|
|
12913
13702
|
${cmd} demo [--force]
|