@synapsor/runner 0.1.16 → 1.1.0

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 (35) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/README.md +59 -3
  3. package/THREAT_MODEL.md +29 -2
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/local-ui.d.ts +3 -0
  6. package/dist/local-ui.d.ts.map +1 -1
  7. package/dist/runner.mjs +6587 -918
  8. package/docs/README.md +16 -3
  9. package/docs/capability-authoring.md +2 -1
  10. package/docs/conformance.md +5 -0
  11. package/docs/current-scope.md +6 -2
  12. package/docs/dsl-reference.md +25 -0
  13. package/docs/http-mcp.md +45 -1
  14. package/docs/limitations.md +22 -7
  15. package/docs/migrating-to-synapsor-spec.md +21 -0
  16. package/docs/oss-vs-cloud.md +4 -3
  17. package/docs/production.md +219 -7
  18. package/docs/release-notes.md +47 -7
  19. package/docs/release-policy.md +58 -6
  20. package/docs/runner-config-reference.md +208 -5
  21. package/docs/running-a-runner-fleet.md +349 -0
  22. package/examples/runner-fleet/Dockerfile +11 -0
  23. package/examples/runner-fleet/README.md +52 -0
  24. package/examples/runner-fleet/docker-compose.yml +76 -0
  25. package/examples/runner-fleet/mint-dev-token.mjs +19 -0
  26. package/examples/runner-fleet/seed/mysql.sql +20 -0
  27. package/examples/runner-fleet/seed/postgres.sql +73 -0
  28. package/examples/runner-fleet/synapsor.runner.json +105 -0
  29. package/examples/support-plan-credit/README.md +16 -1
  30. package/examples/support-plan-credit/contract.synapsor.sql +2 -0
  31. package/examples/support-plan-credit/synapsor.contract.json +15 -0
  32. package/fixtures/protocol/MANIFEST.json +1 -1
  33. package/package.json +3 -1
  34. package/schemas/change-set.v1.schema.json +2 -1
  35. package/schemas/synapsor.runner.schema.json +208 -0
@@ -0,0 +1,11 @@
1
+ FROM node:22-bookworm-slim
2
+
3
+ RUN corepack enable && corepack prepare pnpm@10.14.0 --activate
4
+
5
+ WORKDIR /app
6
+ COPY . .
7
+ RUN corepack pnpm install --frozen-lockfile \
8
+ && corepack pnpm build:runner-package
9
+
10
+ ENTRYPOINT ["node", "apps/runner/dist/cli.js"]
11
+ CMD ["mcp", "serve", "--transport", "streamable-http", "--host", "0.0.0.0", "--port", "8766", "--config", "/config/synapsor.runner.json"]
@@ -0,0 +1,52 @@
1
+ # Two-Runner Fleet Fixture
2
+
3
+ This synthetic fixture starts Postgres, MySQL, and two stateless Streamable
4
+ HTTP Runner services. Both Runners share one bounded Postgres runtime ledger.
5
+ The committed passwords and HS256 key are disposable local-demo values. Never
6
+ reuse them outside this fixture.
7
+
8
+ From the repository root:
9
+
10
+ ```bash
11
+ corepack pnpm install
12
+ docker compose --profile fleet -f examples/runner-fleet/docker-compose.yml up --build -d --wait
13
+ docker compose --profile fleet -f examples/runner-fleet/docker-compose.yml ps
14
+ ```
15
+
16
+ Check both instances:
17
+
18
+ ```bash
19
+ curl --fail http://127.0.0.1:8871/healthz
20
+ curl --fail http://127.0.0.1:8871/readyz
21
+ curl --fail http://127.0.0.1:8872/readyz
22
+ ```
23
+
24
+ The MCP endpoint requires a claim-bearing development JWT. Generate one for
25
+ the fixture only:
26
+
27
+ ```bash
28
+ node examples/runner-fleet/mint-dev-token.mjs acme local-agent
29
+ ```
30
+
31
+ The production path should use `jwt_asymmetric` with an explicit RS256/ES256
32
+ allowlist and a trusted JWKS URL or public PEM. See
33
+ [Running A Runner Fleet](../../docs/running-a-runner-fleet.md).
34
+
35
+ Run the stronger automated verification instead of treating a green Compose
36
+ status as proof:
37
+
38
+ ```bash
39
+ corepack pnpm test:fleet
40
+ ```
41
+
42
+ That test launches two source-tree Runners, uses asymmetric claim-bound
43
+ sessions and concurrent signed reviewers, starts competing workers, kills
44
+ workers before, during, and after writeback, and verifies readiness,
45
+ dead-letter recovery, idempotency, pool pressure, backup/restore, and
46
+ archive-before-retention. It deletes all fixture volumes when complete.
47
+
48
+ Clean up the visual Compose fixture with:
49
+
50
+ ```bash
51
+ docker compose --profile fleet -f examples/runner-fleet/docker-compose.yml down -v --remove-orphans
52
+ ```
@@ -0,0 +1,76 @@
1
+ services:
2
+ postgres:
3
+ image: postgres:16
4
+ environment:
5
+ POSTGRES_DB: synapsor_fleet
6
+ POSTGRES_USER: synapsor_admin
7
+ POSTGRES_PASSWORD: synapsor_admin_password
8
+ ports:
9
+ - "55439:5432"
10
+ healthcheck:
11
+ test: ["CMD-SHELL", "test \"$(psql -U synapsor_admin -d synapsor_fleet -tAc \"SELECT pg_postmaster_start_time() > initialized_at FROM public.synapsor_fixture_ready LIMIT 1\" 2>/dev/null | xargs)\" = t"]
12
+ interval: 1s
13
+ timeout: 3s
14
+ retries: 30
15
+ volumes:
16
+ - ./seed/postgres.sql:/docker-entrypoint-initdb.d/001-schema.sql:ro
17
+
18
+ mysql:
19
+ image: mysql:8
20
+ environment:
21
+ MYSQL_DATABASE: synapsor_fleet
22
+ MYSQL_ROOT_PASSWORD: root_password
23
+ ports:
24
+ - "53309:3306"
25
+ healthcheck:
26
+ test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-proot_password"]
27
+ interval: 2s
28
+ timeout: 5s
29
+ retries: 40
30
+ volumes:
31
+ - ./seed/mysql.sql:/docker-entrypoint-initdb.d/001-schema.sql:ro
32
+
33
+ runner-a:
34
+ profiles: ["fleet"]
35
+ build:
36
+ context: ../..
37
+ dockerfile: examples/runner-fleet/Dockerfile
38
+ command: ["mcp", "serve", "--transport", "streamable-http", "--host", "0.0.0.0", "--port", "8766", "--config", "/config/synapsor.runner.json"]
39
+ depends_on:
40
+ postgres:
41
+ condition: service_healthy
42
+ environment: &runner_environment
43
+ FLEET_POSTGRES_READ_URL: postgresql://synapsor_reader:synapsor_reader_password@postgres:5432/synapsor_fleet
44
+ FLEET_POSTGRES_WRITE_URL: postgresql://synapsor_writer:synapsor_writer_password@postgres:5432/synapsor_fleet
45
+ SYNAPSOR_LEDGER_DATABASE_URL: postgresql://synapsor_admin:synapsor_admin_password@postgres:5432/synapsor_fleet
46
+ SYNAPSOR_SESSION_JWT_SECRET: synthetic-fleet-session-secret-change-before-use-0001
47
+ SYNAPSOR_METRICS_TOKEN: synthetic-fleet-metrics-token-change-before-use
48
+ ports:
49
+ - "8871:8766"
50
+ volumes:
51
+ - ./synapsor.runner.json:/config/synapsor.runner.json:ro
52
+ healthcheck:
53
+ test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8766/readyz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
54
+ interval: 2s
55
+ timeout: 5s
56
+ retries: 30
57
+
58
+ runner-b:
59
+ profiles: ["fleet"]
60
+ build:
61
+ context: ../..
62
+ dockerfile: examples/runner-fleet/Dockerfile
63
+ command: ["mcp", "serve", "--transport", "streamable-http", "--host", "0.0.0.0", "--port", "8766", "--config", "/config/synapsor.runner.json"]
64
+ depends_on:
65
+ postgres:
66
+ condition: service_healthy
67
+ environment: *runner_environment
68
+ ports:
69
+ - "8872:8766"
70
+ volumes:
71
+ - ./synapsor.runner.json:/config/synapsor.runner.json:ro
72
+ healthcheck:
73
+ test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8766/readyz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
74
+ interval: 2s
75
+ timeout: 5s
76
+ retries: 30
@@ -0,0 +1,19 @@
1
+ import crypto from "node:crypto";
2
+
3
+ const tenant = process.argv[2] ?? "acme";
4
+ const subject = process.argv[3] ?? "fleet-demo-agent";
5
+ const secret = "synthetic-fleet-session-secret-change-before-use-0001";
6
+ const now = Math.floor(Date.now() / 1000);
7
+ const header = { alg: "HS256", typ: "JWT" };
8
+ const payload = {
9
+ sub: subject,
10
+ tenant_id: tenant,
11
+ iss: "https://fleet.example.invalid",
12
+ aud: "synapsor-runner-fleet",
13
+ iat: now,
14
+ exp: now + 600,
15
+ };
16
+ const encode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
17
+ const unsigned = `${encode(header)}.${encode(payload)}`;
18
+ const signature = crypto.createHmac("sha256", secret).update(unsigned).digest("base64url");
19
+ process.stdout.write(`${unsigned}.${signature}\n`);
@@ -0,0 +1,20 @@
1
+ CREATE TABLE invoices (
2
+ id varchar(128) PRIMARY KEY,
3
+ tenant_id varchar(128) NOT NULL,
4
+ status varchar(64) NOT NULL,
5
+ late_fee_cents integer NOT NULL,
6
+ waiver_reason varchar(500),
7
+ updated_at datetime(6) NOT NULL
8
+ );
9
+
10
+ INSERT INTO invoices (id, tenant_id, status, late_fee_cents, waiver_reason, updated_at) VALUES
11
+ ('MYSQL-ACME', 'acme', 'overdue', 700, NULL, '2026-07-12 12:00:00.000000');
12
+
13
+ CREATE VIEW slow_invoices AS
14
+ SELECT invoices.*, SLEEP(0.35) AS synthetic_delay
15
+ FROM invoices;
16
+
17
+ CREATE USER IF NOT EXISTS 'synapsor_reader'@'%' IDENTIFIED BY 'synapsor_reader_password';
18
+ GRANT SELECT ON synapsor_fleet.invoices TO 'synapsor_reader'@'%';
19
+ GRANT SELECT ON synapsor_fleet.slow_invoices TO 'synapsor_reader'@'%';
20
+ FLUSH PRIVILEGES;
@@ -0,0 +1,73 @@
1
+ CREATE TABLE public.invoices (
2
+ id text PRIMARY KEY,
3
+ tenant_id text NOT NULL,
4
+ status text NOT NULL,
5
+ late_fee_cents integer NOT NULL,
6
+ waiver_reason text,
7
+ updated_at timestamptz NOT NULL
8
+ );
9
+
10
+ CREATE TABLE public.synapsor_writeback_receipts (
11
+ idempotency_key text PRIMARY KEY,
12
+ job_id text UNIQUE NOT NULL,
13
+ proposal_id text NOT NULL,
14
+ status text NOT NULL,
15
+ result_hash text,
16
+ created_at timestamptz NOT NULL DEFAULT now(),
17
+ completed_at timestamptz
18
+ );
19
+
20
+ CREATE TABLE public.synthetic_handler_receipts (
21
+ idempotency_key text PRIMARY KEY,
22
+ proposal_id text NOT NULL,
23
+ object_id text NOT NULL,
24
+ status text NOT NULL,
25
+ created_at timestamptz NOT NULL DEFAULT now()
26
+ );
27
+
28
+ INSERT INTO public.invoices (id, tenant_id, status, late_fee_cents, waiver_reason, updated_at) VALUES
29
+ ('INV-ACME', 'acme', 'overdue', 2500, NULL, '2026-07-12T12:00:00Z'),
30
+ ('INV-GLOBEX', 'globex', 'overdue', 3100, NULL, '2026-07-12T12:00:00Z'),
31
+ ('INV-RATECO', 'rateco', 'overdue', 900, NULL, '2026-07-12T12:00:00Z'),
32
+ ('INV-QUORUM-RACE', 'acme', 'overdue', 1400, NULL, '2026-07-12T12:00:00Z'),
33
+ ('INV-WORKER-RACE', 'acme', 'overdue', 1600, NULL, '2026-07-12T12:00:00Z'),
34
+ ('INV-DEAD-REQUEUE', 'acme', 'overdue', 1700, NULL, '2026-07-12T12:00:00Z'),
35
+ ('INV-DEAD-DISCARD', 'acme', 'overdue', 1750, NULL, '2026-07-12T12:00:00Z'),
36
+ ('INV-KILL-BEFORE', 'acme', 'overdue', 1200, NULL, '2026-07-12T12:00:00Z'),
37
+ ('INV-KILL-DURING', 'acme', 'overdue', 1500, NULL, '2026-07-12T12:00:00Z'),
38
+ ('INV-KILL-AFTER', 'acme', 'overdue', 1800, NULL, '2026-07-12T12:00:00Z');
39
+
40
+ CREATE FUNCTION public.synthetic_pool_delay() RETURNS integer
41
+ LANGUAGE plpgsql VOLATILE AS $$
42
+ BEGIN
43
+ PERFORM pg_sleep(0.15);
44
+ RETURN 0;
45
+ END
46
+ $$;
47
+
48
+ CREATE VIEW public.slow_invoices AS
49
+ SELECT invoices.*, public.synthetic_pool_delay() AS synthetic_delay
50
+ FROM public.invoices;
51
+
52
+ DO $$
53
+ BEGIN
54
+ CREATE ROLE synapsor_reader LOGIN PASSWORD 'synapsor_reader_password';
55
+ CREATE ROLE synapsor_writer LOGIN PASSWORD 'synapsor_writer_password';
56
+ EXCEPTION WHEN duplicate_object THEN NULL;
57
+ END
58
+ $$;
59
+
60
+ GRANT CONNECT ON DATABASE synapsor_fleet TO synapsor_reader, synapsor_writer;
61
+ GRANT USAGE ON SCHEMA public TO synapsor_reader, synapsor_writer;
62
+ GRANT SELECT ON public.invoices TO synapsor_reader, synapsor_writer;
63
+ GRANT SELECT ON public.slow_invoices TO synapsor_reader;
64
+ GRANT UPDATE (late_fee_cents, waiver_reason, updated_at) ON public.invoices TO synapsor_writer;
65
+ GRANT SELECT, INSERT, UPDATE ON public.synapsor_writeback_receipts TO synapsor_writer;
66
+
67
+ -- Docker's Postgres entrypoint briefly starts a temporary postmaster while it
68
+ -- runs init scripts. The Compose health check compares this timestamp with the
69
+ -- current postmaster start time so dependents wait for the final server.
70
+ CREATE TABLE public.synapsor_fixture_ready (
71
+ initialized_at timestamptz NOT NULL
72
+ );
73
+ INSERT INTO public.synapsor_fixture_ready (initialized_at) VALUES (clock_timestamp());
@@ -0,0 +1,105 @@
1
+ {
2
+ "version": 1,
3
+ "mode": "review",
4
+ "storage": {
5
+ "shared_postgres": {
6
+ "mode": "runtime_store",
7
+ "url_env": "SYNAPSOR_LEDGER_DATABASE_URL",
8
+ "schema": "synapsor_runner",
9
+ "lock_timeout_ms": 5000,
10
+ "max_entries": 5000
11
+ }
12
+ },
13
+ "sources": {
14
+ "fleet_postgres": {
15
+ "engine": "postgres",
16
+ "read_url_env": "FLEET_POSTGRES_READ_URL",
17
+ "write_url_env": "FLEET_POSTGRES_WRITE_URL",
18
+ "statement_timeout_ms": 3000,
19
+ "pool": {
20
+ "max_connections": 2,
21
+ "connection_timeout_ms": 2000,
22
+ "idle_timeout_ms": 30000,
23
+ "queue_timeout_ms": 1000,
24
+ "queue_limit": 8
25
+ }
26
+ }
27
+ },
28
+ "trusted_context": {
29
+ "provider": "http_claims",
30
+ "values": {
31
+ "tenant_id_key": "tenant_id",
32
+ "principal_key": "sub"
33
+ }
34
+ },
35
+ "session_auth": {
36
+ "provider": "jwt_hs256",
37
+ "secret_env": "SYNAPSOR_SESSION_JWT_SECRET",
38
+ "issuer": "https://fleet.example.invalid",
39
+ "audience": "synapsor-runner-fleet",
40
+ "tenant_claim": "tenant_id",
41
+ "principal_claim": "sub",
42
+ "clock_skew_seconds": 10
43
+ },
44
+ "rate_limits": {
45
+ "enabled": true,
46
+ "default": { "requests": 20, "window_seconds": 60 },
47
+ "capabilities": {
48
+ "billing.inspect_invoice": { "requests": 3, "window_seconds": 60 }
49
+ }
50
+ },
51
+ "metrics": {
52
+ "enabled": true,
53
+ "token_env": "SYNAPSOR_METRICS_TOKEN"
54
+ },
55
+ "capabilities": [
56
+ {
57
+ "name": "billing.inspect_invoice",
58
+ "kind": "read",
59
+ "source": "fleet_postgres",
60
+ "target": {
61
+ "schema": "public",
62
+ "table": "invoices",
63
+ "primary_key": "id",
64
+ "tenant_key": "tenant_id"
65
+ },
66
+ "args": {
67
+ "invoice_id": { "type": "string", "required": true, "max_length": 128 }
68
+ },
69
+ "lookup": { "id_from_arg": "invoice_id" },
70
+ "visible_columns": ["id", "tenant_id", "status", "late_fee_cents", "waiver_reason", "updated_at"],
71
+ "evidence": "required",
72
+ "max_rows": 1
73
+ },
74
+ {
75
+ "name": "billing.propose_late_fee_waiver",
76
+ "kind": "proposal",
77
+ "source": "fleet_postgres",
78
+ "target": {
79
+ "schema": "public",
80
+ "table": "invoices",
81
+ "primary_key": "id",
82
+ "tenant_key": "tenant_id"
83
+ },
84
+ "args": {
85
+ "invoice_id": { "type": "string", "required": true, "max_length": 128 },
86
+ "reason": { "type": "string", "required": true, "max_length": 500 }
87
+ },
88
+ "lookup": { "id_from_arg": "invoice_id" },
89
+ "visible_columns": ["id", "tenant_id", "status", "late_fee_cents", "waiver_reason", "updated_at"],
90
+ "evidence": "required",
91
+ "max_rows": 1,
92
+ "patch": {
93
+ "late_fee_cents": { "fixed": 0 },
94
+ "waiver_reason": { "from_arg": "reason" }
95
+ },
96
+ "allowed_columns": ["late_fee_cents", "waiver_reason"],
97
+ "conflict_guard": { "column": "updated_at" },
98
+ "approval": {
99
+ "mode": "human",
100
+ "required_role": "billing_lead",
101
+ "required_approvals": 2
102
+ }
103
+ }
104
+ ]
105
+ }
@@ -196,10 +196,15 @@ The auto-approval policy lives in `contract.synapsor.sql`:
196
196
 
197
197
  ```sql
198
198
  AUTO APPROVE WHEN plan_credit_cents <= 2500
199
+ LIMIT 20 PER DAY
200
+ LIMIT TOTAL 100000 PER DAY
199
201
  ```
200
202
 
201
203
  That compiles into `synapsor.contract.json` as a reviewed approval policy. It is
202
- stored in git, validated by `@synapsor/spec`, and included in Cloud push payloads.
204
+ stored in git, validated by `@synapsor/spec`, and included in Cloud push
205
+ payloads. The policy may approve at most 20 qualifying credits or 100,000 cents
206
+ in total per trusted tenant and UTC day. The first ceiling reached leaves later
207
+ proposals in `pending_review` and records why the policy deferred to a human.
203
208
 
204
209
  To disable local policy approval without changing the contract:
205
210
 
@@ -214,6 +219,16 @@ To disable local policy approval without changing the contract:
214
219
  Auto-approval never applies the write. It only records an approval row and audit
215
220
  event with actor `policy:<policy_name>`.
216
221
 
222
+ After reviewing the ceilings and running `doctor`, an operator can drain the
223
+ approved queue without letting one stale-row conflict abort the remaining jobs:
224
+
225
+ ```bash
226
+ synapsor-runner apply --all-approved --yes \
227
+ --capability support.propose_plan_credit --tenant acme --max 20 \
228
+ --config examples/support-plan-credit/synapsor.runner.json \
229
+ --store ./tmp/support-plan-credit/local.db
230
+ ```
231
+
217
232
  ## Cloud
218
233
 
219
234
  Dry-run the Cloud payload:
@@ -46,6 +46,8 @@ CREATE CAPABILITY support.propose_plan_credit
46
46
  BOUND plan_credit_cents 1..50000
47
47
  APPROVAL ROLE support_reviewer
48
48
  AUTO APPROVE WHEN plan_credit_cents <= 2500
49
+ LIMIT 20 PER DAY
50
+ LIMIT TOTAL 100000 PER DAY
49
51
  WRITEBACK DIRECT SQL
50
52
  END
51
53
 
@@ -166,6 +166,21 @@
166
166
  "policies": [
167
167
  {
168
168
  "kind": "approval",
169
+ "limits": [
170
+ {
171
+ "kind": "count",
172
+ "max": 20,
173
+ "period": "day",
174
+ "scope": "tenant_policy"
175
+ },
176
+ {
177
+ "field": "plan_credit_cents",
178
+ "kind": "total",
179
+ "max": 100000,
180
+ "period": "day",
181
+ "scope": "tenant_policy"
182
+ }
183
+ ],
169
184
  "mode": "green",
170
185
  "name": "support_propose_plan_credit_auto_approval",
171
186
  "rules": [
@@ -33,7 +33,7 @@
33
33
  {
34
34
  "kind": "schema",
35
35
  "name": "change-set.v1.schema.json",
36
- "sha256": "4e52b369b0d3bea30b0ed7b61ec239fc67de3dc691ed4014d6d5eabc7d2be4de"
36
+ "sha256": "119ac84f7189e9433d4eaac4defb6ef7c459a14e55e1563aa7948b746d816af5"
37
37
  },
38
38
  {
39
39
  "kind": "schema",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synapsor/runner",
3
- "version": "0.1.16",
3
+ "version": "1.1.0",
4
4
  "description": "Stop giving AI agents execute_sql; expose reviewed Postgres/MySQL MCP actions with proposals, approval, writeback, and replay.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -25,6 +25,7 @@
25
25
  "examples/openai-agents-stdio/**",
26
26
  "examples/raw-sql-vs-synapsor/**",
27
27
  "examples/reference-support-billing-app/**",
28
+ "examples/runner-fleet/**",
28
29
  "examples/support-plan-credit/**",
29
30
  "examples/support-billing-agent/**",
30
31
  "fixtures/**",
@@ -58,6 +59,7 @@
58
59
  },
59
60
  "dependencies": {
60
61
  "@modelcontextprotocol/sdk": "1.29.0",
62
+ "jose": "6.2.3",
61
63
  "mysql2": "^3.11.0",
62
64
  "pg": "^8.13.0",
63
65
  "zod": "^3.25.0"
@@ -95,7 +95,8 @@
95
95
  "required": ["status"],
96
96
  "properties": {
97
97
  "status": { "enum": ["pending", "approved", "rejected", "canceled"] },
98
- "required_role": { "type": "string" }
98
+ "required_role": { "type": "string" },
99
+ "required_approvals": { "type": "integer", "minimum": 1, "maximum": 10 }
99
100
  }
100
101
  },
101
102
  "writeback": {