@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.
- package/CHANGELOG.md +134 -3
- package/README.md +108 -113
- package/dist/authoring.d.ts +23 -0
- package/dist/authoring.d.ts.map +1 -0
- package/dist/authoring.mjs +1318 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -5
- package/dist/local-ui.d.ts +14 -0
- package/dist/local-ui.d.ts.map +1 -1
- package/dist/runner.mjs +12334 -5256
- package/dist/runtime.mjs +817 -149
- package/dist/shadow.d.ts +36 -0
- package/dist/shadow.d.ts.map +1 -0
- package/dist/shadow.mjs +5362 -0
- package/docs/README.md +25 -2
- package/docs/alternatives.md +122 -0
- package/docs/capability-authoring.md +49 -1
- package/docs/client-recipes.md +218 -0
- package/docs/cloud-mode.md +18 -0
- package/docs/contract-testing.md +9 -7
- package/docs/cursor-plugin.md +78 -0
- package/docs/database-enforced-scope.md +8 -0
- package/docs/dsl-reference.md +45 -4
- package/docs/effect-regression.md +39 -2
- package/docs/fresh-developer-usability.md +110 -0
- package/docs/getting-started-own-database.md +102 -5
- package/docs/guarded-crud-writeback.md +10 -1
- package/docs/host-compatibility.md +59 -0
- package/docs/http-mcp.md +222 -207
- package/docs/limitations.md +9 -2
- package/docs/local-mode.md +11 -7
- package/docs/mcp-audit.md +166 -2
- package/docs/mcp-client-setup.md +28 -11
- package/docs/mcp-clients.md +43 -8
- package/docs/openai-agents-sdk.md +16 -2
- package/docs/oss-vs-cloud.md +3 -0
- package/docs/production.md +72 -7
- package/docs/release-notes.md +131 -4
- package/docs/runner-bundles.md +7 -2
- package/docs/runner-config-reference.md +96 -6
- package/docs/running-a-runner-fleet.md +42 -13
- package/docs/security-boundary.md +58 -8
- package/docs/shadow-studies.md +47 -4
- package/docs/store-lifecycle.md +93 -5
- package/docs/troubleshooting-first-run.md +46 -0
- package/examples/openai-agents-http/README.md +10 -4
- package/examples/openai-agents-http/agent.py +2 -2
- package/examples/runner-fleet/Dockerfile +7 -2
- package/examples/runner-fleet/README.md +11 -7
- package/examples/runner-fleet/docker-compose.yml +4 -4
- package/examples/runner-fleet/mint-dev-token.mjs +1 -1
- package/examples/runner-fleet/start-tls-runner.sh +21 -0
- package/examples/runner-fleet/synapsor.runner.json +27 -1
- package/examples/support-billing-agent/README.md +18 -0
- package/examples/support-billing-agent/app/README.md +6 -0
- package/examples/support-billing-agent/app/contract.ts +28 -0
- package/examples/support-billing-agent/app/effect-adapter.mjs +23 -0
- package/examples/support-billing-agent/app/record-shadow-outcomes.mjs +44 -0
- package/examples/support-billing-agent/scripts/run-evaluation.sh +6 -5
- package/examples/support-plan-credit/README.md +54 -3
- package/examples/support-plan-credit/mcp-client-examples/README.md +23 -0
- package/examples/support-plan-credit/mcp-client-examples/claude-code.sh +34 -0
- package/examples/support-plan-credit/mcp-client-examples/codex.config.toml +24 -0
- package/examples/support-plan-credit/mcp-client-examples/generic-stdio.mjs +35 -0
- package/examples/support-plan-credit/mcp-client-examples/generic-streamable-http.json +4 -1
- package/examples/support-plan-credit/mcp-client-examples/generic-streamable-http.mjs +31 -0
- package/examples/support-plan-credit/mcp-client-examples/google-adk.py +40 -0
- package/examples/support-plan-credit/mcp-client-examples/langchain.mjs +29 -0
- package/examples/support-plan-credit/mcp-client-examples/llamaindex.py +36 -0
- package/examples/support-plan-credit/mcp-client-examples/openai-agents-stdio.ts +1 -1
- package/examples/support-plan-credit/mcp-client-examples/openai-agents-streamable-http.ts +5 -1
- package/examples/support-plan-credit/mcp-client-examples/vscode.mcp.json +20 -0
- package/examples/support-plan-credit/synapsor/actions/support.propose_plan_credit.contract-tests.generated.json +221 -0
- package/examples/support-plan-credit/synapsor/actions/support.propose_plan_credit.ts +34 -0
- package/examples/support-plan-credit/synapsor.contract.json +0 -3
- package/fixtures/mcp-audit/README.md +14 -0
- package/fixtures/mcp-audit/cursor-bypass-config.json +38 -0
- package/fixtures/mcp-audit/dangerous-tools-list.json +33 -0
- package/fixtures/mcp-audit/reviewed-proposal-tools-list.json +60 -0
- package/package.json +18 -3
- package/schemas/mcp-audit-report.schema.json +100 -1
- package/schemas/synapsor.contract-tests.schema.json +1 -1
- package/schemas/synapsor.runner.schema.json +80 -0
package/docs/store-lifecycle.md
CHANGED
|
@@ -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
|
|
36
|
+
## Inspect one complete lifecycle
|
|
37
37
|
|
|
38
|
-
|
|
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
|
|
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
|
|
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="
|
|
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
|
-
|
|
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,
|
|
78
|
-
|
|
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", "
|
|
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["
|
|
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 ["
|
|
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
|
-
|
|
6
|
-
|
|
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
|
|
20
|
-
curl --fail
|
|
21
|
-
curl --fail
|
|
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
|
-
|
|
32
|
-
|
|
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
|
|
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
|
|
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
|
|
@@ -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": "
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
--
|
|
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.
|
|
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
|
-
-
|
|
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
|
|