@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
package/dist/runtime.mjs CHANGED
@@ -11,9 +11,11 @@ import fs2 from "node:fs";
11
11
  import { createServer } from "node:http";
12
12
  import { createServer as createHttpsServer } from "node:https";
13
13
  import path2 from "node:path";
14
+ import { createSecureContext } from "node:tls";
14
15
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
15
16
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
17
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
18
+ import { OAuthProtectedResourceMetadataSchema } from "@modelcontextprotocol/sdk/shared/auth.js";
17
19
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
18
20
 
19
21
  // node_modules/.pnpm/@modelcontextprotocol+ext-apps@1.7.4_@modelcontextprotocol+sdk@1.29.0_zod@3.25.76__zod@3.25.76/node_modules/@modelcontextprotocol/ext-apps/dist/src/server/index.js
@@ -118,7 +120,7 @@ function N3(Z, $, J, X, V) {
118
120
  }
119
121
 
120
122
  // packages/config/src/index.ts
121
- var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["version", "mode", "storage", "sources", "trusted_context", "contexts", "executors", "capabilities", "contracts", "policies", "approvals", "operator_identity", "session_auth", "rate_limits", "metrics", "graduated_trust", "cloud", "governance", "strict", "result_format"]);
123
+ var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["version", "mode", "storage", "sources", "trusted_context", "contexts", "executors", "capabilities", "contracts", "policies", "approvals", "operator_identity", "session_auth", "http_security", "rate_limits", "metrics", "graduated_trust", "cloud", "governance", "strict", "result_format"]);
122
124
  var STORAGE_KEYS = /* @__PURE__ */ new Set(["sqlite_path", "shared_postgres"]);
123
125
  var SHARED_POSTGRES_STORAGE_KEYS = /* @__PURE__ */ new Set(["mode", "url_env", "schema", "lock_timeout_ms", "max_entries"]);
124
126
  var APPROVALS_KEYS = /* @__PURE__ */ new Set(["disable_auto_approval"]);
@@ -180,6 +182,26 @@ var SESSION_AUTH_KEYS = /* @__PURE__ */ new Set([
180
182
  "fetch_timeout_ms",
181
183
  "max_response_bytes"
182
184
  ]);
185
+ var HTTP_SECURITY_KEYS = /* @__PURE__ */ new Set(["deployment", "channel", "static_token", "oauth_resource", "allowed_origins", "allowed_hosts", "limits"]);
186
+ var HTTP_STATIC_TOKEN_KEYS = /* @__PURE__ */ new Set(["active_env", "previous_env"]);
187
+ var HTTP_OAUTH_RESOURCE_KEYS = /* @__PURE__ */ new Set([
188
+ "resource",
189
+ "authorization_servers",
190
+ "scopes_supported",
191
+ "required_scopes",
192
+ "resource_name",
193
+ "resource_documentation"
194
+ ]);
195
+ var HTTP_LIMIT_KEYS = /* @__PURE__ */ new Set([
196
+ "max_request_bytes",
197
+ "max_header_bytes",
198
+ "max_sessions",
199
+ "session_idle_timeout_seconds",
200
+ "request_timeout_ms",
201
+ "headers_timeout_ms",
202
+ "keep_alive_timeout_ms",
203
+ "max_connections"
204
+ ]);
183
205
  var RATE_LIMITS_KEYS = /* @__PURE__ */ new Set(["enabled", "default", "capabilities"]);
184
206
  var RATE_LIMIT_RULE_KEYS = /* @__PURE__ */ new Set(["requests", "window_seconds"]);
185
207
  var CLOUD_KEYS = /* @__PURE__ */ new Set(["base_url_env", "runner_token_env", "runner_id", "runner_version", "project_id", "adapter_id", "source_id", "engines", "capabilities", "session"]);
@@ -337,6 +359,7 @@ function validateRunnerCapabilityConfig(input) {
337
359
  validatePolicies(input.policies, strict, errors);
338
360
  validateOperatorIdentity(input.operator_identity, strict, errors);
339
361
  validateSessionAuth(input.session_auth, input.trusted_context, input.contexts, strict, errors);
362
+ validateHttpSecurity(input.http_security, input.session_auth, input.trusted_context, input.contexts, strict, errors, warnings);
340
363
  validateRateLimits(input.rate_limits, strict, errors);
341
364
  validateMetrics(input.metrics, strict, errors);
342
365
  validateGraduatedTrust(input.graduated_trust, strict, errors);
@@ -347,6 +370,169 @@ function validateRunnerCapabilityConfig(input) {
347
370
  scanForForbiddenFields(input, "$", errors);
348
371
  return { ok: errors.length === 0, errors, warnings };
349
372
  }
373
+ function validateHttpSecurity(value, sessionAuth, trustedContext, contexts, strict, errors, warnings) {
374
+ if (value === void 0) return;
375
+ if (!isRecord(value)) {
376
+ errors.push({ path: "$.http_security", code: "HTTP_SECURITY_NOT_OBJECT", message: "http_security must be a Runner deployment configuration object." });
377
+ return;
378
+ }
379
+ if (strict) checkUnknownKeys(value, HTTP_SECURITY_KEYS, "$.http_security", errors);
380
+ const deployment = value.deployment;
381
+ if (deployment !== void 0 && deployment !== "loopback" && deployment !== "single_tenant" && deployment !== "shared") {
382
+ errors.push({ path: "$.http_security.deployment", code: "INVALID_HTTP_DEPLOYMENT", message: "http_security.deployment must be loopback, single_tenant, or shared." });
383
+ }
384
+ const channel = value.channel;
385
+ if (channel !== void 0 && channel !== "direct_tls" && channel !== "trusted_tls_proxy" && channel !== "insecure_http_break_glass") {
386
+ errors.push({ path: "$.http_security.channel", code: "INVALID_HTTP_CHANNEL", message: "http_security.channel must be direct_tls, trusted_tls_proxy, or insecure_http_break_glass." });
387
+ }
388
+ validateHttpStaticToken(value.static_token, strict, errors);
389
+ validateHttpOauthResource(value.oauth_resource, strict, errors);
390
+ validateExactOrigins(value.allowed_origins, errors);
391
+ validateAllowedHosts(value.allowed_hosts, errors);
392
+ validateHttpLimits(value.limits, strict, errors);
393
+ const contextsUseClaims = isRecord(trustedContext) && trustedContext.provider === "http_claims" || isRecord(contexts) && Object.values(contexts).some((context) => isRecord(context) && context.provider === "http_claims");
394
+ if (deployment === "shared") {
395
+ if (!contextsUseClaims) {
396
+ errors.push({ path: "$.http_security.deployment", code: "SHARED_HTTP_CLAIMS_REQUIRED", message: "shared HTTP deployment requires http_claims trusted context so tenant and principal come from verified per-session identity." });
397
+ }
398
+ if (!isRecord(sessionAuth)) {
399
+ errors.push({ path: "$.session_auth", code: "SHARED_HTTP_SESSION_AUTH_REQUIRED", message: "shared HTTP deployment requires signed session_auth." });
400
+ } else {
401
+ if (!isNonEmptyString(sessionAuth.issuer)) errors.push({ path: "$.session_auth.issuer", code: "SHARED_HTTP_ISSUER_REQUIRED", message: "shared HTTP deployment requires an exact JWT issuer." });
402
+ if (!isNonEmptyString(sessionAuth.audience)) errors.push({ path: "$.session_auth.audience", code: "SHARED_HTTP_AUDIENCE_REQUIRED", message: "shared HTTP deployment requires an exact JWT audience/resource." });
403
+ if (sessionAuth.provider === "jwt_hs256") {
404
+ warnings.push({ path: "$.session_auth.provider", code: "SHARED_HTTP_HS256_WARNING", message: "Shared HTTP uses a symmetric JWT verification key. Prefer jwt_asymmetric with RS256/ES256 and JWKS for production." });
405
+ }
406
+ }
407
+ if (!isRecord(value.oauth_resource)) {
408
+ errors.push({ path: "$.http_security.oauth_resource", code: "SHARED_HTTP_OAUTH_RESOURCE_REQUIRED", message: "shared HTTP deployment requires RFC 9728 protected-resource metadata for an external authorization server." });
409
+ } else if (isRecord(sessionAuth) && isNonEmptyString(sessionAuth.audience) && value.oauth_resource.resource !== sessionAuth.audience) {
410
+ errors.push({ path: "$.http_security.oauth_resource.resource", code: "HTTP_RESOURCE_AUDIENCE_MISMATCH", message: "oauth_resource.resource must exactly match session_auth.audience so tokens are bound to this Runner resource." });
411
+ }
412
+ }
413
+ if (deployment !== "shared" && value.oauth_resource !== void 0 && !contextsUseClaims) {
414
+ errors.push({ path: "$.http_security.oauth_resource", code: "HTTP_OAUTH_CLAIMS_REQUIRED", message: "OAuth protected-resource metadata requires signed http_claims session identity." });
415
+ }
416
+ }
417
+ function validateHttpStaticToken(value, strict, errors) {
418
+ if (value === void 0) return;
419
+ if (!isRecord(value)) {
420
+ errors.push({ path: "$.http_security.static_token", code: "HTTP_STATIC_TOKEN_NOT_OBJECT", message: "http_security.static_token must contain environment-variable names only." });
421
+ return;
422
+ }
423
+ if (strict) checkUnknownKeys(value, HTTP_STATIC_TOKEN_KEYS, "$.http_security.static_token", errors);
424
+ if (value.active_env !== void 0 && !isEnvName(value.active_env)) {
425
+ errors.push({ path: "$.http_security.static_token.active_env", code: "INVALID_HTTP_TOKEN_ENV", message: "active_env must name the environment variable containing the opaque endpoint token." });
426
+ }
427
+ if (value.previous_env !== void 0 && !isEnvName(value.previous_env)) {
428
+ errors.push({ path: "$.http_security.static_token.previous_env", code: "INVALID_HTTP_PREVIOUS_TOKEN_ENV", message: "previous_env must name the one previous token accepted during an operator-controlled rotation window." });
429
+ }
430
+ if (value.active_env !== void 0 && value.active_env === value.previous_env) {
431
+ errors.push({ path: "$.http_security.static_token.previous_env", code: "HTTP_TOKEN_ENV_REUSED", message: "active_env and previous_env must be different environment variables." });
432
+ }
433
+ }
434
+ function validateHttpOauthResource(value, strict, errors) {
435
+ if (value === void 0) return;
436
+ if (!isRecord(value)) {
437
+ errors.push({ path: "$.http_security.oauth_resource", code: "HTTP_OAUTH_RESOURCE_NOT_OBJECT", message: "oauth_resource must describe this Runner protected resource and its external authorization server." });
438
+ return;
439
+ }
440
+ if (strict) checkUnknownKeys(value, HTTP_OAUTH_RESOURCE_KEYS, "$.http_security.oauth_resource", errors);
441
+ if (!isHttpsUrl(value.resource)) {
442
+ errors.push({ path: "$.http_security.oauth_resource.resource", code: "INVALID_HTTP_OAUTH_RESOURCE", message: "oauth_resource.resource must be an HTTPS URL for the exact public MCP endpoint." });
443
+ }
444
+ if (!Array.isArray(value.authorization_servers) || value.authorization_servers.length < 1 || value.authorization_servers.length > 8) {
445
+ errors.push({ path: "$.http_security.oauth_resource.authorization_servers", code: "HTTP_AUTHORIZATION_SERVERS_REQUIRED", message: "oauth_resource.authorization_servers must contain 1 through 8 external authorization-server HTTPS issuer URLs." });
446
+ } else {
447
+ value.authorization_servers.forEach((server, index) => {
448
+ if (!isHttpsUrl(server)) errors.push({ path: `$.http_security.oauth_resource.authorization_servers[${index}]`, code: "INVALID_HTTP_AUTHORIZATION_SERVER", message: "authorization server URLs must use HTTPS and contain no credentials, query, or fragment." });
449
+ });
450
+ }
451
+ for (const key of ["scopes_supported", "required_scopes"]) {
452
+ const scopes = value[key];
453
+ if (scopes !== void 0 && (!Array.isArray(scopes) || scopes.length < 1 || scopes.length > 64 || scopes.some((scope) => !isOAuthScope(scope)))) {
454
+ errors.push({ path: `$.http_security.oauth_resource.${key}`, code: "INVALID_HTTP_OAUTH_SCOPES", message: `${key} must contain 1 through 64 unique, visible OAuth scope strings.` });
455
+ } else if (Array.isArray(scopes) && new Set(scopes).size !== scopes.length) {
456
+ errors.push({ path: `$.http_security.oauth_resource.${key}`, code: "DUPLICATE_HTTP_OAUTH_SCOPE", message: `${key} must not contain duplicate scopes.` });
457
+ }
458
+ }
459
+ if (Array.isArray(value.required_scopes) && Array.isArray(value.scopes_supported)) {
460
+ for (const scope of value.required_scopes) {
461
+ if (!value.scopes_supported.includes(scope)) errors.push({ path: "$.http_security.oauth_resource.required_scopes", code: "UNSUPPORTED_HTTP_OAUTH_SCOPE", message: `Required scope ${String(scope)} is not listed in scopes_supported.` });
462
+ }
463
+ }
464
+ if (value.resource_name !== void 0 && (!isNonEmptyString(value.resource_name) || value.resource_name.trim().length > 128)) {
465
+ errors.push({ path: "$.http_security.oauth_resource.resource_name", code: "INVALID_HTTP_RESOURCE_NAME", message: "resource_name must be 1 through 128 characters." });
466
+ }
467
+ if (value.resource_documentation !== void 0 && !isHttpsUrl(value.resource_documentation)) {
468
+ errors.push({ path: "$.http_security.oauth_resource.resource_documentation", code: "INVALID_HTTP_RESOURCE_DOCUMENTATION", message: "resource_documentation must be an HTTPS URL without credentials, query, or fragment." });
469
+ }
470
+ }
471
+ function validateExactOrigins(value, errors) {
472
+ if (value === void 0) return;
473
+ if (!Array.isArray(value) || value.length > 32) {
474
+ errors.push({ path: "$.http_security.allowed_origins", code: "INVALID_HTTP_ALLOWED_ORIGINS", message: "allowed_origins must be an array of at most 32 exact origins." });
475
+ return;
476
+ }
477
+ value.forEach((origin, index) => {
478
+ if (!isExactHttpOrigin(origin)) errors.push({ path: `$.http_security.allowed_origins[${index}]`, code: "INVALID_HTTP_ALLOWED_ORIGIN", message: "Each allowed origin must be an exact HTTP(S) origin; wildcards, credentials, paths, query strings, and fragments are forbidden." });
479
+ });
480
+ if (new Set(value).size !== value.length) errors.push({ path: "$.http_security.allowed_origins", code: "DUPLICATE_HTTP_ALLOWED_ORIGIN", message: "allowed_origins must not contain duplicates." });
481
+ }
482
+ function validateAllowedHosts(value, errors) {
483
+ if (value === void 0) return;
484
+ if (!Array.isArray(value) || value.length < 1 || value.length > 32 || value.some((host) => !isAllowedHost(host))) {
485
+ errors.push({ path: "$.http_security.allowed_hosts", code: "INVALID_HTTP_ALLOWED_HOSTS", message: "allowed_hosts must contain 1 through 32 exact host or host:port authorities; wildcards and forwarded-host syntax are forbidden." });
486
+ } else if (new Set(value.map((host) => String(host).toLowerCase())).size !== value.length) {
487
+ errors.push({ path: "$.http_security.allowed_hosts", code: "DUPLICATE_HTTP_ALLOWED_HOST", message: "allowed_hosts must not contain duplicates." });
488
+ }
489
+ }
490
+ function validateHttpLimits(value, strict, errors) {
491
+ if (value === void 0) return;
492
+ if (!isRecord(value)) {
493
+ errors.push({ path: "$.http_security.limits", code: "HTTP_LIMITS_NOT_OBJECT", message: "http_security.limits must be an object." });
494
+ return;
495
+ }
496
+ if (strict) checkUnknownKeys(value, HTTP_LIMIT_KEYS, "$.http_security.limits", errors);
497
+ for (const [key, minimum, maximum] of [
498
+ ["max_request_bytes", 1024, 16 * 1024 * 1024],
499
+ ["max_header_bytes", 4096, 64 * 1024],
500
+ ["max_sessions", 1, 1e5],
501
+ ["session_idle_timeout_seconds", 10, 86400],
502
+ ["request_timeout_ms", 1e3, 3e5],
503
+ ["headers_timeout_ms", 1e3, 12e4],
504
+ ["keep_alive_timeout_ms", 1e3, 12e4],
505
+ ["max_connections", 1, 1e5]
506
+ ]) {
507
+ if (value[key] !== void 0 && (!Number.isSafeInteger(value[key]) || Number(value[key]) < minimum || Number(value[key]) > maximum)) {
508
+ errors.push({ path: `$.http_security.limits.${key}`, code: "INVALID_HTTP_LIMIT", message: `${key} must be an integer from ${minimum} through ${maximum}.` });
509
+ }
510
+ }
511
+ }
512
+ function isHttpsUrl(value) {
513
+ if (typeof value !== "string") return false;
514
+ try {
515
+ const url = new URL(value);
516
+ return url.protocol === "https:" && !url.username && !url.password && !url.search && !url.hash;
517
+ } catch {
518
+ return false;
519
+ }
520
+ }
521
+ function isExactHttpOrigin(value) {
522
+ if (typeof value !== "string" || value === "*" || value === "null") return false;
523
+ try {
524
+ const url = new URL(value);
525
+ return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password && url.pathname === "/" && !url.search && !url.hash && url.origin === value;
526
+ } catch {
527
+ return false;
528
+ }
529
+ }
530
+ function isAllowedHost(value) {
531
+ return typeof value === "string" && value.length <= 255 && value === value.trim() && !/[\s,/*\\?#]/.test(value) && !value.includes("://") && /^(?:\[[0-9A-Fa-f:.]+\]|[A-Za-z0-9.-]+)(?::\d{1,5})?$/.test(value);
532
+ }
533
+ function isOAuthScope(value) {
534
+ return typeof value === "string" && value.length >= 1 && value.length <= 128 && /^[\x21\x23-\x5b\x5d-\x7e]+$/.test(value);
535
+ }
350
536
  function validateGovernance(value, strict, errors) {
351
537
  if (value === void 0) return;
352
538
  if (!isRecord(value)) {
@@ -4557,6 +4743,11 @@ var ProposalStore = class {
4557
4743
  const rows = this.db.prepare(query.sql).all(...query.params);
4558
4744
  return rows.map((row) => rowToProposal(row)).filter((proposal) => proposal !== void 0);
4559
4745
  }
4746
+ countProposals(filters = {}) {
4747
+ const query = buildProposalCountQuery(filters);
4748
+ const row = this.db.prepare(query.sql).get(...query.params);
4749
+ return isRecord2(row) ? Number(row.count ?? 0) : 0;
4750
+ }
4560
4751
  listEvidenceBundles(filters = {}) {
4561
4752
  const query = buildEvidenceQuery(filters);
4562
4753
  const rows = this.db.prepare(query.sql).all(...query.params);
@@ -4583,6 +4774,19 @@ var ProposalStore = class {
4583
4774
  const proposalId = replayId.startsWith(prefix) ? replayId.slice(prefix.length) : replayId;
4584
4775
  return this.replay(proposalId);
4585
4776
  }
4777
+ getStoredReplay(replayId) {
4778
+ const row = this.db.prepare("SELECT * FROM replay_records WHERE replay_id = ?").get(replayId);
4779
+ return rowToStoredReplay(row);
4780
+ }
4781
+ getStoredReplayForProposal(proposalId) {
4782
+ const row = this.db.prepare(`
4783
+ SELECT * FROM replay_records
4784
+ WHERE proposal_id = ?
4785
+ ORDER BY created_at DESC, replay_id DESC
4786
+ LIMIT 1
4787
+ `).get(proposalId);
4788
+ return rowToStoredReplay(row);
4789
+ }
4586
4790
  proposalIdForEvidence(evidenceBundleId) {
4587
4791
  const evidence = this.getEvidenceBundle(evidenceBundleId);
4588
4792
  if (evidence?.proposal_id) return evidence.proposal_id;
@@ -4885,6 +5089,19 @@ var ProposalStore = class {
4885
5089
  });
4886
5090
  return job;
4887
5091
  }
5092
+ getWritebackJob(writebackJobId) {
5093
+ return rowToWritebackJob(this.db.prepare("SELECT * FROM writeback_jobs WHERE writeback_job_id = ?").get(writebackJobId));
5094
+ }
5095
+ listWritebackJobs(options = {}) {
5096
+ const clauses = [];
5097
+ const values = [];
5098
+ if (options.proposal_id) {
5099
+ clauses.push("proposal_id = ?");
5100
+ values.push(options.proposal_id);
5101
+ }
5102
+ const limit = Math.min(Math.max(options.limit ?? 100, 1), 1e3);
5103
+ return this.db.prepare(`SELECT * FROM writeback_jobs${clauses.length ? ` WHERE ${clauses.join(" AND ")}` : ""} ORDER BY created_at ASC, writeback_job_id ASC LIMIT ?`).all(...values, limit).map(rowToWritebackJob).filter((job) => Boolean(job));
5104
+ }
4888
5105
  claimWritebackIntent(jobInput, runnerId) {
4889
5106
  const job = parseWritebackJob(jobInput);
4890
5107
  const proposal = this.requireProposal(job.proposal_id);
@@ -6327,6 +6544,7 @@ var ProposalStore = class {
6327
6544
  const highestRisk = comparisons.filter((item) => item.status === "disagreement" || item.status === "partial_agreement" || item.status === "invalid_or_unsafe_scope_attempt").sort(
6328
6545
  (left, right) => (right.risk_score ?? 0) - (left.risk_score ?? 0) || (right.amount_value ?? 0) - (left.amount_value ?? 0) || left.case_id.localeCompare(right.case_id)
6329
6546
  ).slice(0, 10);
6547
+ const suggestedPolicies = suggestedShadowPolicies(comparisons);
6330
6548
  return {
6331
6549
  study,
6332
6550
  total_tasks_observed: comparisons.length,
@@ -6345,7 +6563,8 @@ var ProposalStore = class {
6345
6563
  by_capability: byCapability,
6346
6564
  by_decision_reason: byDecisionReason,
6347
6565
  highest_risk_disagreements: highestRisk,
6348
- suggested_policies: suggestedShadowPolicies(comparisons),
6566
+ suggested_policies: suggestedPolicies,
6567
+ trust_progression: shadowTrustProgression(comparisons, suggestedPolicies),
6349
6568
  comparisons,
6350
6569
  generated_at: latestIsoTimestamp([
6351
6570
  study.updated_at,
@@ -6706,6 +6925,19 @@ function inWhere(column, values) {
6706
6925
  };
6707
6926
  }
6708
6927
  function buildProposalQuery(filters) {
6928
+ const { clauses, params } = proposalQueryParts(filters);
6929
+ const where = clauses.length ? ` WHERE ${clauses.join(" AND ")}` : "";
6930
+ return {
6931
+ sql: `SELECT * FROM proposals${where} ORDER BY created_at DESC, proposal_id DESC${filters.limit ? " LIMIT ?" : ""}`,
6932
+ params: filters.limit ? [...params, filters.limit] : params
6933
+ };
6934
+ }
6935
+ function buildProposalCountQuery(filters) {
6936
+ const { clauses, params } = proposalQueryParts(filters);
6937
+ const where = clauses.length ? ` WHERE ${clauses.join(" AND ")}` : "";
6938
+ return { sql: `SELECT COUNT(*) AS count FROM proposals${where}`, params };
6939
+ }
6940
+ function proposalQueryParts(filters) {
6709
6941
  const clauses = [];
6710
6942
  const params = [];
6711
6943
  addEqual(clauses, params, "proposal_id", filters.proposal);
@@ -6717,7 +6949,7 @@ function buildProposalQuery(filters) {
6717
6949
  addEqual(clauses, params, "action", filters.capability ?? filters.action);
6718
6950
  addObjectFilter(clauses, params, "business_object", "source_table", "object_id", filters.objectType, filters.objectId);
6719
6951
  addTimeRange(clauses, params, "created_at", filters.from, filters.to);
6720
- return finishQuery("SELECT * FROM proposals", clauses, params, filters.limit);
6952
+ return { clauses, params };
6721
6953
  }
6722
6954
  function buildEvidenceQuery(filters) {
6723
6955
  const clauses = [];
@@ -7299,6 +7531,62 @@ function rowToReceipt(row) {
7299
7531
  source_table: row.source_table == null ? void 0 : String(row.source_table)
7300
7532
  };
7301
7533
  }
7534
+ function rowToWritebackJob(row) {
7535
+ if (!isRecord2(row)) return void 0;
7536
+ let payload;
7537
+ try {
7538
+ payload = JSON.parse(String(row.job_json));
7539
+ } catch {
7540
+ throw new ProposalStoreError("WRITEBACK_JOB_CORRUPT", `writeback job ${String(row.writeback_job_id)} payload is not valid JSON`);
7541
+ }
7542
+ if (!isRecord2(payload)) {
7543
+ throw new ProposalStoreError("WRITEBACK_JOB_CORRUPT", `writeback job ${String(row.writeback_job_id)} payload is not an object`);
7544
+ }
7545
+ const handler = payload.schema_version === "synapsor.handler-writeback.v1";
7546
+ let normalizedJob;
7547
+ if (!handler) {
7548
+ try {
7549
+ normalizedJob = parseWritebackJob(payload);
7550
+ } catch {
7551
+ throw new ProposalStoreError("WRITEBACK_JOB_CORRUPT", `writeback job ${String(row.writeback_job_id)} payload is not a supported writeback protocol`);
7552
+ }
7553
+ }
7554
+ const payloadJobId = handler ? payload.writeback_job_id : normalizedJob?.job_id;
7555
+ const payloadProposalId = handler ? payload.proposal_id : normalizedJob?.proposal_id;
7556
+ const payloadProposalHash = handler ? payload.proposal_hash : normalizedJob?.approval_id;
7557
+ if (payloadJobId !== String(row.writeback_job_id) || payloadProposalId !== String(row.proposal_id) || payloadProposalHash !== String(row.proposal_hash)) {
7558
+ throw new ProposalStoreError("WRITEBACK_JOB_CORRUPT", `writeback job ${String(row.writeback_job_id)} index fields do not match its payload`);
7559
+ }
7560
+ return {
7561
+ writeback_job_id: String(row.writeback_job_id),
7562
+ proposal_id: String(row.proposal_id),
7563
+ proposal_hash: String(row.proposal_hash),
7564
+ status: String(row.status),
7565
+ kind: handler ? "app_handler" : "direct_sql",
7566
+ payload,
7567
+ ...normalizedJob ? { normalized_job: normalizedJob } : {},
7568
+ created_at: String(row.created_at),
7569
+ updated_at: String(row.updated_at)
7570
+ };
7571
+ }
7572
+ function rowToStoredReplay(row) {
7573
+ if (!isRecord2(row)) return void 0;
7574
+ let payload;
7575
+ try {
7576
+ payload = JSON.parse(String(row.payload_json));
7577
+ } catch {
7578
+ throw new ProposalStoreError("REPLAY_RECORD_CORRUPT", `replay ${String(row.replay_id)} payload is not valid JSON`);
7579
+ }
7580
+ if (!isRecord2(payload) || !isRecord2(payload.proposal)) {
7581
+ throw new ProposalStoreError("REPLAY_RECORD_CORRUPT", `replay ${String(row.replay_id)} payload is not a supported replay record`);
7582
+ }
7583
+ const replayId = String(row.replay_id);
7584
+ const proposalId = String(row.proposal_id);
7585
+ if (payload.replay_id !== replayId || payload.proposal.proposal_id !== proposalId || !Array.isArray(payload.approvals) || !Array.isArray(payload.events) || !Array.isArray(payload.receipts) || !Array.isArray(payload.query_audit) || !Array.isArray(payload.evidence) || typeof payload.generated_at !== "string") {
7586
+ throw new ProposalStoreError("REPLAY_RECORD_CORRUPT", `replay ${replayId} index fields do not match its payload`);
7587
+ }
7588
+ return payload;
7589
+ }
7302
7590
  function rowToWritebackIntent(row) {
7303
7591
  if (!isRecord2(row)) return void 0;
7304
7592
  const status = String(row.status);
@@ -7829,6 +8117,32 @@ function suggestedShadowPolicies(comparisons) {
7829
8117
  }
7830
8118
  return suggestions;
7831
8119
  }
8120
+ function shadowTrustProgression(comparisons, suggestions) {
8121
+ const outcomes = comparisons.filter((item) => item.outcome !== void 0).length;
8122
+ const comparable = comparisons.filter((item) => item.comparable).length;
8123
+ const exact = comparisons.filter((item) => item.status === "exact_agreement").length;
8124
+ const currentStage = comparisons.length === 0 ? "observe" : outcomes === 0 ? "compare" : suggestions.length === 0 ? "manual_review" : "suggested_bounded_policy";
8125
+ const stageOrder = ["observe", "compare", "manual_review", "suggested_bounded_policy"];
8126
+ const currentIndex = stageOrder.indexOf(currentStage);
8127
+ const details = {
8128
+ observe: `${comparisons.length} task${comparisons.length === 1 ? "" : "s"} observed without source mutation.`,
8129
+ compare: `${outcomes} authoritative outcome${outcomes === 1 ? "" : "s"}; ${comparisons.length - outcomes} unmatched.`,
8130
+ manual_review: `${comparable} comparable task${comparable === 1 ? "" : "s"}; ${exact} exact agreement${exact === 1 ? "" : "s"}. At least 5 exact numeric examples are required before a bounded-policy suggestion.`,
8131
+ suggested_bounded_policy: suggestions.length > 0 ? `${suggestions.length} inactive bounded-policy suggestion${suggestions.length === 1 ? "" : "s"}; a human must review and activate any contract change separately.` : "No policy suggestion is available."
8132
+ };
8133
+ const labels = ["Observe", "Compare", "Manual review", "Suggested bounded policy"];
8134
+ return {
8135
+ current_stage: currentStage,
8136
+ minimum_policy_sample_size: 5,
8137
+ insufficient_sample_size: suggestions.length === 0,
8138
+ stages: stageOrder.map((stage, index) => ({
8139
+ name: labels[index],
8140
+ status: index < currentIndex ? "complete" : index === currentIndex ? "current" : "locked",
8141
+ detail: details[stage]
8142
+ })),
8143
+ automatic_activation: false
8144
+ };
8145
+ }
7832
8146
  function safeSqliteFailure(_error, fallback) {
7833
8147
  return fallback;
7834
8148
  }
@@ -10811,8 +11125,30 @@ async function boundedJwksFetch(resource, init, maxBytes) {
10811
11125
  body.set(chunk, offset);
10812
11126
  offset += chunk.byteLength;
10813
11127
  }
11128
+ if (response.ok) validatePublicJwks(body);
10814
11129
  return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
10815
11130
  }
11131
+ function validatePublicJwks(body) {
11132
+ let parsed;
11133
+ try {
11134
+ parsed = JSON.parse(new TextDecoder().decode(body));
11135
+ } catch {
11136
+ throw new Error("JWKS response is not valid JSON");
11137
+ }
11138
+ if (!isRecord4(parsed) || !Array.isArray(parsed.keys) || parsed.keys.length > 128) {
11139
+ throw new Error("JWKS response must contain a bounded keys array");
11140
+ }
11141
+ const privateMembers = /* @__PURE__ */ new Set(["d", "p", "q", "dp", "dq", "qi", "oth", "k"]);
11142
+ for (const key of parsed.keys) {
11143
+ if (!isRecord4(key) || typeof key.kty !== "string") throw new Error("JWKS contains an invalid key");
11144
+ if (key.kty === "oct" || Object.keys(key).some((member) => privateMembers.has(member))) {
11145
+ throw new Error("JWKS must contain public verification keys only");
11146
+ }
11147
+ }
11148
+ }
11149
+ function isRecord4(value) {
11150
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11151
+ }
10816
11152
  function trimmedEnv(env, name) {
10817
11153
  const value = env[name]?.trim();
10818
11154
  return value || void 0;
@@ -11355,8 +11691,8 @@ function loadCloudLinkedConnection(config, env = process.env) {
11355
11691
  } catch (error) {
11356
11692
  throw new McpRuntimeError("CLOUD_CONNECTION_INVALID", `Unable to read the reviewed Cloud connection file: ${error instanceof Error ? error.message : String(error)}`);
11357
11693
  }
11358
- const root = isRecord4(parsed) ? parsed : {};
11359
- const cloud = isRecord4(root.cloud) ? root.cloud : void 0;
11694
+ const root = isRecord5(parsed) ? parsed : {};
11695
+ const cloud = isRecord5(root.cloud) ? root.cloud : void 0;
11360
11696
  if (!cloud) throw new McpRuntimeError("CLOUD_CONNECTION_INVALID", "Cloud connection file must contain a cloud object.");
11361
11697
  const baseUrlEnv = nonEmptyString(cloud.base_url_env) ?? "SYNAPSOR_CLOUD_BASE_URL";
11362
11698
  const runnerTokenEnv = nonEmptyString(cloud.runner_token_env) ?? "SYNAPSOR_RUNNER_TOKEN";
@@ -11547,13 +11883,13 @@ function cloudSafeChangeSet(changeSet) {
11547
11883
  }
11548
11884
  function stripCloudPrincipalColumn(changeSet, column) {
11549
11885
  const strip = (value) => {
11550
- if (isRecord4(value)) delete value[column];
11886
+ if (isRecord5(value)) delete value[column];
11551
11887
  };
11552
11888
  strip(changeSet.before);
11553
11889
  strip(changeSet.after);
11554
- if ("frozen_set" in changeSet && isRecord4(changeSet.frozen_set) && Array.isArray(changeSet.frozen_set.members)) {
11890
+ if ("frozen_set" in changeSet && isRecord5(changeSet.frozen_set) && Array.isArray(changeSet.frozen_set.members)) {
11555
11891
  for (const member of changeSet.frozen_set.members) {
11556
- if (!isRecord4(member)) continue;
11892
+ if (!isRecord5(member)) continue;
11557
11893
  strip(member.before);
11558
11894
  strip(member.after);
11559
11895
  }
@@ -11740,7 +12076,7 @@ var CloudLinkedSynchronizer = class {
11740
12076
  if (item.kind === "proposal") return this.client.submitProposal(item.payload);
11741
12077
  if (item.kind === "activity") return this.client.submitActivity(item.payload);
11742
12078
  if (item.kind === "result") {
11743
- const result = isRecord4(item.payload.result) ? item.payload.result : void 0;
12079
+ const result = isRecord5(item.payload.result) ? item.payload.result : void 0;
11744
12080
  const leaseId = nonEmptyString(item.payload.lease_id);
11745
12081
  if (!result || !leaseId) throw new McpRuntimeError("CLOUD_RESULT_OUTBOX_INVALID", "Cloud result outbox entry is missing a result or lease identity.");
11746
12082
  return this.client.result(result, leaseId);
@@ -11749,9 +12085,9 @@ var CloudLinkedSynchronizer = class {
11749
12085
  }
11750
12086
  };
11751
12087
  function cloudGovernanceStatusPayload(response) {
11752
- const decision = isRecord4(response.decision) ? response.decision : void 0;
11753
- const job = isRecord4(response.job) ? response.job : void 0;
11754
- const result = isRecord4(response.result) ? response.result : void 0;
12088
+ const decision = isRecord5(response.decision) ? response.decision : void 0;
12089
+ const job = isRecord5(response.job) ? response.job : void 0;
12090
+ const result = isRecord5(response.result) ? response.result : void 0;
11755
12091
  const actor = nonEmptyString(decision?.actor);
11756
12092
  return JSON.parse(JSON.stringify({
11757
12093
  contract_id: nonEmptyString(response.contract_id),
@@ -11844,9 +12180,17 @@ function mergeContractIntoRuntimeConfig(config, contract, origin, seenCapabiliti
11844
12180
  }
11845
12181
  }
11846
12182
  function runtimeContextFromSpec(context) {
12183
+ const unsupportedSessionBinding = context.bindings.find((binding) => binding.source === "session");
12184
+ if (unsupportedSessionBinding) {
12185
+ throw new Error(
12186
+ `SESSION_BINDING_UNSUPPORTED: context ${context.name} binding ${unsupportedSessionBinding.name} uses canonical SESSION source, but Synapsor Runner has no generic web-session trust provider. Use ENVIRONMENT for local stdio, HTTP_CLAIM for verified HTTP JWT claims, or CLOUD_SESSION for verified Cloud-linked identity.`
12187
+ );
12188
+ }
11847
12189
  const tenantBinding = context.bindings.find((binding) => binding.name === context.tenant_binding) ?? context.bindings.find((binding) => binding.name === "tenant_id");
11848
12190
  const principalBinding = context.bindings.find((binding) => binding.name === context.principal_binding) ?? context.bindings.find((binding) => binding.name === "principal");
11849
- const provider = context.bindings.some((binding) => binding.source === "environment") ? "environment" : context.bindings.some((binding) => binding.source === "cloud_session") ? "cloud_session" : context.bindings.some((binding) => binding.source === "http_claim") ? "http_claims" : context.bindings.some((binding) => binding.source === "static_dev") ? "static_dev" : "environment";
12191
+ const provider = context.bindings.some((binding) => binding.source === "environment") ? "environment" : context.bindings.some((binding) => binding.source === "cloud_session") ? "cloud_session" : context.bindings.some((binding) => binding.source === "http_claim") ? "http_claims" : context.bindings.some((binding) => binding.source === "static_dev") ? "static_dev" : (() => {
12192
+ throw new Error(`TRUSTED_CONTEXT_BINDING_UNSUPPORTED: context ${context.name} has no binding source supported by Synapsor Runner.`);
12193
+ })();
11850
12194
  return {
11851
12195
  provider,
11852
12196
  tenant_binding: context.tenant_binding,
@@ -12258,24 +12602,253 @@ async function serveStdio(options = {}) {
12258
12602
  process.once("SIGTERM", close);
12259
12603
  });
12260
12604
  }
12605
+ function resolveHttpSecurity(config, options, host, env, usesSessionAuth) {
12606
+ const configured = config.http_security;
12607
+ const loopback = isLoopbackHost(host);
12608
+ if (options.devNoAuth && !loopback) {
12609
+ throw new McpRuntimeError("HTTP_DEV_NO_AUTH_UNSAFE_HOST", "--dev-no-auth is only allowed with localhost or 127.0.0.1.");
12610
+ }
12611
+ const deployment = configured?.deployment ?? (loopback ? "loopback" : void 0);
12612
+ if (!deployment) {
12613
+ throw new McpRuntimeError(
12614
+ "HTTP_REMOTE_DEPLOYMENT_REQUIRED",
12615
+ "A non-loopback listener requires http_security.deployment single_tenant or shared."
12616
+ );
12617
+ }
12618
+ if (!loopback && deployment === "loopback") {
12619
+ throw new McpRuntimeError("HTTP_LOOPBACK_PROFILE_REMOTE", "http_security.deployment loopback cannot bind a non-loopback listener.");
12620
+ }
12621
+ if (deployment === "shared" && !usesSessionAuth) {
12622
+ throw new McpRuntimeError("HTTP_SHARED_SESSION_AUTH_REQUIRED", "Shared HTTP deployment requires signed per-session http_claims identity.");
12623
+ }
12624
+ if (deployment !== "shared" && usesSessionAuth && !loopback) {
12625
+ throw new McpRuntimeError("HTTP_SHARED_DEPLOYMENT_REQUIRED", "Remote http_claims identity requires http_security.deployment shared.");
12626
+ }
12627
+ const optionChannels = [
12628
+ options.trustedTlsProxy ? "trusted_tls_proxy" : void 0,
12629
+ options.unsafeAllowCleartextHttp ? "insecure_http_break_glass" : void 0
12630
+ ].filter((value) => Boolean(value));
12631
+ if (optionChannels.length > 1) {
12632
+ throw new McpRuntimeError("HTTP_CHANNEL_CONFLICT", "Choose only one of trusted TLS proxy or unsafe cleartext break-glass mode.");
12633
+ }
12634
+ const requestedChannel = optionChannels[0] ?? configured?.channel;
12635
+ if (options.tls && requestedChannel && requestedChannel !== "direct_tls") {
12636
+ throw new McpRuntimeError("HTTP_CHANNEL_CONFLICT", "Runner-owned TLS cannot be combined with trusted-proxy or insecure-cleartext channel declarations.");
12637
+ }
12638
+ let channel;
12639
+ if (options.tls) channel = "direct_tls";
12640
+ else if (requestedChannel === "direct_tls") {
12641
+ throw new McpRuntimeError("HTTP_TLS_MATERIAL_REQUIRED", "http_security.channel direct_tls requires Runner TLS certificate and key material.");
12642
+ } else if (requestedChannel === "trusted_tls_proxy") channel = "trusted_tls_proxy";
12643
+ else if (requestedChannel === "insecure_http_break_glass") channel = "insecure_http_break_glass";
12644
+ else if (loopback) channel = "loopback_cleartext";
12645
+ else {
12646
+ throw new McpRuntimeError(
12647
+ "HTTP_REMOTE_CLEARTEXT_REFUSED",
12648
+ "Refusing non-loopback cleartext HTTP. Configure Runner-owned TLS, an explicit trusted TLS proxy, or --unsafe-allow-cleartext-http break glass."
12649
+ );
12650
+ }
12651
+ if (options.devNoAuth && channel === "insecure_http_break_glass") {
12652
+ throw new McpRuntimeError("HTTP_BREAK_GLASS_AUTH_REQUIRED", "Unsafe cleartext break-glass mode never disables authentication.");
12653
+ }
12654
+ if (options.devNoAuth && deployment !== "loopback") {
12655
+ throw new McpRuntimeError("HTTP_DEV_NO_AUTH_PROFILE_INVALID", "--dev-no-auth is valid only for a loopback development deployment.");
12656
+ }
12657
+ const activeTokenEnv = options.authTokenEnv ?? configured?.static_token?.active_env ?? "SYNAPSOR_RUNNER_HTTP_TOKEN";
12658
+ const previousTokenEnv = options.previousAuthTokenEnv ?? configured?.static_token?.previous_env;
12659
+ if (previousTokenEnv && previousTokenEnv === activeTokenEnv) {
12660
+ throw new McpRuntimeError("HTTP_TOKEN_ENV_REUSED", "Active and previous HTTP token environment variables must be different.");
12661
+ }
12662
+ const activeToken = options.devNoAuth || usesSessionAuth ? void 0 : envValue(env, activeTokenEnv);
12663
+ const previousToken = options.devNoAuth || usesSessionAuth || !previousTokenEnv ? void 0 : envValue(env, previousTokenEnv);
12664
+ if (!options.devNoAuth && !usesSessionAuth && !activeToken) {
12665
+ throw new McpRuntimeError("HTTP_AUTH_TOKEN_MISSING", `${activeTokenEnv} is not set. HTTP MCP requires bearer auth by default.`);
12666
+ }
12667
+ if (previousTokenEnv && !usesSessionAuth && !options.devNoAuth && !previousToken) {
12668
+ throw new McpRuntimeError("HTTP_PREVIOUS_AUTH_TOKEN_MISSING", `${previousTokenEnv} is configured for rotation but is not set.`);
12669
+ }
12670
+ if (activeToken && previousToken && constantTimeTokenEquals(activeToken, previousToken)) {
12671
+ throw new McpRuntimeError("HTTP_TOKEN_ROTATION_DUPLICATE", "Active and previous HTTP endpoint tokens must differ.");
12672
+ }
12673
+ const weakStaticToken = Boolean(activeToken && !strongOpaqueToken(activeToken)) || Boolean(previousToken && !strongOpaqueToken(previousToken));
12674
+ if (!loopback && !usesSessionAuth && weakStaticToken) {
12675
+ throw new McpRuntimeError("HTTP_AUTH_TOKEN_WEAK", "Non-loopback static endpoint tokens must contain at least 32 bytes of high-entropy secret material.");
12676
+ }
12677
+ const configuredOrigins = configured?.allowed_origins ?? [];
12678
+ const allowedOrigins = new Set(configuredOrigins);
12679
+ if (options.corsOrigin) {
12680
+ if (!isExactHttpOrigin2(options.corsOrigin)) {
12681
+ throw new McpRuntimeError("HTTP_CORS_ORIGIN_INVALID", "--cors-origin must be one exact HTTP(S) origin; wildcards, paths, credentials, query, and fragments are forbidden.");
12682
+ }
12683
+ allowedOrigins.add(options.corsOrigin);
12684
+ }
12685
+ const allowedHosts = configured?.allowed_hosts?.map((value) => value.toLowerCase()) ?? defaultAllowedHosts(host);
12686
+ if (!loopback && allowedHosts.length === 0) {
12687
+ throw new McpRuntimeError("HTTP_ALLOWED_HOSTS_REQUIRED", "Non-loopback HTTP requires http_security.allowed_hosts with exact public/direct Host authorities.");
12688
+ }
12689
+ const rawLimits = configured?.limits;
12690
+ const limits = {
12691
+ maxRequestBytes: rawLimits?.max_request_bytes ?? 1048576,
12692
+ maxHeaderBytes: rawLimits?.max_header_bytes ?? 16384,
12693
+ maxSessions: rawLimits?.max_sessions ?? 1024,
12694
+ sessionIdleTimeoutMs: (rawLimits?.session_idle_timeout_seconds ?? 900) * 1e3,
12695
+ requestTimeoutMs: rawLimits?.request_timeout_ms ?? 3e4,
12696
+ headersTimeoutMs: rawLimits?.headers_timeout_ms ?? 1e4,
12697
+ keepAliveTimeoutMs: rawLimits?.keep_alive_timeout_ms ?? 5e3,
12698
+ maxConnections: rawLimits?.max_connections ?? 2048
12699
+ };
12700
+ const oauth = configured?.oauth_resource ? resolveOauthResource(configured.oauth_resource) : void 0;
12701
+ if (deployment === "shared") {
12702
+ if (!oauth) throw new McpRuntimeError("HTTP_OAUTH_RESOURCE_REQUIRED", "Shared HTTP deployment requires RFC 9728 protected-resource metadata.");
12703
+ const auth = config.session_auth;
12704
+ if (!auth?.issuer || !auth.audience) {
12705
+ throw new McpRuntimeError("HTTP_JWT_ISSUER_AUDIENCE_REQUIRED", "Shared HTTP deployment requires exact session_auth issuer and audience/resource.");
12706
+ }
12707
+ if (auth.audience !== configured?.oauth_resource?.resource) {
12708
+ throw new McpRuntimeError("HTTP_RESOURCE_AUDIENCE_MISMATCH", "session_auth.audience must exactly match http_security.oauth_resource.resource.");
12709
+ }
12710
+ }
12711
+ return {
12712
+ deployment,
12713
+ channel,
12714
+ activeToken,
12715
+ previousToken,
12716
+ activeTokenEnv,
12717
+ previousTokenEnv,
12718
+ weakStaticToken,
12719
+ allowedOrigins,
12720
+ allowedHosts,
12721
+ limits,
12722
+ oauth
12723
+ };
12724
+ }
12725
+ function resolveOauthResource(input) {
12726
+ const resource = new URL(input.resource);
12727
+ const pathname = resource.pathname === "/" ? "" : resource.pathname.replace(/\/$/, "");
12728
+ const metadataPath = `/.well-known/oauth-protected-resource${pathname}`;
12729
+ const metadataUrl = new URL(metadataPath || "/.well-known/oauth-protected-resource", resource.origin).toString();
12730
+ const metadata = OAuthProtectedResourceMetadataSchema.parse({
12731
+ resource: input.resource,
12732
+ authorization_servers: input.authorization_servers,
12733
+ ...input.scopes_supported ? { scopes_supported: input.scopes_supported } : {},
12734
+ bearer_methods_supported: ["header"],
12735
+ ...input.resource_name ? { resource_name: input.resource_name } : {},
12736
+ ...input.resource_documentation ? { resource_documentation: input.resource_documentation } : {}
12737
+ });
12738
+ return { metadata, metadataUrl, metadataPath, requiredScopes: input.required_scopes ?? [] };
12739
+ }
12740
+ function defaultAllowedHosts(host) {
12741
+ if (!isLoopbackHost(host)) {
12742
+ return host === "0.0.0.0" || host === "::" ? [] : [host.toLowerCase()];
12743
+ }
12744
+ return ["localhost", "127.0.0.1", "[::1]", host.toLowerCase()];
12745
+ }
12746
+ function applyHttpServerLimits(server, limits) {
12747
+ server.requestTimeout = limits.requestTimeoutMs;
12748
+ server.headersTimeout = limits.headersTimeoutMs;
12749
+ server.keepAliveTimeout = limits.keepAliveTimeoutMs;
12750
+ server.maxConnections = limits.maxConnections;
12751
+ }
12752
+ function validateTlsMaterial(tls) {
12753
+ if (!tls) return;
12754
+ try {
12755
+ createSecureContext({ cert: tls.cert, key: tls.key, ca: tls.ca });
12756
+ } catch {
12757
+ throw new McpRuntimeError("HTTP_TLS_MATERIAL_INVALID", "HTTP TLS certificate, private key, or CA material is invalid.");
12758
+ }
12759
+ }
12760
+ function validateHttpRequestSecurity(request, response, security) {
12761
+ if ((request.url?.length ?? 0) > 8192) {
12762
+ writeJson(response, 414, { ok: false, error: "uri_too_long" });
12763
+ return false;
12764
+ }
12765
+ const host = headerValue(request.headers.host);
12766
+ if (!host || !hostAllowed(host, security.allowedHosts)) {
12767
+ writeJson(response, 403, { ok: false, error: "host_forbidden" });
12768
+ return false;
12769
+ }
12770
+ const origin = headerValue(request.headers.origin);
12771
+ if (origin && !security.allowedOrigins.has(origin)) {
12772
+ writeJson(response, 403, { ok: false, error: "origin_forbidden" });
12773
+ return false;
12774
+ }
12775
+ setHttpSecurityHeaders(response);
12776
+ if (origin) setCorsHeaders(response, origin);
12777
+ return true;
12778
+ }
12779
+ function hostAllowed(rawHost, allowedHosts) {
12780
+ const actual = parseHostAuthority(rawHost);
12781
+ if (!actual) return false;
12782
+ return allowedHosts.some((allowed) => {
12783
+ const expected = parseHostAuthority(allowed);
12784
+ if (!expected || expected.hostname !== actual.hostname) return false;
12785
+ return expected.port ? expected.port === actual.port : true;
12786
+ });
12787
+ }
12788
+ function parseHostAuthority(value) {
12789
+ if (!value || value !== value.trim() || /[\s,/?#\\]/.test(value)) return void 0;
12790
+ try {
12791
+ const parsed = new URL(`http://${value}`);
12792
+ if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) return void 0;
12793
+ return { hostname: parsed.hostname.toLowerCase(), port: parsed.port };
12794
+ } catch {
12795
+ return void 0;
12796
+ }
12797
+ }
12798
+ function isExactHttpOrigin2(value) {
12799
+ if (value === "*" || value === "null") return false;
12800
+ try {
12801
+ const origin = new URL(value);
12802
+ return (origin.protocol === "http:" || origin.protocol === "https:") && !origin.username && !origin.password && origin.pathname === "/" && !origin.search && !origin.hash && origin.origin === value;
12803
+ } catch {
12804
+ return false;
12805
+ }
12806
+ }
12807
+ function setHttpSecurityHeaders(response) {
12808
+ response.setHeader("cache-control", "no-store");
12809
+ response.setHeader("x-content-type-options", "nosniff");
12810
+ response.setHeader("referrer-policy", "no-referrer");
12811
+ }
12812
+ function strongOpaqueToken(token) {
12813
+ if (Buffer.byteLength(token, "utf8") < 32) return false;
12814
+ if (new Set(token).size < 12) return false;
12815
+ return !/^(.)\1+$/.test(token) && !/(?:password|secret|token|changeme|example|development)/i.test(token);
12816
+ }
12817
+ function httpAuthChallenge(security, insufficientScope = false) {
12818
+ const parts = ["Bearer"];
12819
+ if (insufficientScope) parts.push('error="insufficient_scope"');
12820
+ if (security.oauth) {
12821
+ parts.push(`resource_metadata="${security.oauth.metadataUrl}"`);
12822
+ if (security.oauth.requiredScopes.length) parts.push(`scope="${security.oauth.requiredScopes.join(" ")}"`);
12823
+ }
12824
+ return parts.join(" ");
12825
+ }
12826
+ function maybeServeOauthMetadata(request, response, security, pathname) {
12827
+ if (!security.oauth || request.method !== "GET") return false;
12828
+ if (pathname !== security.oauth.metadataPath && pathname !== "/.well-known/oauth-protected-resource") return false;
12829
+ writeJson(response, 200, security.oauth.metadata);
12830
+ return true;
12831
+ }
12832
+ function writeAuthenticationFailure(response, security, status, error) {
12833
+ response.setHeader("www-authenticate", httpAuthChallenge(security, status === 403));
12834
+ writeJson(response, status, { ok: false, error });
12835
+ }
12261
12836
  async function startHttpMcpServer(options = {}) {
12262
12837
  const host = options.host ?? "127.0.0.1";
12263
12838
  const port = options.port ?? 8765;
12264
- const authTokenEnv = options.authTokenEnv ?? "SYNAPSOR_RUNNER_HTTP_TOKEN";
12265
12839
  const env = options.env ?? process.env;
12266
12840
  const devNoAuth = options.devNoAuth === true;
12267
12841
  const config = resolveRuntimeConfig(options.config ?? loadRuntimeConfigFromFile(options.configPath));
12268
- const metricsAccess = resolveMetricsEndpointAccess(config, env, host);
12269
- if (devNoAuth && !isLoopbackHost(host)) {
12270
- throw new McpRuntimeError("HTTP_DEV_NO_AUTH_UNSAFE_HOST", "--dev-no-auth is only allowed with localhost or 127.0.0.1.");
12271
- }
12842
+ assertValidRunnerCapabilityConfig(config);
12272
12843
  if (configUsesHttpClaims(config)) {
12273
12844
  throw new McpRuntimeError("HTTP_CLAIMS_REQUIRES_STREAMABLE", "http_claims trusted context requires spec MCP Streamable HTTP sessions; the legacy JSON-RPC bridge cannot bind per-session context.");
12274
12845
  }
12275
- const authToken = devNoAuth ? void 0 : envValue(env, authTokenEnv);
12276
- if (!devNoAuth && !authToken) {
12277
- throw new McpRuntimeError("HTTP_AUTH_TOKEN_MISSING", `${authTokenEnv} is not set. HTTP MCP requires bearer auth by default.`);
12846
+ const security = resolveHttpSecurity(config, options, host, env, false);
12847
+ const metricsAccess = resolveMetricsEndpointAccess(config, env, host);
12848
+ if (options.tls?.requestClientCert && !options.tls.ca) {
12849
+ throw new McpRuntimeError("MTLS_CA_REQUIRED", "HTTP mTLS requires a CA bundle when client certificates are required.");
12278
12850
  }
12851
+ validateTlsMaterial(options.tls);
12279
12852
  const cloudTools = config.mode === "cloud" ? await fetchCloudToolMetadata(config, env) : void 0;
12280
12853
  if (options.readRow && Object.values(config.sources ?? {}).some((source) => source.database_scope?.mode === "postgres_rls")) {
12281
12854
  throw new McpRuntimeError("POSTGRES_RLS_CUSTOM_READER_UNVERIFIED", "Hardened postgres_rls mode requires Runner's verified PostgreSQL reader; a custom readRow cannot be attested by the stock server.");
@@ -12290,19 +12863,27 @@ async function startHttpMcpServer(options = {}) {
12290
12863
  cloudTools
12291
12864
  });
12292
12865
  const readinessCheck = options.readinessCheck ?? (() => checkRunnerReadiness(config, env));
12293
- const server = createServer((request, response) => {
12866
+ const requestHandler = (request, response) => {
12294
12867
  void handleHttpMcpRequest({
12295
12868
  request,
12296
12869
  response,
12297
12870
  runtime,
12298
- authToken,
12299
12871
  devNoAuth,
12300
- corsOrigin: options.corsOrigin,
12872
+ security,
12301
12873
  readinessCheck,
12302
12874
  metricsAccess,
12303
12875
  metricsProvider: () => renderRuntimeMetrics(runtime.store, runtime.poolMetrics(), runtime.rateLimitMetrics(), readinessCheck)
12304
12876
  });
12305
- });
12877
+ };
12878
+ const server = options.tls ? createHttpsServer({
12879
+ cert: options.tls.cert,
12880
+ key: options.tls.key,
12881
+ ca: options.tls.ca,
12882
+ requestCert: options.tls.requestClientCert === true,
12883
+ rejectUnauthorized: options.tls.requestClientCert === true,
12884
+ maxHeaderSize: security.limits.maxHeaderBytes
12885
+ }, requestHandler) : createServer({ maxHeaderSize: security.limits.maxHeaderBytes }, requestHandler);
12886
+ applyHttpServerLimits(server, security.limits);
12306
12887
  try {
12307
12888
  await new Promise((resolve2, reject) => {
12308
12889
  server.once("error", reject);
@@ -12318,13 +12899,19 @@ async function startHttpMcpServer(options = {}) {
12318
12899
  const address = server.address();
12319
12900
  const actualHost = address.address === "::" ? host : address.address;
12320
12901
  const actualPort = address.port;
12321
- const url = `http://${actualHost}:${actualPort}/mcp`;
12902
+ const scheme = options.tls ? "https" : "http";
12903
+ const url = `${scheme}://${actualHost}:${actualPort}/mcp`;
12322
12904
  if (options.log !== false) {
12323
12905
  const log = options.log ?? process.stderr;
12324
12906
  log.write(`Synapsor Runner HTTP MCP listening on ${url}
12325
12907
  `);
12326
- log.write(devNoAuth ? "Auth: disabled for localhost development only\n" : `Auth: bearer token from ${authTokenEnv}
12908
+ log.write(`Channel: ${security.channel}; deployment: ${security.deployment}
12909
+ `);
12910
+ if (options.tls) log.write(options.tls.requestClientCert ? "TLS: enabled, client certificates required in addition to Bearer auth\n" : "TLS: enabled\n");
12911
+ log.write(devNoAuth ? "Auth: disabled for loopback development only\n" : `Auth: opaque Bearer endpoint token from ${security.activeTokenEnv}${security.previousTokenEnv ? `; previous rotation token from ${security.previousTokenEnv}` : ""}
12327
12912
  `);
12913
+ if (security.weakStaticToken) log.write("Auth warning: loopback endpoint token is shorter or more predictable than the production requirement; generate at least 32 random bytes.\n");
12914
+ if (security.channel === "insecure_http_break_glass") log.write("SECURITY WARNING: remote Bearer traffic is using explicit insecure cleartext break glass. Credentials and data can be intercepted.\n");
12328
12915
  log.write(`Config: ${options.configPath ?? "synapsor.runner.json"}
12329
12916
  `);
12330
12917
  log.write(`Store: ${options.storePath ?? config.storage?.sqlite_path ?? "./.synapsor/local.db"}
@@ -12340,29 +12927,23 @@ async function startHttpMcpServer(options = {}) {
12340
12927
  async function startStreamableHttpMcpServer(options = {}) {
12341
12928
  const host = options.host ?? "127.0.0.1";
12342
12929
  const port = options.port ?? 8766;
12343
- const authTokenEnv = options.authTokenEnv ?? "SYNAPSOR_RUNNER_HTTP_TOKEN";
12344
12930
  const env = options.env ?? process.env;
12345
12931
  const devNoAuth = options.devNoAuth === true;
12346
12932
  const config = resolveRuntimeConfig(options.config ?? loadRuntimeConfigFromFile(options.configPath));
12347
12933
  assertValidRunnerCapabilityConfig(config);
12348
12934
  const usesSessionAuth = configUsesHttpClaims(config);
12935
+ const security = resolveHttpSecurity(config, options, host, env, usesSessionAuth);
12349
12936
  const metricsAccess = resolveMetricsEndpointAccess(config, env, host);
12350
- if (devNoAuth && !isLoopbackHost(host)) {
12351
- throw new McpRuntimeError("HTTP_DEV_NO_AUTH_UNSAFE_HOST", "--dev-no-auth is only allowed with localhost or 127.0.0.1.");
12352
- }
12353
12937
  if (devNoAuth && usesSessionAuth) {
12354
12938
  throw new McpRuntimeError("HTTP_CLAIMS_AUTH_REQUIRED", "http_claims trusted context cannot run with --dev-no-auth.");
12355
12939
  }
12356
- const authToken = devNoAuth || usesSessionAuth ? void 0 : envValue(env, authTokenEnv);
12357
- if (!devNoAuth && !usesSessionAuth && !authToken) {
12358
- throw new McpRuntimeError("HTTP_AUTH_TOKEN_MISSING", `${authTokenEnv} is not set. Streamable HTTP MCP requires bearer auth by default.`);
12359
- }
12360
12940
  assertRuntimeStoreStartupReady(config, env);
12361
12941
  const sessionVerifier = usesSessionAuth ? sessionAuthVerifier(config, env, options.configPath ? path2.dirname(path2.resolve(options.configPath)) : process.cwd()) : void 0;
12362
12942
  const readinessCheck = options.readinessCheck ?? (() => checkRunnerReadiness(config, env));
12363
12943
  if (options.tls?.requestClientCert && !options.tls.ca) {
12364
12944
  throw new McpRuntimeError("MTLS_CA_REQUIRED", "Streamable HTTP mTLS requires a CA bundle when client certificates are required.");
12365
12945
  }
12946
+ validateTlsMaterial(options.tls);
12366
12947
  const cloudTools = config.mode === "cloud" ? await fetchCloudToolMetadata(config, env) : void 0;
12367
12948
  if (options.readRow && Object.values(config.sources ?? {}).some((source) => source.database_scope?.mode === "postgres_rls")) {
12368
12949
  throw new McpRuntimeError("POSTGRES_RLS_CUSTOM_READER_UNVERIFIED", "Hardened postgres_rls mode requires Runner's verified PostgreSQL reader; a custom readRow cannot be attested by the stock server.");
@@ -12375,6 +12956,7 @@ async function startStreamableHttpMcpServer(options = {}) {
12375
12956
  const sharedResources = createMcpRuntimeSharedResources(config, env, options.readRow, Date.now, options.credentialResolver);
12376
12957
  const sessions = /* @__PURE__ */ new Map();
12377
12958
  const openSessions = /* @__PURE__ */ new Set();
12959
+ const initializingSessions = { count: 0 };
12378
12960
  const requestHandler = (request, response) => {
12379
12961
  void handleStreamableHttpMcpRequest({
12380
12962
  request,
@@ -12387,12 +12969,12 @@ async function startStreamableHttpMcpServer(options = {}) {
12387
12969
  env,
12388
12970
  toolNameStyle: options.toolNameStyle,
12389
12971
  resultFormat: options.resultFormat,
12390
- authToken,
12391
12972
  sessionVerifier,
12392
12973
  devNoAuth,
12393
- corsOrigin: options.corsOrigin,
12974
+ security,
12394
12975
  sessions,
12395
12976
  openSessions,
12977
+ initializingSessions,
12396
12978
  readinessCheck,
12397
12979
  metricsAccess,
12398
12980
  metricsProvider: () => renderRuntimeMetrics(sharedStore, sharedResources.poolMetrics(), sharedResources.rateLimitMetrics(), readinessCheck)
@@ -12403,8 +12985,14 @@ async function startStreamableHttpMcpServer(options = {}) {
12403
12985
  key: options.tls.key,
12404
12986
  ca: options.tls.ca,
12405
12987
  requestCert: options.tls.requestClientCert === true,
12406
- rejectUnauthorized: options.tls.requestClientCert === true
12407
- }, requestHandler) : createServer(requestHandler);
12988
+ rejectUnauthorized: options.tls.requestClientCert === true,
12989
+ maxHeaderSize: security.limits.maxHeaderBytes
12990
+ }, requestHandler) : createServer({ maxHeaderSize: security.limits.maxHeaderBytes }, requestHandler);
12991
+ applyHttpServerLimits(server, security.limits);
12992
+ const sessionReaper = setInterval(() => {
12993
+ void pruneExpiredStreamableSessions(sessions, openSessions, security.limits.sessionIdleTimeoutMs, Date.now());
12994
+ }, Math.min(3e4, Math.max(1e3, Math.floor(security.limits.sessionIdleTimeoutMs / 2))));
12995
+ sessionReaper.unref?.();
12408
12996
  try {
12409
12997
  await new Promise((resolve2, reject) => {
12410
12998
  server.once("error", reject);
@@ -12414,6 +13002,7 @@ async function startStreamableHttpMcpServer(options = {}) {
12414
13002
  });
12415
13003
  });
12416
13004
  } catch (error) {
13005
+ clearInterval(sessionReaper);
12417
13006
  await cloudSynchronizer?.stop();
12418
13007
  await closeStreamableSessions(openSessions);
12419
13008
  await sharedResources.close();
@@ -12429,10 +13018,16 @@ async function startStreamableHttpMcpServer(options = {}) {
12429
13018
  const log = options.log ?? process.stderr;
12430
13019
  log.write(`Synapsor Runner Streamable HTTP MCP listening on ${url}
12431
13020
  `);
12432
- if (options.tls) log.write(options.tls.requestClientCert ? "TLS: enabled, client certificates required\n" : "TLS: enabled\n");
12433
- log.write(devNoAuth ? "Auth: disabled for localhost development only\n" : usesSessionAuth ? `Auth: signed per-session JWT (${config.session_auth?.provider})
12434
- ` : `Auth: bearer token from ${authTokenEnv}
13021
+ log.write(`Channel: ${security.channel}; deployment: ${security.deployment}
13022
+ `);
13023
+ if (options.tls) log.write(options.tls.requestClientCert ? "TLS: enabled, client certificates required in addition to Bearer auth\n" : "TLS: enabled\n");
13024
+ log.write(devNoAuth ? "Auth: disabled for localhost development only\n" : usesSessionAuth ? `Auth: signed per-session JWT (${config.session_auth?.provider}); issuer and resource checked on every request
13025
+ ` : `Auth: opaque Bearer endpoint token from ${security.activeTokenEnv}${security.previousTokenEnv ? `; previous rotation token from ${security.previousTokenEnv}` : ""}
12435
13026
  `);
13027
+ if (security.oauth) log.write(`OAuth resource metadata: ${security.oauth.metadataUrl}
13028
+ `);
13029
+ if (security.weakStaticToken) log.write("Auth warning: loopback endpoint token is shorter or more predictable than the production requirement; generate at least 32 random bytes.\n");
13030
+ if (security.channel === "insecure_http_break_glass") log.write("SECURITY WARNING: remote Bearer traffic is using explicit insecure cleartext break glass. Credentials and data can be intercepted.\n");
12436
13031
  for (const assurance of describeIsolationAssurance(config)) {
12437
13032
  log.write(`Isolation ${assurance.source}: ${assurance.mode}; trusted context: ${assurance.trusted_context.request_binding}
12438
13033
  `);
@@ -12448,19 +13043,23 @@ async function startStreamableHttpMcpServer(options = {}) {
12448
13043
  host: actualHost,
12449
13044
  port: actualPort,
12450
13045
  url,
12451
- close: () => closeStreamableHttpServer(server, openSessions, sharedResources, sharedStore, cloudSynchronizer)
13046
+ close: () => {
13047
+ clearInterval(sessionReaper);
13048
+ return closeStreamableHttpServer(server, openSessions, sharedResources, sharedStore, cloudSynchronizer);
13049
+ }
12452
13050
  };
12453
13051
  }
12454
13052
  async function handleStreamableHttpMcpRequest(input) {
12455
- const { request, response, config, storePath, sharedStore, sharedResources, cloudTools, env, toolNameStyle, resultFormat, authToken, sessionVerifier, devNoAuth, corsOrigin, sessions, openSessions, readinessCheck, metricsAccess, metricsProvider } = input;
13053
+ const { request, response, config, storePath, sharedStore, sharedResources, cloudTools, env, toolNameStyle, resultFormat, sessionVerifier, devNoAuth, security, sessions, openSessions, initializingSessions, readinessCheck, metricsAccess, metricsProvider } = input;
12456
13054
  try {
12457
- setCorsHeaders(response, corsOrigin);
12458
- if (request.method === "OPTIONS" && corsOrigin) {
13055
+ if (!validateHttpRequestSecurity(request, response, security)) return;
13056
+ if (request.method === "OPTIONS" && request.headers.origin) {
12459
13057
  response.statusCode = 204;
12460
13058
  response.end();
12461
13059
  return;
12462
13060
  }
12463
13061
  const url = new URL(request.url ?? "/", "http://localhost");
13062
+ if (maybeServeOauthMetadata(request, response, security, url.pathname)) return;
12464
13063
  if (request.method === "GET" && url.pathname === "/healthz") {
12465
13064
  writeJson(response, 200, {
12466
13065
  ok: true,
@@ -12482,11 +13081,13 @@ async function handleStreamableHttpMcpRequest(input) {
12482
13081
  writeJson(response, 404, { ok: false, error: "not_found" });
12483
13082
  return;
12484
13083
  }
12485
- const authentication = await authenticateStreamableRequest(config, request.headers.authorization, sessionVerifier, authToken, devNoAuth);
12486
- if (!authentication) {
12487
- writeJson(response, 401, { ok: false, error: "unauthorized" });
13084
+ const authResult = await authenticateStreamableRequest(config, request.headers.authorization, sessionVerifier, security, devNoAuth);
13085
+ if (!authResult.ok) {
13086
+ writeAuthenticationFailure(response, security, authResult.status, authResult.error);
12488
13087
  return;
12489
13088
  }
13089
+ const authentication = authResult.authentication;
13090
+ await pruneExpiredStreamableSessions(sessions, openSessions, security.limits.sessionIdleTimeoutMs, Date.now());
12490
13091
  const sessionId = headerValue(request.headers["mcp-session-id"]);
12491
13092
  if (sessionId) {
12492
13093
  const existing = sessions.get(sessionId);
@@ -12495,9 +13096,10 @@ async function handleStreamableHttpMcpRequest(input) {
12495
13096
  return;
12496
13097
  }
12497
13098
  if (existing.authFingerprint !== authentication.fingerprint) {
12498
- writeJson(response, 401, { ok: false, error: "session_auth_mismatch" });
13099
+ writeAuthenticationFailure(response, security, 401, "unauthorized");
12499
13100
  return;
12500
13101
  }
13102
+ existing.lastSeenAt = Date.now();
12501
13103
  await existing.transport.handleRequest(request, response);
12502
13104
  return;
12503
13105
  }
@@ -12505,54 +13107,73 @@ async function handleStreamableHttpMcpRequest(input) {
12505
13107
  writeJson(response, 400, jsonRpcError(null, -32e3, "MCP initialize request is required before using this Streamable HTTP session."));
12506
13108
  return;
12507
13109
  }
12508
- const parsedBody = JSON.parse(await readRequestBody(request));
12509
- if (!containsInitializeRequest(parsedBody)) {
12510
- writeJson(response, 400, jsonRpcError(requestIdFromPayload(parsedBody), -32e3, "First Streamable HTTP MCP request must be initialize."));
13110
+ if (openSessions.size + initializingSessions.count >= security.limits.maxSessions) {
13111
+ response.setHeader("retry-after", "1");
13112
+ writeJson(response, 503, { ok: false, error: "session_capacity_exhausted", retryable: true, retry_after_ms: 1e3 });
12511
13113
  return;
12512
13114
  }
12513
- let session;
12514
- const transport = new StreamableHTTPServerTransport({
12515
- sessionIdGenerator: () => crypto3.randomUUID(),
12516
- onsessioninitialized: (newSessionId) => {
12517
- if (session) {
12518
- session.sessionId = newSessionId;
12519
- sessions.set(newSessionId, session);
12520
- }
12521
- },
12522
- onsessionclosed: (closedSessionId) => {
12523
- const closed = sessions.get(closedSessionId);
12524
- if (closed) {
12525
- disposeStreamableSession(closed, sessions, openSessions);
13115
+ initializingSessions.count += 1;
13116
+ let initializingSession;
13117
+ try {
13118
+ const parsedBody = JSON.parse(await readRequestBody(request, security.limits.maxRequestBytes));
13119
+ if (!containsInitializeRequest(parsedBody)) {
13120
+ writeJson(response, 400, jsonRpcError(requestIdFromPayload(parsedBody), -32e3, "First Streamable HTTP MCP request must be initialize."));
13121
+ return;
13122
+ }
13123
+ const transport = new StreamableHTTPServerTransport({
13124
+ sessionIdGenerator: () => crypto3.randomUUID(),
13125
+ onsessioninitialized: (newSessionId) => {
13126
+ if (initializingSession) {
13127
+ initializingSession.sessionId = newSessionId;
13128
+ sessions.set(newSessionId, initializingSession);
13129
+ }
13130
+ },
13131
+ onsessionclosed: (closedSessionId) => {
13132
+ const closed = sessions.get(closedSessionId);
13133
+ if (closed) {
13134
+ disposeStreamableSession(closed, sessions, openSessions);
13135
+ }
12526
13136
  }
13137
+ });
13138
+ const runtime = createMcpRuntime(config, {
13139
+ env,
13140
+ storePath,
13141
+ store: sharedStore,
13142
+ sharedResources,
13143
+ resultFormat,
13144
+ cloudTools,
13145
+ trustedContext: authentication.context
13146
+ });
13147
+ initializingSession = { transport, runtime, authFingerprint: authentication.fingerprint, lastSeenAt: Date.now() };
13148
+ openSessions.add(initializingSession);
13149
+ transport.onclose = () => {
13150
+ if (initializingSession) disposeStreamableSession(initializingSession, sessions, openSessions);
13151
+ };
13152
+ await createSynapsorMcpServer(runtime, { toolNameStyle }).connect(transport);
13153
+ await transport.handleRequest(request, response, parsedBody);
13154
+ } catch (error) {
13155
+ if (initializingSession) {
13156
+ disposeStreamableSession(initializingSession, sessions, openSessions);
13157
+ await initializingSession.transport.close().catch(() => void 0);
12527
13158
  }
12528
- });
12529
- const runtime = createMcpRuntime(config, {
12530
- env,
12531
- storePath,
12532
- store: sharedStore,
12533
- sharedResources,
12534
- resultFormat,
12535
- cloudTools,
12536
- trustedContext: authentication.context
12537
- });
12538
- session = { transport, runtime, authFingerprint: authentication.fingerprint };
12539
- openSessions.add(session);
12540
- transport.onclose = () => {
12541
- if (session) disposeStreamableSession(session, sessions, openSessions);
12542
- };
12543
- await createSynapsorMcpServer(runtime, { toolNameStyle }).connect(transport);
12544
- await transport.handleRequest(request, response, parsedBody);
13159
+ throw error;
13160
+ } finally {
13161
+ initializingSessions.count -= 1;
13162
+ }
12545
13163
  } catch (error) {
12546
- const message2 = sanitizeHttpError(error, authToken);
12547
- if (!response.headersSent) writeJson(response, 200, jsonRpcError(null, -32e3, message2));
13164
+ const message2 = sanitizeHttpError(error, security.activeToken, security.previousToken);
13165
+ if (!response.headersSent && error instanceof McpRuntimeError && error.code === "HTTP_BODY_TOO_LARGE") {
13166
+ writeJson(response, 413, { ok: false, error: "request_too_large" });
13167
+ } else if (!response.headersSent) writeJson(response, 200, jsonRpcError(null, -32e3, message2));
12548
13168
  else response.end();
12549
13169
  }
12550
13170
  }
12551
13171
  async function handleHttpMcpRequest(input) {
12552
- const { request, response, runtime, authToken, devNoAuth, corsOrigin, readinessCheck, metricsAccess, metricsProvider } = input;
13172
+ const { request, response, runtime, devNoAuth, security, readinessCheck, metricsAccess, metricsProvider } = input;
12553
13173
  try {
12554
- setCommonHttpHeaders(response, corsOrigin);
12555
- if (request.method === "OPTIONS" && corsOrigin) {
13174
+ setCommonHttpHeaders(response);
13175
+ if (!validateHttpRequestSecurity(request, response, security)) return;
13176
+ if (request.method === "OPTIONS" && request.headers.origin) {
12556
13177
  response.statusCode = 204;
12557
13178
  response.end();
12558
13179
  return;
@@ -12583,13 +13204,13 @@ async function handleHttpMcpRequest(input) {
12583
13204
  writeJson(response, 405, { ok: false, error: "method_not_allowed" });
12584
13205
  return;
12585
13206
  }
12586
- if (!devNoAuth && !validBearerToken(request.headers.authorization, authToken ?? "")) {
12587
- writeJson(response, 401, { ok: false, error: "unauthorized" });
13207
+ if (!devNoAuth && !validBearerTokens(request.headers.authorization, [security.activeToken, security.previousToken])) {
13208
+ writeAuthenticationFailure(response, security, 401, "unauthorized");
12588
13209
  return;
12589
13210
  }
12590
- const body = await readRequestBody(request);
13211
+ const body = await readRequestBody(request, security.limits.maxRequestBytes);
12591
13212
  const payload = JSON.parse(body);
12592
- if (!isRecord4(payload)) {
13213
+ if (!isRecord5(payload)) {
12593
13214
  writeJson(response, 400, jsonRpcError(null, -32600, "JSON-RPC request must be an object."));
12594
13215
  return;
12595
13216
  }
@@ -12599,15 +13220,18 @@ async function handleHttpMcpRequest(input) {
12599
13220
  writeJson(response, 400, jsonRpcError(id, -32600, "JSON-RPC method is required."));
12600
13221
  return;
12601
13222
  }
12602
- const result = await handleHttpJsonRpcMethod(runtime, method, isRecord4(payload.params) ? payload.params : {});
13223
+ const result = await handleHttpJsonRpcMethod(runtime, method, isRecord5(payload.params) ? payload.params : {});
12603
13224
  writeJson(response, 200, {
12604
13225
  jsonrpc: "2.0",
12605
13226
  id,
12606
- result: sanitizeHttpPayload(result, authToken)
13227
+ result: sanitizeHttpPayload(result, security.activeToken, security.previousToken)
12607
13228
  });
12608
13229
  } catch (error) {
12609
- const message2 = sanitizeHttpError(error, authToken);
12610
- writeJson(response, 200, jsonRpcError(null, -32e3, message2));
13230
+ const message2 = sanitizeHttpError(error, security.activeToken, security.previousToken);
13231
+ if (!response.headersSent && error instanceof McpRuntimeError && error.code === "HTTP_BODY_TOO_LARGE") {
13232
+ writeJson(response, 413, { ok: false, error: "request_too_large" });
13233
+ } else if (!response.headersSent) writeJson(response, 200, jsonRpcError(null, -32e3, message2));
13234
+ else response.end();
12611
13235
  }
12612
13236
  }
12613
13237
  async function handleHttpJsonRpcMethod(runtime, method, params) {
@@ -12619,7 +13243,7 @@ async function handleHttpJsonRpcMethod(runtime, method, params) {
12619
13243
  if (method === "tools/call") {
12620
13244
  const name = typeof params.name === "string" ? params.name : void 0;
12621
13245
  if (!name) throw new McpRuntimeError("HTTP_TOOL_NAME_REQUIRED", "tools/call requires params.name.");
12622
- const args = isRecord4(params.arguments) ? params.arguments : isRecord4(params.args) ? params.args : {};
13246
+ const args = isRecord5(params.arguments) ? params.arguments : isRecord5(params.args) ? params.args : {};
12623
13247
  return await toolCallResult(runtime, name, args);
12624
13248
  }
12625
13249
  if (method === "resources/read") {
@@ -12649,11 +13273,21 @@ function httpToolMetadata(tool) {
12649
13273
  };
12650
13274
  }
12651
13275
  function validBearerToken(header, expected) {
12652
- if (!header?.startsWith("Bearer ")) return false;
12653
- const actual = header.slice("Bearer ".length);
12654
- const actualBuffer = Buffer.from(actual);
12655
- const expectedBuffer = Buffer.from(expected);
12656
- return actualBuffer.length === expectedBuffer.length && crypto3.timingSafeEqual(actualBuffer, expectedBuffer);
13276
+ return validBearerTokens(header, [expected]);
13277
+ }
13278
+ function validBearerTokens(header, expected) {
13279
+ const actual = bearerToken(header);
13280
+ if (!actual) return false;
13281
+ let matched = 0;
13282
+ for (const candidate of expected.filter((value) => Boolean(value))) {
13283
+ matched |= Number(constantTimeTokenEquals(actual, candidate));
13284
+ }
13285
+ return matched === 1;
13286
+ }
13287
+ function constantTimeTokenEquals(actual, expected) {
13288
+ const actualDigest = crypto3.createHash("sha256").update(actual, "utf8").digest();
13289
+ const expectedDigest = crypto3.createHash("sha256").update(expected, "utf8").digest();
13290
+ return crypto3.timingSafeEqual(actualDigest, expectedDigest);
12657
13291
  }
12658
13292
  function resolveMetricsEndpointAccess(config, env, host) {
12659
13293
  if (config.metrics?.enabled !== true) return { enabled: false };
@@ -12760,12 +13394,12 @@ async function checkRunnerReadiness(config, env = process.env, timeoutMs = 3e3)
12760
13394
  if (!executorName || checkedExecutors.has(executorName)) continue;
12761
13395
  checkedExecutors.add(executorName);
12762
13396
  components.push(await readinessComponent(`executor:${executorName}`, "EXECUTOR_READY", "EXECUTOR_UNAVAILABLE", timeoutMs, async () => {
12763
- const executor = isRecord4(config.executors?.[executorName]) ? config.executors?.[executorName] : void 0;
13397
+ const executor = isRecord5(config.executors?.[executorName]) ? config.executors?.[executorName] : void 0;
12764
13398
  if (!executor) throw new Error("executor missing");
12765
13399
  if (executor.type === "http_handler") {
12766
13400
  if (typeof executor.url_env !== "string" || !envValue(env, executor.url_env)) throw new Error("handler URL unavailable");
12767
13401
  const handlerUrl = envValue(env, executor.url_env);
12768
- const auth = isRecord4(executor.auth) ? executor.auth : void 0;
13402
+ const auth = isRecord5(executor.auth) ? executor.auth : void 0;
12769
13403
  if (auth?.type === "bearer_env" && (typeof auth.token_env !== "string" || !envValue(env, auth.token_env))) throw new Error("handler token unavailable");
12770
13404
  const response = await fetch(handlerUrl, {
12771
13405
  method: "HEAD",
@@ -12858,20 +13492,25 @@ function sessionAuthVerifier(config, env, baseDir) {
12858
13492
  throw new McpRuntimeError("SESSION_AUTH_INVALID", message2);
12859
13493
  }
12860
13494
  }
12861
- async function authenticateStreamableRequest(config, authorization, sessionVerifier, staticToken, devNoAuth) {
12862
- if (devNoAuth) return { fingerprint: "dev-no-auth" };
13495
+ async function authenticateStreamableRequest(config, authorization, sessionVerifier, security, devNoAuth) {
13496
+ if (devNoAuth) return { ok: true, authentication: { fingerprint: "dev-no-auth" } };
12863
13497
  const token = bearerToken(authorization);
12864
- if (!token) return void 0;
13498
+ if (!token) return { ok: false, status: 401, error: "unauthorized" };
12865
13499
  if (!configUsesHttpClaims(config)) {
12866
- if (!staticToken || !validBearerToken(authorization, staticToken)) return void 0;
12867
- return { fingerprint: tokenFingerprint(token) };
13500
+ if (!validBearerTokens(authorization, [security.activeToken, security.previousToken])) {
13501
+ return { ok: false, status: 401, error: "unauthorized" };
13502
+ }
13503
+ return { ok: true, authentication: { fingerprint: tokenFingerprint(token) } };
12868
13504
  }
12869
13505
  try {
12870
- if (!sessionVerifier) return void 0;
13506
+ if (!sessionVerifier) return { ok: false, status: 401, error: "unauthorized" };
12871
13507
  const context = await verifySessionJwt(config, token, sessionVerifier);
12872
- return { fingerprint: tokenFingerprint(token), context };
12873
- } catch {
12874
- return void 0;
13508
+ return { ok: true, authentication: { fingerprint: tokenFingerprint(token), context } };
13509
+ } catch (error) {
13510
+ if (error instanceof McpRuntimeError && error.code === "HTTP_INSUFFICIENT_SCOPE") {
13511
+ return { ok: false, status: 403, error: "insufficient_scope" };
13512
+ }
13513
+ return { ok: false, status: 401, error: "unauthorized" };
12875
13514
  }
12876
13515
  }
12877
13516
  async function verifySessionJwt(config, token, verifier) {
@@ -12881,12 +13520,34 @@ async function verifySessionJwt(config, token, verifier) {
12881
13520
  const tenant = safeSessionClaim(claims[auth.tenant_claim ?? "tenant_id"]);
12882
13521
  const principal = safeSessionClaim(claims[auth.principal_claim ?? "sub"]);
12883
13522
  if (!tenant || !principal) throw new Error("JWT trusted context claims are missing or unsafe");
13523
+ const requiredScopes = config.http_security?.oauth_resource?.required_scopes ?? [];
13524
+ if (requiredScopes.length > 0) {
13525
+ const granted = safeJwtScopes(claims.scope, claims.scp);
13526
+ if (!requiredScopes.every((scope) => granted.has(scope))) {
13527
+ throw new McpRuntimeError("HTTP_INSUFFICIENT_SCOPE", "JWT does not grant the required MCP resource scope.");
13528
+ }
13529
+ }
12884
13530
  return { tenant_id: tenant, principal, provenance: "http_claims" };
12885
13531
  }
12886
13532
  function bearerToken(header) {
12887
- if (!header?.startsWith("Bearer ")) return void 0;
12888
- const token = header.slice("Bearer ".length).trim();
12889
- return token || void 0;
13533
+ const match = /^Bearer[ \t]+([^\s,]+)$/i.exec(header ?? "");
13534
+ const token = match?.[1];
13535
+ return token && token.length <= 16384 ? token : void 0;
13536
+ }
13537
+ function safeJwtScopes(scope, scp) {
13538
+ const values = [];
13539
+ if (typeof scope === "string" && scope.length <= 8192 && !/[\u0000-\u001f\u007f]/.test(scope)) {
13540
+ values.push(...scope.split(/\s+/).filter(Boolean));
13541
+ } else if (scope !== void 0) {
13542
+ throw new Error("JWT scope claim is unsafe");
13543
+ }
13544
+ if (Array.isArray(scp) && scp.length <= 64 && scp.every((value) => typeof value === "string" && value.length <= 128 && !/[\s\u0000-\u001f\u007f]/.test(value))) {
13545
+ values.push(...scp);
13546
+ } else if (scp !== void 0) {
13547
+ throw new Error("JWT scp claim is unsafe");
13548
+ }
13549
+ if (values.length > 128 || values.some((value) => value.length > 128)) throw new Error("JWT scope claim is unsafe");
13550
+ return new Set(values);
12890
13551
  }
12891
13552
  function tokenFingerprint(token) {
12892
13553
  return `sha256:${crypto3.createHash("sha256").update(token).digest("hex")}`;
@@ -12906,10 +13567,10 @@ function containsInitializeRequest(payload) {
12906
13567
  }
12907
13568
  function requestIdFromPayload(payload) {
12908
13569
  if (Array.isArray(payload)) {
12909
- const request = payload.find((message2) => isRecord4(message2) && "id" in message2);
12910
- return isRecord4(request) ? request.id ?? null : null;
13570
+ const request = payload.find((message2) => isRecord5(message2) && "id" in message2);
13571
+ return isRecord5(request) ? request.id ?? null : null;
12911
13572
  }
12912
- return isRecord4(payload) ? payload.id ?? null : null;
13573
+ return isRecord5(payload) ? payload.id ?? null : null;
12913
13574
  }
12914
13575
  function openaiToolNameAlias(canonicalName) {
12915
13576
  const sanitized = canonicalName.replace(/[^A-Za-z0-9_-]+/g, "__").replace(/_{3,}/g, "__").replace(/^_+|_+$/g, "");
@@ -12964,14 +13625,14 @@ function writeJson(response, statusCode, payload) {
12964
13625
  response.end(`${JSON.stringify(payload, null, 2)}
12965
13626
  `);
12966
13627
  }
12967
- async function readRequestBody(request) {
13628
+ async function readRequestBody(request, maxBytes = 1048576) {
12968
13629
  const chunks = [];
12969
13630
  let bytes = 0;
12970
13631
  for await (const chunk of request) {
12971
13632
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
12972
13633
  bytes += buffer.length;
12973
- if (bytes > 1024 * 1024) {
12974
- throw new McpRuntimeError("HTTP_BODY_TOO_LARGE", "HTTP MCP request body exceeds 1 MiB.");
13634
+ if (bytes > maxBytes) {
13635
+ throw new McpRuntimeError("HTTP_BODY_TOO_LARGE", `HTTP MCP request body exceeds the configured ${maxBytes}-byte limit.`);
12975
13636
  }
12976
13637
  chunks.push(buffer);
12977
13638
  }
@@ -12984,21 +13645,21 @@ function jsonRpcError(id, code, message2) {
12984
13645
  error: { code, message: message2 }
12985
13646
  };
12986
13647
  }
12987
- function sanitizeHttpError(error, authToken) {
13648
+ function sanitizeHttpError(error, ...authTokens) {
12988
13649
  const raw = error instanceof Error ? error.message : String(error);
12989
- return sanitizeHttpString(raw, authToken);
13650
+ return sanitizeHttpString(raw, ...authTokens);
12990
13651
  }
12991
- function sanitizeHttpPayload(value, authToken) {
12992
- if (typeof value === "string") return sanitizeHttpString(value, authToken);
12993
- if (Array.isArray(value)) return value.map((item) => sanitizeHttpPayload(item, authToken));
12994
- if (isRecord4(value)) {
12995
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitizeHttpPayload(item, authToken)]));
13652
+ function sanitizeHttpPayload(value, ...authTokens) {
13653
+ if (typeof value === "string") return sanitizeHttpString(value, ...authTokens);
13654
+ if (Array.isArray(value)) return value.map((item) => sanitizeHttpPayload(item, ...authTokens));
13655
+ if (isRecord5(value)) {
13656
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitizeHttpPayload(item, ...authTokens)]));
12996
13657
  }
12997
13658
  return value;
12998
13659
  }
12999
- function sanitizeHttpString(value, authToken) {
13660
+ function sanitizeHttpString(value, ...authTokens) {
13000
13661
  let redacted = value.replace(/(?:postgres(?:ql)?|mysql):\/\/[^\s"']+/gi, "[redacted-database-url]");
13001
- if (authToken) redacted = redacted.split(authToken).join("[redacted-token]");
13662
+ for (const authToken of authTokens) if (authToken) redacted = redacted.split(authToken).join("[redacted-token]");
13002
13663
  return redacted;
13003
13664
  }
13004
13665
  function isLoopbackHost(host) {
@@ -13032,6 +13693,13 @@ async function closeStreamableSessions(sessions) {
13032
13693
  disposeStreamableSession(session);
13033
13694
  }
13034
13695
  }
13696
+ async function pruneExpiredStreamableSessions(sessions, openSessions, idleTimeoutMs, now) {
13697
+ const expired = [...openSessions].filter((session) => now - session.lastSeenAt >= idleTimeoutMs);
13698
+ for (const session of expired) {
13699
+ disposeStreamableSession(session, sessions, openSessions);
13700
+ await session.transport.close().catch(() => void 0);
13701
+ }
13702
+ }
13035
13703
  function disposeStreamableSession(session, sessionMap, openSessions) {
13036
13704
  if (session.closed) return;
13037
13705
  session.closed = true;
@@ -13058,7 +13726,7 @@ async function toolCallResult(runtime, toolName, args) {
13058
13726
  }
13059
13727
  async function withProposalReviewPresentation(runtime, result) {
13060
13728
  const directId = typeof result.proposal_id === "string" ? result.proposal_id : void 0;
13061
- const nested = isRecord4(result.proposal) ? result.proposal : void 0;
13729
+ const nested = isRecord5(result.proposal) ? result.proposal : void 0;
13062
13730
  const proposalId = directId ?? (typeof nested?.id === "string" ? nested.id : void 0);
13063
13731
  if (!proposalId || proposalId === "wrp_unknown") return result;
13064
13732
  try {
@@ -13139,11 +13807,11 @@ function toolDescriptionWithCanonical(description, canonicalName, exposedName) {
13139
13807
  ${description}`;
13140
13808
  }
13141
13809
  function zodInputShapeFromJsonSchema(schema) {
13142
- const properties = isRecord4(schema.properties) ? schema.properties : {};
13810
+ const properties = isRecord5(schema.properties) ? schema.properties : {};
13143
13811
  const required = Array.isArray(schema.required) ? new Set(schema.required.map(String)) : /* @__PURE__ */ new Set();
13144
13812
  const shape = {};
13145
13813
  for (const [name, rawProperty] of Object.entries(properties)) {
13146
- const property = isRecord4(rawProperty) ? rawProperty : {};
13814
+ const property = isRecord5(rawProperty) ? rawProperty : {};
13147
13815
  let valueSchema;
13148
13816
  if (Array.isArray(property.enum)) {
13149
13817
  const allowed = property.enum.map((item) => scalar2(item));
@@ -13546,13 +14214,13 @@ function resultEnvelopeFromLegacy(legacy, capability, canonicalName) {
13546
14214
  const kind = capability?.kind ?? (typeof legacy.proposal_id === "string" ? "proposal" : "read");
13547
14215
  const evidenceBundleId = typeof legacy.evidence_bundle_id === "string" ? legacy.evidence_bundle_id : void 0;
13548
14216
  const sourceChanged = Boolean(legacy.source_database_changed ?? legacy.source_database_mutated ?? false);
13549
- const context = isRecord4(legacy.trusted_context) ? legacy.trusted_context : void 0;
13550
- const target = isRecord4(legacy.target) ? legacy.target : void 0;
14217
+ const context = isRecord5(legacy.trusted_context) ? legacy.trusted_context : void 0;
14218
+ const target = isRecord5(legacy.target) ? legacy.target : void 0;
13551
14219
  if (kind === "proposal") {
13552
14220
  const proposalId = typeof legacy.proposal_id === "string" ? legacy.proposal_id : "wrp_unknown";
13553
14221
  const targetType = typeof target?.type === "string" ? target.type : capability?.target.table ?? "object";
13554
14222
  const targetId = target?.id !== void 0 ? String(target.id) : "unknown";
13555
- const approval = isRecord4(legacy.approval) ? legacy.approval : void 0;
14223
+ const approval = isRecord5(legacy.approval) ? legacy.approval : void 0;
13556
14224
  const state = typeof legacy.status === "string" ? legacy.status : "review_required";
13557
14225
  const approvalRequired = legacy.approval_required !== false;
13558
14226
  const executor = writebackExecutorName(legacy.writeback);
@@ -13568,7 +14236,7 @@ function resultEnvelopeFromLegacy(legacy, capability, canonicalName) {
13568
14236
  id: proposalId,
13569
14237
  state,
13570
14238
  target: `${targetType}:${targetId}`,
13571
- diff: isRecord4(legacy.diff) ? legacy.diff : {},
14239
+ diff: isRecord5(legacy.diff) ? legacy.diff : {},
13572
14240
  approval_required: approvalRequired,
13573
14241
  ...approval ? { approval } : {},
13574
14242
  writeback: {
@@ -13589,7 +14257,7 @@ function resultEnvelopeFromLegacy(legacy, capability, canonicalName) {
13589
14257
  }
13590
14258
  };
13591
14259
  }
13592
- const businessObject = isRecord4(legacy.business_object) ? legacy.business_object : void 0;
14260
+ const businessObject = isRecord5(legacy.business_object) ? legacy.business_object : void 0;
13593
14261
  const objectType = typeof businessObject?.type === "string" ? businessObject.type : capability?.target.table ?? "record";
13594
14262
  const objectId = businessObject?.id !== void 0 ? String(businessObject.id) : String(legacy.action ?? action);
13595
14263
  return {
@@ -13597,7 +14265,7 @@ function resultEnvelopeFromLegacy(legacy, capability, canonicalName) {
13597
14265
  summary: kind === "aggregate_read" ? `Read one reviewed aggregate through ${action}. Source member rows exposed: no. Source database changed: no.` : `Read ${objectType} ${objectId} through ${action}. Source database changed: no.`,
13598
14266
  action,
13599
14267
  kind,
13600
- data: isRecord4(legacy.data) ? legacy.data : {},
14268
+ data: isRecord5(legacy.data) ? legacy.data : {},
13601
14269
  proposal: null,
13602
14270
  error: null,
13603
14271
  evidence: evidenceBundleId ? evidenceHandle(evidenceBundleId) : null,
@@ -13611,7 +14279,7 @@ function resultEnvelopeFromLegacy(legacy, capability, canonicalName) {
13611
14279
  };
13612
14280
  }
13613
14281
  function writebackExecutorName(value) {
13614
- if (!isRecord4(value)) return void 0;
14282
+ if (!isRecord5(value)) return void 0;
13615
14283
  return typeof value.executor === "string" ? value.executor : typeof value.mode === "string" ? value.mode : void 0;
13616
14284
  }
13617
14285
  function capabilityWritebackMode2(capability) {
@@ -13645,7 +14313,7 @@ function assertProposalWritebackResolvable(config, capability) {
13645
14313
  throw new McpRuntimeError("WRITEBACK_UNRESOLVED", `capability ${capability.name} declares HANDLER writeback but has no executor name.`);
13646
14314
  }
13647
14315
  const executor = config.executors?.[executorName];
13648
- if (!isRecord4(executor)) {
14316
+ if (!isRecord5(executor)) {
13649
14317
  throw new McpRuntimeError("WRITEBACK_UNRESOLVED", `capability ${capability.name} declares HANDLER writeback but executor ${executorName} is not configured.`);
13650
14318
  }
13651
14319
  if (executor.type !== "http_handler" && executor.type !== "command_handler") {
@@ -13912,26 +14580,26 @@ function errorChain(error) {
13912
14580
  while (current !== void 0 && current !== null && chain.length < 6 && !seen.has(current)) {
13913
14581
  chain.push(current);
13914
14582
  seen.add(current);
13915
- current = isRecord4(current) ? current.cause : void 0;
14583
+ current = isRecord5(current) ? current.cause : void 0;
13916
14584
  }
13917
14585
  return chain;
13918
14586
  }
13919
14587
  function errorStringProperty(error, property) {
13920
- if (!isRecord4(error)) return void 0;
14588
+ if (!isRecord5(error)) return void 0;
13921
14589
  const value = error[property];
13922
14590
  if (typeof value !== "string" && typeof value !== "number") return void 0;
13923
14591
  const normalized = String(value).trim().toUpperCase();
13924
14592
  return normalized || void 0;
13925
14593
  }
13926
14594
  function errorNumberProperty(error, property) {
13927
- if (!isRecord4(error)) return void 0;
14595
+ if (!isRecord5(error)) return void 0;
13928
14596
  const value = error[property];
13929
14597
  const normalized = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
13930
14598
  return Number.isInteger(normalized) ? normalized : void 0;
13931
14599
  }
13932
14600
  function errorMessage(error) {
13933
14601
  if (error instanceof Error) return error.message;
13934
- if (isRecord4(error) && typeof error.message === "string") return error.message;
14602
+ if (isRecord5(error) && typeof error.message === "string") return error.message;
13935
14603
  return typeof error === "string" ? error : "";
13936
14604
  }
13937
14605
  function effectivePrincipalScope(config, capability, context) {
@@ -14680,7 +15348,7 @@ function validateToolArgs(capability, args) {
14680
15348
  if (!Array.isArray(value)) throw new McpRuntimeError("ARGUMENT_TYPE_INVALID", `${name} must be an array of reviewed objects.`);
14681
15349
  if (value.length < 1 || value.length > spec.max_items) throw new McpRuntimeError("ARGUMENT_ITEM_COUNT_INVALID", `${name} must contain 1 through ${spec.max_items} items.`);
14682
15350
  for (const [index, item] of value.entries()) {
14683
- if (!isRecord4(item)) throw new McpRuntimeError("ARGUMENT_ITEM_TYPE_INVALID", `${name}[${index}] must be an object.`);
15351
+ if (!isRecord5(item)) throw new McpRuntimeError("ARGUMENT_ITEM_TYPE_INVALID", `${name}[${index}] must be an object.`);
14684
15352
  for (const key of Object.keys(item)) if (!Object.prototype.hasOwnProperty.call(spec.fields, key)) throw new McpRuntimeError("ARGUMENT_ITEM_FIELD_NOT_ALLOWED", `${name}[${index}].${key} is not a reviewed item field.`);
14685
15353
  for (const [fieldName, fieldSpec] of Object.entries(spec.fields)) validateScalarArg(`${name}[${index}].${fieldName}`, fieldSpec, item[fieldName]);
14686
15354
  }
@@ -14782,7 +15450,7 @@ function buildPatch(capability, args) {
14782
15450
  function batchItemsFromArgs(capability, args) {
14783
15451
  const argumentName = capability.operation?.batch?.items_from_arg;
14784
15452
  const value = argumentName ? args[argumentName] : void 0;
14785
- if (!argumentName || !Array.isArray(value) || value.some((item) => !isRecord4(item))) {
15453
+ if (!argumentName || !Array.isArray(value) || value.some((item) => !isRecord5(item))) {
14786
15454
  throw new McpRuntimeError("BATCH_ITEMS_REQUIRED", `Bounded INSERT capability ${capability.name} requires its reviewed object-array argument.`);
14787
15455
  }
14788
15456
  return value;
@@ -15020,7 +15688,7 @@ function stableId(prefix, input) {
15020
15688
  function hashJson(input) {
15021
15689
  return `sha256:${crypto3.createHash("sha256").update(JSON.stringify(input)).digest("hex")}`;
15022
15690
  }
15023
- function isRecord4(value) {
15691
+ function isRecord5(value) {
15024
15692
  return typeof value === "object" && value !== null && !Array.isArray(value);
15025
15693
  }
15026
15694
  function toolErrorPayload(error) {