@graphorin/server 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +3 -3
  3. package/dist/app-audit-binding.d.ts +15 -0
  4. package/dist/app-audit-binding.d.ts.map +1 -0
  5. package/dist/app-audit-binding.js +136 -0
  6. package/dist/app-audit-binding.js.map +1 -0
  7. package/dist/app-daemons.d.ts +29 -0
  8. package/dist/app-daemons.d.ts.map +1 -0
  9. package/dist/app-daemons.js +27 -0
  10. package/dist/app-daemons.js.map +1 -0
  11. package/dist/app-lifecycle.js +272 -0
  12. package/dist/app-lifecycle.js.map +1 -0
  13. package/dist/app-metrics.js +79 -0
  14. package/dist/app-metrics.js.map +1 -0
  15. package/dist/app-middleware.js +92 -0
  16. package/dist/app-middleware.js.map +1 -0
  17. package/dist/app-routes.js +131 -0
  18. package/dist/app-routes.js.map +1 -0
  19. package/dist/app-ws.js +65 -0
  20. package/dist/app-ws.js.map +1 -0
  21. package/dist/app.d.ts +16 -16
  22. package/dist/app.d.ts.map +1 -1
  23. package/dist/app.js +35 -607
  24. package/dist/app.js.map +1 -1
  25. package/dist/commentary/built-in-patterns.d.ts +1 -1
  26. package/dist/commentary/built-in-patterns.js +1 -1
  27. package/dist/commentary/built-in-patterns.js.map +1 -1
  28. package/dist/config.d.ts +100 -1
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +30 -2
  31. package/dist/config.js.map +1 -1
  32. package/dist/health/checks.d.ts +15 -1
  33. package/dist/health/checks.d.ts.map +1 -1
  34. package/dist/health/checks.js +21 -0
  35. package/dist/health/checks.js.map +1 -1
  36. package/dist/health/routes.js +1 -1
  37. package/dist/index.d.ts +8 -2
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +19 -24
  40. package/dist/index.js.map +1 -1
  41. package/dist/internal/context.d.ts +2 -2
  42. package/dist/internal/wire-error.js +20 -0
  43. package/dist/internal/wire-error.js.map +1 -0
  44. package/dist/lifecycle/pre-bind.d.ts +1 -1
  45. package/dist/metrics/catalog.d.ts.map +1 -1
  46. package/dist/metrics/catalog.js.map +1 -1
  47. package/dist/metrics/tools-bridge.d.ts +15 -0
  48. package/dist/metrics/tools-bridge.d.ts.map +1 -0
  49. package/dist/metrics/tools-bridge.js +103 -0
  50. package/dist/metrics/tools-bridge.js.map +1 -0
  51. package/dist/middleware/audit.d.ts +1 -1
  52. package/dist/middleware/auth.js +1 -1
  53. package/dist/middleware/csrf.js +1 -1
  54. package/dist/middleware/index.js +1 -1
  55. package/dist/middleware/scope.d.ts.map +1 -1
  56. package/dist/middleware/scope.js +44 -4
  57. package/dist/middleware/scope.js.map +1 -1
  58. package/dist/package.js +1 -1
  59. package/dist/package.js.map +1 -1
  60. package/dist/registry/index.d.ts +33 -0
  61. package/dist/registry/index.d.ts.map +1 -1
  62. package/dist/registry/index.js.map +1 -1
  63. package/dist/replay/routes.d.ts +1 -1
  64. package/dist/replay/routes.js +2 -2
  65. package/dist/routes/agents.d.ts.map +1 -1
  66. package/dist/routes/agents.js +64 -18
  67. package/dist/routes/agents.js.map +1 -1
  68. package/dist/routes/auth.js +2 -2
  69. package/dist/routes/auth.js.map +1 -1
  70. package/dist/routes/sessions.js +5 -5
  71. package/dist/routes/sessions.js.map +1 -1
  72. package/dist/routes/tokens.d.ts +1 -1
  73. package/dist/routes/tokens.d.ts.map +1 -1
  74. package/dist/routes/tokens.js +21 -1
  75. package/dist/routes/tokens.js.map +1 -1
  76. package/dist/routes/workflows.d.ts.map +1 -1
  77. package/dist/routes/workflows.js +165 -19
  78. package/dist/routes/workflows.js.map +1 -1
  79. package/dist/runtime/retention.d.ts +105 -0
  80. package/dist/runtime/retention.d.ts.map +1 -0
  81. package/dist/runtime/retention.js +154 -0
  82. package/dist/runtime/retention.js.map +1 -0
  83. package/dist/runtime/run-state.d.ts +2 -0
  84. package/dist/runtime/run-state.d.ts.map +1 -1
  85. package/dist/runtime/run-state.js +34 -2
  86. package/dist/runtime/run-state.js.map +1 -1
  87. package/dist/sse/events.js +3 -4
  88. package/dist/sse/events.js.map +1 -1
  89. package/dist/tools-audit-bridge.d.ts +29 -0
  90. package/dist/tools-audit-bridge.d.ts.map +1 -0
  91. package/dist/tools-audit-bridge.js +103 -0
  92. package/dist/tools-audit-bridge.js.map +1 -0
  93. package/dist/workflows/timer-daemon.d.ts +69 -0
  94. package/dist/workflows/timer-daemon.d.ts.map +1 -0
  95. package/dist/workflows/timer-daemon.js +37 -0
  96. package/dist/workflows/timer-daemon.js.map +1 -0
  97. package/dist/ws/dispatcher.d.ts +1 -1
  98. package/dist/ws/dispatcher.js +19 -12
  99. package/dist/ws/dispatcher.js.map +1 -1
  100. package/dist/ws/index.d.ts +2 -2
  101. package/dist/ws/index.js +3 -3
  102. package/dist/ws/replay-buffer.d.ts +35 -1
  103. package/dist/ws/replay-buffer.d.ts.map +1 -1
  104. package/dist/ws/replay-buffer.js +35 -3
  105. package/dist/ws/replay-buffer.js.map +1 -1
  106. package/dist/ws/upgrade.d.ts.map +1 -1
  107. package/dist/ws/upgrade.js +24 -18
  108. package/dist/ws/upgrade.js.map +1 -1
  109. package/package.json +30 -29
  110. package/src/app-audit-binding.ts +227 -0
  111. package/src/app-daemons.ts +73 -0
  112. package/src/app-lifecycle.ts +476 -0
  113. package/src/app-metrics.ts +144 -0
  114. package/src/app-middleware.ts +149 -0
  115. package/src/app-routes.ts +305 -0
  116. package/src/app-ws.ts +111 -0
  117. package/src/app.ts +317 -0
  118. package/src/commentary/audit-bridge.ts +135 -0
  119. package/src/commentary/built-in-patterns.ts +79 -0
  120. package/src/commentary/index.ts +32 -0
  121. package/src/commentary/sanitizer.ts +238 -0
  122. package/src/commentary/types.ts +130 -0
  123. package/src/config.ts +472 -0
  124. package/src/consolidator/daemon.ts +141 -0
  125. package/src/consolidator/index.ts +14 -0
  126. package/src/errors/index.ts +247 -0
  127. package/src/health/checks.ts +322 -0
  128. package/src/health/index.ts +34 -0
  129. package/src/health/routes.ts +154 -0
  130. package/src/index.ts +243 -0
  131. package/src/internal/context.ts +77 -0
  132. package/src/internal/ids.ts +17 -0
  133. package/src/internal/json.ts +47 -0
  134. package/src/internal/wire-error.ts +44 -0
  135. package/src/lifecycle/hooks.ts +66 -0
  136. package/src/lifecycle/index.ts +16 -0
  137. package/src/lifecycle/pre-bind.ts +138 -0
  138. package/src/metrics/catalog.ts +112 -0
  139. package/src/metrics/index.ts +12 -0
  140. package/src/metrics/registry.ts +337 -0
  141. package/src/metrics/tools-bridge.ts +124 -0
  142. package/src/middleware/audit.ts +114 -0
  143. package/src/middleware/auth.ts +195 -0
  144. package/src/middleware/cors.ts +72 -0
  145. package/src/middleware/csrf.ts +98 -0
  146. package/src/middleware/idempotency.ts +336 -0
  147. package/src/middleware/index.ts +38 -0
  148. package/src/middleware/rate-limit.ts +71 -0
  149. package/src/middleware/request-state.ts +79 -0
  150. package/src/middleware/scope.ts +161 -0
  151. package/src/registry/index.ts +278 -0
  152. package/src/replay/index.ts +14 -0
  153. package/src/replay/routes.ts +255 -0
  154. package/src/routes/agents.ts +363 -0
  155. package/src/routes/audit.ts +207 -0
  156. package/src/routes/auth.ts +80 -0
  157. package/src/routes/health.ts +42 -0
  158. package/src/routes/index.ts +16 -0
  159. package/src/routes/mcp.ts +97 -0
  160. package/src/routes/memory.ts +152 -0
  161. package/src/routes/sessions.ts +250 -0
  162. package/src/routes/skills.ts +91 -0
  163. package/src/routes/tokens.ts +166 -0
  164. package/src/routes/workflows.ts +616 -0
  165. package/src/runtime/retention.ts +284 -0
  166. package/src/runtime/run-state.ts +422 -0
  167. package/src/sse/events.ts +303 -0
  168. package/src/sse/index.ts +7 -0
  169. package/src/tools-audit-bridge.ts +120 -0
  170. package/src/triggers/daemon.ts +198 -0
  171. package/src/triggers/index.ts +16 -0
  172. package/src/triggers/routes.ts +139 -0
  173. package/src/workflows/timer-daemon.ts +102 -0
  174. package/src/ws/dispatcher.ts +537 -0
  175. package/src/ws/index.ts +45 -0
  176. package/src/ws/replay-buffer.ts +209 -0
  177. package/src/ws/subjects.ts +162 -0
  178. package/src/ws/ticket.ts +174 -0
  179. package/src/ws/upgrade.ts +507 -0
  180. package/dist/lifecycle/index.js +0 -3
  181. package/dist/routes/index.js +0 -12
@@ -1,4 +1,4 @@
1
- import { parseScope, scopeMatches } from "@graphorin/security/auth";
1
+ import { parseScope, scopeMatches, tryParseScope } from "@graphorin/security/auth";
2
2
 
3
3
  //#region src/middleware/scope.ts
4
4
  /**
@@ -23,6 +23,10 @@ function createScopeMiddleware(requirement) {
23
23
  message: "Authentication required for this endpoint."
24
24
  }, 401);
25
25
  const required = resolveRequirement(requirement, c.req.path, c.req.param());
26
+ if (required === void 0) return c.json({
27
+ error: "scope-denied",
28
+ message: "Token lacks the required scope (unparseable resource id)."
29
+ }, 403);
26
30
  const granted = auth.grantedScopes;
27
31
  let allowed = false;
28
32
  for (const scope of granted) if (scopeMatches(scope, required)) {
@@ -37,14 +41,50 @@ function createScopeMiddleware(requirement) {
37
41
  await next();
38
42
  };
39
43
  }
44
+ /**
45
+ * W-107: authentication-only gate. Lets any authenticated principal
46
+ * (token or the anonymous trusted-loopback operator) through and
47
+ * rejects only `unauthenticated` with 401. Used where possession of a
48
+ * valid principal is the whole requirement - e.g. the ws-ticket mint,
49
+ * which adds no rights of its own (the ticket carries the principal's
50
+ * scopes and every subscribe is per-subject gated).
51
+ *
52
+ * @stable
53
+ */
54
+ function createAuthenticatedMiddleware() {
55
+ return async (c, next) => {
56
+ if (c.get("state").auth.kind === "unauthenticated") return c.json({
57
+ error: "auth-required",
58
+ message: "Authentication required for this endpoint."
59
+ }, 401);
60
+ await next();
61
+ };
62
+ }
63
+ /**
64
+ * W-107: imperative scope check for handlers that must resolve the
65
+ * resource BEFORE the requirement is known (per-resource run control:
66
+ * snapshot first, then require the owning agent's/workflow's scope).
67
+ * Mirrors the middleware exactly: anonymous allows, unauthenticated
68
+ * denies, an unparseable requirement denies.
69
+ *
70
+ * @stable
71
+ */
72
+ function checkScope(auth, required) {
73
+ if (auth.kind === "anonymous") return true;
74
+ if (auth.kind === "unauthenticated") return false;
75
+ const parsed = typeof required === "string" ? tryParseScope(required) : required;
76
+ if (parsed === void 0) return false;
77
+ for (const scope of auth.grantedScopes) if (scopeMatches(scope, parsed)) return true;
78
+ return false;
79
+ }
40
80
  function resolveRequirement(requirement, path, params) {
41
81
  if (typeof requirement === "function") {
42
82
  const next = requirement(path, params);
43
- return typeof next === "string" ? parseScope(next) : next;
83
+ return typeof next === "string" ? tryParseScope(next) : next;
44
84
  }
45
- return typeof requirement === "string" ? parseScope(requirement) : requirement;
85
+ return typeof requirement === "string" ? tryParseScope(requirement) : requirement;
46
86
  }
47
87
 
48
88
  //#endregion
49
- export { createScopeMiddleware };
89
+ export { checkScope, createAuthenticatedMiddleware, createScopeMiddleware };
50
90
  //# sourceMappingURL=scope.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"scope.js","names":[],"sources":["../../src/middleware/scope.ts"],"sourcesContent":["/**\n * Per-route scope enforcement. Reads the verified token populated by\n * the auth middleware and short-circuits with `403` when the granted\n * set does not match the required scope.\n *\n * @packageDocumentation\n */\n\nimport { type ParsedScope, parseScope, scopeMatches } from '@graphorin/security/auth';\nimport type { MiddlewareHandler } from 'hono';\n\nimport type { ServerVariables } from '../internal/context.js';\n\n/**\n * Required-scope spec accepted by {@link createScopeMiddleware}. Either\n * a single string (`'agents:invoke'`), a parsed scope, or a function\n * that derives the required scope from the request (e.g. to insert the\n * `:id` segment lazily).\n *\n * @stable\n */\nexport type ScopeRequirement =\n | string\n | ParsedScope\n | ((path: string, params: Record<string, string>) => string | ParsedScope);\n\n/**\n * @stable\n */\nexport function createScopeMiddleware(\n requirement: ScopeRequirement,\n): MiddlewareHandler<{ Variables: ServerVariables }> {\n return async (c, next) => {\n const auth = c.get('state').auth;\n if (auth.kind === 'anonymous') {\n // IP-13: auth is disabled server-wide (auth.kind='none'). There is no\n // token to scope-check - the trusted-loopback operator is allowed\n // through every gate.\n await next();\n return;\n }\n if (auth.kind === 'unauthenticated') {\n return c.json(\n {\n error: 'auth-required',\n message: 'Authentication required for this endpoint.',\n },\n 401,\n );\n }\n const required = resolveRequirement(\n requirement,\n c.req.path,\n c.req.param() as Record<string, string>,\n );\n const granted = auth.grantedScopes;\n let allowed = false;\n for (const scope of granted) {\n if (scopeMatches(scope, required)) {\n allowed = true;\n break;\n }\n }\n if (!allowed) {\n return c.json(\n {\n error: 'scope-denied',\n message: `Token lacks required scope '${required.raw}'.`,\n hint: `Mint a token with the '${required.raw}' (or admin:*) scope.`,\n },\n 403,\n );\n }\n await next();\n };\n}\n\nfunction resolveRequirement(\n requirement: ScopeRequirement,\n path: string,\n params: Record<string, string>,\n): ParsedScope {\n if (typeof requirement === 'function') {\n const next = requirement(path, params);\n return typeof next === 'string' ? parseScope(next) : next;\n }\n return typeof requirement === 'string' ? parseScope(requirement) : requirement;\n}\n"],"mappings":";;;;;;;;;;;;;AA6BA,SAAgB,sBACd,aACmD;AACnD,QAAO,OAAO,GAAG,SAAS;EACxB,MAAM,OAAO,EAAE,IAAI,QAAQ,CAAC;AAC5B,MAAI,KAAK,SAAS,aAAa;AAI7B,SAAM,MAAM;AACZ;;AAEF,MAAI,KAAK,SAAS,kBAChB,QAAO,EAAE,KACP;GACE,OAAO;GACP,SAAS;GACV,EACD,IACD;EAEH,MAAM,WAAW,mBACf,aACA,EAAE,IAAI,MACN,EAAE,IAAI,OAAO,CACd;EACD,MAAM,UAAU,KAAK;EACrB,IAAI,UAAU;AACd,OAAK,MAAM,SAAS,QAClB,KAAI,aAAa,OAAO,SAAS,EAAE;AACjC,aAAU;AACV;;AAGJ,MAAI,CAAC,QACH,QAAO,EAAE,KACP;GACE,OAAO;GACP,SAAS,+BAA+B,SAAS,IAAI;GACrD,MAAM,0BAA0B,SAAS,IAAI;GAC9C,EACD,IACD;AAEH,QAAM,MAAM;;;AAIhB,SAAS,mBACP,aACA,MACA,QACa;AACb,KAAI,OAAO,gBAAgB,YAAY;EACrC,MAAM,OAAO,YAAY,MAAM,OAAO;AACtC,SAAO,OAAO,SAAS,WAAW,WAAW,KAAK,GAAG;;AAEvD,QAAO,OAAO,gBAAgB,WAAW,WAAW,YAAY,GAAG"}
1
+ {"version":3,"file":"scope.js","names":[],"sources":["../../src/middleware/scope.ts"],"sourcesContent":["/**\n * Per-route scope enforcement. Reads the verified token populated by\n * the auth middleware and short-circuits with `403` when the granted\n * set does not match the required scope.\n *\n * @packageDocumentation\n */\n\nimport {\n type ParsedScope,\n parseScope,\n scopeMatches,\n tryParseScope,\n} from '@graphorin/security/auth';\nimport type { MiddlewareHandler } from 'hono';\n\nimport type { ServerVariables } from '../internal/context.js';\n\n/**\n * Required-scope spec accepted by {@link createScopeMiddleware}. Either\n * a single string (`'agents:invoke'`), a parsed scope, or a function\n * that derives the required scope from the request (e.g. to insert the\n * `:id` segment lazily).\n *\n * @stable\n */\nexport type ScopeRequirement =\n | string\n | ParsedScope\n | ((path: string, params: Record<string, string>) => string | ParsedScope);\n\n/**\n * @stable\n */\nexport function createScopeMiddleware(\n requirement: ScopeRequirement,\n): MiddlewareHandler<{ Variables: ServerVariables }> {\n return async (c, next) => {\n const auth = c.get('state').auth;\n if (auth.kind === 'anonymous') {\n // IP-13: auth is disabled server-wide (auth.kind='none'). There is no\n // token to scope-check - the trusted-loopback operator is allowed\n // through every gate.\n await next();\n return;\n }\n if (auth.kind === 'unauthenticated') {\n return c.json(\n {\n error: 'auth-required',\n message: 'Authentication required for this endpoint.',\n },\n 401,\n );\n }\n const required = resolveRequirement(\n requirement,\n c.req.path,\n c.req.param() as Record<string, string>,\n );\n // W-107: a dynamic requirement built from an arbitrary URL segment\n // may not parse (target charset/length limits). An unparseable\n // requirement can never be granted - answer 403, never throw a 500.\n if (required === undefined) {\n return c.json(\n {\n error: 'scope-denied',\n message: 'Token lacks the required scope (unparseable resource id).',\n },\n 403,\n );\n }\n const granted = auth.grantedScopes;\n let allowed = false;\n for (const scope of granted) {\n if (scopeMatches(scope, required)) {\n allowed = true;\n break;\n }\n }\n if (!allowed) {\n return c.json(\n {\n error: 'scope-denied',\n message: `Token lacks required scope '${required.raw}'.`,\n hint: `Mint a token with the '${required.raw}' (or admin:*) scope.`,\n },\n 403,\n );\n }\n await next();\n };\n}\n\n/**\n * W-107: authentication-only gate. Lets any authenticated principal\n * (token or the anonymous trusted-loopback operator) through and\n * rejects only `unauthenticated` with 401. Used where possession of a\n * valid principal is the whole requirement - e.g. the ws-ticket mint,\n * which adds no rights of its own (the ticket carries the principal's\n * scopes and every subscribe is per-subject gated).\n *\n * @stable\n */\nexport function createAuthenticatedMiddleware(): MiddlewareHandler<{\n Variables: ServerVariables;\n}> {\n return async (c, next) => {\n const auth = c.get('state').auth;\n if (auth.kind === 'unauthenticated') {\n return c.json(\n {\n error: 'auth-required',\n message: 'Authentication required for this endpoint.',\n },\n 401,\n );\n }\n await next();\n };\n}\n\n/**\n * W-107: imperative scope check for handlers that must resolve the\n * resource BEFORE the requirement is known (per-resource run control:\n * snapshot first, then require the owning agent's/workflow's scope).\n * Mirrors the middleware exactly: anonymous allows, unauthenticated\n * denies, an unparseable requirement denies.\n *\n * @stable\n */\nexport function checkScope(\n auth: import('../internal/context.js').AuthState,\n required: string | ParsedScope,\n): boolean {\n if (auth.kind === 'anonymous') return true;\n if (auth.kind === 'unauthenticated') return false;\n const parsed = typeof required === 'string' ? tryParseScope(required) : required;\n if (parsed === undefined) return false;\n for (const scope of auth.grantedScopes) {\n if (scopeMatches(scope, parsed)) return true;\n }\n return false;\n}\n\nfunction resolveRequirement(\n requirement: ScopeRequirement,\n path: string,\n params: Record<string, string>,\n): ParsedScope | undefined {\n // W-107: dynamic requirements interpolate raw URL segments - use the\n // non-throwing parser so `GET /sessions/<junk>` yields 403, not 500.\n if (typeof requirement === 'function') {\n const next = requirement(path, params);\n return typeof next === 'string' ? tryParseScope(next) : next;\n }\n return typeof requirement === 'string' ? tryParseScope(requirement) : requirement;\n}\n\n// `parseScope` stays imported for API parity with older call sites.\nvoid parseScope;\n"],"mappings":";;;;;;;;;;;;;AAkCA,SAAgB,sBACd,aACmD;AACnD,QAAO,OAAO,GAAG,SAAS;EACxB,MAAM,OAAO,EAAE,IAAI,QAAQ,CAAC;AAC5B,MAAI,KAAK,SAAS,aAAa;AAI7B,SAAM,MAAM;AACZ;;AAEF,MAAI,KAAK,SAAS,kBAChB,QAAO,EAAE,KACP;GACE,OAAO;GACP,SAAS;GACV,EACD,IACD;EAEH,MAAM,WAAW,mBACf,aACA,EAAE,IAAI,MACN,EAAE,IAAI,OAAO,CACd;AAID,MAAI,aAAa,OACf,QAAO,EAAE,KACP;GACE,OAAO;GACP,SAAS;GACV,EACD,IACD;EAEH,MAAM,UAAU,KAAK;EACrB,IAAI,UAAU;AACd,OAAK,MAAM,SAAS,QAClB,KAAI,aAAa,OAAO,SAAS,EAAE;AACjC,aAAU;AACV;;AAGJ,MAAI,CAAC,QACH,QAAO,EAAE,KACP;GACE,OAAO;GACP,SAAS,+BAA+B,SAAS,IAAI;GACrD,MAAM,0BAA0B,SAAS,IAAI;GAC9C,EACD,IACD;AAEH,QAAM,MAAM;;;;;;;;;;;;;AAchB,SAAgB,gCAEb;AACD,QAAO,OAAO,GAAG,SAAS;AAExB,MADa,EAAE,IAAI,QAAQ,CAAC,KACnB,SAAS,kBAChB,QAAO,EAAE,KACP;GACE,OAAO;GACP,SAAS;GACV,EACD,IACD;AAEH,QAAM,MAAM;;;;;;;;;;;;AAahB,SAAgB,WACd,MACA,UACS;AACT,KAAI,KAAK,SAAS,YAAa,QAAO;AACtC,KAAI,KAAK,SAAS,kBAAmB,QAAO;CAC5C,MAAM,SAAS,OAAO,aAAa,WAAW,cAAc,SAAS,GAAG;AACxE,KAAI,WAAW,OAAW,QAAO;AACjC,MAAK,MAAM,SAAS,KAAK,cACvB,KAAI,aAAa,OAAO,OAAO,CAAE,QAAO;AAE1C,QAAO;;AAGT,SAAS,mBACP,aACA,MACA,QACyB;AAGzB,KAAI,OAAO,gBAAgB,YAAY;EACrC,MAAM,OAAO,YAAY,MAAM,OAAO;AACtC,SAAO,OAAO,SAAS,WAAW,cAAc,KAAK,GAAG;;AAE1D,QAAO,OAAO,gBAAgB,WAAW,cAAc,YAAY,GAAG"}
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.6.1";
2
+ var version = "0.7.0";
3
3
 
4
4
  //#endregion
5
5
  export { version };
@@ -1 +1 @@
1
- {"version":3,"file":"package.js","names":[],"sources":["../package.json"],"sourcesContent":["{\n \"name\": \"@graphorin/server\",\n \"version\": \"0.6.1\",\n \"description\": \"Standalone server runtime for the Graphorin framework: createServer({...}) factory, Hono HTTP app, REST endpoints (agents / workflows / sessions / memory / skills / mcp / tokens / audit / health / health/secrets / metrics / triggers / runs/:runId/replay / sessions/:id/replay / audit/verify), per-request authentication + scope middleware (HMAC-SHA256 + pepper, server pepper required), brute-force lockout, IETF draft-07 Idempotency-Key middleware with SQLite backing + LRU read cache, deny-by-default CORS, double-submit CSRF for browser flows, sliding-window rate limit, audit middleware that streams every authenticated request through the tamper-evident hash chain, programmatic AgentRegistry / WorkflowRegistry, lifecycle hooks (beforeStart / onReady / beforeShutdown), pre-bind secrets resolution + storage migration runner that fails fast on missing pepper / unresolvable SecretRef / missing encryption peer, graceful SIGTERM drain with state preservation, durable triggers daemon (cron / interval / idle / event survive process restart per the catch-up policy contract), consolidator daemon lifecycle integration with hard stop timeout, per-subsystem health rollup (storage / embedder / secrets / encryption / consolidator / triggers / replay-buffer), Prometheus text exposition `/v1/metrics` with the canonical graphorin_* metric inventory, scope-enforced replay endpoints (sanitized by default, raw requires `traces:read:raw` admin scope, every invocation appended to the audit chain), and `POST /v1/audit/verify` chain integrity check. Created and maintained by Oleksiy Stepurenko.\",\n \"license\": \"MIT\",\n \"author\": \"Oleksiy Stepurenko\",\n \"homepage\": \"https://github.com/o-stepper/graphorin/tree/main/packages/server\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/o-stepper/graphorin.git\",\n \"directory\": \"packages/server\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/o-stepper/graphorin/issues\"\n },\n \"keywords\": [\n \"graphorin\",\n \"ai\",\n \"agents\",\n \"framework\",\n \"server\",\n \"http\",\n \"rest\",\n \"hono\",\n \"idempotency\",\n \"auth\",\n \"scopes\",\n \"rate-limit\",\n \"cors\",\n \"csrf\",\n \"audit\",\n \"lifecycle\",\n \"graceful-shutdown\"\n ],\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=22.0.0\"\n },\n \"main\": \"./dist/index.js\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\"\n },\n \"./config\": {\n \"types\": \"./dist/config.d.ts\",\n \"import\": \"./dist/config.js\"\n },\n \"./middleware\": {\n \"types\": \"./dist/middleware/index.d.ts\",\n \"import\": \"./dist/middleware/index.js\"\n },\n \"./errors\": {\n \"types\": \"./dist/errors/index.d.ts\",\n \"import\": \"./dist/errors/index.js\"\n },\n \"./registry\": {\n \"types\": \"./dist/registry/index.d.ts\",\n \"import\": \"./dist/registry/index.js\"\n },\n \"./ws\": {\n \"types\": \"./dist/ws/index.d.ts\",\n \"import\": \"./dist/ws/index.js\"\n },\n \"./sse\": {\n \"types\": \"./dist/sse/index.d.ts\",\n \"import\": \"./dist/sse/index.js\"\n },\n \"./commentary\": {\n \"types\": \"./dist/commentary/index.d.ts\",\n \"import\": \"./dist/commentary/index.js\"\n },\n \"./triggers\": {\n \"types\": \"./dist/triggers/index.d.ts\",\n \"import\": \"./dist/triggers/index.js\"\n },\n \"./consolidator\": {\n \"types\": \"./dist/consolidator/index.d.ts\",\n \"import\": \"./dist/consolidator/index.js\"\n },\n \"./health\": {\n \"types\": \"./dist/health/index.d.ts\",\n \"import\": \"./dist/health/index.js\"\n },\n \"./metrics\": {\n \"types\": \"./dist/metrics/index.d.ts\",\n \"import\": \"./dist/metrics/index.js\"\n },\n \"./replay\": {\n \"types\": \"./dist/replay/index.d.ts\",\n \"import\": \"./dist/replay/index.js\"\n },\n \"./package.json\": \"./package.json\"\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"CHANGELOG.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsdown\",\n \"typecheck\": \"tsc --noEmit && tsc -p tsconfig.tests.json\",\n \"test\": \"vitest run\",\n \"lint\": \"biome check .\",\n \"clean\": \"rimraf dist .turbo *.tsbuildinfo\"\n },\n \"dependencies\": {\n \"@graphorin/core\": \"workspace:*\",\n \"@graphorin/observability\": \"workspace:*\",\n \"@graphorin/protocol\": \"workspace:*\",\n \"@graphorin/security\": \"workspace:*\",\n \"@graphorin/store-sqlite\": \"workspace:*\",\n \"@graphorin/triggers\": \"workspace:*\",\n \"@hono/node-server\": \"^1.19.0\",\n \"@hono/node-ws\": \"^1.3.0\",\n \"hono\": \"^4.12.25\",\n \"zod\": \"^3.25.0\"\n },\n \"peerDependencies\": {\n \"@graphorin/agent\": \"workspace:>=0.5.0 <1.0.0\",\n \"@graphorin/memory\": \"workspace:>=0.5.0 <1.0.0\",\n \"@graphorin/sessions\": \"workspace:>=0.5.0 <1.0.0\",\n \"@graphorin/workflow\": \"workspace:>=0.5.0 <1.0.0\"\n },\n \"peerDependenciesMeta\": {\n \"@graphorin/agent\": {\n \"optional\": true\n },\n \"@graphorin/memory\": {\n \"optional\": true\n },\n \"@graphorin/sessions\": {\n \"optional\": true\n },\n \"@graphorin/workflow\": {\n \"optional\": true\n }\n },\n \"publishConfig\": {\n \"access\": \"public\",\n \"provenance\": true\n },\n \"devDependencies\": {\n \"@graphorin/agent\": \"workspace:*\",\n \"@graphorin/memory\": \"workspace:*\",\n \"@graphorin/sessions\": \"workspace:*\",\n \"@graphorin/workflow\": \"workspace:*\",\n \"@types/better-sqlite3\": \"^7.6.13\",\n \"@types/ws\": \"^8.18.1\",\n \"better-sqlite3\": \"^12.9.0\",\n \"ws\": \"^8.20.1\"\n }\n}\n"],"mappings":";cAEa"}
1
+ {"version":3,"file":"package.js","names":[],"sources":["../package.json"],"sourcesContent":["{\n \"name\": \"@graphorin/server\",\n \"version\": \"0.7.0\",\n \"description\": \"Standalone server runtime for the Graphorin framework: createServer({...}) factory, Hono HTTP app, REST endpoints (agents / workflows / sessions / memory / skills / mcp / tokens / audit / health / health/secrets / metrics / triggers / runs/:runId/replay / sessions/:id/replay / audit/verify), per-request authentication + scope middleware (HMAC-SHA256 + pepper, server pepper required), brute-force lockout, IETF draft-07 Idempotency-Key middleware with SQLite backing + LRU read cache, deny-by-default CORS, double-submit CSRF for browser flows, sliding-window rate limit, audit middleware that streams every authenticated request through the tamper-evident hash chain, programmatic AgentRegistry / WorkflowRegistry, lifecycle hooks (beforeStart / onReady / beforeShutdown), pre-bind secrets resolution + storage migration runner that fails fast on missing pepper / unresolvable SecretRef / missing encryption peer, graceful SIGTERM drain with state preservation, durable triggers daemon (cron / interval / idle / event survive process restart per the catch-up policy contract), consolidator daemon lifecycle integration with hard stop timeout, per-subsystem health rollup (storage / embedder / secrets / encryption / consolidator / triggers / replay-buffer), Prometheus text exposition `/v1/metrics` with the canonical graphorin_* metric inventory, scope-enforced replay endpoints (sanitized by default, raw requires `traces:read:raw` admin scope, every invocation appended to the audit chain), and `POST /v1/audit/verify` chain integrity check. Created and maintained by Oleksiy Stepurenko.\",\n \"license\": \"MIT\",\n \"author\": \"Oleksiy Stepurenko\",\n \"homepage\": \"https://github.com/o-stepper/graphorin/tree/main/packages/server\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/o-stepper/graphorin.git\",\n \"directory\": \"packages/server\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/o-stepper/graphorin/issues\"\n },\n \"keywords\": [\n \"graphorin\",\n \"ai\",\n \"agents\",\n \"framework\",\n \"server\",\n \"http\",\n \"rest\",\n \"hono\",\n \"idempotency\",\n \"auth\",\n \"scopes\",\n \"rate-limit\",\n \"cors\",\n \"csrf\",\n \"audit\",\n \"lifecycle\",\n \"graceful-shutdown\"\n ],\n \"type\": \"module\",\n \"sideEffects\": false,\n \"engines\": {\n \"node\": \">=22.12.0\"\n },\n \"main\": \"./dist/index.js\",\n \"module\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"default\": \"./dist/index.js\"\n },\n \"./config\": {\n \"types\": \"./dist/config.d.ts\",\n \"default\": \"./dist/config.js\"\n },\n \"./middleware\": {\n \"types\": \"./dist/middleware/index.d.ts\",\n \"default\": \"./dist/middleware/index.js\"\n },\n \"./errors\": {\n \"types\": \"./dist/errors/index.d.ts\",\n \"default\": \"./dist/errors/index.js\"\n },\n \"./registry\": {\n \"types\": \"./dist/registry/index.d.ts\",\n \"default\": \"./dist/registry/index.js\"\n },\n \"./ws\": {\n \"types\": \"./dist/ws/index.d.ts\",\n \"default\": \"./dist/ws/index.js\"\n },\n \"./sse\": {\n \"types\": \"./dist/sse/index.d.ts\",\n \"default\": \"./dist/sse/index.js\"\n },\n \"./commentary\": {\n \"types\": \"./dist/commentary/index.d.ts\",\n \"default\": \"./dist/commentary/index.js\"\n },\n \"./triggers\": {\n \"types\": \"./dist/triggers/index.d.ts\",\n \"default\": \"./dist/triggers/index.js\"\n },\n \"./consolidator\": {\n \"types\": \"./dist/consolidator/index.d.ts\",\n \"default\": \"./dist/consolidator/index.js\"\n },\n \"./health\": {\n \"types\": \"./dist/health/index.d.ts\",\n \"default\": \"./dist/health/index.js\"\n },\n \"./metrics\": {\n \"types\": \"./dist/metrics/index.d.ts\",\n \"default\": \"./dist/metrics/index.js\"\n },\n \"./replay\": {\n \"types\": \"./dist/replay/index.d.ts\",\n \"default\": \"./dist/replay/index.js\"\n },\n \"./package.json\": \"./package.json\"\n },\n \"files\": [\n \"dist\",\n \"src\",\n \"README.md\",\n \"CHANGELOG.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsdown\",\n \"typecheck\": \"tsc --noEmit && tsc -p tsconfig.tests.json\",\n \"test\": \"vitest run\",\n \"lint\": \"biome check .\",\n \"clean\": \"rimraf dist .turbo *.tsbuildinfo\"\n },\n \"dependencies\": {\n \"@graphorin/core\": \"workspace:*\",\n \"@graphorin/protocol\": \"workspace:*\",\n \"@graphorin/security\": \"workspace:*\",\n \"@graphorin/store-sqlite\": \"workspace:*\",\n \"@graphorin/tools\": \"workspace:*\",\n \"@graphorin/triggers\": \"workspace:*\",\n \"@hono/node-server\": \"^1.19.0\",\n \"@hono/node-ws\": \"^1.3.0\",\n \"hono\": \"^4.12.25\",\n \"zod\": \"^3.25.0\"\n },\n \"peerDependencies\": {\n \"@graphorin/agent\": \"workspace:>=0.7.0 <1.0.0\",\n \"@graphorin/memory\": \"workspace:>=0.7.0 <1.0.0\",\n \"@graphorin/sessions\": \"workspace:>=0.7.0 <1.0.0\",\n \"@graphorin/workflow\": \"workspace:>=0.7.0 <1.0.0\"\n },\n \"peerDependenciesMeta\": {\n \"@graphorin/agent\": {\n \"optional\": true\n },\n \"@graphorin/memory\": {\n \"optional\": true\n },\n \"@graphorin/sessions\": {\n \"optional\": true\n },\n \"@graphorin/workflow\": {\n \"optional\": true\n }\n },\n \"publishConfig\": {\n \"access\": \"public\",\n \"provenance\": true\n },\n \"devDependencies\": {\n \"@graphorin/agent\": \"workspace:*\",\n \"@graphorin/sessions\": \"workspace:*\",\n \"@graphorin/workflow\": \"workspace:*\",\n \"@types/better-sqlite3\": \"^7.6.13\",\n \"@types/ws\": \"^8.18.1\",\n \"better-sqlite3\": \"^12.9.0\",\n \"ws\": \"^8.20.1\"\n }\n}\n"],"mappings":";cAEa"}
@@ -9,6 +9,13 @@
9
9
  * receive `undefined` and the route handler decides how to surface
10
10
  * the miss (typically a 404 with a typed error body).
11
11
  *
12
+ * W-053: compatibility of `ServerAgentLike` / `ServerWorkflowLike` with
13
+ * the real `Agent` / `Workflow` surfaces is gated at compile time by
14
+ * `tests/registry-contract.types.test.ts` - a drift in either direction
15
+ * fails the server package's typecheck, not a user's runtime. When a
16
+ * mismatch appears, fix THESE structural types (they are the copy), not
17
+ * the source interfaces.
18
+ *
12
19
  * @packageDocumentation
13
20
  */
14
21
  /**
@@ -51,9 +58,35 @@ interface ServerWorkflowLike {
51
58
  }): AsyncIterable<unknown>;
52
59
  resume?(threadId: string, directive?: {
53
60
  readonly resume?: unknown;
61
+ }, opts?: {
62
+ readonly signal?: AbortSignal;
63
+ }): AsyncIterable<unknown>;
64
+ /** W-119: replay a failed/aborted thread (`POST /:id/retry`). */
65
+ retry?(threadId: string, opts?: {
66
+ readonly signal?: AbortSignal;
67
+ }): AsyncIterable<unknown>;
68
+ /** W-119: fire a due durable timer (`POST /:id/tick`). */
69
+ tick?(threadId: string, opts?: {
70
+ readonly now?: number;
71
+ }): Promise<{
72
+ readonly fired: boolean;
73
+ readonly nextWakeAt: number | null;
74
+ }>;
75
+ /**
76
+ * W-119: resolve a NAMED awakeable/approval (`POST /:id/resume` with
77
+ * `name`) - `approve` is the same primitive under the hood.
78
+ */
79
+ resolveAwakeable?(threadId: string, name: string, value?: unknown, opts?: {
80
+ readonly signal?: AbortSignal;
54
81
  }): AsyncIterable<unknown>;
82
+ /** W-119: fork a new thread from a checkpoint (`POST /:id/fork`). */
83
+ fork?(threadId: string, fromCheckpointId: string): Promise<{
84
+ readonly newThreadId: string;
85
+ }>;
55
86
  getState?(threadId: string): Promise<unknown>;
56
87
  listCheckpoints?(threadId: string): Promise<ReadonlyArray<unknown>>;
88
+ /** W-005: per-thread checkpoint erasure (`DELETE /:id/threads/:threadId`). */
89
+ deleteThread?(threadId: string): Promise<void>;
57
90
  }
58
91
  /**
59
92
  * Snapshot record returned by {@link AgentRegistry.list}.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/registry/index.ts"],"sourcesContent":[],"mappings":";;AAoBA;;;;;;AAgCA;;;;;;;;AAgBA;AAWA;AAkCA;AAYA;AAiBa,UA1HI,eAAA,CA0HS;EAGR,SAAA,EAAA,EAAA,MAAA;EAaC,GAAA,CAAA,KAAA,EAAA,OAAA,EAAA,OAoBiB,CApBjB,EAAA;IAQK,SAAA,MAAA,CAAA,EA7IA,WA6IA;IAAd,SAAA,SAAA,CAAA,EAAA,MAAA;IAYc,SAAA,MAAA,CAAA,EAAA,MAAA;EAAY,CAAA,CAAA,EArJ/B,OAqJ+B,CAAA,OAAA,CAAA;EAkBvB;;;;;;EAoC0B,MAAA,EAAA,KAAA,EAAA,OAAA,EAAA,QAAA,EAAA;sBAjMf;;;MAInB;;;;;;;;UASY,kBAAA;;;sBAIiB;;MAC7B;;;MACmE;+BACzC;sCACO,QAAQ;;;;;;;UAQ7B,YAAA;;;kBAGC;;;;;;;UAQD,eAAA;;;kBAGC;;;;;;;UA+BD,iBAAA;;kBAEC;;kBAEA;;;;;;;UAQD,oBAAA;;qBAEI;;kBAEH;;;;;;;;;;;;cAaL,aAAA;;kBAGK;;mBAaC;;UAQT,cAAc;wBAYA;;;;;;cAkBX,gBAAA;;kBAGK;;mBAaC;;UAQT,cAAc;wBAYA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/registry/index.ts"],"sourcesContent":[],"mappings":";;AA2BA;;;;;;AAgCA;;;;;;;;;;;;;;;;AAyCA;AAWA;AAkCA;AAYiB,UAlIA,eAAA,CAkIoB;EAiBxB,SAAA,EAAA,EAAA,MAAa;EAGR,GAAA,CAAA,KAAA,EAAA,OAAA,EAAA,OAiCM,CAjCN,EAAA;IAaC,SAAA,MAAA,CAAA,EA9JK,WA8JL;IAQK,SAAA,SAAA,CAAA,EAAA,MAAA;IAAd,SAAA,MAAA,CAAA,EAAA,MAAA;EAYc,CAAA,CAAA,EA9KnB,OA8KmB,CAAA,OAAA,CAAA;EAAY;AAkBpC;;;;;EAoCwB,MAAA,EAAA,KAAA,EAAA,OAAA,EAAA,QAAA,EAAA;IAAe,SAAA,MAAA,CAAA,EA1Nf,WA0Ne;;;MAtNlC;;;;;;;;UASY,kBAAA;;;sBAIiB;;MAC7B;;;;sBAI0B;MAC1B;;;sBAEiD;MAAgB;;;;MAKjE;;;;;;;;;sBAS0B;MAC1B;;qDAEgD;;;+BACtB;sCACO,QAAQ;;mCAEX;;;;;;;UAQlB,YAAA;;;kBAGC;;;;;;;UAQD,eAAA;;;kBAGC;;;;;;;UA+BD,iBAAA;;kBAEC;;kBAEA;;;;;;;UAQD,oBAAA;;qBAEI;;kBAEH;;;;;;;;;;;;cAaL,aAAA;;kBAGK;;mBAaC;;UAQT,cAAc;wBAYA;;;;;;cAkBX,gBAAA;;kBAGK;;mBAaC;;UAQT,cAAc;wBAYA"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#entries"],"sources":["../../src/registry/index.ts"],"sourcesContent":["/**\n * Registry plumbing that lets the server route handlers locate user-\n * defined agents, workflows, sessions, memory, skills, and MCP\n * server bindings without taking a hard peer dependency on every\n * sibling package.\n *\n * Every entry is keyed by string id; lookups never throw - callers\n * receive `undefined` and the route handler decides how to surface\n * the miss (typically a 404 with a typed error body).\n *\n * @packageDocumentation\n */\n\n/**\n * Minimal shape the server needs from an `Agent`. Compatible with\n * the `Agent` interface from `@graphorin/agent` but kept\n * structurally so we avoid the peer dependency.\n *\n * @stable\n */\nexport interface ServerAgentLike {\n readonly id: string;\n run(\n input: unknown,\n options?: {\n readonly signal?: AbortSignal;\n readonly sessionId?: string;\n readonly userId?: string;\n },\n ): Promise<unknown>;\n /**\n * Streaming surface (IP-2). `@graphorin/agent` agents satisfy this\n * structurally; `POST /agents/:id/stream` consumes it and emits\n * every event onto the run's WS subject. Optional so plain\n * run-only fixtures keep working (they emit a single terminal frame).\n */\n stream?(\n input: unknown,\n options?: {\n readonly signal?: AbortSignal;\n readonly sessionId?: string;\n readonly userId?: string;\n },\n ): AsyncIterable<unknown>;\n}\n\n/**\n * Minimal shape the server needs from a `Workflow`. Mirrors the\n * `Workflow` surface from `@graphorin/workflow`.\n *\n * @stable\n */\nexport interface ServerWorkflowLike {\n readonly name: string;\n execute(\n input: unknown,\n options?: { readonly signal?: AbortSignal; readonly threadId?: string },\n ): AsyncIterable<unknown>;\n resume?(threadId: string, directive?: { readonly resume?: unknown }): AsyncIterable<unknown>;\n getState?(threadId: string): Promise<unknown>;\n listCheckpoints?(threadId: string): Promise<ReadonlyArray<unknown>>;\n}\n\n/**\n * Snapshot record returned by {@link AgentRegistry.list}.\n *\n * @stable\n */\nexport interface AgentSummary {\n readonly id: string;\n readonly description?: string;\n readonly tags?: ReadonlyArray<string>;\n}\n\n/**\n * Snapshot record returned by {@link WorkflowRegistry.list}.\n *\n * @stable\n */\nexport interface WorkflowSummary {\n readonly id: string;\n readonly description?: string;\n readonly tags?: ReadonlyArray<string>;\n}\n\n/**\n * Internal registry entry - pairs the user-supplied object with the\n * metadata routes serve to clients.\n *\n * @internal\n */\ninterface AgentEntry {\n readonly id: string;\n readonly agent: ServerAgentLike;\n readonly description?: string;\n readonly tags?: ReadonlyArray<string>;\n}\n\n/**\n * @internal\n */\ninterface WorkflowEntry {\n readonly id: string;\n readonly workflow: ServerWorkflowLike;\n readonly description?: string;\n readonly tags?: ReadonlyArray<string>;\n}\n\n/**\n * Registration descriptor accepted by {@link AgentRegistry.register}.\n *\n * @stable\n */\nexport interface AgentRegistration {\n readonly id: string;\n readonly agent: ServerAgentLike;\n readonly description?: string;\n readonly tags?: ReadonlyArray<string>;\n}\n\n/**\n * Registration descriptor accepted by {@link WorkflowRegistry.register}.\n *\n * @stable\n */\nexport interface WorkflowRegistration {\n readonly id: string;\n readonly workflow: ServerWorkflowLike;\n readonly description?: string;\n readonly tags?: ReadonlyArray<string>;\n}\n\n/**\n * Read/write registry for agents the server should expose. Every\n * mutation is synchronous and the lookup is `O(1)`.\n *\n * The class is intentionally tiny - extension points (e.g. lazy\n * factory loading, per-tenant scoping) live in higher-level packages\n * and consume this surface as a primitive.\n *\n * @stable\n */\nexport class AgentRegistry {\n readonly #entries: Map<string, AgentEntry> = new Map();\n\n register(entry: AgentRegistration): void {\n this.#entries.set(entry.id, {\n id: entry.id,\n agent: entry.agent,\n ...(entry.description !== undefined ? { description: entry.description } : {}),\n ...(entry.tags !== undefined ? { tags: Object.freeze(entry.tags.slice()) } : {}),\n });\n }\n\n unregister(id: string): boolean {\n return this.#entries.delete(id);\n }\n\n get(id: string): ServerAgentLike | undefined {\n return this.#entries.get(id)?.agent;\n }\n\n has(id: string): boolean {\n return this.#entries.has(id);\n }\n\n list(): ReadonlyArray<AgentSummary> {\n return Object.freeze(\n [...this.#entries.values()].map((entry) =>\n Object.freeze({\n id: entry.id,\n ...(entry.description !== undefined ? { description: entry.description } : {}),\n ...(entry.tags !== undefined ? { tags: entry.tags } : {}),\n }),\n ),\n );\n }\n\n describe(id: string): AgentSummary | undefined {\n const entry = this.#entries.get(id);\n if (entry === undefined) return undefined;\n return Object.freeze({\n id: entry.id,\n ...(entry.description !== undefined ? { description: entry.description } : {}),\n ...(entry.tags !== undefined ? { tags: entry.tags } : {}),\n });\n }\n\n size(): number {\n return this.#entries.size;\n }\n}\n\n/**\n * @stable\n */\nexport class WorkflowRegistry {\n readonly #entries: Map<string, WorkflowEntry> = new Map();\n\n register(entry: WorkflowRegistration): void {\n this.#entries.set(entry.id, {\n id: entry.id,\n workflow: entry.workflow,\n ...(entry.description !== undefined ? { description: entry.description } : {}),\n ...(entry.tags !== undefined ? { tags: Object.freeze(entry.tags.slice()) } : {}),\n });\n }\n\n unregister(id: string): boolean {\n return this.#entries.delete(id);\n }\n\n get(id: string): ServerWorkflowLike | undefined {\n return this.#entries.get(id)?.workflow;\n }\n\n has(id: string): boolean {\n return this.#entries.has(id);\n }\n\n list(): ReadonlyArray<WorkflowSummary> {\n return Object.freeze(\n [...this.#entries.values()].map((entry) =>\n Object.freeze({\n id: entry.id,\n ...(entry.description !== undefined ? { description: entry.description } : {}),\n ...(entry.tags !== undefined ? { tags: entry.tags } : {}),\n }),\n ),\n );\n }\n\n describe(id: string): WorkflowSummary | undefined {\n const entry = this.#entries.get(id);\n if (entry === undefined) return undefined;\n return Object.freeze({\n id: entry.id,\n ...(entry.description !== undefined ? { description: entry.description } : {}),\n ...(entry.tags !== undefined ? { tags: entry.tags } : {}),\n });\n }\n\n size(): number {\n return this.#entries.size;\n }\n}\n"],"mappings":";;;;;;;;;;;AA8IA,IAAa,gBAAb,MAA2B;CACzB,CAASA,0BAAoC,IAAI,KAAK;CAEtD,SAAS,OAAgC;AACvC,QAAKA,QAAS,IAAI,MAAM,IAAI;GAC1B,IAAI,MAAM;GACV,OAAO,MAAM;GACb,GAAI,MAAM,gBAAgB,SAAY,EAAE,aAAa,MAAM,aAAa,GAAG,EAAE;GAC7E,GAAI,MAAM,SAAS,SAAY,EAAE,MAAM,OAAO,OAAO,MAAM,KAAK,OAAO,CAAC,EAAE,GAAG,EAAE;GAChF,CAAC;;CAGJ,WAAW,IAAqB;AAC9B,SAAO,MAAKA,QAAS,OAAO,GAAG;;CAGjC,IAAI,IAAyC;AAC3C,SAAO,MAAKA,QAAS,IAAI,GAAG,EAAE;;CAGhC,IAAI,IAAqB;AACvB,SAAO,MAAKA,QAAS,IAAI,GAAG;;CAG9B,OAAoC;AAClC,SAAO,OAAO,OACZ,CAAC,GAAG,MAAKA,QAAS,QAAQ,CAAC,CAAC,KAAK,UAC/B,OAAO,OAAO;GACZ,IAAI,MAAM;GACV,GAAI,MAAM,gBAAgB,SAAY,EAAE,aAAa,MAAM,aAAa,GAAG,EAAE;GAC7E,GAAI,MAAM,SAAS,SAAY,EAAE,MAAM,MAAM,MAAM,GAAG,EAAE;GACzD,CAAC,CACH,CACF;;CAGH,SAAS,IAAsC;EAC7C,MAAM,QAAQ,MAAKA,QAAS,IAAI,GAAG;AACnC,MAAI,UAAU,OAAW,QAAO;AAChC,SAAO,OAAO,OAAO;GACnB,IAAI,MAAM;GACV,GAAI,MAAM,gBAAgB,SAAY,EAAE,aAAa,MAAM,aAAa,GAAG,EAAE;GAC7E,GAAI,MAAM,SAAS,SAAY,EAAE,MAAM,MAAM,MAAM,GAAG,EAAE;GACzD,CAAC;;CAGJ,OAAe;AACb,SAAO,MAAKA,QAAS;;;;;;AAOzB,IAAa,mBAAb,MAA8B;CAC5B,CAASA,0BAAuC,IAAI,KAAK;CAEzD,SAAS,OAAmC;AAC1C,QAAKA,QAAS,IAAI,MAAM,IAAI;GAC1B,IAAI,MAAM;GACV,UAAU,MAAM;GAChB,GAAI,MAAM,gBAAgB,SAAY,EAAE,aAAa,MAAM,aAAa,GAAG,EAAE;GAC7E,GAAI,MAAM,SAAS,SAAY,EAAE,MAAM,OAAO,OAAO,MAAM,KAAK,OAAO,CAAC,EAAE,GAAG,EAAE;GAChF,CAAC;;CAGJ,WAAW,IAAqB;AAC9B,SAAO,MAAKA,QAAS,OAAO,GAAG;;CAGjC,IAAI,IAA4C;AAC9C,SAAO,MAAKA,QAAS,IAAI,GAAG,EAAE;;CAGhC,IAAI,IAAqB;AACvB,SAAO,MAAKA,QAAS,IAAI,GAAG;;CAG9B,OAAuC;AACrC,SAAO,OAAO,OACZ,CAAC,GAAG,MAAKA,QAAS,QAAQ,CAAC,CAAC,KAAK,UAC/B,OAAO,OAAO;GACZ,IAAI,MAAM;GACV,GAAI,MAAM,gBAAgB,SAAY,EAAE,aAAa,MAAM,aAAa,GAAG,EAAE;GAC7E,GAAI,MAAM,SAAS,SAAY,EAAE,MAAM,MAAM,MAAM,GAAG,EAAE;GACzD,CAAC,CACH,CACF;;CAGH,SAAS,IAAyC;EAChD,MAAM,QAAQ,MAAKA,QAAS,IAAI,GAAG;AACnC,MAAI,UAAU,OAAW,QAAO;AAChC,SAAO,OAAO,OAAO;GACnB,IAAI,MAAM;GACV,GAAI,MAAM,gBAAgB,SAAY,EAAE,aAAa,MAAM,aAAa,GAAG,EAAE;GAC7E,GAAI,MAAM,SAAS,SAAY,EAAE,MAAM,MAAM,MAAM,GAAG,EAAE;GACzD,CAAC;;CAGJ,OAAe;AACb,SAAO,MAAKA,QAAS"}
1
+ {"version":3,"file":"index.js","names":["#entries"],"sources":["../../src/registry/index.ts"],"sourcesContent":["/**\n * Registry plumbing that lets the server route handlers locate user-\n * defined agents, workflows, sessions, memory, skills, and MCP\n * server bindings without taking a hard peer dependency on every\n * sibling package.\n *\n * Every entry is keyed by string id; lookups never throw - callers\n * receive `undefined` and the route handler decides how to surface\n * the miss (typically a 404 with a typed error body).\n *\n * W-053: compatibility of `ServerAgentLike` / `ServerWorkflowLike` with\n * the real `Agent` / `Workflow` surfaces is gated at compile time by\n * `tests/registry-contract.types.test.ts` - a drift in either direction\n * fails the server package's typecheck, not a user's runtime. When a\n * mismatch appears, fix THESE structural types (they are the copy), not\n * the source interfaces.\n *\n * @packageDocumentation\n */\n\n/**\n * Minimal shape the server needs from an `Agent`. Compatible with\n * the `Agent` interface from `@graphorin/agent` but kept\n * structurally so we avoid the peer dependency.\n *\n * @stable\n */\nexport interface ServerAgentLike {\n readonly id: string;\n run(\n input: unknown,\n options?: {\n readonly signal?: AbortSignal;\n readonly sessionId?: string;\n readonly userId?: string;\n },\n ): Promise<unknown>;\n /**\n * Streaming surface (IP-2). `@graphorin/agent` agents satisfy this\n * structurally; `POST /agents/:id/stream` consumes it and emits\n * every event onto the run's WS subject. Optional so plain\n * run-only fixtures keep working (they emit a single terminal frame).\n */\n stream?(\n input: unknown,\n options?: {\n readonly signal?: AbortSignal;\n readonly sessionId?: string;\n readonly userId?: string;\n },\n ): AsyncIterable<unknown>;\n}\n\n/**\n * Minimal shape the server needs from a `Workflow`. Mirrors the\n * `Workflow` surface from `@graphorin/workflow`.\n *\n * @stable\n */\nexport interface ServerWorkflowLike {\n readonly name: string;\n execute(\n input: unknown,\n options?: { readonly signal?: AbortSignal; readonly threadId?: string },\n ): AsyncIterable<unknown>;\n resume?(\n threadId: string,\n directive?: { readonly resume?: unknown },\n opts?: { readonly signal?: AbortSignal },\n ): AsyncIterable<unknown>;\n /** W-119: replay a failed/aborted thread (`POST /:id/retry`). */\n retry?(threadId: string, opts?: { readonly signal?: AbortSignal }): AsyncIterable<unknown>;\n /** W-119: fire a due durable timer (`POST /:id/tick`). */\n tick?(\n threadId: string,\n opts?: { readonly now?: number },\n ): Promise<{ readonly fired: boolean; readonly nextWakeAt: number | null }>;\n /**\n * W-119: resolve a NAMED awakeable/approval (`POST /:id/resume` with\n * `name`) - `approve` is the same primitive under the hood.\n */\n resolveAwakeable?(\n threadId: string,\n name: string,\n value?: unknown,\n opts?: { readonly signal?: AbortSignal },\n ): AsyncIterable<unknown>;\n /** W-119: fork a new thread from a checkpoint (`POST /:id/fork`). */\n fork?(threadId: string, fromCheckpointId: string): Promise<{ readonly newThreadId: string }>;\n getState?(threadId: string): Promise<unknown>;\n listCheckpoints?(threadId: string): Promise<ReadonlyArray<unknown>>;\n /** W-005: per-thread checkpoint erasure (`DELETE /:id/threads/:threadId`). */\n deleteThread?(threadId: string): Promise<void>;\n}\n\n/**\n * Snapshot record returned by {@link AgentRegistry.list}.\n *\n * @stable\n */\nexport interface AgentSummary {\n readonly id: string;\n readonly description?: string;\n readonly tags?: ReadonlyArray<string>;\n}\n\n/**\n * Snapshot record returned by {@link WorkflowRegistry.list}.\n *\n * @stable\n */\nexport interface WorkflowSummary {\n readonly id: string;\n readonly description?: string;\n readonly tags?: ReadonlyArray<string>;\n}\n\n/**\n * Internal registry entry - pairs the user-supplied object with the\n * metadata routes serve to clients.\n *\n * @internal\n */\ninterface AgentEntry {\n readonly id: string;\n readonly agent: ServerAgentLike;\n readonly description?: string;\n readonly tags?: ReadonlyArray<string>;\n}\n\n/**\n * @internal\n */\ninterface WorkflowEntry {\n readonly id: string;\n readonly workflow: ServerWorkflowLike;\n readonly description?: string;\n readonly tags?: ReadonlyArray<string>;\n}\n\n/**\n * Registration descriptor accepted by {@link AgentRegistry.register}.\n *\n * @stable\n */\nexport interface AgentRegistration {\n readonly id: string;\n readonly agent: ServerAgentLike;\n readonly description?: string;\n readonly tags?: ReadonlyArray<string>;\n}\n\n/**\n * Registration descriptor accepted by {@link WorkflowRegistry.register}.\n *\n * @stable\n */\nexport interface WorkflowRegistration {\n readonly id: string;\n readonly workflow: ServerWorkflowLike;\n readonly description?: string;\n readonly tags?: ReadonlyArray<string>;\n}\n\n/**\n * Read/write registry for agents the server should expose. Every\n * mutation is synchronous and the lookup is `O(1)`.\n *\n * The class is intentionally tiny - extension points (e.g. lazy\n * factory loading, per-tenant scoping) live in higher-level packages\n * and consume this surface as a primitive.\n *\n * @stable\n */\nexport class AgentRegistry {\n readonly #entries: Map<string, AgentEntry> = new Map();\n\n register(entry: AgentRegistration): void {\n this.#entries.set(entry.id, {\n id: entry.id,\n agent: entry.agent,\n ...(entry.description !== undefined ? { description: entry.description } : {}),\n ...(entry.tags !== undefined ? { tags: Object.freeze(entry.tags.slice()) } : {}),\n });\n }\n\n unregister(id: string): boolean {\n return this.#entries.delete(id);\n }\n\n get(id: string): ServerAgentLike | undefined {\n return this.#entries.get(id)?.agent;\n }\n\n has(id: string): boolean {\n return this.#entries.has(id);\n }\n\n list(): ReadonlyArray<AgentSummary> {\n return Object.freeze(\n [...this.#entries.values()].map((entry) =>\n Object.freeze({\n id: entry.id,\n ...(entry.description !== undefined ? { description: entry.description } : {}),\n ...(entry.tags !== undefined ? { tags: entry.tags } : {}),\n }),\n ),\n );\n }\n\n describe(id: string): AgentSummary | undefined {\n const entry = this.#entries.get(id);\n if (entry === undefined) return undefined;\n return Object.freeze({\n id: entry.id,\n ...(entry.description !== undefined ? { description: entry.description } : {}),\n ...(entry.tags !== undefined ? { tags: entry.tags } : {}),\n });\n }\n\n size(): number {\n return this.#entries.size;\n }\n}\n\n/**\n * @stable\n */\nexport class WorkflowRegistry {\n readonly #entries: Map<string, WorkflowEntry> = new Map();\n\n register(entry: WorkflowRegistration): void {\n this.#entries.set(entry.id, {\n id: entry.id,\n workflow: entry.workflow,\n ...(entry.description !== undefined ? { description: entry.description } : {}),\n ...(entry.tags !== undefined ? { tags: Object.freeze(entry.tags.slice()) } : {}),\n });\n }\n\n unregister(id: string): boolean {\n return this.#entries.delete(id);\n }\n\n get(id: string): ServerWorkflowLike | undefined {\n return this.#entries.get(id)?.workflow;\n }\n\n has(id: string): boolean {\n return this.#entries.has(id);\n }\n\n list(): ReadonlyArray<WorkflowSummary> {\n return Object.freeze(\n [...this.#entries.values()].map((entry) =>\n Object.freeze({\n id: entry.id,\n ...(entry.description !== undefined ? { description: entry.description } : {}),\n ...(entry.tags !== undefined ? { tags: entry.tags } : {}),\n }),\n ),\n );\n }\n\n describe(id: string): WorkflowSummary | undefined {\n const entry = this.#entries.get(id);\n if (entry === undefined) return undefined;\n return Object.freeze({\n id: entry.id,\n ...(entry.description !== undefined ? { description: entry.description } : {}),\n ...(entry.tags !== undefined ? { tags: entry.tags } : {}),\n });\n }\n\n size(): number {\n return this.#entries.size;\n }\n}\n"],"mappings":";;;;;;;;;;;AA8KA,IAAa,gBAAb,MAA2B;CACzB,CAASA,0BAAoC,IAAI,KAAK;CAEtD,SAAS,OAAgC;AACvC,QAAKA,QAAS,IAAI,MAAM,IAAI;GAC1B,IAAI,MAAM;GACV,OAAO,MAAM;GACb,GAAI,MAAM,gBAAgB,SAAY,EAAE,aAAa,MAAM,aAAa,GAAG,EAAE;GAC7E,GAAI,MAAM,SAAS,SAAY,EAAE,MAAM,OAAO,OAAO,MAAM,KAAK,OAAO,CAAC,EAAE,GAAG,EAAE;GAChF,CAAC;;CAGJ,WAAW,IAAqB;AAC9B,SAAO,MAAKA,QAAS,OAAO,GAAG;;CAGjC,IAAI,IAAyC;AAC3C,SAAO,MAAKA,QAAS,IAAI,GAAG,EAAE;;CAGhC,IAAI,IAAqB;AACvB,SAAO,MAAKA,QAAS,IAAI,GAAG;;CAG9B,OAAoC;AAClC,SAAO,OAAO,OACZ,CAAC,GAAG,MAAKA,QAAS,QAAQ,CAAC,CAAC,KAAK,UAC/B,OAAO,OAAO;GACZ,IAAI,MAAM;GACV,GAAI,MAAM,gBAAgB,SAAY,EAAE,aAAa,MAAM,aAAa,GAAG,EAAE;GAC7E,GAAI,MAAM,SAAS,SAAY,EAAE,MAAM,MAAM,MAAM,GAAG,EAAE;GACzD,CAAC,CACH,CACF;;CAGH,SAAS,IAAsC;EAC7C,MAAM,QAAQ,MAAKA,QAAS,IAAI,GAAG;AACnC,MAAI,UAAU,OAAW,QAAO;AAChC,SAAO,OAAO,OAAO;GACnB,IAAI,MAAM;GACV,GAAI,MAAM,gBAAgB,SAAY,EAAE,aAAa,MAAM,aAAa,GAAG,EAAE;GAC7E,GAAI,MAAM,SAAS,SAAY,EAAE,MAAM,MAAM,MAAM,GAAG,EAAE;GACzD,CAAC;;CAGJ,OAAe;AACb,SAAO,MAAKA,QAAS;;;;;;AAOzB,IAAa,mBAAb,MAA8B;CAC5B,CAASA,0BAAuC,IAAI,KAAK;CAEzD,SAAS,OAAmC;AAC1C,QAAKA,QAAS,IAAI,MAAM,IAAI;GAC1B,IAAI,MAAM;GACV,UAAU,MAAM;GAChB,GAAI,MAAM,gBAAgB,SAAY,EAAE,aAAa,MAAM,aAAa,GAAG,EAAE;GAC7E,GAAI,MAAM,SAAS,SAAY,EAAE,MAAM,OAAO,OAAO,MAAM,KAAK,OAAO,CAAC,EAAE,GAAG,EAAE;GAChF,CAAC;;CAGJ,WAAW,IAAqB;AAC9B,SAAO,MAAKA,QAAS,OAAO,GAAG;;CAGjC,IAAI,IAA4C;AAC9C,SAAO,MAAKA,QAAS,IAAI,GAAG,EAAE;;CAGhC,IAAI,IAAqB;AACvB,SAAO,MAAKA,QAAS,IAAI,GAAG;;CAG9B,OAAuC;AACrC,SAAO,OAAO,OACZ,CAAC,GAAG,MAAKA,QAAS,QAAQ,CAAC,CAAC,KAAK,UAC/B,OAAO,OAAO;GACZ,IAAI,MAAM;GACV,GAAI,MAAM,gBAAgB,SAAY,EAAE,aAAa,MAAM,aAAa,GAAG,EAAE;GAC7E,GAAI,MAAM,SAAS,SAAY,EAAE,MAAM,MAAM,MAAM,GAAG,EAAE;GACzD,CAAC,CACH,CACF;;CAGH,SAAS,IAAyC;EAChD,MAAM,QAAQ,MAAKA,QAAS,IAAI,GAAG;AACnC,MAAI,UAAU,OAAW,QAAO;AAChC,SAAO,OAAO,OAAO;GACnB,IAAI,MAAM;GACV,GAAI,MAAM,gBAAgB,SAAY,EAAE,aAAa,MAAM,aAAa,GAAG,EAAE;GAC7E,GAAI,MAAM,SAAS,SAAY,EAAE,MAAM,MAAM,MAAM,GAAG,EAAE;GACzD,CAAC;;CAGJ,OAAe;AACb,SAAO,MAAKA,QAAS"}
@@ -1,6 +1,6 @@
1
1
  import { ServerVariables } from "../internal/context.js";
2
- import { AuditDb } from "@graphorin/security/audit";
3
2
  import { Hono } from "hono";
3
+ import { AuditDb } from "@graphorin/security/audit";
4
4
 
5
5
  //#region src/replay/routes.d.ts
6
6
 
@@ -1,7 +1,7 @@
1
- import { appendAudit } from "@graphorin/security/audit";
2
1
  import { Hono } from "hono";
3
- import { z } from "zod";
2
+ import { appendAudit } from "@graphorin/security/audit";
4
3
  import { parseScope, scopeMatches } from "@graphorin/security/auth";
4
+ import { z } from "zod";
5
5
 
6
6
  //#region src/replay/routes.ts
7
7
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"agents.d.ts","names":[],"sources":["../../src/routes/agents.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;UA8BiB,eAAA;mBACE;iBACF;;;;;;;wBAAe;;;;;iBAsBhB,iBAAA,OAAwB,kBAAkB;aAAkB;;;;;;;;;iBA+L5D,eAAA,OAAsB,kBAAkB;aAAkB"}
1
+ {"version":3,"file":"agents.d.ts","names":[],"sources":["../../src/routes/agents.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;UAmCiB,eAAA;mBACE;iBACF;;;;;;;wBAAe;;;;;iBAsBhB,iBAAA,OAAwB,kBAAkB;aAAkB;;;;;;;;;iBAoM5D,eAAA,OAAsB,kBAAkB;aAAkB"}
@@ -1,10 +1,28 @@
1
- import { AgentNotFoundError } from "../errors/index.js";
2
- import { createScopeMiddleware } from "../middleware/scope.js";
3
1
  import { newRequestId } from "../internal/ids.js";
2
+ import { checkScope, createAuthenticatedMiddleware, createScopeMiddleware } from "../middleware/scope.js";
3
+ import { AgentNotFoundError } from "../errors/index.js";
4
+ import { requiredRunScope } from "../runtime/run-state.js";
4
5
  import { Hono } from "hono";
5
6
  import { z } from "zod";
7
+ import { toWireAgentEvent } from "@graphorin/core";
6
8
 
7
9
  //#region src/routes/agents.ts
10
+ /**
11
+ * Agent REST routes.
12
+ *
13
+ * GET /agents (scope `agents:read`)
14
+ * GET /agents/:id (scope `agents:read`)
15
+ * POST /agents/:id/run (idempotent; scope `agents:invoke[:id]`)
16
+ * POST /agents/:id/stream (scope `agents:invoke[:id]`)
17
+ * GET /runs/:runId/state (scope `agents:read`)
18
+ * POST /runs/:runId/abort (scope `agents:invoke`)
19
+ * POST /runs/:runId/resume (idempotent; scope `agents:invoke`)
20
+ *
21
+ * Agents are looked up via {@link AgentRegistry}; missing entries
22
+ * surface a 404 with a typed error body.
23
+ *
24
+ * @packageDocumentation
25
+ */
8
26
  const RunBodySchema = z.object({
9
27
  input: z.unknown().optional(),
10
28
  sessionId: z.string().min(1).optional(),
@@ -37,14 +55,7 @@ function createAgentRoutes(deps) {
37
55
  }, 404);
38
56
  }
39
57
  const parsed = RunBodySchema.safeParse(await safelyParseJson(c));
40
- if (!parsed.success) return c.json({
41
- error: "config-invalid",
42
- message: "Invalid run body.",
43
- issues: parsed.error.issues.map((i) => ({
44
- path: i.path,
45
- message: i.message
46
- }))
47
- }, 400);
58
+ if (!parsed.success) return invalidBodyResponse(c, parsed.error);
48
59
  const runId = newRunId();
49
60
  const sessionId = parsed.data.sessionId;
50
61
  const userId = parsed.data.userId;
@@ -85,11 +96,12 @@ function createAgentRoutes(deps) {
85
96
  message: err.message
86
97
  }, 404);
87
98
  }
88
- const runId = newRunId();
89
99
  const parsed = RunBodySchema.safeParse(await safelyParseJson(c));
90
- const sessionId = parsed.success ? parsed.data.sessionId : void 0;
91
- const userId = parsed.success ? parsed.data.userId : void 0;
92
- const input = parsed.success ? parsed.data.input ?? "" : "";
100
+ if (!parsed.success) return invalidBodyResponse(c, parsed.error);
101
+ const runId = newRunId();
102
+ const sessionId = parsed.data.sessionId;
103
+ const userId = parsed.data.userId;
104
+ const input = parsed.data.input ?? "";
93
105
  const tracker = deps.runs.start(runId, {
94
106
  kind: "agent",
95
107
  agentId: id,
@@ -136,7 +148,7 @@ function backgroundStreamAgent(agent, input, opts) {
136
148
  };
137
149
  if (typeof agent.stream === "function") for await (const ev of agent.stream(input, callOpts)) {
138
150
  if (opts.signal.aborted) break;
139
- emit(typeof ev.type === "string" ? ev.type : "agent.event", ev);
151
+ emit(typeof ev.type === "string" ? ev.type : "agent.event", toWireAgentEvent(ev));
140
152
  }
141
153
  else {
142
154
  const output = await agent.run(input, callOpts);
@@ -164,17 +176,32 @@ function backgroundStreamAgent(agent, input, opts) {
164
176
  */
165
177
  function createRunRoutes(deps) {
166
178
  const app = new Hono();
167
- app.get("/:runId/state", createScopeMiddleware("agents:read"), (c) => {
179
+ app.get("/:runId/state", createAuthenticatedMiddleware(), (c) => {
168
180
  const runId = c.req.param("runId");
169
181
  const state = deps.runs.snapshot(runId);
170
182
  if (state === void 0) return c.json({
171
183
  error: "run-not-found",
172
184
  message: `Run '${runId}' is not registered.`
173
185
  }, 404);
186
+ const required = requiredRunScope(state, "read");
187
+ if (!checkScope(c.get("state").auth, required)) return c.json({
188
+ error: "scope-denied",
189
+ message: `Token lacks required scope '${required}'.`
190
+ }, 403);
174
191
  return c.json(state);
175
192
  });
176
- app.post("/:runId/abort", createScopeMiddleware("agents:invoke"), (c) => {
193
+ app.post("/:runId/abort", createAuthenticatedMiddleware(), (c) => {
177
194
  const runId = c.req.param("runId");
195
+ const state = deps.runs.snapshot(runId);
196
+ if (state === void 0) return c.json({
197
+ error: "run-not-found",
198
+ message: `Run '${runId}' is not registered.`
199
+ }, 404);
200
+ const required = requiredRunScope(state, "control");
201
+ if (!checkScope(c.get("state").auth, required)) return c.json({
202
+ error: "scope-denied",
203
+ message: `Token lacks required scope '${required}'.`
204
+ }, 403);
178
205
  if (!deps.runs.abort(runId)) return c.json({
179
206
  error: "run-not-found",
180
207
  message: `Run '${runId}' is not registered.`
@@ -184,13 +211,18 @@ function createRunRoutes(deps) {
184
211
  status: "aborted"
185
212
  });
186
213
  });
187
- app.post("/:runId/resume", createScopeMiddleware("agents:invoke"), async (c) => {
214
+ app.post("/:runId/resume", createAuthenticatedMiddleware(), async (c) => {
188
215
  const runId = c.req.param("runId");
189
216
  const state = deps.runs.snapshot(runId);
190
217
  if (state === void 0) return c.json({
191
218
  error: "run-not-found",
192
219
  message: `Run '${runId}' is not registered.`
193
220
  }, 404);
221
+ const requiredResume = requiredRunScope(state, "control");
222
+ if (!checkScope(c.get("state").auth, requiredResume)) return c.json({
223
+ error: "scope-denied",
224
+ message: `Token lacks required scope '${requiredResume}'.`
225
+ }, 403);
194
226
  return c.json({
195
227
  error: "resume-not-implemented",
196
228
  runId,
@@ -200,6 +232,20 @@ function createRunRoutes(deps) {
200
232
  });
201
233
  return app;
202
234
  }
235
+ /**
236
+ * W-151: byte-identical 400 for an invalid run/stream body - both
237
+ * sibling routes validate through this one helper.
238
+ */
239
+ function invalidBodyResponse(c, error) {
240
+ return c.json({
241
+ error: "config-invalid",
242
+ message: "Invalid run body.",
243
+ issues: error.issues.map((i) => ({
244
+ path: i.path,
245
+ message: i.message
246
+ }))
247
+ }, 400);
248
+ }
203
249
  async function safelyParseJson(c) {
204
250
  try {
205
251
  return await c.req.json();
@@ -1 +1 @@
1
- {"version":3,"file":"agents.js","names":["callOptions: { signal?: AbortSignal; sessionId?: string; userId?: string }"],"sources":["../../src/routes/agents.ts"],"sourcesContent":["/**\n * Agent REST routes.\n *\n * GET /agents (scope `agents:read`)\n * GET /agents/:id (scope `agents:read`)\n * POST /agents/:id/run (idempotent; scope `agents:invoke[:id]`)\n * POST /agents/:id/stream (scope `agents:invoke[:id]`)\n * GET /runs/:runId/state (scope `agents:read`)\n * POST /runs/:runId/abort (scope `agents:invoke`)\n * POST /runs/:runId/resume (idempotent; scope `agents:invoke`)\n *\n * Agents are looked up via {@link AgentRegistry}; missing entries\n * surface a 404 with a typed error body.\n *\n * @packageDocumentation\n */\n\nimport type { Context } from 'hono';\nimport { Hono } from 'hono';\nimport { z } from 'zod';\nimport { AgentNotFoundError } from '../errors/index.js';\nimport type { ServerVariables } from '../internal/context.js';\nimport { newRequestId } from '../internal/ids.js';\nimport { createScopeMiddleware } from '../middleware/scope.js';\nimport type { AgentRegistry } from '../registry/index.js';\nimport type { RunStateTracker } from '../runtime/run-state.js';\n\n/**\n * @stable\n */\nexport interface AgentRoutesDeps {\n readonly agents: AgentRegistry;\n readonly runs: RunStateTracker;\n readonly newRunId?: () => string;\n /**\n * Streaming dispatcher (IP-2). When present, `POST /:id/stream`\n * actually runs the agent and emits every event onto the\n * `agent:<id>/runs/<runId>/events` subject.\n */\n readonly dispatcher?: import('../ws/dispatcher.js').WsDispatcher;\n}\n\nconst RunBodySchema = z\n .object({\n input: z.unknown().optional(),\n sessionId: z.string().min(1).optional(),\n userId: z.string().min(1).optional(),\n })\n .strict()\n .default({});\n\n/**\n * @stable\n */\nexport function createAgentRoutes(deps: AgentRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n const newRunId = deps.newRunId ?? newRequestId;\n\n app.get('/', createScopeMiddleware('agents:read'), (c) => c.json({ agents: deps.agents.list() }));\n\n app.get(\n '/:id',\n createScopeMiddleware((_path, params) => `agents:read:${params.id}`),\n (c) => {\n const id = c.req.param('id');\n const summary = deps.agents.describe(id);\n if (summary === undefined) {\n return c.json(\n { error: 'agent-not-found', message: `Agent '${id}' is not registered.` },\n 404,\n );\n }\n return c.json(summary);\n },\n );\n\n app.post(\n '/:id/run',\n createScopeMiddleware((_path, params) => `agents:invoke:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n const agent = deps.agents.get(id);\n if (agent === undefined) {\n const err = new AgentNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n const parsed = RunBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid run body.',\n issues: parsed.error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n }\n const runId = newRunId();\n const sessionId = parsed.data.sessionId;\n const userId = parsed.data.userId;\n const tracker = deps.runs.start(runId, {\n kind: 'agent',\n agentId: id,\n ...(sessionId !== undefined ? { sessionId } : {}),\n ...(userId !== undefined ? { userId } : {}),\n });\n try {\n const callOptions: { signal?: AbortSignal; sessionId?: string; userId?: string } = {\n signal: tracker.signal,\n };\n if (sessionId !== undefined) callOptions.sessionId = sessionId;\n if (userId !== undefined) callOptions.userId = userId;\n const result = await agent.run(parsed.data.input ?? '', callOptions);\n deps.runs.complete(runId, 'completed');\n return c.json({ runId, status: 'completed', result }, 200);\n } catch (err) {\n const aborted = tracker.signal.aborted;\n deps.runs.complete(runId, aborted ? 'aborted' : 'failed', err);\n return c.json(\n {\n runId,\n status: aborted ? 'aborted' : 'failed',\n error: aborted ? 'run-aborted' : 'run-failed',\n message: err instanceof Error ? err.message : String(err),\n },\n aborted ? 408 : 500,\n );\n }\n },\n );\n\n app.post(\n '/:id/stream',\n createScopeMiddleware((_path, params) => `agents:invoke:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n if (!deps.agents.has(id)) {\n const err = new AgentNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n const runId = newRunId();\n const parsed = RunBodySchema.safeParse(await safelyParseJson(c));\n const sessionId = parsed.success ? parsed.data.sessionId : undefined;\n const userId = parsed.success ? parsed.data.userId : undefined;\n const input = parsed.success ? (parsed.data.input ?? '') : '';\n // IP-2: actually run the agent. The old handler parsed the input\n // and threw it away - the run sat 'pending' forever while the 202\n // advertised subjects nothing would ever emit on.\n const tracker = deps.runs.start(runId, {\n kind: 'agent',\n agentId: id,\n ...(sessionId !== undefined ? { sessionId } : {}),\n ...(userId !== undefined ? { userId } : {}),\n });\n const subject = `agent:${id}/runs/${runId}/events`;\n const agent = deps.agents.get(id);\n if (agent !== undefined) {\n backgroundStreamAgent(agent, input, {\n signal: tracker.signal,\n ...(sessionId !== undefined ? { sessionId } : {}),\n ...(userId !== undefined ? { userId } : {}),\n runs: deps.runs,\n runId,\n subject,\n ...(deps.dispatcher !== undefined ? { dispatcher: deps.dispatcher } : {}),\n });\n }\n return c.json(\n {\n runId,\n status: 'running',\n subscribe: {\n // The SSE URL previously advertised here pointed at a path\n // that was never mounted - the WebSocket subject is the\n // delivery channel (IP-2).\n websocket: subject,\n },\n },\n 202,\n );\n },\n );\n\n return app;\n}\n\n/**\n * IP-2: consume the agent's event stream in the background and emit\n * every event onto the run subject. Falls back to `run(...)` (a single\n * terminal frame) for registry entries without a `stream` surface.\n */\nfunction backgroundStreamAgent(\n agent: ReturnType<AgentRegistry['get']> & object,\n input: unknown,\n opts: {\n readonly signal: AbortSignal;\n readonly sessionId?: string;\n readonly userId?: string;\n readonly runs: RunStateTracker;\n readonly runId: string;\n readonly subject: string;\n readonly dispatcher?: import('../ws/dispatcher.js').WsDispatcher;\n },\n): void {\n const emit = (type: string, payload: unknown): void => {\n opts.dispatcher?.emit(opts.subject, { type, payload });\n };\n void (async () => {\n try {\n const callOpts = {\n signal: opts.signal,\n ...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),\n ...(opts.userId !== undefined ? { userId: opts.userId } : {}),\n };\n if (typeof agent.stream === 'function') {\n for await (const ev of agent.stream(input, callOpts)) {\n if (opts.signal.aborted) break;\n const type =\n typeof (ev as { type?: unknown }).type === 'string'\n ? (ev as { type: string }).type\n : 'agent.event';\n emit(type, ev);\n }\n } else {\n const output = await agent.run(input, callOpts);\n emit('agent.end', { runId: opts.runId, output });\n }\n opts.runs.complete(opts.runId, opts.signal.aborted ? 'aborted' : 'completed');\n } catch (err) {\n emit('agent.error', {\n runId: opts.runId,\n message: err instanceof Error ? err.message : String(err),\n });\n opts.runs.complete(opts.runId, opts.signal.aborted ? 'aborted' : 'failed', err);\n }\n })();\n}\n\n/**\n * Companion router for the `/runs/...` surface. Kept separate so the\n * `createServer` factory can mount it under the top-level base path\n * rather than under `/agents`.\n *\n * @stable\n */\nexport function createRunRoutes(deps: AgentRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n\n app.get('/:runId/state', createScopeMiddleware('agents:read'), (c) => {\n const runId = c.req.param('runId');\n const state = deps.runs.snapshot(runId);\n if (state === undefined) {\n return c.json({ error: 'run-not-found', message: `Run '${runId}' is not registered.` }, 404);\n }\n return c.json(state);\n });\n\n app.post('/:runId/abort', createScopeMiddleware('agents:invoke'), (c) => {\n const runId = c.req.param('runId');\n const aborted = deps.runs.abort(runId);\n if (!aborted) {\n return c.json({ error: 'run-not-found', message: `Run '${runId}' is not registered.` }, 404);\n }\n return c.json({ runId, status: 'aborted' });\n });\n\n app.post('/:runId/resume', createScopeMiddleware('agents:invoke'), async (c) => {\n const runId = c.req.param('runId');\n const state = deps.runs.snapshot(runId);\n if (state === undefined) {\n return c.json({ error: 'run-not-found', message: `Run '${runId}' is not registered.` }, 404);\n }\n // IP-14: a 202 that persists nothing and resumes nothing is a lie\n // the client SDK was built on. Until the server can rehydrate the\n // RunState and re-enter the agent loop (the persisted-approvals\n // work - Wave 3), the honest answer is 501 with the working\n // library-side path: `agent.run(result.state, { directive })`\n // executes approved tools for real (AG-1/AG-9).\n return c.json(\n {\n error: 'resume-not-implemented',\n runId,\n status: state.status,\n message:\n 'Server-side HITL resume is not implemented yet. Resume library-side: ' +\n 'agent.run(result.state, { directive }) - the suspended RunState is on the AgentResult.',\n },\n 501,\n );\n });\n\n return app;\n}\n\nasync function safelyParseJson(c: Context<{ Variables: ServerVariables }>): Promise<unknown> {\n try {\n return await c.req.json();\n } catch {\n return {};\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"agents.js","names":["callOptions: { signal?: AbortSignal; sessionId?: string; userId?: string }"],"sources":["../../src/routes/agents.ts"],"sourcesContent":["/**\n * Agent REST routes.\n *\n * GET /agents (scope `agents:read`)\n * GET /agents/:id (scope `agents:read`)\n * POST /agents/:id/run (idempotent; scope `agents:invoke[:id]`)\n * POST /agents/:id/stream (scope `agents:invoke[:id]`)\n * GET /runs/:runId/state (scope `agents:read`)\n * POST /runs/:runId/abort (scope `agents:invoke`)\n * POST /runs/:runId/resume (idempotent; scope `agents:invoke`)\n *\n * Agents are looked up via {@link AgentRegistry}; missing entries\n * surface a 404 with a typed error body.\n *\n * @packageDocumentation\n */\n\nimport { toWireAgentEvent } from '@graphorin/core';\nimport type { Context } from 'hono';\nimport { Hono } from 'hono';\nimport { z } from 'zod';\nimport { AgentNotFoundError } from '../errors/index.js';\nimport type { ServerVariables } from '../internal/context.js';\nimport { newRequestId } from '../internal/ids.js';\nimport {\n checkScope,\n createAuthenticatedMiddleware,\n createScopeMiddleware,\n} from '../middleware/scope.js';\nimport type { AgentRegistry } from '../registry/index.js';\nimport { type RunStateTracker, requiredRunScope } from '../runtime/run-state.js';\n\n/**\n * @stable\n */\nexport interface AgentRoutesDeps {\n readonly agents: AgentRegistry;\n readonly runs: RunStateTracker;\n readonly newRunId?: () => string;\n /**\n * Streaming dispatcher (IP-2). When present, `POST /:id/stream`\n * actually runs the agent and emits every event onto the\n * `agent:<id>/runs/<runId>/events` subject.\n */\n readonly dispatcher?: import('../ws/dispatcher.js').WsDispatcher;\n}\n\nconst RunBodySchema = z\n .object({\n input: z.unknown().optional(),\n sessionId: z.string().min(1).optional(),\n userId: z.string().min(1).optional(),\n })\n .strict()\n .default({});\n\n/**\n * @stable\n */\nexport function createAgentRoutes(deps: AgentRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n const newRunId = deps.newRunId ?? newRequestId;\n\n app.get('/', createScopeMiddleware('agents:read'), (c) => c.json({ agents: deps.agents.list() }));\n\n app.get(\n '/:id',\n createScopeMiddleware((_path, params) => `agents:read:${params.id}`),\n (c) => {\n const id = c.req.param('id');\n const summary = deps.agents.describe(id);\n if (summary === undefined) {\n return c.json(\n { error: 'agent-not-found', message: `Agent '${id}' is not registered.` },\n 404,\n );\n }\n return c.json(summary);\n },\n );\n\n app.post(\n '/:id/run',\n createScopeMiddleware((_path, params) => `agents:invoke:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n const agent = deps.agents.get(id);\n if (agent === undefined) {\n const err = new AgentNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n const parsed = RunBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return invalidBodyResponse(c, parsed.error);\n }\n const runId = newRunId();\n const sessionId = parsed.data.sessionId;\n const userId = parsed.data.userId;\n const tracker = deps.runs.start(runId, {\n kind: 'agent',\n agentId: id,\n ...(sessionId !== undefined ? { sessionId } : {}),\n ...(userId !== undefined ? { userId } : {}),\n });\n try {\n const callOptions: { signal?: AbortSignal; sessionId?: string; userId?: string } = {\n signal: tracker.signal,\n };\n if (sessionId !== undefined) callOptions.sessionId = sessionId;\n if (userId !== undefined) callOptions.userId = userId;\n const result = await agent.run(parsed.data.input ?? '', callOptions);\n deps.runs.complete(runId, 'completed');\n return c.json({ runId, status: 'completed', result }, 200);\n } catch (err) {\n const aborted = tracker.signal.aborted;\n deps.runs.complete(runId, aborted ? 'aborted' : 'failed', err);\n return c.json(\n {\n runId,\n status: aborted ? 'aborted' : 'failed',\n error: aborted ? 'run-aborted' : 'run-failed',\n message: err instanceof Error ? err.message : String(err),\n },\n aborted ? 408 : 500,\n );\n }\n },\n );\n\n app.post(\n '/:id/stream',\n createScopeMiddleware((_path, params) => `agents:invoke:${params.id}`),\n async (c) => {\n const id = c.req.param('id');\n if (!deps.agents.has(id)) {\n const err = new AgentNotFoundError(id);\n return c.json({ error: err.kind, message: err.message }, 404);\n }\n // W-151: validate the body BEFORE any run is registered - the old\n // handler swallowed a failed parse and launched the agent on an\n // empty prompt, burning provider tokens behind a 202 that looked\n // successful. Identical validation to the sibling /run route\n // (empty/absent bodies stay valid via the schema default).\n const parsed = RunBodySchema.safeParse(await safelyParseJson(c));\n if (!parsed.success) {\n return invalidBodyResponse(c, parsed.error);\n }\n const runId = newRunId();\n const sessionId = parsed.data.sessionId;\n const userId = parsed.data.userId;\n const input = parsed.data.input ?? '';\n // IP-2: actually run the agent. The old handler parsed the input\n // and threw it away - the run sat 'pending' forever while the 202\n // advertised subjects nothing would ever emit on.\n const tracker = deps.runs.start(runId, {\n kind: 'agent',\n agentId: id,\n ...(sessionId !== undefined ? { sessionId } : {}),\n ...(userId !== undefined ? { userId } : {}),\n });\n const subject = `agent:${id}/runs/${runId}/events`;\n const agent = deps.agents.get(id);\n if (agent !== undefined) {\n backgroundStreamAgent(agent, input, {\n signal: tracker.signal,\n ...(sessionId !== undefined ? { sessionId } : {}),\n ...(userId !== undefined ? { userId } : {}),\n runs: deps.runs,\n runId,\n subject,\n ...(deps.dispatcher !== undefined ? { dispatcher: deps.dispatcher } : {}),\n });\n }\n return c.json(\n {\n runId,\n status: 'running',\n subscribe: {\n // The SSE URL previously advertised here pointed at a path\n // that was never mounted - the WebSocket subject is the\n // delivery channel (IP-2).\n websocket: subject,\n },\n },\n 202,\n );\n },\n );\n\n return app;\n}\n\n/**\n * IP-2: consume the agent's event stream in the background and emit\n * every event onto the run subject. Falls back to `run(...)` (a single\n * terminal frame) for registry entries without a `stream` surface.\n */\nfunction backgroundStreamAgent(\n agent: ReturnType<AgentRegistry['get']> & object,\n input: unknown,\n opts: {\n readonly signal: AbortSignal;\n readonly sessionId?: string;\n readonly userId?: string;\n readonly runs: RunStateTracker;\n readonly runId: string;\n readonly subject: string;\n readonly dispatcher?: import('../ws/dispatcher.js').WsDispatcher;\n },\n): void {\n const emit = (type: string, payload: unknown): void => {\n opts.dispatcher?.emit(opts.subject, { type, payload });\n };\n void (async () => {\n try {\n const callOpts = {\n signal: opts.signal,\n ...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),\n ...(opts.userId !== undefined ? { userId: opts.userId } : {}),\n };\n if (typeof agent.stream === 'function') {\n for await (const ev of agent.stream(input, callOpts)) {\n if (opts.signal.aborted) break;\n const type =\n typeof (ev as { type?: unknown }).type === 'string'\n ? (ev as { type: string }).type\n : 'agent.event';\n // W-046: binary-bearing variants (file.generated,\n // tool.execute.partial, agent.end with its full RunState) must\n // be projected to their JSON-safe wire twins before the\n // dispatcher stringifies the frame. Unknown types pass through.\n emit(type, toWireAgentEvent(ev as Parameters<typeof toWireAgentEvent>[0]));\n }\n } else {\n const output = await agent.run(input, callOpts);\n emit('agent.end', { runId: opts.runId, output });\n }\n opts.runs.complete(opts.runId, opts.signal.aborted ? 'aborted' : 'completed');\n } catch (err) {\n emit('agent.error', {\n runId: opts.runId,\n message: err instanceof Error ? err.message : String(err),\n });\n opts.runs.complete(opts.runId, opts.signal.aborted ? 'aborted' : 'failed', err);\n }\n })();\n}\n\n/**\n * Companion router for the `/runs/...` surface. Kept separate so the\n * `createServer` factory can mount it under the top-level base path\n * rather than under `/agents`.\n *\n * @stable\n */\nexport function createRunRoutes(deps: AgentRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n\n // W-107: run control binds to the run's OWNING resource. The\n // requirement is only known after the snapshot resolves the\n // descriptor, so the outer gate is authentication-only and the\n // handlers authorize imperatively: 404 for unknown runs first\n // (runIds are unguessable newRequestId values and ephemeral - the\n // ordering is deliberate and matches WS run.cancel), then 403 when\n // the grant does not cover the owning agent/workflow. Bare\n // `agents:read`/`agents:invoke` grants keep covering their\n // per-resource requirements.\n app.get('/:runId/state', createAuthenticatedMiddleware(), (c) => {\n const runId = c.req.param('runId');\n const state = deps.runs.snapshot(runId);\n if (state === undefined) {\n return c.json({ error: 'run-not-found', message: `Run '${runId}' is not registered.` }, 404);\n }\n const required = requiredRunScope(state, 'read');\n if (!checkScope(c.get('state').auth, required)) {\n return c.json(\n { error: 'scope-denied', message: `Token lacks required scope '${required}'.` },\n 403,\n );\n }\n return c.json(state);\n });\n\n app.post('/:runId/abort', createAuthenticatedMiddleware(), (c) => {\n const runId = c.req.param('runId');\n const state = deps.runs.snapshot(runId);\n if (state === undefined) {\n return c.json({ error: 'run-not-found', message: `Run '${runId}' is not registered.` }, 404);\n }\n const required = requiredRunScope(state, 'control');\n if (!checkScope(c.get('state').auth, required)) {\n return c.json(\n { error: 'scope-denied', message: `Token lacks required scope '${required}'.` },\n 403,\n );\n }\n const aborted = deps.runs.abort(runId);\n if (!aborted) {\n return c.json({ error: 'run-not-found', message: `Run '${runId}' is not registered.` }, 404);\n }\n return c.json({ runId, status: 'aborted' });\n });\n\n app.post('/:runId/resume', createAuthenticatedMiddleware(), async (c) => {\n const runId = c.req.param('runId');\n const state = deps.runs.snapshot(runId);\n if (state === undefined) {\n return c.json({ error: 'run-not-found', message: `Run '${runId}' is not registered.` }, 404);\n }\n const requiredResume = requiredRunScope(state, 'control');\n if (!checkScope(c.get('state').auth, requiredResume)) {\n return c.json(\n { error: 'scope-denied', message: `Token lacks required scope '${requiredResume}'.` },\n 403,\n );\n }\n // IP-14: a 202 that persists nothing and resumes nothing is a lie\n // the client SDK was built on. Until the server can rehydrate the\n // RunState and re-enter the agent loop (the persisted-approvals\n // work - Wave 3), the honest answer is 501 with the working\n // library-side path: `agent.run(result.state, { directive })`\n // executes approved tools for real (AG-1/AG-9).\n return c.json(\n {\n error: 'resume-not-implemented',\n runId,\n status: state.status,\n message:\n 'Server-side HITL resume is not implemented yet. Resume library-side: ' +\n 'agent.run(result.state, { directive }) - the suspended RunState is on the AgentResult.',\n },\n 501,\n );\n });\n\n return app;\n}\n\n/**\n * W-151: byte-identical 400 for an invalid run/stream body - both\n * sibling routes validate through this one helper.\n */\nfunction invalidBodyResponse(\n c: Context<{ Variables: ServerVariables }>,\n error: { issues: ReadonlyArray<{ path: ReadonlyArray<PropertyKey>; message: string }> },\n) {\n return c.json(\n {\n error: 'config-invalid',\n message: 'Invalid run body.',\n issues: error.issues.map((i) => ({ path: i.path, message: i.message })),\n },\n 400,\n );\n}\n\nasync function safelyParseJson(c: Context<{ Variables: ServerVariables }>): Promise<unknown> {\n try {\n return await c.req.json();\n } catch {\n return {};\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { createScopeMiddleware } from "../middleware/scope.js";
1
+ import { createAuthenticatedMiddleware } from "../middleware/scope.js";
2
2
  import { Hono } from "hono";
3
3
 
4
4
  //#region src/routes/auth.ts
@@ -26,7 +26,7 @@ import { Hono } from "hono";
26
26
  */
27
27
  function createAuthRoutes(deps) {
28
28
  const app = new Hono();
29
- app.post("/session/ws-ticket", createScopeMiddleware("agents:invoke"), (c) => {
29
+ app.post("/session/ws-ticket", createAuthenticatedMiddleware(), (c) => {
30
30
  const auth = c.get("state").auth;
31
31
  const principal = auth.kind === "token" ? {
32
32
  tokenId: auth.token.tokenId,
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","names":[],"sources":["../../src/routes/auth.ts"],"sourcesContent":["/**\n * Auth-related REST routes that exist purely to support the WebSocket\n * surface in `@graphorin/server/ws`.\n *\n * POST /session/ws-ticket (scope `agents:invoke`)\n *\n * Browser clients call the endpoint with a regular HTTP `Authorization:\n * Bearer <token>` header to mint a single-use, short-lived ticket; the\n * ticket value is then attached to the WebSocket upgrade request as a\n * second `Sec-WebSocket-Protocol` token (`ticket.<value>`). The\n * server-side WS upgrade handler (see `../ws/upgrade.ts`) consumes the\n * ticket against the same in-memory store so the round-trip is\n * race-free.\n *\n * @packageDocumentation\n */\n\nimport { Hono } from 'hono';\n\nimport type { ServerVariables } from '../internal/context.js';\nimport { createScopeMiddleware } from '../middleware/scope.js';\nimport type { WsTicketStore } from '../ws/ticket.js';\n\n/**\n * Stable shape consumed by {@link createAuthRoutes}.\n *\n * @stable\n */\nexport interface AuthRoutesDeps {\n readonly tickets: WsTicketStore;\n}\n\n/**\n * Build the auth router. The router is mounted at the same base path\n * as the rest of the REST surface (defaults to `/v1`).\n *\n * @stable\n */\nexport function createAuthRoutes(deps: AuthRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n\n app.post('/session/ws-ticket', createScopeMiddleware('agents:invoke'), (c) => {\n const auth = c.get('state').auth;\n // IP-13: in the no-auth loopback mode (`auth.kind='none'`) there is no\n // bearer token, but the anonymous principal is fully authorized - mint a\n // ticket bound to a synthetic `anonymous` id so a browser client written\n // for token mode still completes the round-trip.\n const principal =\n auth.kind === 'token'\n ? { tokenId: auth.token.tokenId, scopes: auth.grantedScopes }\n : auth.kind === 'anonymous'\n ? { tokenId: 'anonymous', scopes: auth.grantedScopes }\n : undefined;\n if (principal === undefined) {\n return c.json(\n {\n error: 'auth-required',\n message: 'Bearer token required to mint a WS ticket.',\n },\n 401,\n );\n }\n const ticket = deps.tickets.issue(principal);\n return c.json(\n {\n ticket: ticket.value,\n expiresAt: ticket.expiresAt,\n ttlMs: deps.tickets.ttlMs,\n },\n 201,\n );\n });\n\n return app;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,iBAAiB,MAA4D;CAC3F,MAAM,MAAM,IAAI,MAAsC;AAEtD,KAAI,KAAK,sBAAsB,sBAAsB,gBAAgB,GAAG,MAAM;EAC5E,MAAM,OAAO,EAAE,IAAI,QAAQ,CAAC;EAK5B,MAAM,YACJ,KAAK,SAAS,UACV;GAAE,SAAS,KAAK,MAAM;GAAS,QAAQ,KAAK;GAAe,GAC3D,KAAK,SAAS,cACZ;GAAE,SAAS;GAAa,QAAQ,KAAK;GAAe,GACpD;AACR,MAAI,cAAc,OAChB,QAAO,EAAE,KACP;GACE,OAAO;GACP,SAAS;GACV,EACD,IACD;EAEH,MAAM,SAAS,KAAK,QAAQ,MAAM,UAAU;AAC5C,SAAO,EAAE,KACP;GACE,QAAQ,OAAO;GACf,WAAW,OAAO;GAClB,OAAO,KAAK,QAAQ;GACrB,EACD,IACD;GACD;AAEF,QAAO"}
1
+ {"version":3,"file":"auth.js","names":[],"sources":["../../src/routes/auth.ts"],"sourcesContent":["/**\n * Auth-related REST routes that exist purely to support the WebSocket\n * surface in `@graphorin/server/ws`.\n *\n * POST /session/ws-ticket (scope `agents:invoke`)\n *\n * Browser clients call the endpoint with a regular HTTP `Authorization:\n * Bearer <token>` header to mint a single-use, short-lived ticket; the\n * ticket value is then attached to the WebSocket upgrade request as a\n * second `Sec-WebSocket-Protocol` token (`ticket.<value>`). The\n * server-side WS upgrade handler (see `../ws/upgrade.ts`) consumes the\n * ticket against the same in-memory store so the round-trip is\n * race-free.\n *\n * @packageDocumentation\n */\n\nimport { Hono } from 'hono';\n\nimport type { ServerVariables } from '../internal/context.js';\nimport { createAuthenticatedMiddleware } from '../middleware/scope.js';\nimport type { WsTicketStore } from '../ws/ticket.js';\n\n/**\n * Stable shape consumed by {@link createAuthRoutes}.\n *\n * @stable\n */\nexport interface AuthRoutesDeps {\n readonly tickets: WsTicketStore;\n}\n\n/**\n * Build the auth router. The router is mounted at the same base path\n * as the rest of the REST surface (defaults to `/v1`).\n *\n * @stable\n */\nexport function createAuthRoutes(deps: AuthRoutesDeps): Hono<{ Variables: ServerVariables }> {\n const app = new Hono<{ Variables: ServerVariables }>();\n\n // W-107: the ticket ADDS no rights - it carries the principal's own\n // scopes and every subscribe is per-subject gated on the dispatcher -\n // so possession of a valid principal is the whole requirement. The\n // old `agents:invoke` gate locked read-only tokens out of the\n // browser WS path entirely.\n app.post('/session/ws-ticket', createAuthenticatedMiddleware(), (c) => {\n const auth = c.get('state').auth;\n // IP-13: in the no-auth loopback mode (`auth.kind='none'`) there is no\n // bearer token, but the anonymous principal is fully authorized - mint a\n // ticket bound to a synthetic `anonymous` id so a browser client written\n // for token mode still completes the round-trip.\n const principal =\n auth.kind === 'token'\n ? { tokenId: auth.token.tokenId, scopes: auth.grantedScopes }\n : auth.kind === 'anonymous'\n ? { tokenId: 'anonymous', scopes: auth.grantedScopes }\n : undefined;\n if (principal === undefined) {\n return c.json(\n {\n error: 'auth-required',\n message: 'Bearer token required to mint a WS ticket.',\n },\n 401,\n );\n }\n const ticket = deps.tickets.issue(principal);\n return c.json(\n {\n ticket: ticket.value,\n expiresAt: ticket.expiresAt,\n ttlMs: deps.tickets.ttlMs,\n },\n 201,\n );\n });\n\n return app;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,iBAAiB,MAA4D;CAC3F,MAAM,MAAM,IAAI,MAAsC;AAOtD,KAAI,KAAK,sBAAsB,+BAA+B,GAAG,MAAM;EACrE,MAAM,OAAO,EAAE,IAAI,QAAQ,CAAC;EAK5B,MAAM,YACJ,KAAK,SAAS,UACV;GAAE,SAAS,KAAK,MAAM;GAAS,QAAQ,KAAK;GAAe,GAC3D,KAAK,SAAS,cACZ;GAAE,SAAS;GAAa,QAAQ,KAAK;GAAe,GACpD;AACR,MAAI,cAAc,OAChB,QAAO,EAAE,KACP;GACE,OAAO;GACP,SAAS;GACV,EACD,IACD;EAEH,MAAM,SAAS,KAAK,QAAQ,MAAM,UAAU;AAC5C,SAAO,EAAE,KACP;GACE,QAAQ,OAAO;GACf,WAAW,OAAO;GAClB,OAAO,KAAK,QAAQ;GACrB,EACD,IACD;GACD;AAEF,QAAO"}
@@ -52,7 +52,7 @@ function createSessionRoutes(deps) {
52
52
  });
53
53
  return c.json({ session }, 201);
54
54
  });
55
- app.get("/:id", createScopeMiddleware("sessions:read"), async (c) => {
55
+ app.get("/:id", createScopeMiddleware((_path, params) => `sessions:read:${params.id}`), async (c) => {
56
56
  const id = c.req.param("id");
57
57
  const session = await deps.sessions.get(id);
58
58
  if (session === null) return c.json({
@@ -61,7 +61,7 @@ function createSessionRoutes(deps) {
61
61
  }, 404);
62
62
  return c.json({ session });
63
63
  });
64
- app.delete("/:id", createScopeMiddleware("sessions:write"), async (c) => {
64
+ app.delete("/:id", createScopeMiddleware((_path, params) => `sessions:write:${params.id}`), async (c) => {
65
65
  const id = c.req.param("id");
66
66
  if (!await deps.sessions.remove(id)) return c.json({
67
67
  error: "session-not-found",
@@ -69,7 +69,7 @@ function createSessionRoutes(deps) {
69
69
  }, 404);
70
70
  return c.body(null, 204);
71
71
  });
72
- app.get("/:id/messages", createScopeMiddleware("sessions:read"), async (c) => {
72
+ app.get("/:id/messages", createScopeMiddleware((_path, params) => `sessions:read:${params.id}`), async (c) => {
73
73
  const id = c.req.param("id");
74
74
  const parsed = MessagesQuerySchema.safeParse({ limit: c.req.query("limit") });
75
75
  if (!parsed.success) return c.json({
@@ -88,7 +88,7 @@ function createSessionRoutes(deps) {
88
88
  messages
89
89
  });
90
90
  });
91
- app.get("/:id/handoffs", createScopeMiddleware("sessions:read"), async (c) => {
91
+ app.get("/:id/handoffs", createScopeMiddleware((_path, params) => `sessions:read:${params.id}`), async (c) => {
92
92
  const id = c.req.param("id");
93
93
  const handoffs = await deps.sessions.listHandoffs(id);
94
94
  return c.json({
@@ -96,7 +96,7 @@ function createSessionRoutes(deps) {
96
96
  handoffs
97
97
  });
98
98
  });
99
- app.post("/:id/export", createScopeMiddleware("sessions:export"), async (c) => {
99
+ app.post("/:id/export", createScopeMiddleware((_path, params) => `sessions:export:${params.id}`), async (c) => {
100
100
  const id = c.req.param("id");
101
101
  if (deps.sessions.exportSession === void 0) return c.json({
102
102
  error: "session-export-unsupported",