@synapsor/runner 1.5.3 → 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 (39) hide show
  1. package/CHANGELOG.md +44 -2
  2. package/README.md +20 -12
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/runner.mjs +2332 -501
  5. package/dist/runtime.mjs +788 -148
  6. package/dist/shadow.mjs +101 -1
  7. package/docs/capability-authoring.md +6 -1
  8. package/docs/cloud-mode.md +18 -0
  9. package/docs/database-enforced-scope.md +8 -0
  10. package/docs/dsl-reference.md +45 -4
  11. package/docs/getting-started-own-database.md +6 -3
  12. package/docs/guarded-crud-writeback.md +10 -1
  13. package/docs/http-mcp.md +222 -207
  14. package/docs/limitations.md +9 -2
  15. package/docs/local-mode.md +5 -2
  16. package/docs/mcp-client-setup.md +24 -11
  17. package/docs/mcp-clients.md +10 -4
  18. package/docs/openai-agents-sdk.md +16 -2
  19. package/docs/production.md +72 -7
  20. package/docs/release-notes.md +44 -3
  21. package/docs/runner-bundles.md +7 -2
  22. package/docs/runner-config-reference.md +96 -6
  23. package/docs/running-a-runner-fleet.md +42 -13
  24. package/docs/security-boundary.md +38 -3
  25. package/docs/store-lifecycle.md +93 -5
  26. package/examples/openai-agents-http/README.md +10 -4
  27. package/examples/openai-agents-http/agent.py +2 -2
  28. package/examples/runner-fleet/Dockerfile +7 -2
  29. package/examples/runner-fleet/README.md +11 -7
  30. package/examples/runner-fleet/docker-compose.yml +4 -4
  31. package/examples/runner-fleet/mint-dev-token.mjs +1 -1
  32. package/examples/runner-fleet/start-tls-runner.sh +21 -0
  33. package/examples/runner-fleet/synapsor.runner.json +27 -1
  34. package/examples/support-plan-credit/README.md +6 -1
  35. package/examples/support-plan-credit/mcp-client-examples/generic-streamable-http.json +4 -1
  36. package/examples/support-plan-credit/mcp-client-examples/openai-agents-streamable-http.ts +4 -0
  37. package/examples/support-plan-credit/synapsor.contract.json +0 -3
  38. package/package.json +1 -1
  39. 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:
@@ -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 },
@@ -252,13 +252,16 @@ verification gate.
252
252
  For Streamable HTTP, run:
253
253
 
254
254
  ```bash
255
+ export SYNAPSOR_RUNNER_HTTP_TOKEN="$(node -e 'process.stdout.write(require("node:crypto").randomBytes(32).toString("base64url"))')"
256
+
255
257
  synapsor-runner mcp serve \
256
258
  --transport streamable-http \
257
259
  --alias-mode openai \
258
260
  --host 127.0.0.1 \
259
261
  --port 8766 \
260
262
  --config examples/support-plan-credit/synapsor.runner.json \
261
- --store ./tmp/support-plan-credit/local.db
263
+ --store ./tmp/support-plan-credit/local.db \
264
+ --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
262
265
  ```
263
266
 
264
267
  The OpenAI client receives `support__inspect_customer` and
@@ -266,6 +269,8 @@ The OpenAI client receives `support__inspect_customer` and
266
269
  tool metadata and results. For Claude, Cursor, or generic MCP clients, omit
267
270
  `--alias-mode openai` to expose `support.inspect_customer` and
268
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.
269
274
 
270
275
  ## How The Policy Is Governed
271
276
 
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "name": "synapsor-runner",
3
3
  "transport": "streamable-http",
4
- "url": "http://127.0.0.1:8766/mcp"
4
+ "url": "http://127.0.0.1:8766/mcp",
5
+ "headers_from_env": {
6
+ "Authorization": "Bearer $SYNAPSOR_RUNNER_HTTP_TOKEN"
7
+ }
5
8
  }
@@ -1,9 +1,13 @@
1
1
  import { Agent, MCPServerStreamableHttp, run } from "@openai/agents";
2
2
 
3
3
  // Start Runner with --alias-mode openai so model-visible tool names are valid OpenAI function names.
4
+ const token = process.env.SYNAPSOR_RUNNER_HTTP_TOKEN;
5
+ if (!token) throw new Error("set SYNAPSOR_RUNNER_HTTP_TOKEN in the launching environment");
6
+
4
7
  const synapsor = new MCPServerStreamableHttp({
5
8
  name: "Synapsor Runner",
6
9
  url: "http://127.0.0.1:8766/mcp",
10
+ requestInit: { headers: { Authorization: `Bearer ${token}` } },
7
11
  });
8
12
 
9
13
  await synapsor.connect();
@@ -182,9 +182,6 @@
182
182
  "mode": "human",
183
183
  "required_role": "support_reviewer"
184
184
  },
185
- "conflict_guard": {
186
- "weak_guard_ack": true
187
- },
188
185
  "numeric_bounds": {
189
186
  "amount_cents": {
190
187
  "maximum": 50000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synapsor/runner",
3
- "version": "1.5.3",
3
+ "version": "1.5.4",
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",
@@ -293,6 +293,80 @@
293
293
  "max_response_bytes": { "type": "integer", "minimum": 1024, "maximum": 10485760 }
294
294
  }
295
295
  },
296
+ "http_security": {
297
+ "description": "Runner-only deployment security for networked MCP. Credential values remain in environment or secret-manager wiring and never enter portable contracts.",
298
+ "type": "object",
299
+ "additionalProperties": false,
300
+ "properties": {
301
+ "deployment": {
302
+ "description": "Trust topology. shared requires signed per-session http_claims identity; single_tenant may use one opaque service token.",
303
+ "enum": ["loopback", "single_tenant", "shared"]
304
+ },
305
+ "channel": {
306
+ "description": "How a remote listener is protected in transit. insecure_http_break_glass is unsafe and still requires authentication.",
307
+ "enum": ["direct_tls", "trusted_tls_proxy", "insecure_http_break_glass"]
308
+ },
309
+ "static_token": {
310
+ "description": "Environment-variable names for the current opaque endpoint token and at most one previous rotation token.",
311
+ "type": "object",
312
+ "additionalProperties": false,
313
+ "properties": {
314
+ "active_env": { "$ref": "#/$defs/env_name" },
315
+ "previous_env": { "$ref": "#/$defs/env_name" }
316
+ }
317
+ },
318
+ "oauth_resource": {
319
+ "description": "RFC 9728 metadata for Runner as an OAuth protected resource. The external authorization server issues tokens; Runner only verifies them.",
320
+ "type": "object",
321
+ "additionalProperties": false,
322
+ "required": ["resource", "authorization_servers"],
323
+ "properties": {
324
+ "resource": { "type": "string", "pattern": "^https://" },
325
+ "authorization_servers": {
326
+ "type": "array",
327
+ "minItems": 1,
328
+ "maxItems": 8,
329
+ "uniqueItems": true,
330
+ "items": { "type": "string", "pattern": "^https://" }
331
+ },
332
+ "scopes_supported": { "type": "array", "minItems": 1, "maxItems": 64, "uniqueItems": true, "items": { "$ref": "#/$defs/oauth_scope" } },
333
+ "required_scopes": { "type": "array", "minItems": 1, "maxItems": 64, "uniqueItems": true, "items": { "$ref": "#/$defs/oauth_scope" } },
334
+ "resource_name": { "type": "string", "minLength": 1, "maxLength": 128 },
335
+ "resource_documentation": { "type": "string", "pattern": "^https://" }
336
+ }
337
+ },
338
+ "allowed_origins": {
339
+ "description": "Exact browser origins accepted on HTTP MCP requests. Native clients may omit Origin. Wildcards are forbidden.",
340
+ "type": "array",
341
+ "maxItems": 32,
342
+ "uniqueItems": true,
343
+ "items": { "type": "string", "pattern": "^https?://[^/*]+(?::[0-9]+)?$" }
344
+ },
345
+ "allowed_hosts": {
346
+ "description": "Exact HTTP Host authorities accepted by the direct listener or trusted proxy. Forwarded host headers are never authority.",
347
+ "type": "array",
348
+ "minItems": 1,
349
+ "maxItems": 32,
350
+ "uniqueItems": true,
351
+ "items": { "type": "string", "minLength": 1, "maxLength": 255, "pattern": "^(?:\\[[0-9A-Fa-f:.]+\\]|[A-Za-z0-9.-]+)(?::[0-9]{1,5})?$" }
352
+ },
353
+ "limits": {
354
+ "description": "Bounded HTTP request, header, connection, and session resource limits.",
355
+ "type": "object",
356
+ "additionalProperties": false,
357
+ "properties": {
358
+ "max_request_bytes": { "type": "integer", "minimum": 1024, "maximum": 16777216 },
359
+ "max_header_bytes": { "type": "integer", "minimum": 4096, "maximum": 65536 },
360
+ "max_sessions": { "type": "integer", "minimum": 1, "maximum": 100000 },
361
+ "session_idle_timeout_seconds": { "type": "integer", "minimum": 10, "maximum": 86400 },
362
+ "request_timeout_ms": { "type": "integer", "minimum": 1000, "maximum": 300000 },
363
+ "headers_timeout_ms": { "type": "integer", "minimum": 1000, "maximum": 120000 },
364
+ "keep_alive_timeout_ms": { "type": "integer", "minimum": 1000, "maximum": 120000 },
365
+ "max_connections": { "type": "integer", "minimum": 1, "maximum": 100000 }
366
+ }
367
+ }
368
+ }
369
+ },
296
370
  "cloud": {
297
371
  "type": "object",
298
372
  "additionalProperties": false,
@@ -347,6 +421,12 @@
347
421
  "type": "string",
348
422
  "pattern": "^[A-Z_][A-Z0-9_]*$"
349
423
  },
424
+ "oauth_scope": {
425
+ "type": "string",
426
+ "minLength": 1,
427
+ "maxLength": 128,
428
+ "pattern": "^[!#-\\[\\]-~]+$"
429
+ },
350
430
  "identifier": {
351
431
  "type": "string",
352
432
  "pattern": "^[A-Za-z_][A-Za-z0-9_]*$"