@synapsor/runner 0.1.0-alpha.1 → 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 (66) hide show
  1. package/README.md +426 -19
  2. package/TRADEMARKS.md +23 -0
  3. package/dist/cli.d.ts +4 -0
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +20 -8723
  6. package/dist/runner.mjs +12958 -0
  7. package/docs/README.md +53 -0
  8. package/docs/app-owned-executors.md +21 -0
  9. package/docs/cloud-mode.md +24 -0
  10. package/docs/current-scope.md +24 -0
  11. package/docs/dependency-license-inventory.md +35 -0
  12. package/docs/getting-started-own-database.md +460 -0
  13. package/docs/http-mcp.md +276 -0
  14. package/docs/licensing.md +36 -0
  15. package/docs/limitations.md +95 -0
  16. package/docs/local-mode.md +351 -0
  17. package/docs/mcp-audit.md +152 -0
  18. package/docs/mcp-client-setup.md +270 -0
  19. package/docs/openai-agents-sdk.md +57 -0
  20. package/docs/recipes.md +61 -0
  21. package/docs/release-notes.md +158 -0
  22. package/docs/security-boundary.md +94 -0
  23. package/docs/troubleshooting-first-run.md +248 -0
  24. package/docs/use-your-own-database.md +18 -0
  25. package/docs/writeback-executors.md +220 -0
  26. package/examples/app-owned-writeback/README.md +120 -0
  27. package/examples/app-owned-writeback/business-actions.md +221 -0
  28. package/examples/app-owned-writeback/command-handler.mjs +46 -0
  29. package/examples/app-owned-writeback/node-fastify-handler.mjs +55 -0
  30. package/examples/app-owned-writeback/python-fastapi-handler.py +57 -0
  31. package/examples/dangerous-mcp-tools.json +88 -0
  32. package/examples/mcp-postgres-billing-app-handler/README.md +82 -0
  33. package/examples/mcp-postgres-billing-app-handler/app-handler.mjs +197 -0
  34. package/examples/mcp-postgres-billing-app-handler/docker-compose.yml +13 -0
  35. package/examples/mcp-postgres-billing-app-handler/schema.sql +59 -0
  36. package/examples/mcp-postgres-billing-app-handler/scripts/run-demo.sh +99 -0
  37. package/examples/mcp-postgres-billing-app-handler/seed.sql +39 -0
  38. package/examples/mcp-postgres-billing-app-handler/synapsor.runner.json +157 -0
  39. package/examples/openai-agents-http/README.md +64 -0
  40. package/examples/openai-agents-http/agent.py +54 -0
  41. package/examples/openai-agents-http/requirements.txt +1 -0
  42. package/examples/openai-agents-stdio/README.md +66 -0
  43. package/examples/openai-agents-stdio/agent.py +72 -0
  44. package/examples/openai-agents-stdio/requirements.txt +1 -0
  45. package/examples/reference-support-billing-app/README.md +137 -0
  46. package/examples/reference-support-billing-app/docker-compose.yml +13 -0
  47. package/examples/reference-support-billing-app/mcp-client.generic.json +11 -0
  48. package/examples/reference-support-billing-app/schema.sql +68 -0
  49. package/examples/reference-support-billing-app/scripts/run-demo.sh +7 -0
  50. package/examples/reference-support-billing-app/seed.sql +33 -0
  51. package/examples/reference-support-billing-app/synapsor.runner.json +241 -0
  52. package/fixtures/benchmark/mcp-efficiency.json +53 -0
  53. package/fixtures/benchmark/mcp-efficiency.txt +25 -0
  54. package/fixtures/protocol/MANIFEST.json +54 -0
  55. package/fixtures/protocol/change-set.late-fee-waiver.v1.json +72 -0
  56. package/fixtures/protocol/execution-receipt.applied.v1.json +14 -0
  57. package/fixtures/protocol/execution-receipt.conflict.v1.json +15 -0
  58. package/fixtures/protocol/runner-registration.v1.json +22 -0
  59. package/fixtures/protocol/writeback-job.late-fee-waiver.v1.json +44 -0
  60. package/package.json +14 -4
  61. package/recipes/accounts.trial_extension.json +42 -0
  62. package/recipes/billing.late_fee_waiver.json +46 -0
  63. package/recipes/credits.account_credit.json +45 -0
  64. package/recipes/orders.refund_review.json +57 -0
  65. package/recipes/support.ticket_resolution.json +51 -0
  66. package/dist/bin.cjs +0 -13
@@ -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
+ }
@@ -0,0 +1,64 @@
1
+ # OpenAI Agents SDK + Synapsor Runner Streamable HTTP MCP
2
+
3
+ This example shows an OpenAI Agents SDK app connecting to a long-running
4
+ Synapsor Runner Streamable HTTP MCP server.
5
+
6
+ Use HTTP when your agent runs as an app/server and should connect to Runner
7
+ over a local/private network endpoint instead of launching a stdio child
8
+ process.
9
+
10
+ This example uses `synapsor-runner mcp serve-streamable-http`, the
11
+ spec-compatible HTTP MCP transport with `initialize` and session behavior. Use
12
+ `synapsor-runner mcp serve-http` only when you intentionally want the smaller
13
+ JSON-RPC bridge and an app-owned wrapper.
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
+
20
+ The model still sees a semantic action. It does not receive raw SQL, database
21
+ URLs, write credentials, approval tools, or commit tools.
22
+
23
+ ## Terminal 1: Start Synapsor Runner HTTP MCP
24
+
25
+ ```bash
26
+ export DATABASE_URL="<postgres-or-mysql-read-url>"
27
+ export SYNAPSOR_TENANT_ID="acme"
28
+ export SYNAPSOR_PRINCIPAL="openai_agent_demo"
29
+ export SYNAPSOR_RUNNER_HTTP_TOKEN="dev-token"
30
+
31
+ npx -y -p @synapsor/runner@alpha synapsor-runner mcp serve-streamable-http \
32
+ --config ./synapsor.runner.json \
33
+ --store ./.synapsor/local.db \
34
+ --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN \
35
+ --alias-mode openai
36
+ ```
37
+
38
+ ## Terminal 2: Run The Agent
39
+
40
+ ```bash
41
+ python -m venv .venv
42
+ . .venv/bin/activate
43
+ pip install -r requirements.txt
44
+
45
+ export OPENAI_API_KEY="..."
46
+ export SYNAPSOR_RUNNER_HTTP_URL="http://127.0.0.1:8766/mcp"
47
+ export SYNAPSOR_RUNNER_HTTP_TOKEN="dev-token"
48
+ export SYNAPSOR_INVOICE_ID="INV-3001"
49
+
50
+ python agent.py
51
+ ```
52
+
53
+ Expected behavior:
54
+
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`;
58
+ - Synapsor applies trusted tenant/principal context from the server process;
59
+ - the response includes scoped data and evidence handles;
60
+ - no SQL/write/approval tool is exposed to the model;
61
+ - evidence/query audit are saved in the local Runner store.
62
+
63
+ For production-like deployment, keep HTTP MCP behind private networking/TLS,
64
+ bearer auth, and rate limits. See [HTTP MCP](../../docs/http-mcp.md).