@synapsor/runner 0.1.0-alpha.10 → 0.1.0-alpha.11

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.
Files changed (34) hide show
  1. package/README.md +41 -2
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/runner.mjs +264 -65
  4. package/docs/README.md +17 -0
  5. package/docs/app-owned-executors.md +21 -0
  6. package/docs/cloud-mode.md +24 -0
  7. package/docs/current-scope.md +24 -0
  8. package/docs/dependency-license-inventory.md +35 -0
  9. package/docs/http-mcp.md +35 -1
  10. package/docs/licensing.md +36 -0
  11. package/docs/mcp-client-setup.md +39 -0
  12. package/docs/openai-agents-sdk.md +57 -0
  13. package/docs/release-notes.md +30 -1
  14. package/docs/use-your-own-database.md +18 -0
  15. package/docs/writeback-executors.md +11 -0
  16. package/examples/mcp-postgres-billing-app-handler/README.md +82 -0
  17. package/examples/mcp-postgres-billing-app-handler/app-handler.mjs +197 -0
  18. package/examples/mcp-postgres-billing-app-handler/docker-compose.yml +13 -0
  19. package/examples/mcp-postgres-billing-app-handler/schema.sql +59 -0
  20. package/examples/mcp-postgres-billing-app-handler/scripts/run-demo.sh +99 -0
  21. package/examples/mcp-postgres-billing-app-handler/seed.sql +39 -0
  22. package/examples/mcp-postgres-billing-app-handler/synapsor.runner.json +157 -0
  23. package/examples/openai-agents-http/README.md +10 -2
  24. package/examples/openai-agents-stdio/README.md +8 -4
  25. package/examples/openai-agents-stdio/agent.py +2 -0
  26. package/fixtures/benchmark/mcp-efficiency.json +53 -0
  27. package/fixtures/benchmark/mcp-efficiency.txt +25 -0
  28. package/fixtures/protocol/MANIFEST.json +54 -0
  29. package/fixtures/protocol/change-set.late-fee-waiver.v1.json +72 -0
  30. package/fixtures/protocol/execution-receipt.applied.v1.json +14 -0
  31. package/fixtures/protocol/execution-receipt.conflict.v1.json +15 -0
  32. package/fixtures/protocol/runner-registration.v1.json +22 -0
  33. package/fixtures/protocol/writeback-job.late-fee-waiver.v1.json +44 -0
  34. package/package.json +3 -1
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ import http from "node:http";
3
+ import { createRequire } from "node:module";
4
+
5
+ const { Pool } = await loadPg();
6
+
7
+ const port = Number(process.env.BILLING_APP_HANDLER_PORT || "8787");
8
+ const expectedToken = process.env.BILLING_APP_HANDLER_TOKEN || "dev-handler-token";
9
+ const databaseUrl = process.env.BILLING_APP_WRITE_URL;
10
+
11
+ if (!databaseUrl) {
12
+ console.error("BILLING_APP_WRITE_URL is required.");
13
+ process.exit(1);
14
+ }
15
+
16
+ const pool = new Pool({ connectionString: databaseUrl });
17
+
18
+ const server = http.createServer(async (request, response) => {
19
+ try {
20
+ if (request.method === "GET" && request.url === "/healthz") {
21
+ return writeJson(response, 200, { ok: true });
22
+ }
23
+ if (request.method !== "POST" || request.url !== "/synapsor/writeback") {
24
+ return writeJson(response, 404, { status: "failed", safe_error_code: "NOT_FOUND", source_database_mutated: false });
25
+ }
26
+ if (request.headers.authorization !== `Bearer ${expectedToken}`) {
27
+ return writeJson(response, 401, { status: "failed", safe_error_code: "UNAUTHORIZED", source_database_mutated: false });
28
+ }
29
+ const body = await readJson(request);
30
+ const receipt = await applyAccountCredit(body);
31
+ return writeJson(response, 200, receipt);
32
+ } catch (error) {
33
+ const message = error instanceof Error ? error.message : String(error);
34
+ return writeJson(response, 500, {
35
+ status: "failed",
36
+ safe_error_code: "HANDLER_EXCEPTION",
37
+ source_database_mutated: false,
38
+ details: { message: message.slice(0, 300) },
39
+ });
40
+ }
41
+ });
42
+
43
+ server.listen(port, "127.0.0.1", () => {
44
+ console.error(`Billing app handler listening on http://127.0.0.1:${port}/synapsor/writeback`);
45
+ });
46
+
47
+ process.once("SIGINT", shutdown);
48
+ process.once("SIGTERM", shutdown);
49
+
50
+ async function shutdown() {
51
+ server.close();
52
+ await pool.end();
53
+ process.exit(0);
54
+ }
55
+
56
+ async function applyAccountCredit(request) {
57
+ const changeSet = request.change_set || request;
58
+ if (changeSet.action !== "billing.propose_account_credit") {
59
+ return {
60
+ status: "failed",
61
+ safe_error_code: "UNSUPPORTED_ACTION",
62
+ source_database_mutated: false,
63
+ };
64
+ }
65
+
66
+ const proposalId = String(request.proposal_id || "");
67
+ const idempotencyKey = String(request.idempotency_key || proposalId);
68
+ const tenantId = String(changeSet.scope?.tenant_id || changeSet.tenant_guard?.value || changeSet.guards?.tenant?.value || "");
69
+ const principal = String(changeSet.principal?.id || changeSet.runner_hint || "approved_operator");
70
+ const invoiceId = String(changeSet.scope?.object_id || changeSet.target?.primary_key?.value || changeSet.source?.primary_key?.value || "");
71
+ const amountCents = Number(changeSet.patch?.credit_requested_cents);
72
+ const reason = String(changeSet.patch?.credit_reason || "approved account credit");
73
+ const expectedVersion = changeSet.guards?.expected_version?.value;
74
+
75
+ if (!proposalId || !tenantId || !invoiceId || !Number.isInteger(amountCents) || amountCents <= 0 || !expectedVersion) {
76
+ return {
77
+ status: "failed",
78
+ safe_error_code: "BAD_WRITEBACK_REQUEST",
79
+ source_database_mutated: false,
80
+ };
81
+ }
82
+
83
+ const client = await pool.connect();
84
+ try {
85
+ await client.query("BEGIN");
86
+
87
+ const duplicate = await client.query(
88
+ "SELECT id FROM public.account_credits WHERE idempotency_key = $1",
89
+ [idempotencyKey],
90
+ );
91
+ if (duplicate.rowCount && duplicate.rows[0]) {
92
+ await client.query("COMMIT");
93
+ return {
94
+ status: "already_applied",
95
+ rows_affected: 0,
96
+ source_database_mutated: false,
97
+ details: {
98
+ credit_id: duplicate.rows[0].id,
99
+ },
100
+ };
101
+ }
102
+
103
+ const invoiceResult = await client.query(
104
+ "SELECT id, tenant_id, customer_id, updated_at FROM public.invoices WHERE id = $1 AND tenant_id = $2 FOR UPDATE",
105
+ [invoiceId, tenantId],
106
+ );
107
+ const invoice = invoiceResult.rows[0];
108
+ if (!invoice) {
109
+ await client.query("ROLLBACK");
110
+ return {
111
+ status: "conflict",
112
+ rows_affected: 0,
113
+ safe_error_code: "INVOICE_NOT_FOUND_OR_WRONG_TENANT",
114
+ source_database_mutated: false,
115
+ };
116
+ }
117
+
118
+ const currentVersion = timestampString(invoice.updated_at);
119
+ const expected = timestampString(expectedVersion);
120
+ if (currentVersion !== expected) {
121
+ await client.query("ROLLBACK");
122
+ return {
123
+ status: "conflict",
124
+ rows_affected: 0,
125
+ previous_version: currentVersion,
126
+ safe_error_code: "ROW_CHANGED_AFTER_PROPOSAL",
127
+ source_database_mutated: false,
128
+ };
129
+ }
130
+
131
+ const creditId = `CR-${proposalId.replace(/[^A-Za-z0-9]/g, "").slice(-12) || Date.now()}`;
132
+ await client.query(
133
+ `INSERT INTO public.account_credits
134
+ (id, tenant_id, invoice_id, customer_id, amount_cents, reason, idempotency_key, created_by)
135
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
136
+ [creditId, tenantId, invoiceId, invoice.customer_id, amountCents, reason, idempotencyKey, principal],
137
+ );
138
+ const update = await client.query(
139
+ `UPDATE public.invoices
140
+ SET credit_requested_cents = $1,
141
+ credit_reason = $2,
142
+ credited_cents = credited_cents + $1,
143
+ updated_at = now()
144
+ WHERE id = $3 AND tenant_id = $4
145
+ RETURNING updated_at`,
146
+ [amountCents, reason, invoiceId, tenantId],
147
+ );
148
+ await client.query("COMMIT");
149
+
150
+ return {
151
+ status: "applied",
152
+ rows_affected: 2,
153
+ previous_version: currentVersion,
154
+ new_version: timestampString(update.rows[0]?.updated_at),
155
+ source_database_mutated: true,
156
+ details: {
157
+ effects: [
158
+ { type: "db.insert", table: "account_credits", id: creditId },
159
+ { type: "db.update", table: "invoices", id: invoiceId },
160
+ { type: "event", name: "billing.account_credit_created" },
161
+ ],
162
+ },
163
+ };
164
+ } catch (error) {
165
+ await client.query("ROLLBACK").catch(() => {});
166
+ throw error;
167
+ } finally {
168
+ client.release();
169
+ }
170
+ }
171
+
172
+ function timestampString(value) {
173
+ if (value instanceof Date) return value.toISOString().replace(".000Z", "Z");
174
+ return new Date(String(value)).toISOString().replace(".000Z", "Z");
175
+ }
176
+
177
+ async function readJson(request) {
178
+ const chunks = [];
179
+ for await (const chunk of request) chunks.push(Buffer.from(chunk));
180
+ const text = Buffer.concat(chunks).toString("utf8");
181
+ return text ? JSON.parse(text) : {};
182
+ }
183
+
184
+ function writeJson(response, statusCode, body) {
185
+ response.statusCode = statusCode;
186
+ response.setHeader("content-type", "application/json; charset=utf-8");
187
+ response.end(`${JSON.stringify(body, null, 2)}\n`);
188
+ }
189
+
190
+ async function loadPg() {
191
+ try {
192
+ return await import("pg");
193
+ } catch {
194
+ const requireFromRunnerPackage = createRequire(new URL("../../apps/runner/package.json", import.meta.url));
195
+ return requireFromRunnerPackage("pg");
196
+ }
197
+ }
@@ -0,0 +1,13 @@
1
+ services:
2
+ postgres:
3
+ image: postgres:16
4
+ container_name: synapsor_runner_billing_app_handler
5
+ environment:
6
+ POSTGRES_DB: synapsor_billing_app_handler
7
+ POSTGRES_USER: synapsor_admin
8
+ POSTGRES_PASSWORD: synapsor_admin_password
9
+ ports:
10
+ - "55437:5432"
11
+ volumes:
12
+ - ./schema.sql:/docker-entrypoint-initdb.d/001_schema.sql:ro
13
+ - ./seed.sql:/docker-entrypoint-initdb.d/002_seed.sql:ro
@@ -0,0 +1,59 @@
1
+ CREATE TABLE IF NOT EXISTS public.tenants (
2
+ id text PRIMARY KEY,
3
+ name text NOT NULL,
4
+ created_at timestamptz NOT NULL DEFAULT now(),
5
+ updated_at timestamptz NOT NULL DEFAULT now()
6
+ );
7
+
8
+ CREATE TABLE IF NOT EXISTS public.customers (
9
+ id text PRIMARY KEY,
10
+ tenant_id text NOT NULL REFERENCES public.tenants(id),
11
+ name text NOT NULL,
12
+ plan text NOT NULL,
13
+ created_at timestamptz NOT NULL DEFAULT now(),
14
+ updated_at timestamptz NOT NULL DEFAULT now()
15
+ );
16
+
17
+ CREATE TABLE IF NOT EXISTS public.invoices (
18
+ id text PRIMARY KEY,
19
+ tenant_id text NOT NULL REFERENCES public.tenants(id),
20
+ customer_id text NOT NULL REFERENCES public.customers(id),
21
+ status text NOT NULL,
22
+ balance_cents integer NOT NULL,
23
+ late_fee_cents integer NOT NULL,
24
+ waiver_reason text,
25
+ credit_requested_cents integer NOT NULL DEFAULT 0,
26
+ credit_reason text,
27
+ credited_cents integer NOT NULL DEFAULT 0,
28
+ updated_at timestamptz NOT NULL DEFAULT now()
29
+ );
30
+
31
+ CREATE TABLE IF NOT EXISTS public.account_credits (
32
+ id text PRIMARY KEY,
33
+ tenant_id text NOT NULL REFERENCES public.tenants(id),
34
+ invoice_id text NOT NULL REFERENCES public.invoices(id),
35
+ customer_id text NOT NULL REFERENCES public.customers(id),
36
+ amount_cents integer NOT NULL CHECK (amount_cents > 0),
37
+ reason text NOT NULL,
38
+ idempotency_key text NOT NULL UNIQUE,
39
+ created_by text NOT NULL,
40
+ created_at timestamptz NOT NULL DEFAULT now()
41
+ );
42
+
43
+ DO $$
44
+ BEGIN
45
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'synapsor_reader') THEN
46
+ CREATE ROLE synapsor_reader LOGIN PASSWORD 'synapsor_reader_password';
47
+ END IF;
48
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'synapsor_writer') THEN
49
+ CREATE ROLE synapsor_writer LOGIN PASSWORD 'synapsor_writer_password';
50
+ END IF;
51
+ END
52
+ $$;
53
+
54
+ GRANT CONNECT ON DATABASE synapsor_billing_app_handler TO synapsor_reader, synapsor_writer;
55
+ GRANT USAGE ON SCHEMA public TO synapsor_reader, synapsor_writer;
56
+ GRANT CREATE ON SCHEMA public TO synapsor_writer;
57
+ GRANT SELECT ON public.tenants, public.customers, public.invoices, public.account_credits TO synapsor_reader, synapsor_writer;
58
+ GRANT UPDATE (late_fee_cents, waiver_reason, credit_requested_cents, credit_reason, credited_cents, updated_at) ON public.invoices TO synapsor_writer;
59
+ GRANT INSERT ON public.account_credits TO synapsor_writer;
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
5
+ EXAMPLE_DIR="$ROOT/examples/mcp-postgres-billing-app-handler"
6
+ COMPOSE_FILE="$EXAMPLE_DIR/docker-compose.yml"
7
+ CONFIG="$EXAMPLE_DIR/synapsor.runner.json"
8
+ STORE="$ROOT/tmp/billing-app-handler/local.db"
9
+ BIN="${SYNAPSOR_RUNNER_BIN:-$ROOT/bin/synapsor-runner}"
10
+ HANDLER_LOG="$ROOT/tmp/billing-app-handler/handler.log"
11
+
12
+ cleanup() {
13
+ if [[ -n "${HANDLER_PID:-}" ]]; then
14
+ kill "$HANDLER_PID" >/dev/null 2>&1 || true
15
+ wait "$HANDLER_PID" >/dev/null 2>&1 || true
16
+ fi
17
+ docker compose -f "$COMPOSE_FILE" down -v >/dev/null 2>&1 || true
18
+ }
19
+ trap cleanup EXIT
20
+
21
+ mkdir -p "$ROOT/tmp/billing-app-handler"
22
+ rm -f "$STORE" "$HANDLER_LOG"
23
+
24
+ docker compose -f "$COMPOSE_FILE" down -v >/dev/null 2>&1 || true
25
+ docker compose -f "$COMPOSE_FILE" up -d >/dev/null
26
+
27
+ for _ in $(seq 1 90); do
28
+ invoice_count="$(docker exec synapsor_runner_billing_app_handler psql -U synapsor_admin -d synapsor_billing_app_handler -Atc "SELECT count(*) FROM public.invoices" 2>/dev/null || true)"
29
+ if [[ "$invoice_count" == "2" ]]; then
30
+ sleep 1
31
+ invoice_count="$(docker exec synapsor_runner_billing_app_handler psql -U synapsor_admin -d synapsor_billing_app_handler -Atc "SELECT count(*) FROM public.invoices" 2>/dev/null || true)"
32
+ if [[ "$invoice_count" == "2" ]]; then
33
+ break
34
+ fi
35
+ fi
36
+ sleep 1
37
+ done
38
+ invoice_count="$(docker exec synapsor_runner_billing_app_handler psql -U synapsor_admin -d synapsor_billing_app_handler -Atc "SELECT count(*) FROM public.invoices")"
39
+ if [[ "$invoice_count" != "2" ]]; then
40
+ echo "Postgres fixture did not finish seeding invoices." >&2
41
+ exit 1
42
+ fi
43
+
44
+ export BILLING_APP_READ_URL="postgresql://synapsor_reader:synapsor_reader_password@localhost:55437/synapsor_billing_app_handler"
45
+ export BILLING_APP_WRITE_URL="postgresql://synapsor_writer:synapsor_writer_password@localhost:55437/synapsor_billing_app_handler"
46
+ export BILLING_APP_HANDLER_URL="http://127.0.0.1:8787/synapsor/writeback"
47
+ export BILLING_APP_HANDLER_TOKEN="dev-handler-token"
48
+ export SYNAPSOR_TENANT_ID="acme"
49
+ export SYNAPSOR_PRINCIPAL="local_billing_operator"
50
+
51
+ node "$EXAMPLE_DIR/app-handler.mjs" >"$HANDLER_LOG" 2>&1 &
52
+ HANDLER_PID="$!"
53
+
54
+ for _ in $(seq 1 90); do
55
+ if node -e "fetch('http://127.0.0.1:8787/healthz').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" >/dev/null 2>&1; then
56
+ break
57
+ fi
58
+ sleep 1
59
+ done
60
+ node -e "fetch('http://127.0.0.1:8787/healthz').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
61
+
62
+ "$BIN" config validate --config "$CONFIG" >/dev/null
63
+ "$BIN" tools preview --config "$CONFIG" --store "$STORE" > "$ROOT/tmp/billing-app-handler/tools.txt"
64
+ grep -F "billing.propose_account_credit" "$ROOT/tmp/billing-app-handler/tools.txt" >/dev/null
65
+ grep -F "execute_sql absent" "$ROOT/tmp/billing-app-handler/tools.txt" >/dev/null
66
+
67
+ printf '{"invoice_id":"INV-3001","amount_cents":2500,"reason":"support-approved credit"}\n' > "$ROOT/tmp/billing-app-handler/credit-input.json"
68
+ "$BIN" propose billing.propose_account_credit \
69
+ --input "$ROOT/tmp/billing-app-handler/credit-input.json" \
70
+ --config "$CONFIG" \
71
+ --store "$STORE" > "$ROOT/tmp/billing-app-handler/propose-credit.txt"
72
+ grep -F "Source DB changed:" "$ROOT/tmp/billing-app-handler/propose-credit.txt" >/dev/null
73
+ grep -F "no" "$ROOT/tmp/billing-app-handler/propose-credit.txt" >/dev/null
74
+
75
+ credit_count_before="$(docker exec synapsor_runner_billing_app_handler psql -U synapsor_admin -d synapsor_billing_app_handler -Atc "SELECT count(*) FROM public.account_credits")"
76
+ if [[ "$credit_count_before" != "0" ]]; then
77
+ echo "expected no account credits before approval, got $credit_count_before" >&2
78
+ exit 1
79
+ fi
80
+
81
+ "$BIN" proposals approve latest --yes --store "$STORE" >/dev/null
82
+ "$BIN" apply latest --config "$CONFIG" --store "$STORE" > "$ROOT/tmp/billing-app-handler/apply-credit.txt"
83
+ grep -F "App-owned writeback applied." "$ROOT/tmp/billing-app-handler/apply-credit.txt" >/dev/null
84
+ grep -F "source database changed by handler: yes" "$ROOT/tmp/billing-app-handler/apply-credit.txt" >/dev/null
85
+
86
+ credit_count_after="$(docker exec synapsor_runner_billing_app_handler psql -U synapsor_admin -d synapsor_billing_app_handler -Atc "SELECT count(*) FROM public.account_credits WHERE invoice_id = 'INV-3001' AND amount_cents = 2500")"
87
+ if [[ "$credit_count_after" != "1" ]]; then
88
+ echo "expected one inserted account credit, got $credit_count_after" >&2
89
+ exit 1
90
+ fi
91
+
92
+ "$BIN" apply latest --config "$CONFIG" --store "$STORE" > "$ROOT/tmp/billing-app-handler/apply-credit-retry.txt"
93
+ grep -F "App-owned writeback already applied." "$ROOT/tmp/billing-app-handler/apply-credit-retry.txt" >/dev/null
94
+
95
+ "$BIN" replay show latest --store "$STORE" > "$ROOT/tmp/billing-app-handler/replay.txt"
96
+ grep -F "billing.propose_account_credit" "$ROOT/tmp/billing-app-handler/replay.txt" >/dev/null
97
+
98
+ echo "App-owned billing handler demo passed."
99
+ echo "Verified: proposal first, source unchanged before approval, account credit inserted by app handler, idempotent retry, replay."
@@ -0,0 +1,39 @@
1
+ INSERT INTO public.tenants (id, name, created_at, updated_at)
2
+ VALUES
3
+ ('acme', 'Acme Robotics', '2026-06-20T10:00:00Z', '2026-06-20T10:00:00Z'),
4
+ ('globex', 'Globex Labs', '2026-06-20T10:00:00Z', '2026-06-20T10:00:00Z')
5
+ ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, updated_at = EXCLUDED.updated_at;
6
+
7
+ INSERT INTO public.customers (id, tenant_id, name, plan, created_at, updated_at)
8
+ VALUES
9
+ ('cust_acme_1', 'acme', 'Acme Robotics', 'enterprise', '2026-06-20T10:00:00Z', '2026-06-20T10:00:00Z'),
10
+ ('cust_globex_1', 'globex', 'Globex Labs', 'builder', '2026-06-20T10:00:00Z', '2026-06-20T10:00:00Z')
11
+ ON CONFLICT (id) DO UPDATE SET tenant_id = EXCLUDED.tenant_id, name = EXCLUDED.name, plan = EXCLUDED.plan, updated_at = EXCLUDED.updated_at;
12
+
13
+ INSERT INTO public.invoices (
14
+ id,
15
+ tenant_id,
16
+ customer_id,
17
+ status,
18
+ balance_cents,
19
+ late_fee_cents,
20
+ waiver_reason,
21
+ credit_requested_cents,
22
+ credit_reason,
23
+ credited_cents,
24
+ updated_at
25
+ )
26
+ VALUES
27
+ ('INV-3001', 'acme', 'cust_acme_1', 'overdue', 25500, 5500, NULL, 0, NULL, 0, '2026-06-20T14:31:08Z'),
28
+ ('INV-9001', 'globex', 'cust_globex_1', 'overdue', 25500, 5500, NULL, 0, NULL, 0, '2026-06-20T14:31:08Z')
29
+ ON CONFLICT (id) DO UPDATE SET
30
+ tenant_id = EXCLUDED.tenant_id,
31
+ customer_id = EXCLUDED.customer_id,
32
+ status = EXCLUDED.status,
33
+ balance_cents = EXCLUDED.balance_cents,
34
+ late_fee_cents = EXCLUDED.late_fee_cents,
35
+ waiver_reason = EXCLUDED.waiver_reason,
36
+ credit_requested_cents = EXCLUDED.credit_requested_cents,
37
+ credit_reason = EXCLUDED.credit_reason,
38
+ credited_cents = EXCLUDED.credited_cents,
39
+ updated_at = EXCLUDED.updated_at;
@@ -0,0 +1,157 @@
1
+ {
2
+ "version": 1,
3
+ "mode": "review",
4
+ "storage": {
5
+ "sqlite_path": "./tmp/billing-app-handler/local.db"
6
+ },
7
+ "sources": {
8
+ "billing_postgres": {
9
+ "engine": "postgres",
10
+ "read_url_env": "BILLING_APP_READ_URL",
11
+ "write_url_env": "BILLING_APP_WRITE_URL",
12
+ "statement_timeout_ms": 3000
13
+ }
14
+ },
15
+ "trusted_context": {
16
+ "provider": "environment",
17
+ "values": {
18
+ "tenant_id_env": "SYNAPSOR_TENANT_ID",
19
+ "principal_env": "SYNAPSOR_PRINCIPAL"
20
+ }
21
+ },
22
+ "contexts": {
23
+ "local_operator": {
24
+ "provider": "environment",
25
+ "values": {
26
+ "tenant_id_env": "SYNAPSOR_TENANT_ID",
27
+ "principal_env": "SYNAPSOR_PRINCIPAL"
28
+ }
29
+ }
30
+ },
31
+ "executors": {
32
+ "billing_app_handler": {
33
+ "type": "http_handler",
34
+ "url_env": "BILLING_APP_HANDLER_URL",
35
+ "method": "POST",
36
+ "auth": {
37
+ "type": "bearer_env",
38
+ "token_env": "BILLING_APP_HANDLER_TOKEN"
39
+ },
40
+ "timeout_ms": 5000
41
+ }
42
+ },
43
+ "capabilities": [
44
+ {
45
+ "name": "billing.inspect_invoice",
46
+ "kind": "read",
47
+ "source": "billing_postgres",
48
+ "context": "local_operator",
49
+ "target": {
50
+ "schema": "public",
51
+ "table": "invoices",
52
+ "primary_key": "id",
53
+ "tenant_key": "tenant_id"
54
+ },
55
+ "args": {
56
+ "invoice_id": { "type": "string", "required": true, "max_length": 128 }
57
+ },
58
+ "lookup": { "id_from_arg": "invoice_id" },
59
+ "visible_columns": [
60
+ "id",
61
+ "tenant_id",
62
+ "customer_id",
63
+ "status",
64
+ "balance_cents",
65
+ "late_fee_cents",
66
+ "waiver_reason",
67
+ "credit_requested_cents",
68
+ "credit_reason",
69
+ "credited_cents",
70
+ "updated_at"
71
+ ],
72
+ "evidence": "required",
73
+ "max_rows": 1
74
+ },
75
+ {
76
+ "name": "billing.propose_late_fee_waiver",
77
+ "kind": "proposal",
78
+ "source": "billing_postgres",
79
+ "context": "local_operator",
80
+ "target": {
81
+ "schema": "public",
82
+ "table": "invoices",
83
+ "primary_key": "id",
84
+ "tenant_key": "tenant_id"
85
+ },
86
+ "args": {
87
+ "invoice_id": { "type": "string", "required": true, "max_length": 128 },
88
+ "reason": { "type": "string", "required": true, "max_length": 500 }
89
+ },
90
+ "lookup": { "id_from_arg": "invoice_id" },
91
+ "visible_columns": [
92
+ "id",
93
+ "tenant_id",
94
+ "customer_id",
95
+ "status",
96
+ "balance_cents",
97
+ "late_fee_cents",
98
+ "waiver_reason",
99
+ "updated_at"
100
+ ],
101
+ "evidence": "required",
102
+ "max_rows": 1,
103
+ "patch": {
104
+ "late_fee_cents": { "fixed": 0 },
105
+ "waiver_reason": { "from_arg": "reason" }
106
+ },
107
+ "allowed_columns": ["late_fee_cents", "waiver_reason"],
108
+ "numeric_bounds": {
109
+ "late_fee_cents": { "minimum": 0, "maximum": 10000 }
110
+ },
111
+ "conflict_guard": { "column": "updated_at" },
112
+ "approval": { "mode": "human", "required_role": "billing_lead" }
113
+ },
114
+ {
115
+ "name": "billing.propose_account_credit",
116
+ "kind": "proposal",
117
+ "source": "billing_postgres",
118
+ "context": "local_operator",
119
+ "executor": "billing_app_handler",
120
+ "target": {
121
+ "schema": "public",
122
+ "table": "invoices",
123
+ "primary_key": "id",
124
+ "tenant_key": "tenant_id"
125
+ },
126
+ "args": {
127
+ "invoice_id": { "type": "string", "required": true, "max_length": 128 },
128
+ "amount_cents": { "type": "number", "required": true, "minimum": 1, "maximum": 10000 },
129
+ "reason": { "type": "string", "required": true, "max_length": 500 }
130
+ },
131
+ "lookup": { "id_from_arg": "invoice_id" },
132
+ "visible_columns": [
133
+ "id",
134
+ "tenant_id",
135
+ "customer_id",
136
+ "status",
137
+ "balance_cents",
138
+ "credited_cents",
139
+ "credit_requested_cents",
140
+ "credit_reason",
141
+ "updated_at"
142
+ ],
143
+ "evidence": "required",
144
+ "max_rows": 1,
145
+ "patch": {
146
+ "credit_requested_cents": { "from_arg": "amount_cents" },
147
+ "credit_reason": { "from_arg": "reason" }
148
+ },
149
+ "allowed_columns": ["credit_requested_cents", "credit_reason"],
150
+ "numeric_bounds": {
151
+ "credit_requested_cents": { "minimum": 1, "maximum": 10000 }
152
+ },
153
+ "conflict_guard": { "column": "updated_at" },
154
+ "approval": { "mode": "human", "required_role": "billing_lead" }
155
+ }
156
+ ]
157
+ }
@@ -12,6 +12,11 @@ spec-compatible HTTP MCP transport with `initialize` and session behavior. Use
12
12
  `synapsor-runner mcp serve-http` only when you intentionally want the smaller
13
13
  JSON-RPC bridge and an app-owned wrapper.
14
14
 
15
+ OpenAI function names cannot contain dots, so start Runner with
16
+ `--alias-mode openai`. The model sees aliases such as
17
+ `billing__inspect_invoice`; Runner keeps `billing.inspect_invoice` in MCP
18
+ metadata and maps calls back to the canonical Synapsor capability.
19
+
15
20
  The model still sees a semantic action. It does not receive raw SQL, database
16
21
  URLs, write credentials, approval tools, or commit tools.
17
22
 
@@ -26,7 +31,8 @@ export SYNAPSOR_RUNNER_HTTP_TOKEN="dev-token"
26
31
  npx -y -p @synapsor/runner@alpha synapsor-runner mcp serve-streamable-http \
27
32
  --config ./synapsor.runner.json \
28
33
  --store ./.synapsor/local.db \
29
- --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
34
+ --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN \
35
+ --alias-mode openai
30
36
  ```
31
37
 
32
38
  ## Terminal 2: Run The Agent
@@ -46,7 +52,9 @@ python agent.py
46
52
 
47
53
  Expected behavior:
48
54
 
49
- - the agent calls `billing.inspect_invoice` through Synapsor HTTP MCP;
55
+ - the agent calls the OpenAI-safe alias `billing__inspect_invoice` through
56
+ Synapsor HTTP MCP;
57
+ - Runner maps that alias back to canonical `billing.inspect_invoice`;
50
58
  - Synapsor applies trusted tenant/principal context from the server process;
51
59
  - the response includes scoped data and evidence handles;
52
60
  - no SQL/write/approval tool is exposed to the model;
@@ -4,7 +4,10 @@ This example shows an OpenAI Agents SDK app launching Synapsor Runner as a
4
4
  local stdio MCP server.
5
5
 
6
6
  Use stdio when the agent process can start the MCP server on the same machine.
7
- The model sees Synapsor semantic tools such as `billing.inspect_invoice`. It
7
+ The model sees Synapsor semantic tools through OpenAI-safe aliases such as
8
+ `billing__inspect_invoice`. Runner keeps the canonical Synapsor capability
9
+ name, such as `billing.inspect_invoice`, in MCP metadata and maps calls back to
10
+ it. The model
8
11
  does not receive raw SQL, database URLs, write credentials, approval tools, or
9
12
  commit tools.
10
13
 
@@ -52,11 +55,12 @@ export SYNAPSOR_INVOICE_ID="INV-3001"
52
55
 
53
56
  Expected behavior:
54
57
 
55
- - the agent can inspect the scoped invoice through Synapsor;
58
+ - the agent can inspect the scoped invoice through Synapsor using an
59
+ OpenAI-safe tool alias;
60
+ - Runner maps the alias back to the canonical Synapsor capability;
56
61
  - the agent cannot run SQL;
57
62
  - the agent cannot approve or commit writes;
58
63
  - evidence/query audit are saved in the local Runner store.
59
64
 
60
65
  If your installed OpenAI Agents SDK does not expose `MCPServerStdio`, update the
61
- SDK or use the HTTP example, which wraps Synapsor HTTP MCP with a small JSON-RPC
62
- client.
66
+ SDK or use the Streamable HTTP example.
@@ -35,6 +35,8 @@ async def main() -> None:
35
35
  config_path,
36
36
  "--store",
37
37
  store_path,
38
+ "--alias-mode",
39
+ "openai",
38
40
  ],
39
41
  "env": {
40
42
  **os.environ,