@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.
- package/CHANGELOG.md +47 -1
- package/README.md +59 -3
- package/THREAT_MODEL.md +29 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/local-ui.d.ts +3 -0
- package/dist/local-ui.d.ts.map +1 -1
- package/dist/runner.mjs +6587 -918
- package/docs/README.md +16 -3
- package/docs/capability-authoring.md +2 -1
- package/docs/conformance.md +5 -0
- package/docs/current-scope.md +6 -2
- package/docs/dsl-reference.md +25 -0
- package/docs/http-mcp.md +45 -1
- package/docs/limitations.md +22 -7
- package/docs/migrating-to-synapsor-spec.md +21 -0
- package/docs/oss-vs-cloud.md +4 -3
- package/docs/production.md +219 -7
- package/docs/release-notes.md +47 -7
- package/docs/release-policy.md +58 -6
- package/docs/runner-config-reference.md +208 -5
- package/docs/running-a-runner-fleet.md +349 -0
- package/examples/runner-fleet/Dockerfile +11 -0
- package/examples/runner-fleet/README.md +52 -0
- package/examples/runner-fleet/docker-compose.yml +76 -0
- package/examples/runner-fleet/mint-dev-token.mjs +19 -0
- package/examples/runner-fleet/seed/mysql.sql +20 -0
- package/examples/runner-fleet/seed/postgres.sql +73 -0
- package/examples/runner-fleet/synapsor.runner.json +105 -0
- package/examples/support-plan-credit/README.md +16 -1
- package/examples/support-plan-credit/contract.synapsor.sql +2 -0
- package/examples/support-plan-credit/synapsor.contract.json +15 -0
- package/fixtures/protocol/MANIFEST.json +1 -1
- package/package.json +3 -1
- package/schemas/change-set.v1.schema.json +2 -1
- 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
|
|
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:
|
|
@@ -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": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synapsor/runner",
|
|
3
|
-
"version": "
|
|
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": {
|