@synapsor/runner 1.5.0 → 1.5.4

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 (83) hide show
  1. package/CHANGELOG.md +134 -3
  2. package/README.md +108 -113
  3. package/dist/authoring.d.ts +23 -0
  4. package/dist/authoring.d.ts.map +1 -0
  5. package/dist/authoring.mjs +1318 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +4 -5
  8. package/dist/local-ui.d.ts +14 -0
  9. package/dist/local-ui.d.ts.map +1 -1
  10. package/dist/runner.mjs +12334 -5256
  11. package/dist/runtime.mjs +817 -149
  12. package/dist/shadow.d.ts +36 -0
  13. package/dist/shadow.d.ts.map +1 -0
  14. package/dist/shadow.mjs +5362 -0
  15. package/docs/README.md +25 -2
  16. package/docs/alternatives.md +122 -0
  17. package/docs/capability-authoring.md +49 -1
  18. package/docs/client-recipes.md +218 -0
  19. package/docs/cloud-mode.md +18 -0
  20. package/docs/contract-testing.md +9 -7
  21. package/docs/cursor-plugin.md +78 -0
  22. package/docs/database-enforced-scope.md +8 -0
  23. package/docs/dsl-reference.md +45 -4
  24. package/docs/effect-regression.md +39 -2
  25. package/docs/fresh-developer-usability.md +110 -0
  26. package/docs/getting-started-own-database.md +102 -5
  27. package/docs/guarded-crud-writeback.md +10 -1
  28. package/docs/host-compatibility.md +59 -0
  29. package/docs/http-mcp.md +222 -207
  30. package/docs/limitations.md +9 -2
  31. package/docs/local-mode.md +11 -7
  32. package/docs/mcp-audit.md +166 -2
  33. package/docs/mcp-client-setup.md +28 -11
  34. package/docs/mcp-clients.md +43 -8
  35. package/docs/openai-agents-sdk.md +16 -2
  36. package/docs/oss-vs-cloud.md +3 -0
  37. package/docs/production.md +72 -7
  38. package/docs/release-notes.md +131 -4
  39. package/docs/runner-bundles.md +7 -2
  40. package/docs/runner-config-reference.md +96 -6
  41. package/docs/running-a-runner-fleet.md +42 -13
  42. package/docs/security-boundary.md +58 -8
  43. package/docs/shadow-studies.md +47 -4
  44. package/docs/store-lifecycle.md +93 -5
  45. package/docs/troubleshooting-first-run.md +46 -0
  46. package/examples/openai-agents-http/README.md +10 -4
  47. package/examples/openai-agents-http/agent.py +2 -2
  48. package/examples/runner-fleet/Dockerfile +7 -2
  49. package/examples/runner-fleet/README.md +11 -7
  50. package/examples/runner-fleet/docker-compose.yml +4 -4
  51. package/examples/runner-fleet/mint-dev-token.mjs +1 -1
  52. package/examples/runner-fleet/start-tls-runner.sh +21 -0
  53. package/examples/runner-fleet/synapsor.runner.json +27 -1
  54. package/examples/support-billing-agent/README.md +18 -0
  55. package/examples/support-billing-agent/app/README.md +6 -0
  56. package/examples/support-billing-agent/app/contract.ts +28 -0
  57. package/examples/support-billing-agent/app/effect-adapter.mjs +23 -0
  58. package/examples/support-billing-agent/app/record-shadow-outcomes.mjs +44 -0
  59. package/examples/support-billing-agent/scripts/run-evaluation.sh +6 -5
  60. package/examples/support-plan-credit/README.md +54 -3
  61. package/examples/support-plan-credit/mcp-client-examples/README.md +23 -0
  62. package/examples/support-plan-credit/mcp-client-examples/claude-code.sh +34 -0
  63. package/examples/support-plan-credit/mcp-client-examples/codex.config.toml +24 -0
  64. package/examples/support-plan-credit/mcp-client-examples/generic-stdio.mjs +35 -0
  65. package/examples/support-plan-credit/mcp-client-examples/generic-streamable-http.json +4 -1
  66. package/examples/support-plan-credit/mcp-client-examples/generic-streamable-http.mjs +31 -0
  67. package/examples/support-plan-credit/mcp-client-examples/google-adk.py +40 -0
  68. package/examples/support-plan-credit/mcp-client-examples/langchain.mjs +29 -0
  69. package/examples/support-plan-credit/mcp-client-examples/llamaindex.py +36 -0
  70. package/examples/support-plan-credit/mcp-client-examples/openai-agents-stdio.ts +1 -1
  71. package/examples/support-plan-credit/mcp-client-examples/openai-agents-streamable-http.ts +5 -1
  72. package/examples/support-plan-credit/mcp-client-examples/vscode.mcp.json +20 -0
  73. package/examples/support-plan-credit/synapsor/actions/support.propose_plan_credit.contract-tests.generated.json +221 -0
  74. package/examples/support-plan-credit/synapsor/actions/support.propose_plan_credit.ts +34 -0
  75. package/examples/support-plan-credit/synapsor.contract.json +0 -3
  76. package/fixtures/mcp-audit/README.md +14 -0
  77. package/fixtures/mcp-audit/cursor-bypass-config.json +38 -0
  78. package/fixtures/mcp-audit/dangerous-tools-list.json +33 -0
  79. package/fixtures/mcp-audit/reviewed-proposal-tools-list.json +60 -0
  80. package/package.json +18 -3
  81. package/schemas/mcp-audit-report.schema.json +100 -1
  82. package/schemas/synapsor.contract-tests.schema.json +1 -1
  83. package/schemas/synapsor.runner.schema.json +80 -0
@@ -33,20 +33,108 @@ sqlite3 -readonly ./.synapsor/local.db '.tables'
33
33
  Do not mutate the database directly. Its internal tables may change between
34
34
  releases and are not a public storage API; use Runner commands for automation.
35
35
 
36
- ## Inspect the ledger
36
+ ## Inspect one complete lifecycle
37
37
 
38
- | Question | Command |
38
+ Start with no id:
39
+
40
+ ```bash
41
+ synapsor-runner lifecycle --store ./.synapsor/local.db
42
+ ```
43
+
44
+ `lifecycle`, `lifecycle show`, and `lifecycle show latest` are exact aliases.
45
+ They choose the newest proposal deterministically by `created_at` descending,
46
+ then proposal id as the tie-breaker. The default view answers:
47
+
48
+ - what capability and business object the model requested;
49
+ - which trusted tenant and principal scoped it;
50
+ - approval state and progress;
51
+ - whether a writeback job or intent exists;
52
+ - the latest guarded outcome and whether the source changed;
53
+ - replay and Cloud-link status; and
54
+ - the next safe read-only or operator command.
55
+
56
+ Use `--details` for one causal proposal-to-receipt/replay timeline, or `--json`
57
+ for one stable `synapsor.lifecycle-view.v1` JSON document:
58
+
59
+ ```bash
60
+ synapsor-runner lifecycle --details --store ./.synapsor/local.db
61
+ synapsor-runner lifecycle --json --store ./.synapsor/local.db
62
+ ```
63
+
64
+ The JSON is a domain view, not a dump of internal SQLite/PostgreSQL tables.
65
+ Absent stages are represented explicitly as empty arrays or `null`; Runner
66
+ does not fabricate a job, receipt, replay, or Cloud synchronization.
67
+
68
+ ### Browse and filter without copying ids
69
+
70
+ ```bash
71
+ synapsor-runner lifecycle list --limit 20 --store ./.synapsor/local.db
72
+ synapsor-runner lifecycle show --object invoice:INV-3001 --details --store ./.synapsor/local.db
73
+ synapsor-runner lifecycle list --tenant acme --capability billing.propose_late_fee_waiver
74
+ synapsor-runner lifecycle show --principal support-agent-17 --status applied
75
+ ```
76
+
77
+ List/show support tenant, principal, capability, business object, status/state,
78
+ and time-window filters. Filtered `show` selects the newest match and reports
79
+ the total match count; use `lifecycle list` to browse all matches. List JSON
80
+ uses `synapsor.lifecycle-list.v1`.
81
+
82
+ ### Start from any handle you already have
83
+
84
+ ```bash
85
+ synapsor-runner lifecycle show wrp_... --details
86
+ synapsor-runner lifecycle show ev_... --details
87
+ synapsor-runner lifecycle show replay_... --details
88
+ synapsor-runner lifecycle show wbj_... --details
89
+ synapsor-runner lifecycle show wbi:wbj_... --details
90
+ synapsor-runner lifecycle show receipt:42 --details
91
+ synapsor-runner lifecycle show audit:17 --details
92
+ ```
93
+
94
+ Runner resolves proposal, evidence, replay, writeback-job, writeback-intent,
95
+ receipt, and query-audit handles to the owning proposal. Numeric receipt and
96
+ query-audit ids require the `receipt:` or `audit:` namespace; a bare number is
97
+ rejected rather than guessed.
98
+
99
+ Lifecycle inspection is read-only. It does not contact the source database or
100
+ Cloud, create a writeback job, acquire a worker lease, approve, apply, reconcile,
101
+ retry, or synchronize anything. It applies the same tenant/principal visibility
102
+ rules and secret/kept-out-field protections as the focused views.
103
+
104
+ ### Shared PostgreSQL runtime store
105
+
106
+ For `storage.shared_postgres.mode = "runtime_store"`, pass the reviewed config:
107
+
108
+ ```bash
109
+ synapsor-runner lifecycle --config ./synapsor.runner.json
110
+ synapsor-runner lifecycle show --object invoice:INV-3001 --details \
111
+ --config ./synapsor.runner.json
112
+ ```
113
+
114
+ Runner reuses the existing bounded read bridge to the authoritative shared
115
+ ledger. It does not create or synchronize a persistent local mirror during
116
+ inspection. Connection failures fail safely; the command never falls back to an
117
+ unrelated local store.
118
+
119
+ ## Focused inspection commands
120
+
121
+ The existing commands remain useful when you need one record type:
122
+
123
+ | Question | Focused command |
39
124
  | --- | --- |
40
- | What did the model propose? | `synapsor-runner proposals show <proposal-id> --details` |
125
+ | What did the model propose? | `synapsor-runner proposals show latest --details` |
41
126
  | What data supported it? | `synapsor-runner evidence list --proposal <proposal-id>` then `evidence show <evidence-id> --details` |
42
127
  | What query was run? | `synapsor-runner query-audit list --proposal <proposal-id>` |
43
- | Who approved or rejected it? | `synapsor-runner proposals show <proposal-id> --details` |
44
128
  | Did guarded writeback apply? | `synapsor-runner receipts list --proposal <proposal-id>` |
45
- | What happened end to end? | `synapsor-runner replay show --proposal <proposal-id> --details` |
129
+ | What replay snapshot exists? | `synapsor-runner replay show latest --details` |
46
130
  | What happened to one object? | `synapsor-runner activity search --object invoice:INV-3001` |
47
131
  | What are the latest events? | `synapsor-runner events tail` |
48
132
  | How large is the store? | `synapsor-runner store stats --store ./.synapsor/local.db` |
49
133
 
134
+ `proposals writeback-job` is intentionally absent from inspection examples:
135
+ it materializes a job and is therefore an operator mutation, not a read-only
136
+ view.
137
+
50
138
  ## Server leases
51
139
 
52
140
  MCP server modes write a small lease file next to the store:
@@ -12,6 +12,52 @@ Use JSON for automation:
12
12
  npx -y -p @synapsor/runner synapsor-runner doctor --first-run --json
13
13
  ```
14
14
 
15
+ ## Safe Action Draft Does Not Appear As A Tool
16
+
17
+ This is expected before activation. `start --action`, agent edits, `action
18
+ validate`, and `action watch` can create or refresh only a disabled draft. They
19
+ must not alter the active model-facing tools.
20
+
21
+ Check status without exposing credentials:
22
+
23
+ ```bash
24
+ synapsor-runner action status --json
25
+ synapsor-runner tools preview \
26
+ --config ./synapsor.runner.json \
27
+ --store ./.synapsor/local.db
28
+ ```
29
+
30
+ Open the secured Workbench, run the real source-unchanged staging Data PR
31
+ preview, review the complete digest, and activate it there. There is
32
+ intentionally no activation CLI command to hand to a coding agent.
33
+
34
+ ## Activated Tool Does Not Appear In Cursor
35
+
36
+ Some MCP hosts do not refresh `tools/list` for a running stdio session. First
37
+ confirm that Runner's active tool surface changed:
38
+
39
+ ```bash
40
+ synapsor-runner action status --json
41
+ synapsor-runner mcp status cursor --project --check-launch
42
+ ```
43
+
44
+ Then reconnect or restart the project MCP server as directed by the Workbench.
45
+ Do not work around a stale host session by adding approval, apply, activation,
46
+ credentials, tenant values, or raw SQL to the Cursor configuration.
47
+
48
+ ## Safe Action Validation Reports Review Placeholders
49
+
50
+ The composer fails closed while any `__REVIEW_*__` authority placeholder or
51
+ dynamic TypeScript expression remains. Review the reported field and source,
52
+ then make the authority explicit in the restricted `defineCapability({...})`
53
+ object. Runner will not infer trusted tenant/principal bindings, hidden fields,
54
+ write columns, bounds, conflict guards, approval, or executor authority from
55
+ application code.
56
+
57
+ Use the generated explanation and test manifest after validation. Do not edit
58
+ digest-addressed files under `.synapsor/drafts/` or `.synapsor/active/`; edit
59
+ the TypeScript source and validate again.
60
+
15
61
  ## Smoke Proposal Missing From Another Runner
16
62
 
17
63
  What happened:
@@ -40,7 +40,7 @@ OpenAI Agents Streamable HTTP example smoke passed.
40
40
  export DATABASE_URL="<postgres-or-mysql-read-url>"
41
41
  export SYNAPSOR_TENANT_ID="acme"
42
42
  export SYNAPSOR_PRINCIPAL="openai_agent_demo"
43
- export SYNAPSOR_RUNNER_HTTP_TOKEN="dev-token"
43
+ export SYNAPSOR_RUNNER_HTTP_TOKEN="$(node -e 'process.stdout.write(require("node:crypto").randomBytes(32).toString("base64url"))')"
44
44
 
45
45
  npx -y -p @synapsor/runner synapsor-runner mcp serve-streamable-http \
46
46
  --config ./synapsor.runner.json \
@@ -49,6 +49,10 @@ npx -y -p @synapsor/runner synapsor-runner mcp serve-streamable-http \
49
49
  --alias-mode openai
50
50
  ```
51
51
 
52
+ The operator must provision that same opaque value to the authorized client.
53
+ It is endpoint access for this loopback/single-service example, not user or
54
+ tenant identity. Do not put it in source code or the client config.
55
+
52
56
  ## Terminal 2: Run The Agent
53
57
 
54
58
  ```bash
@@ -58,7 +62,7 @@ pip install -r requirements.txt
58
62
 
59
63
  export OPENAI_API_KEY="..."
60
64
  export SYNAPSOR_RUNNER_HTTP_URL="http://127.0.0.1:8766/mcp"
61
- export SYNAPSOR_RUNNER_HTTP_TOKEN="dev-token"
65
+ read -rsp "Provisioned Synapsor endpoint token: " SYNAPSOR_MCP_ACCESS_TOKEN; printf '\n'; export SYNAPSOR_MCP_ACCESS_TOKEN
62
66
  export SYNAPSOR_INVOICE_ID="INV-3001"
63
67
 
64
68
  python agent.py
@@ -74,5 +78,7 @@ Expected behavior:
74
78
  - no SQL/write/approval tool is exposed to the model;
75
79
  - evidence/query audit are saved in the local Runner store.
76
80
 
77
- For production-like deployment, keep HTTP MCP behind private networking/TLS,
78
- bearer auth, and rate limits. See [HTTP MCP](../../docs/http-mcp.md).
81
+ For production-like deployment, non-loopback HTTP requires Runner TLS or an
82
+ explicitly trusted TLS proxy. A shared user/tenant service uses a short-lived
83
+ signed access token from an external identity provider rather than distributing
84
+ one opaque token. See [HTTP MCP](../../docs/http-mcp.md).
@@ -13,14 +13,14 @@ except ImportError as exc:
13
13
 
14
14
 
15
15
  async def main() -> None:
16
- required = ["OPENAI_API_KEY", "SYNAPSOR_RUNNER_HTTP_URL", "SYNAPSOR_RUNNER_HTTP_TOKEN"]
16
+ required = ["OPENAI_API_KEY", "SYNAPSOR_RUNNER_HTTP_URL", "SYNAPSOR_MCP_ACCESS_TOKEN"]
17
17
  missing = [name for name in required if not os.environ.get(name)]
18
18
  if missing:
19
19
  raise SystemExit(f"Missing required environment variables: {', '.join(missing)}")
20
20
 
21
21
  invoice_id = os.environ.get("SYNAPSOR_INVOICE_ID", "INV-3001")
22
22
  mcp_url = os.environ["SYNAPSOR_RUNNER_HTTP_URL"]
23
- token = os.environ["SYNAPSOR_RUNNER_HTTP_TOKEN"]
23
+ token = os.environ["SYNAPSOR_MCP_ACCESS_TOKEN"]
24
24
 
25
25
  async with MCPServerStreamableHttp(
26
26
  params={
@@ -1,11 +1,16 @@
1
1
  FROM node:22-bookworm-slim
2
2
 
3
+ RUN apt-get update \
4
+ && apt-get install -y --no-install-recommends openssl \
5
+ && rm -rf /var/lib/apt/lists/*
6
+
3
7
  RUN corepack enable && corepack prepare pnpm@10.14.0 --activate
4
8
 
5
9
  WORKDIR /app
6
10
  COPY . .
7
11
  RUN corepack pnpm install --frozen-lockfile \
8
- && corepack pnpm build:runner-package
12
+ && corepack pnpm build:runner-package \
13
+ && chmod +x examples/runner-fleet/start-tls-runner.sh
9
14
 
10
- ENTRYPOINT ["node", "apps/runner/dist/cli.js"]
15
+ ENTRYPOINT ["examples/runner-fleet/start-tls-runner.sh"]
11
16
  CMD ["mcp", "serve", "--transport", "streamable-http", "--host", "0.0.0.0", "--port", "8766", "--config", "/config/synapsor.runner.json"]
@@ -2,8 +2,11 @@
2
2
 
3
3
  This synthetic fixture starts Postgres, MySQL, and two stateless Streamable
4
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.
5
+ Each container generates a one-day self-signed certificate at startup so the
6
+ non-loopback Runner listener exercises direct TLS rather than insecure HTTP.
7
+ The committed database passwords and HS256 key are disposable local-demo
8
+ values. Never reuse them outside this fixture; production should use a trusted
9
+ certificate and asymmetric identity-provider tokens.
7
10
 
8
11
  From the repository root:
9
12
 
@@ -16,9 +19,9 @@ docker compose --profile fleet -f examples/runner-fleet/docker-compose.yml ps
16
19
  Check both instances:
17
20
 
18
21
  ```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
+ curl --fail --insecure https://127.0.0.1:8871/healthz
23
+ curl --fail --insecure https://127.0.0.1:8871/readyz
24
+ curl --fail --insecure https://127.0.0.1:8872/readyz
22
25
  ```
23
26
 
24
27
  The MCP endpoint requires a claim-bearing development JWT. Generate one for
@@ -28,8 +31,9 @@ the fixture only:
28
31
  node examples/runner-fleet/mint-dev-token.mjs acme local-agent
29
32
  ```
30
33
 
31
- The production path should use `jwt_asymmetric` with an explicit RS256/ES256
32
- allowlist and a trusted JWKS URL or public PEM. See
34
+ `--insecure` is acceptable only for this synthetic self-signed fixture. The
35
+ production path should use a trusted TLS chain and `jwt_asymmetric` with an
36
+ explicit RS256/ES256 allowlist plus a trusted JWKS URL or public PEM. See
33
37
  [Running A Runner Fleet](../../docs/running-a-runner-fleet.md).
34
38
 
35
39
  Run the stronger automated verification instead of treating a green Compose
@@ -46,11 +46,11 @@ services:
46
46
  SYNAPSOR_SESSION_JWT_SECRET: synthetic-fleet-session-secret-change-before-use-0001
47
47
  SYNAPSOR_METRICS_TOKEN: synthetic-fleet-metrics-token-change-before-use
48
48
  ports:
49
- - "8871:8766"
49
+ - "127.0.0.1:8871:8766"
50
50
  volumes:
51
51
  - ./synapsor.runner.json:/config/synapsor.runner.json:ro
52
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))"]
53
+ test: ["CMD-SHELL", "NODE_TLS_REJECT_UNAUTHORIZED=0 node -e \"fetch('https://127.0.0.1:8766/readyz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""]
54
54
  interval: 2s
55
55
  timeout: 5s
56
56
  retries: 30
@@ -66,11 +66,11 @@ services:
66
66
  condition: service_healthy
67
67
  environment: *runner_environment
68
68
  ports:
69
- - "8872:8766"
69
+ - "127.0.0.1:8872:8766"
70
70
  volumes:
71
71
  - ./synapsor.runner.json:/config/synapsor.runner.json:ro
72
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))"]
73
+ test: ["CMD-SHELL", "NODE_TLS_REJECT_UNAUTHORIZED=0 node -e \"fetch('https://127.0.0.1:8766/readyz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""]
74
74
  interval: 2s
75
75
  timeout: 5s
76
76
  retries: 30
@@ -9,7 +9,7 @@ const payload = {
9
9
  sub: subject,
10
10
  tenant_id: tenant,
11
11
  iss: "https://fleet.example.invalid",
12
- aud: "synapsor-runner-fleet",
12
+ aud: "https://runner-fleet.example.invalid/mcp",
13
13
  iat: now,
14
14
  exp: now + 600,
15
15
  };
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ tls_dir="${SYNAPSOR_FLEET_TLS_DIR:-/tmp/synapsor-runner-tls}"
5
+ mkdir -p "$tls_dir"
6
+ chmod 700 "$tls_dir"
7
+
8
+ openssl req -x509 -newkey rsa:2048 -sha256 -nodes -days 1 \
9
+ -subj "/CN=localhost" \
10
+ -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" \
11
+ -keyout "$tls_dir/server.key" \
12
+ -out "$tls_dir/server.crt" \
13
+ >/dev/null 2>&1
14
+ chmod 600 "$tls_dir/server.key" "$tls_dir/server.crt"
15
+
16
+ export SYNAPSOR_FLEET_TLS_CERT_PEM="$(<"$tls_dir/server.crt")"
17
+ export SYNAPSOR_FLEET_TLS_KEY_PEM="$(<"$tls_dir/server.key")"
18
+
19
+ exec node apps/runner/dist/cli.js "$@" \
20
+ --tls-cert-env SYNAPSOR_FLEET_TLS_CERT_PEM \
21
+ --tls-key-env SYNAPSOR_FLEET_TLS_KEY_PEM
@@ -36,11 +36,37 @@
36
36
  "provider": "jwt_hs256",
37
37
  "secret_env": "SYNAPSOR_SESSION_JWT_SECRET",
38
38
  "issuer": "https://fleet.example.invalid",
39
- "audience": "synapsor-runner-fleet",
39
+ "audience": "https://runner-fleet.example.invalid/mcp",
40
40
  "tenant_claim": "tenant_id",
41
41
  "principal_claim": "sub",
42
42
  "clock_skew_seconds": 10
43
43
  },
44
+ "http_security": {
45
+ "deployment": "shared",
46
+ "oauth_resource": {
47
+ "resource": "https://runner-fleet.example.invalid/mcp",
48
+ "authorization_servers": ["https://fleet.example.invalid"],
49
+ "resource_name": "Synthetic Synapsor Runner fleet"
50
+ },
51
+ "allowed_hosts": [
52
+ "127.0.0.1:8766",
53
+ "127.0.0.1:8871",
54
+ "127.0.0.1:8872",
55
+ "localhost:8766",
56
+ "localhost:8871",
57
+ "localhost:8872"
58
+ ],
59
+ "limits": {
60
+ "max_request_bytes": 65536,
61
+ "max_header_bytes": 8192,
62
+ "max_sessions": 100,
63
+ "session_idle_timeout_seconds": 120,
64
+ "request_timeout_ms": 15000,
65
+ "headers_timeout_ms": 5000,
66
+ "keep_alive_timeout_ms": 5000,
67
+ "max_connections": 200
68
+ }
69
+ },
44
70
  "rate_limits": {
45
71
  "enabled": true,
46
72
  "default": { "requests": 20, "window_seconds": 60 },
@@ -63,6 +63,21 @@ The demo proves:
63
63
  - the shadow report compares cases with authoritative human outcomes before the
64
64
  effect fixture checks the expected business change.
65
65
 
66
+ The app-owned integration files make those two evaluation paths reusable:
67
+
68
+ - `app/contract.ts` is the code-first TypeScript form of the reviewed late-fee
69
+ action and compiles to the same canonical spec as JSON/DSL;
70
+ - `app/record-shadow-outcomes.mjs` records authoritative app outcomes through
71
+ `@synapsor/runner/shadow` without approval or source mutation;
72
+ - `app/effect-adapter.mjs` emits one deterministic, propose-only result through
73
+ the provider-neutral command adapter;
74
+ - `.github/workflows/effect-regression.yml` runs the effect check and uploads
75
+ JUnit output for relevant pull requests.
76
+
77
+ This repository result is deterministic application evidence, not an external
78
+ model benchmark. Use `--result-origin external-model` only when your adapter
79
+ actually invokes a model.
80
+
66
81
  RLS is defense in depth, not a claim that a fully compromised Runner process is
67
82
  contained. A process that can choose arbitrary trusted context or replace the
68
83
  database credential remains outside this guarantee. Production deployments
@@ -107,6 +122,9 @@ Relevant files:
107
122
  - `scripts/run-demo.sh`
108
123
  - `scripts/run-evaluation.sh`
109
124
  - `app/README.md`
125
+ - `app/contract.ts`
126
+ - `app/record-shadow-outcomes.mjs`
127
+ - `app/effect-adapter.mjs`
110
128
  - `shadow-study/cases.jsonl` and `shadow-study/outcomes.jsonl`: deterministic
111
129
  true-shadow reference data for agent-versus-authoritative-outcome reports.
112
130
 
@@ -11,3 +11,9 @@ writeback with tenant, primary-key, allowed-column, idempotency, and
11
11
  For richer app actions, such as inserting a credit ledger row and emitting an
12
12
  event, use the app-owned handler examples in `../app-owned-writeback/` and
13
13
  `../mcp-postgres-billing-app-handler/`.
14
+
15
+ `contract.ts` demonstrates the optional typed authoring frontend without
16
+ changing the canonical contract. `record-shadow-outcomes.mjs` is the normal-app
17
+ bridge for authoritative human outcomes. `effect-adapter.mjs` is a deterministic
18
+ propose-only CI adapter; it receives a bounded fixture path and emits one
19
+ canonical result. Neither helper approves, applies, or mutates the source.
@@ -0,0 +1,28 @@
1
+ import { defineCapability } from "@synapsor/runner/authoring";
2
+
3
+ export const waiveLateFee = defineCapability({
4
+ name: "billing.propose_late_fee_waiver",
5
+ kind: "proposal",
6
+ context: "local_operator",
7
+ source: "app_postgres",
8
+ subject: { resource: "billing.invoice", primary_key: "id", tenant_key: "tenant_id" },
9
+ args: {
10
+ invoice_id: { type: "string", required: true, max_length: 128 },
11
+ reason: { type: "string", required: true, max_length: 500 },
12
+ },
13
+ lookup: { id_from_arg: "invoice_id" },
14
+ visible_fields: ["id", "tenant_id", "late_fee_cents", "waiver_reason", "updated_at"],
15
+ kept_out_fields: ["card_token", "internal_risk_note"],
16
+ evidence: { required: true, query_audit: true },
17
+ max_rows: 1,
18
+ proposal: {
19
+ action: "waive_late_fee",
20
+ operation: { kind: "update" },
21
+ allowed_fields: ["late_fee_cents", "waiver_reason"],
22
+ patch: { late_fee_cents: { fixed: 0 }, waiver_reason: { from_arg: "reason" } },
23
+ numeric_bounds: { late_fee_cents: { minimum: 0, maximum: 10000 } },
24
+ conflict_guard: { column: "updated_at" },
25
+ approval: { mode: "human", required_role: "billing_lead" },
26
+ writeback: { mode: "direct_sql" },
27
+ },
28
+ });
@@ -0,0 +1,23 @@
1
+ import fs from "node:fs";
2
+
3
+ if (process.env.SYNAPSOR_EFFECT_MODE !== "propose_only") {
4
+ throw new Error("reference effect adapter requires SYNAPSOR_EFFECT_MODE=propose_only");
5
+ }
6
+ if (process.env.DATABASE_URL || process.env.SYNAPSOR_DATABASE_WRITE_URL) {
7
+ throw new Error("reference effect adapter must not receive database credentials");
8
+ }
9
+ const fixturePath = process.env.SYNAPSOR_EFFECT_FIXTURE_PATH;
10
+ if (!fixturePath) throw new Error("SYNAPSOR_EFFECT_FIXTURE_PATH is required");
11
+ const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
12
+ const expected = fixture.expected;
13
+
14
+ process.stdout.write(`${JSON.stringify({
15
+ schema_version: "synapsor.effect-result.v1",
16
+ fixture_id: fixture.fixture_id,
17
+ capability_calls: expected.capability_calls.map((name) => ({ name })),
18
+ trusted_context: expected.trusted_context,
19
+ proposal: expected.proposal,
20
+ observed_fields: Object.keys(expected.proposal.diff ?? {}).sort(),
21
+ evidence: { mode: "fixture", new_source_reads: false },
22
+ source_database_changed: false,
23
+ })}\n`);
@@ -0,0 +1,44 @@
1
+ import { createShadowOutcomeRecorder } from "@synapsor/runner/shadow";
2
+
3
+ const [storePath, studyId] = process.argv.slice(2);
4
+ if (!storePath || !studyId) {
5
+ throw new Error("usage: node record-shadow-outcomes.mjs <store-path> <study-id>");
6
+ }
7
+
8
+ const recorder = createShadowOutcomeRecorder({
9
+ storePath,
10
+ studyId,
11
+ actor: "support_reference_app",
12
+ source: "support-app-audit",
13
+ });
14
+ try {
15
+ recorder.record({
16
+ requestId: "req-waiver-exact",
17
+ tenantId: "acme",
18
+ businessObject: "invoice",
19
+ objectId: "INV-3001",
20
+ disposition: "applied",
21
+ actualEffect: {
22
+ before: { late_fee_cents: 5500, waiver_reason: null },
23
+ after: { late_fee_cents: 0, waiver_reason: "courtesy waiver" },
24
+ patch: { late_fee_cents: 0, waiver_reason: "courtesy waiver" },
25
+ },
26
+ occurredAt: "2026-07-19T11:00:00.000Z",
27
+ reference: "ticket:SUP-184",
28
+ reason: "customer qualified",
29
+ });
30
+ recorder.record({
31
+ requestId: "req-waiver-rejected",
32
+ tenantId: "acme",
33
+ businessObject: "invoice",
34
+ objectId: "INV-REJECTED",
35
+ disposition: "rejected_no_action",
36
+ occurredAt: "2026-07-19T11:01:00.000Z",
37
+ reference: "ticket:SUP-REJECTED",
38
+ reason: "customer already received a courtesy waiver",
39
+ });
40
+ } finally {
41
+ recorder.close();
42
+ }
43
+
44
+ process.stdout.write("Recorded 2 authoritative outcomes through @synapsor/runner/shadow.\n");
@@ -23,10 +23,9 @@ corepack pnpm runner shadow case import \
23
23
  --input "$ROOT_DIR/examples/support-billing-agent/shadow-study/cases.jsonl" \
24
24
  --store "$STORE_PATH" >/dev/null
25
25
 
26
- corepack pnpm runner shadow outcome import \
27
- --study sst_support_reference \
28
- --input "$ROOT_DIR/examples/support-billing-agent/shadow-study/outcomes.jsonl" \
29
- --store "$STORE_PATH" >/dev/null
26
+ corepack pnpm build:runner-package >/dev/null
27
+ node "$ROOT_DIR/examples/support-billing-agent/app/record-shadow-outcomes.mjs" \
28
+ "$STORE_PATH" sst_support_reference >/dev/null
30
29
 
31
30
  corepack pnpm runner shadow report \
32
31
  --study sst_support_reference \
@@ -54,7 +53,9 @@ NODE
54
53
 
55
54
  corepack pnpm runner effect run \
56
55
  --dataset "$ROOT_DIR/fixtures/effects/dataset.json" \
57
- --results-dir "$ROOT_DIR/fixtures/effects/results" \
56
+ --adapter node \
57
+ --adapter-arg "$ROOT_DIR/examples/support-billing-agent/app/effect-adapter.mjs" \
58
+ --result-origin deterministic-application \
58
59
  --format json \
59
60
  --out "$EFFECT_PATH" >/dev/null
60
61
 
@@ -15,7 +15,7 @@ policy-tier demonstration.
15
15
  ## Prerequisites
16
16
 
17
17
  - Docker
18
- - Node 22.5+
18
+ - Node 22.13+
19
19
  - Port `55438` available
20
20
 
21
21
  Install the stable CLI once for the commands below:
@@ -50,6 +50,42 @@ network connection or connecting to a database. From a source checkout,
50
50
  `corepack pnpm verify:adoption` runs this boundary check plus the no-database
51
51
  quick demo and MCP config validation.
52
52
 
53
+ ## Clone And Validate The Same Safe Action
54
+
55
+ The checked-in code-first action is
56
+ [`synapsor/actions/support.propose_plan_credit.ts`](synapsor/actions/support.propose_plan_credit.ts).
57
+ It is a reviewable frontend for the same canonical JSON contract, not another
58
+ runtime format. Editing it cannot change active tools.
59
+
60
+ After cloning the repository and installing dependencies, another developer can
61
+ run the deterministic team gate without a database or Cloud account:
62
+
63
+ ```bash
64
+ corepack pnpm install --frozen-lockfile
65
+ corepack pnpm verify:safe-action-team
66
+ ```
67
+
68
+ The verifier copies this example to a disposable directory, compiles the action
69
+ only as a disabled draft, compares the regenerated boundary tests with the
70
+ checked-in file, proves the config and active contract are unchanged, and emits
71
+ text, JSON, and JUnit reports under `tmp/safe-action-team-ci/`. Its static gate
72
+ covers the exact proposal effect, trusted scope, evidence requirement, approval
73
+ policy, operator separation, kept-out fields, argument bounds, and conflict
74
+ guard. It performs no approval, activation, source read, or source mutation.
75
+
76
+ The same gate runs in
77
+ [`safe-action-ci.yml`](https://github.com/Synapsor/Synapsor-Runner/blob/main/.github/workflows/safe-action-ci.yml).
78
+ CI adds a disposable PostgreSQL service and runs the generated allowed-effect,
79
+ cross-tenant-denial, and source-unchanged assertions. It uses an amount above
80
+ the auto-approval threshold, performs no approval or apply, and requires source
81
+ rows and source receipt state to remain equivalent before and after the live
82
+ suite.
83
+ For a real staging preview, each developer supplies their own
84
+ `PLAN_CREDIT_POSTGRES_READ_URL`, `PLAN_CREDIT_POSTGRES_WRITE_URL`,
85
+ `SYNAPSOR_TENANT_ID`, and `SYNAPSOR_PRINCIPAL` outside Git, then follows the
86
+ Quick Start below. Cursor receives the same proposal-only tools; activation,
87
+ approval, and apply remain outside the agent.
88
+
53
89
  ## Quick Start
54
90
 
55
91
  ```bash
@@ -199,20 +235,33 @@ shape without guessing which state should change.
199
235
  Copy the matching template from [`mcp-client-examples/`](mcp-client-examples/):
200
236
 
201
237
  - Claude Desktop: `claude-desktop.json`
238
+ - Claude Code: `claude-code.sh`
239
+ - Codex: `codex.config.toml`
202
240
  - Cursor project/global: `cursor-project.mcp.json`, `cursor-global.mcp.json`
241
+ - VS Code: `vscode.mcp.json`
203
242
  - OpenAI Agents SDK: stdio and Streamable HTTP TypeScript examples
204
- - generic MCP: stdio and Streamable HTTP JSON examples
243
+ - LangChain/LangGraph: `langchain.mjs`
244
+ - Google ADK: `google-adk.py`
245
+ - LlamaIndex: `llamaindex.py`
246
+ - generic MCP: stdio and Streamable HTTP JSON and JavaScript examples
247
+
248
+ See [Client And Framework Recipes](../../docs/client-recipes.md) for current
249
+ official references, exact proposal input, evidence labels, and the mechanical
250
+ verification gate.
205
251
 
206
252
  For Streamable HTTP, run:
207
253
 
208
254
  ```bash
255
+ export SYNAPSOR_RUNNER_HTTP_TOKEN="$(node -e 'process.stdout.write(require("node:crypto").randomBytes(32).toString("base64url"))')"
256
+
209
257
  synapsor-runner mcp serve \
210
258
  --transport streamable-http \
211
259
  --alias-mode openai \
212
260
  --host 127.0.0.1 \
213
261
  --port 8766 \
214
262
  --config examples/support-plan-credit/synapsor.runner.json \
215
- --store ./tmp/support-plan-credit/local.db
263
+ --store ./tmp/support-plan-credit/local.db \
264
+ --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
216
265
  ```
217
266
 
218
267
  The OpenAI client receives `support__inspect_customer` and
@@ -220,6 +269,8 @@ The OpenAI client receives `support__inspect_customer` and
220
269
  tool metadata and results. For Claude, Cursor, or generic MCP clients, omit
221
270
  `--alias-mode openai` to expose `support.inspect_customer` and
222
271
  `support.propose_plan_credit`. Approval and apply remain outside MCP.
272
+ The HTTP client reads the same opaque endpoint token from its protected
273
+ environment; no token value belongs in the checked-in recipe.
223
274
 
224
275
  ## How The Policy Is Governed
225
276