@kindgi/api 0.1.4-rc.0 → 0.1.4-rc.2

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 (262) hide show
  1. package/dist/agent-binding.d.ts +20 -2
  2. package/dist/agent-binding.d.ts.map +1 -1
  3. package/dist/agent-pins.d.ts.map +1 -1
  4. package/dist/agent-pins.js +4 -1
  5. package/dist/agent-pins.js.map +1 -1
  6. package/dist/app.d.ts +8 -0
  7. package/dist/app.d.ts.map +1 -1
  8. package/dist/app.js +30 -5
  9. package/dist/app.js.map +1 -1
  10. package/dist/deploy-versions.d.ts +10 -5
  11. package/dist/deploy-versions.d.ts.map +1 -1
  12. package/dist/deploy-versions.js +4 -10
  13. package/dist/deploy-versions.js.map +1 -1
  14. package/dist/derive-agent-version.d.ts +9 -1
  15. package/dist/derive-agent-version.d.ts.map +1 -1
  16. package/dist/derive-agent-version.js +11 -1
  17. package/dist/derive-agent-version.js.map +1 -1
  18. package/dist/errors.d.ts.map +1 -1
  19. package/dist/errors.js +26 -0
  20. package/dist/errors.js.map +1 -1
  21. package/dist/eval-case-binding.d.ts +10 -0
  22. package/dist/eval-case-binding.d.ts.map +1 -1
  23. package/dist/eval-run-binding.d.ts +21 -3
  24. package/dist/eval-run-binding.d.ts.map +1 -1
  25. package/dist/eval-run-binding.js.map +1 -1
  26. package/dist/eval-run-dispatcher.d.ts +3 -0
  27. package/dist/eval-run-dispatcher.d.ts.map +1 -1
  28. package/dist/eval-run-dispatcher.js.map +1 -1
  29. package/dist/flow-binding.d.ts +8 -0
  30. package/dist/flow-binding.d.ts.map +1 -1
  31. package/dist/flow-pins.d.ts +20 -7
  32. package/dist/flow-pins.d.ts.map +1 -1
  33. package/dist/flow-pins.js +36 -11
  34. package/dist/flow-pins.js.map +1 -1
  35. package/dist/gate-policy-binding.d.ts +164 -0
  36. package/dist/gate-policy-binding.d.ts.map +1 -0
  37. package/dist/gate-policy-binding.js +12 -0
  38. package/dist/gate-policy-binding.js.map +1 -0
  39. package/dist/gate.d.ts +56 -0
  40. package/dist/gate.d.ts.map +1 -0
  41. package/dist/gate.js +359 -0
  42. package/dist/gate.js.map +1 -0
  43. package/dist/guardrail-binding.d.ts +8 -0
  44. package/dist/guardrail-binding.d.ts.map +1 -1
  45. package/dist/handler-binding.d.ts +17 -2
  46. package/dist/handler-binding.d.ts.map +1 -1
  47. package/dist/index.d.ts +11 -4
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +3 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/judged-dispatcher.d.ts +20 -1
  52. package/dist/judged-dispatcher.d.ts.map +1 -1
  53. package/dist/judged-dispatcher.js +57 -7
  54. package/dist/judged-dispatcher.js.map +1 -1
  55. package/dist/judgment-binding.d.ts +38 -0
  56. package/dist/judgment-binding.d.ts.map +1 -1
  57. package/dist/judgment-binding.js +19 -0
  58. package/dist/judgment-binding.js.map +1 -1
  59. package/dist/live-version-binding.d.ts +206 -0
  60. package/dist/live-version-binding.d.ts.map +1 -0
  61. package/dist/live-version-binding.js +4 -0
  62. package/dist/live-version-binding.js.map +1 -0
  63. package/dist/middleware/authorize.d.ts.map +1 -1
  64. package/dist/middleware/authorize.js +13 -2
  65. package/dist/middleware/authorize.js.map +1 -1
  66. package/dist/middleware/project-ref.d.ts +25 -0
  67. package/dist/middleware/project-ref.d.ts.map +1 -0
  68. package/dist/middleware/project-ref.js +72 -0
  69. package/dist/middleware/project-ref.js.map +1 -0
  70. package/dist/openapi/generate.d.ts.map +1 -1
  71. package/dist/openapi/generate.js +4 -0
  72. package/dist/openapi/generate.js.map +1 -1
  73. package/dist/openapi/operations.d.ts +6 -0
  74. package/dist/openapi/operations.d.ts.map +1 -1
  75. package/dist/openapi/operations.js +524 -27
  76. package/dist/openapi/operations.js.map +1 -1
  77. package/dist/openapi/schemas.d.ts +43 -0
  78. package/dist/openapi/schemas.d.ts.map +1 -1
  79. package/dist/openapi/schemas.js +1050 -7
  80. package/dist/openapi/schemas.js.map +1 -1
  81. package/dist/publish-refused.d.ts +18 -0
  82. package/dist/publish-refused.d.ts.map +1 -0
  83. package/dist/publish-refused.js +22 -0
  84. package/dist/publish-refused.js.map +1 -0
  85. package/dist/registry-read-only.d.ts +32 -0
  86. package/dist/registry-read-only.d.ts.map +1 -0
  87. package/dist/registry-read-only.js +22 -0
  88. package/dist/registry-read-only.js.map +1 -0
  89. package/dist/retention-binding.d.ts +29 -0
  90. package/dist/retention-binding.d.ts.map +1 -1
  91. package/dist/routes/agent-releases.d.ts +40 -0
  92. package/dist/routes/agent-releases.d.ts.map +1 -0
  93. package/dist/routes/agent-releases.js +462 -0
  94. package/dist/routes/agent-releases.js.map +1 -0
  95. package/dist/routes/agents.d.ts +7 -1
  96. package/dist/routes/agents.d.ts.map +1 -1
  97. package/dist/routes/agents.js +38 -4
  98. package/dist/routes/agents.js.map +1 -1
  99. package/dist/routes/approvals.d.ts.map +1 -1
  100. package/dist/routes/approvals.js +27 -9
  101. package/dist/routes/approvals.js.map +1 -1
  102. package/dist/routes/audit.d.ts.map +1 -1
  103. package/dist/routes/audit.js +7 -0
  104. package/dist/routes/audit.js.map +1 -1
  105. package/dist/routes/auth.js +1 -1
  106. package/dist/routes/auth.js.map +1 -1
  107. package/dist/routes/blocks.d.ts.map +1 -1
  108. package/dist/routes/blocks.js +37 -12
  109. package/dist/routes/blocks.js.map +1 -1
  110. package/dist/routes/conversations.d.ts +0 -7
  111. package/dist/routes/conversations.d.ts.map +1 -1
  112. package/dist/routes/conversations.js +19 -3
  113. package/dist/routes/conversations.js.map +1 -1
  114. package/dist/routes/deployments.d.ts +6 -0
  115. package/dist/routes/deployments.d.ts.map +1 -1
  116. package/dist/routes/deployments.js +49 -5
  117. package/dist/routes/deployments.js.map +1 -1
  118. package/dist/routes/env.d.ts.map +1 -1
  119. package/dist/routes/env.js +1 -0
  120. package/dist/routes/env.js.map +1 -1
  121. package/dist/routes/eval-comparison.d.ts +4 -2
  122. package/dist/routes/eval-comparison.d.ts.map +1 -1
  123. package/dist/routes/eval-comparison.js +59 -10
  124. package/dist/routes/eval-comparison.js.map +1 -1
  125. package/dist/routes/eval-runs.d.ts +7 -1
  126. package/dist/routes/eval-runs.d.ts.map +1 -1
  127. package/dist/routes/eval-runs.js +34 -3
  128. package/dist/routes/eval-runs.js.map +1 -1
  129. package/dist/routes/eval-versions.d.ts +25 -0
  130. package/dist/routes/eval-versions.d.ts.map +1 -0
  131. package/dist/routes/eval-versions.js +66 -0
  132. package/dist/routes/eval-versions.js.map +1 -0
  133. package/dist/routes/flows.d.ts +14 -7
  134. package/dist/routes/flows.d.ts.map +1 -1
  135. package/dist/routes/flows.js +14 -8
  136. package/dist/routes/flows.js.map +1 -1
  137. package/dist/routes/gate-policies.d.ts +19 -0
  138. package/dist/routes/gate-policies.d.ts.map +1 -0
  139. package/dist/routes/gate-policies.js +191 -0
  140. package/dist/routes/gate-policies.js.map +1 -0
  141. package/dist/routes/gate-policy-spec.d.ts +17 -0
  142. package/dist/routes/gate-policy-spec.d.ts.map +1 -0
  143. package/dist/routes/gate-policy-spec.js +192 -0
  144. package/dist/routes/gate-policy-spec.js.map +1 -0
  145. package/dist/routes/gate-policy-wire.d.ts +3 -0
  146. package/dist/routes/gate-policy-wire.d.ts.map +1 -0
  147. package/dist/routes/gate-policy-wire.js +18 -0
  148. package/dist/routes/gate-policy-wire.js.map +1 -0
  149. package/dist/routes/guardrails.d.ts.map +1 -1
  150. package/dist/routes/guardrails.js +4 -0
  151. package/dist/routes/guardrails.js.map +1 -1
  152. package/dist/routes/hierarchy-errors.d.ts +21 -0
  153. package/dist/routes/hierarchy-errors.d.ts.map +1 -1
  154. package/dist/routes/hierarchy-errors.js +21 -0
  155. package/dist/routes/hierarchy-errors.js.map +1 -1
  156. package/dist/routes/judged-suites.js +7 -0
  157. package/dist/routes/judged-suites.js.map +1 -1
  158. package/dist/routes/judgments.d.ts +4 -1
  159. package/dist/routes/judgments.d.ts.map +1 -1
  160. package/dist/routes/judgments.js +92 -19
  161. package/dist/routes/judgments.js.map +1 -1
  162. package/dist/routes/live-scope-wire.d.ts +32 -0
  163. package/dist/routes/live-scope-wire.d.ts.map +1 -0
  164. package/dist/routes/live-scope-wire.js +71 -0
  165. package/dist/routes/live-scope-wire.js.map +1 -0
  166. package/dist/routes/policies.d.ts +6 -3
  167. package/dist/routes/policies.d.ts.map +1 -1
  168. package/dist/routes/policies.js +55 -3
  169. package/dist/routes/policies.js.map +1 -1
  170. package/dist/routes/projects.d.ts.map +1 -1
  171. package/dist/routes/projects.js +59 -9
  172. package/dist/routes/projects.js.map +1 -1
  173. package/dist/routes/retention.d.ts.map +1 -1
  174. package/dist/routes/retention.js +18 -2
  175. package/dist/routes/retention.js.map +1 -1
  176. package/dist/routes/reviewers.d.ts +8 -1
  177. package/dist/routes/reviewers.d.ts.map +1 -1
  178. package/dist/routes/reviewers.js +20 -3
  179. package/dist/routes/reviewers.js.map +1 -1
  180. package/dist/routes/runs.d.ts.map +1 -1
  181. package/dist/routes/runs.js +23 -13
  182. package/dist/routes/runs.js.map +1 -1
  183. package/dist/routes/secrets.d.ts.map +1 -1
  184. package/dist/routes/secrets.js +2 -0
  185. package/dist/routes/secrets.js.map +1 -1
  186. package/dist/routes/segments.d.ts +17 -0
  187. package/dist/routes/segments.d.ts.map +1 -0
  188. package/dist/routes/segments.js +68 -0
  189. package/dist/routes/segments.js.map +1 -0
  190. package/dist/routes/teams.d.ts.map +1 -1
  191. package/dist/routes/teams.js +53 -8
  192. package/dist/routes/teams.js.map +1 -1
  193. package/dist/routes/tools.d.ts.map +1 -1
  194. package/dist/routes/tools.js +4 -0
  195. package/dist/routes/tools.js.map +1 -1
  196. package/dist/routes/uuid-param.d.ts +11 -0
  197. package/dist/routes/uuid-param.d.ts.map +1 -0
  198. package/dist/routes/uuid-param.js +19 -0
  199. package/dist/routes/uuid-param.js.map +1 -0
  200. package/dist/tool-binding.d.ts +8 -0
  201. package/dist/tool-binding.d.ts.map +1 -1
  202. package/openapi.json +6484 -2402
  203. package/package.json +21 -21
  204. package/src/agent-binding.ts +21 -2
  205. package/src/agent-pins.ts +3 -1
  206. package/src/app.ts +47 -4
  207. package/src/deploy-versions.ts +12 -15
  208. package/src/derive-agent-version.ts +12 -1
  209. package/src/errors.ts +26 -0
  210. package/src/eval-case-binding.ts +7 -0
  211. package/src/eval-run-binding.ts +31 -3
  212. package/src/eval-run-dispatcher.ts +3 -0
  213. package/src/flow-binding.ts +9 -0
  214. package/src/flow-pins.ts +49 -9
  215. package/src/gate-policy-binding.ts +171 -0
  216. package/src/gate.ts +467 -0
  217. package/src/guardrail-binding.ts +9 -0
  218. package/src/handler-binding.ts +17 -2
  219. package/src/index.ts +47 -1
  220. package/src/judged-dispatcher.ts +100 -9
  221. package/src/judgment-binding.ts +62 -0
  222. package/src/live-version-binding.ts +237 -0
  223. package/src/middleware/authorize.ts +18 -2
  224. package/src/middleware/project-ref.ts +88 -0
  225. package/src/openapi/generate.ts +5 -0
  226. package/src/openapi/operations.ts +664 -27
  227. package/src/openapi/schemas.ts +1175 -33
  228. package/src/publish-refused.ts +31 -0
  229. package/src/registry-read-only.ts +43 -0
  230. package/src/retention-binding.ts +30 -0
  231. package/src/routes/agent-releases.ts +581 -0
  232. package/src/routes/agents.ts +53 -3
  233. package/src/routes/approvals.ts +36 -8
  234. package/src/routes/audit.ts +10 -0
  235. package/src/routes/auth.ts +1 -1
  236. package/src/routes/blocks.ts +39 -14
  237. package/src/routes/conversations.ts +31 -3
  238. package/src/routes/deployments.ts +75 -5
  239. package/src/routes/env.ts +1 -0
  240. package/src/routes/eval-comparison.ts +59 -11
  241. package/src/routes/eval-runs.ts +46 -3
  242. package/src/routes/eval-versions.ts +110 -0
  243. package/src/routes/flows.ts +34 -11
  244. package/src/routes/gate-policies.ts +226 -0
  245. package/src/routes/gate-policy-spec.ts +229 -0
  246. package/src/routes/gate-policy-wire.ts +20 -0
  247. package/src/routes/guardrails.ts +7 -0
  248. package/src/routes/hierarchy-errors.ts +23 -0
  249. package/src/routes/judged-suites.ts +5 -0
  250. package/src/routes/judgments.ts +107 -23
  251. package/src/routes/live-scope-wire.ts +90 -0
  252. package/src/routes/policies.ts +61 -3
  253. package/src/routes/projects.ts +72 -9
  254. package/src/routes/retention.ts +21 -2
  255. package/src/routes/reviewers.ts +23 -4
  256. package/src/routes/runs.ts +34 -20
  257. package/src/routes/secrets.ts +2 -0
  258. package/src/routes/segments.ts +75 -0
  259. package/src/routes/teams.ts +66 -8
  260. package/src/routes/tools.ts +7 -0
  261. package/src/routes/uuid-param.ts +28 -0
  262. package/src/tool-binding.ts +9 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kindgi/api",
3
- "version": "0.1.4-rc.0",
3
+ "version": "0.1.4-rc.2",
4
4
  "description": "REST + SSE HTTP surface for Kindgi™. createApp assembles a Hono app serving the /v1/* REST API (bearer or session-token auth; OpenAPI 3.1 document at /v1/openapi.json) and an optional S3-compatible /s3/* surface (SigV4), over caller-plugged bindings for the runtime (runs, agents, flows, HITL, supervisor) and for storage. Route conventions: docs/API-ROUTE-CONVENTIONS.md.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -32,30 +32,30 @@
32
32
  "openapi.json"
33
33
  ],
34
34
  "dependencies": {
35
- "@kindgi/agents": "0.1.4-rc.0",
36
- "@kindgi/audit-events": "0.1.4-rc.0",
37
- "@kindgi/compliance": "0.1.4-rc.0",
38
- "@kindgi/authz": "0.1.4-rc.0",
39
- "@kindgi/blob-binding": "0.1.4-rc.0",
40
- "@kindgi/capabilities": "0.1.4-rc.0",
41
- "@kindgi/crypto": "0.1.4-rc.0",
42
- "@kindgi/flow": "0.1.4-rc.0",
43
- "@kindgi/guardrails": "0.1.4-rc.0",
44
- "@kindgi/memory": "0.1.4-rc.0",
45
- "@kindgi/platform": "0.1.4-rc.0",
46
- "@kindgi/provenance": "0.1.4-rc.0",
47
- "@kindgi/runtime": "0.1.4-rc.0",
48
- "@kindgi/policy-contract": "0.1.4-rc.0",
49
- "@kindgi/schema": "0.1.4-rc.0",
50
- "@kindgi/tools": "0.1.4-rc.0",
51
- "@kindgi/types": "0.1.4-rc.0",
35
+ "@kindgi/agents": "0.1.4-rc.2",
36
+ "@kindgi/audit-events": "0.1.4-rc.2",
37
+ "@kindgi/compliance": "0.1.4-rc.2",
38
+ "@kindgi/authz": "0.1.4-rc.2",
39
+ "@kindgi/blob-binding": "0.1.4-rc.2",
40
+ "@kindgi/capabilities": "0.1.4-rc.2",
41
+ "@kindgi/crypto": "0.1.4-rc.2",
42
+ "@kindgi/flow": "0.1.4-rc.2",
43
+ "@kindgi/guardrails": "0.1.4-rc.2",
44
+ "@kindgi/memory": "0.1.4-rc.2",
45
+ "@kindgi/platform": "0.1.4-rc.2",
46
+ "@kindgi/provenance": "0.1.4-rc.2",
47
+ "@kindgi/runtime": "0.1.4-rc.2",
48
+ "@kindgi/policy-contract": "0.1.4-rc.2",
49
+ "@kindgi/schema": "0.1.4-rc.2",
50
+ "@kindgi/tools": "0.1.4-rc.2",
51
+ "@kindgi/types": "0.1.4-rc.2",
52
52
  "@scalar/hono-api-reference": "^0.12.2",
53
53
  "hono": "^4.6.14"
54
54
  },
55
55
  "devDependencies": {
56
- "@kindgi/audit-events-inmemory": "0.1.4-rc.0",
57
- "@kindgi/specs": "0.1.4-rc.0",
58
- "@kindgi/testing": "0.1.4-rc.0",
56
+ "@kindgi/audit-events-inmemory": "0.1.4-rc.2",
57
+ "@kindgi/specs": "0.1.4-rc.2",
58
+ "@kindgi/testing": "0.1.4-rc.2",
59
59
  "@types/aws4": "^1.11.6",
60
60
  "@types/node": "^22.10.5",
61
61
  "aws4": "^1.13.2",
@@ -4,7 +4,9 @@
4
4
  import type { Agent, AgentId } from '@kindgi/agents';
5
5
  import type { TupleEnqueueHook } from '@kindgi/authz';
6
6
  import type { Scope } from '@kindgi/platform';
7
- import type { Cursor, ProjectId, Semver, TenantId } from '@kindgi/types';
7
+ import type { Cursor, LiveScope, ProjectId, Semver, TenantId } from '@kindgi/types';
8
+
9
+ import type { RegistryReadOnly } from './registry-read-only.js';
8
10
 
9
11
  /**
10
12
  * Caller-plugged surface for the agent catalog. Same shape as
@@ -22,6 +24,13 @@ import type { Cursor, ProjectId, Semver, TenantId } from '@kindgi/types';
22
24
  * cursor round-trips as a string; it never inspects the payload.
23
25
  */
24
26
  export interface AgentRegistryBinding {
27
+ /**
28
+ * Set when this registry takes no writes (under `kindgi dev`, the
29
+ * pack's files are the source of its agents): every write is refused
30
+ * with `409 registry-read-only` and this reason, before the binding is
31
+ * called. See `RegistryReadOnly`.
32
+ */
33
+ readonly readOnly?: RegistryReadOnly;
25
34
  /**
26
35
  * Cursor-paginated list of agents (latest version per id, sorted by
27
36
  * agent id ascending). Optional `nameFilter` is a prefix match on the
@@ -71,7 +80,8 @@ export interface AgentRegistryBinding {
71
80
  * Soft-tombstone a specific `(agentId, version)`. Returns
72
81
  * `{ unregistered: true }` on success; `{ unregistered: false }`
73
82
  * when the version was unknown OR already tombstoned — the route
74
- * flips the latter to `404`. Head row is preserved; when this
83
+ * flips the latter to `404` — or, with `live`, when it's a scope's live
84
+ * version (`409 agent-version-live`). Head row is preserved; when this
75
85
  * unregister leaves NO active versions, the agent has no latest
76
86
  * version — it enters the derived "retired" state
77
87
  * (GET returns 410 gone) but the identity persists. Publish of a
@@ -229,6 +239,15 @@ export type AgentPublishOutcome =
229
239
 
230
240
  export type AgentUnregisterOutcome = {
231
241
  readonly unregistered: boolean;
242
+ /**
243
+ * Set when the version wasn't unregistered because it's live: the
244
+ * scopes whose live pin it is (evals step 4). Unregister stops a version
245
+ * being chosen, and a live pin is a standing choice, so the operator
246
+ * moves those pins first (rollback, unpin, or promote another version).
247
+ * The route answers `409 agent-version-live`. Absent: a registry that
248
+ * doesn't know live versions, as before.
249
+ */
250
+ readonly live?: readonly LiveScope[];
232
251
  };
233
252
 
234
253
  export type AgentReinstateVersionOutcome =
package/src/agent-pins.ts CHANGED
@@ -10,6 +10,7 @@ import type { AgentRegistryBinding } from './agent-binding.js';
10
10
  import type { BlockRegistryBinding } from './block-binding.js';
11
11
  import { resolveBlockPins } from './block-pins.js';
12
12
  import { type DeployedVersionOutcome, deployVersion } from './deploy-versions.js';
13
+ import { PublishRefused } from './publish-refused.js';
13
14
  import type { ToolRegistryBinding } from './tool-binding.js';
14
15
 
15
16
  /** One block reference that matched no published version. */
@@ -120,7 +121,8 @@ export async function publishDeployedAgent(
120
121
  publish: async (version) => {
121
122
  const outcome = await agents.publish({ tenantId, projectId, agent: version, enqueueTuples });
122
123
  if (outcome.kind === 'ok') return 'ok';
123
- return outcome.kind === 'project-not-found' ? 'skipped' : 'taken';
124
+ if (outcome.kind === 'already-registered') return 'taken';
125
+ throw new PublishRefused('agent', `${agent.id}@${version.version}`, outcome.kind);
124
126
  },
125
127
  });
126
128
  }
package/src/app.ts CHANGED
@@ -48,6 +48,7 @@ import type {
48
48
  } from './identity-provider-binding.js';
49
49
  import type { ImageRegistryBinding } from './image-registry-binding.js';
50
50
  import type { JudgmentRegistryBinding } from './judgment-binding.js';
51
+ import type { AgentReleaseBindings } from './live-version-binding.js';
51
52
  import type { MCPClientProbeBinding, MCPEndpointRegistryBinding } from './mcp-endpoint-binding.js';
52
53
  import type { MemoryBinding } from './memory-binding.js';
53
54
  import { type TokenResolver, bearerAuthMiddleware } from './middleware/auth.js';
@@ -59,6 +60,7 @@ import {
59
60
  idempotencyMiddleware,
60
61
  } from './middleware/idempotency.js';
61
62
  import { principalMiddleware } from './middleware/principal.js';
63
+ import { PROJECT_REF_ROUTES, refuseBadProjectId } from './middleware/project-ref.js';
62
64
  import { publicRunCorsMiddleware, publicRunRouteMatcher } from './middleware/public-run-routes.js';
63
65
  import { requestIdMiddleware } from './middleware/request-id.js';
64
66
  import { sigv4Middleware } from './middleware/sigv4.js';
@@ -93,6 +95,7 @@ import { evalRunsRouters } from './routes/eval-runs.js';
93
95
  import { evalSuitesRouter } from './routes/eval-suites.js';
94
96
  import { eventTriggersRouter } from './routes/event-triggers.js';
95
97
  import { flowsRouter } from './routes/flows.js';
98
+ import { gatePoliciesRouter } from './routes/gate-policies.js';
96
99
  import { guardrailsRouter } from './routes/guardrails.js';
97
100
  import { identityRouter } from './routes/identity.js';
98
101
  import { judgedSuitesRouter } from './routes/judged-suites.js';
@@ -609,6 +612,13 @@ export interface CreateAppInput {
609
612
  * and `GET /v1/eval-suites/:suiteId/versions/:version/cases`.
610
613
  */
611
614
  readonly evalCaseStore?: EvalCaseStoreBinding;
615
+ /**
616
+ * Optional. Live versions of agents per scope, and their promotions
617
+ * (`/v1/agents/{id}/live`, `/live-versions`, `/promotions`,
618
+ * `/live/rollback`, `/live/unpin`). Absent → a run takes the latest
619
+ * version, as before.
620
+ */
621
+ readonly agentReleases?: AgentReleaseBindings;
612
622
  /**
613
623
  * Optional. Push-based pub/sub binding used by SSE endpoints to
614
624
  * deliver run events without polling. When present, `GET
@@ -963,6 +973,11 @@ export function createApp(input: CreateAppInput): Hono<AppEnv> {
963
973
 
964
974
  const tenantHierarchyBinding: TenantHierarchyBinding = input.tenantHierarchyBinding;
965
975
  v1.use('*', idempotencyMiddleware(input.idempotencyStore ?? createInMemoryIdempotencyStore()));
976
+ // A `projectId` in a write's body that the route can't use is refused
977
+ // before any binding sees it: a 400 if it isn't a UUID, a 404 if it
978
+ // names no project (T247).
979
+ const projectRef = refuseBadProjectId(input.projectBinding);
980
+ for (const path of PROJECT_REF_ROUTES) v1.use(path, projectRef);
966
981
  v1.route(
967
982
  '/runs',
968
983
  runsRouter(
@@ -1001,7 +1016,7 @@ export function createApp(input: CreateAppInput): Hono<AppEnv> {
1001
1016
  // so `/v1/approvals/reviewers/*` resolves here rather than being
1002
1017
  // captured by the `:approvalId` param on the approvals router.
1003
1018
  if (input.reviewerRegistry !== undefined) {
1004
- v1.route('/approvals/reviewers', reviewersRouter(input.reviewerRegistry));
1019
+ v1.route('/approvals/reviewers', reviewersRouter(input.reviewerRegistry, authorizer));
1005
1020
  }
1006
1021
  if (input.reviewerBinding !== undefined && input.hitlBinding !== undefined) {
1007
1022
  v1.route(
@@ -1029,9 +1044,22 @@ export function createApp(input: CreateAppInput): Hono<AppEnv> {
1029
1044
  if (input.agentRegistry !== undefined) {
1030
1045
  v1.route(
1031
1046
  '/agents',
1032
- agentsRouter(input.agentRegistry, authorizer, input.toolRegistry, input.blockRegistry),
1047
+ agentsRouter(
1048
+ input.agentRegistry,
1049
+ authorizer,
1050
+ input.toolRegistry,
1051
+ input.blockRegistry,
1052
+ input.agentReleases,
1053
+ {
1054
+ ...(input.evalRunBinding !== undefined && { evalRuns: input.evalRunBinding }),
1055
+ ...(input.projectBinding !== undefined && { projects: input.projectBinding }),
1056
+ },
1057
+ ),
1033
1058
  );
1034
1059
  }
1060
+ if (input.agentReleases?.gatePolicies !== undefined) {
1061
+ v1.route('/gate-policies', gatePoliciesRouter(input.agentReleases.gatePolicies, authorizer));
1062
+ }
1035
1063
  if (input.flowRegistry !== undefined) {
1036
1064
  v1.route(
1037
1065
  '/flows',
@@ -1039,7 +1067,11 @@ export function createApp(input: CreateAppInput): Hono<AppEnv> {
1039
1067
  input.flowRegistry,
1040
1068
  authorizer,
1041
1069
  input.toolRegistry !== undefined && input.agentRegistry !== undefined
1042
- ? { tools: input.toolRegistry, agents: input.agentRegistry }
1070
+ ? {
1071
+ tools: input.toolRegistry,
1072
+ agents: input.agentRegistry,
1073
+ ...(input.agentReleases !== undefined && { live: input.agentReleases.live }),
1074
+ }
1043
1075
  : undefined,
1044
1076
  ),
1045
1077
  );
@@ -1149,6 +1181,7 @@ export function createApp(input: CreateAppInput): Hono<AppEnv> {
1149
1181
  authorizer,
1150
1182
  input.conversationBinding,
1151
1183
  input.flowRegistry,
1184
+ input.reviewerBinding,
1152
1185
  ),
1153
1186
  );
1154
1187
  v1.route('/judge-classes', judgeClassesRouter(input.judgmentRegistry, authorizer));
@@ -1265,6 +1298,7 @@ export function createApp(input: CreateAppInput): Hono<AppEnv> {
1265
1298
  }),
1266
1299
  ...(input.agentRegistry !== undefined && { agentRegistry: input.agentRegistry }),
1267
1300
  ...(input.flowRegistry !== undefined && { flowRegistry: input.flowRegistry }),
1301
+ ...(input.agentReleases !== undefined && { liveVersions: input.agentReleases.live }),
1268
1302
  // The deployments router's agents publish loop resolves the
1269
1303
  // Default project through this binding. Optional at wiring so
1270
1304
  // app compositions without a project binding still boot; the loop
@@ -1289,7 +1323,16 @@ export function createApp(input: CreateAppInput): Hono<AppEnv> {
1289
1323
  // `evalSuiteRegistry` binding: the dispatcher itself surfaces
1290
1324
  // `suite-not-found` when the caller-plugged binding can't resolve
1291
1325
  // the suite id, so the API layer stays consumer-neutral.
1292
- const evalRuns = evalRunsRouters(input.evalRunBinding);
1326
+ const evalRuns = evalRunsRouters(
1327
+ input.evalRunBinding,
1328
+ input.flowRegistry !== undefined
1329
+ ? {
1330
+ flows: input.flowRegistry,
1331
+ ...(input.agentRegistry !== undefined && { agents: input.agentRegistry }),
1332
+ ...(input.toolRegistry !== undefined && { tools: input.toolRegistry }),
1333
+ }
1334
+ : undefined,
1335
+ );
1293
1336
  v1.route('/eval-suites', evalRuns.start);
1294
1337
  v1.route('/eval-runs', evalRuns.readback);
1295
1338
  }
@@ -21,8 +21,7 @@ export interface PinnedDefinition {
21
21
  *
22
22
  * - `registered`: the definition's version was free, and is now
23
23
  * registered with these pins;
24
- * - `unchanged`: it's registered with the same definition and pins (or
25
- * its project is gone, which a deploy has always left as it is);
24
+ * - `unchanged`: it's registered with the same definition and pins;
26
25
  * - `reused`: an earlier deploy already registered this definition and
27
26
  * these pins under another number (`version`), so a redeploy changes
28
27
  * nothing;
@@ -39,8 +38,8 @@ export type DeployedVersionOutcome =
39
38
  readonly pinChanges?: readonly PinChange[];
40
39
  };
41
40
 
42
- /** What registering one version did: stored it, found the number taken, or left it (no project). */
43
- export type PublishOutcome = 'ok' | 'taken' | 'skipped';
41
+ /** What registering one version did: stored it, or found the number taken. */
42
+ export type PublishOutcome = 'ok' | 'taken';
44
43
 
45
44
  export interface DeployVersionInput<T extends PinnedDefinition> {
46
45
  /** Names the definition in an error, e.g. `agent "acme.matcher"`. */
@@ -51,7 +50,11 @@ export interface DeployVersionInput<T extends PinnedDefinition> {
51
50
  readonly pinsDigest: string;
52
51
  /** Every active version registered under the definition's id. */
53
52
  readonly existing: readonly T[];
54
- /** Register one version. */
53
+ /**
54
+ * Register one version: `taken` when the number is (an unregistered
55
+ * version holds it). A registry's refusal (e.g. `project-not-found`)
56
+ * throws `PublishRefused`, and the deploy rolls back.
57
+ */
55
58
  readonly publish: (version: T) => Promise<PublishOutcome>;
56
59
  }
57
60
 
@@ -78,9 +81,7 @@ export async function deployVersion<T extends PinnedDefinition>(
78
81
  if (atAuthored !== undefined && same(atAuthored)) return { kind: 'unchanged', version: authored };
79
82
  if (atAuthored === undefined) {
80
83
  const outcome = await input.publish({ ...definition, pins, pinsDigest });
81
- if (outcome !== 'taken') {
82
- return { kind: outcome === 'ok' ? 'registered' : 'unchanged', version: authored };
83
- }
84
+ if (outcome === 'ok') return { kind: 'registered', version: authored };
84
85
  // Taken with no active version: an unregistered version holds the
85
86
  // number. Register the next one.
86
87
  }
@@ -103,9 +104,7 @@ export async function deployVersion<T extends PinnedDefinition>(
103
104
  }
104
105
  const taken = new Set(existing.map((v) => v.version));
105
106
  const version = await registerNextFree(input, reason, taken);
106
- return version === undefined
107
- ? { kind: 'unchanged', version: authored }
108
- : { kind: 'renumbered', version, reason, ...changes };
107
+ return { kind: 'renumbered', version, reason, ...changes };
109
108
  }
110
109
 
111
110
  /** Why the definition's version can't be registered as it is. */
@@ -120,13 +119,12 @@ function derivationReason(
120
119
  /**
121
120
  * Register the definition with its pins under the first free version
122
121
  * after its own (`nextVersion`), recording where it came from.
123
- * `undefined` when it was left as it is (its project is gone).
124
122
  */
125
123
  async function registerNextFree<T extends PinnedDefinition>(
126
124
  input: DeployVersionInput<T>,
127
125
  reason: VersionDerivationReason,
128
126
  taken: ReadonlySet<string>,
129
- ): Promise<string | undefined> {
127
+ ): Promise<string> {
130
128
  const { definition, pins, pinsDigest } = input;
131
129
  const authored = definition.version;
132
130
  let candidate = nextVersion(authored);
@@ -140,7 +138,6 @@ async function registerNextFree<T extends PinnedDefinition>(
140
138
  derivedFrom: { version: authored, reason },
141
139
  });
142
140
  if (outcome === 'ok') return candidate;
143
- if (outcome === 'skipped') return undefined;
144
141
  // Taken: an unregistered version holds this one too.
145
142
  }
146
143
  candidate = nextVersion(candidate);
@@ -151,7 +148,7 @@ async function registerNextFree<T extends PinnedDefinition>(
151
148
  }
152
149
 
153
150
  /** A definition: everything but its version and what the runtime sets. */
154
- function definitionKey(definition: PinnedDefinition): string {
151
+ export function definitionKey(definition: PinnedDefinition): string {
155
152
  const { version: _v, pins: _p, pinsDigest: _d, derivedFrom: _f, ...rest } = definition;
156
153
  return canonicalize(rest);
157
154
  }
@@ -16,6 +16,7 @@ import type { ProjectId, Semver, TenantId } from '@kindgi/types';
16
16
  import type { AgentRegistryBinding, AgentVersionRecord } from './agent-binding.js';
17
17
  import { activeAgentVersions } from './agent-pins.js';
18
18
  import type { BlockRegistryBinding } from './block-binding.js';
19
+ import { definitionKey } from './deploy-versions.js';
19
20
 
20
21
  /** Which data-block pins to swap, by kind then block id → exact version. */
21
22
  export interface PinSwaps {
@@ -48,6 +49,8 @@ export interface SwapIssue {
48
49
 
49
50
  export type DeriveAgentVersionOutcome =
50
51
  | { readonly kind: 'ok'; readonly agent: Agent }
52
+ /** An active version already has this definition and these pins: that one, unchanged. */
53
+ | { readonly kind: 'reused'; readonly agent: Agent }
51
54
  | { readonly kind: 'not-found' }
52
55
  | { readonly kind: 'unpinned' }
53
56
  | { readonly kind: 'invalid'; readonly issues: readonly SwapIssue[] }
@@ -63,7 +66,10 @@ const MAX_TRIES = 100;
63
66
  * reaching an agent without a code change. The new version is the old
64
67
  * one's bag with those pins swapped (`derivedFrom: { version, reason:
65
68
  * 'edited', label, by }`), numbered the next free patch after the
66
- * agent's highest version (versions never change).
69
+ * agent's highest version (versions never change). If an active version
70
+ * already holds that definition and those pins (the same swap derived
71
+ * before, or a deploy that registered it), that version is returned
72
+ * unchanged (`reused`) rather than a duplicate.
67
73
  *
68
74
  * Only data-block pins swap: a swap must name a block the version
69
75
  * already references (adding one is a code change), at a published,
@@ -114,6 +120,11 @@ async function publishNextFree(
114
120
  ): Promise<DeriveAgentVersionOutcome> {
115
121
  const { agents, tenantId, agentId } = input;
116
122
  const active = await activeAgentVersions(agents, tenantId, agentId);
123
+ // The same swap again (or a deploy that registered it) finds the
124
+ // version that already holds it: versions are never duplicated.
125
+ const key = definitionKey(derived);
126
+ const same = active.find((a) => a.pinsDigest === derived.pinsDigest && definitionKey(a) === key);
127
+ if (same !== undefined) return { kind: 'reused', agent: same };
117
128
  const highest =
118
129
  latestVersion(active.map((a) => a.version as unknown as string)) ??
119
130
  (derived.version as unknown as string);
package/src/errors.ts CHANGED
@@ -67,6 +67,7 @@ export const ERROR_CODE_TO_STATUS: Readonly<Record<string, number>> = {
67
67
  'duplicate-edge-id': 409,
68
68
  'agent-version-mismatch': 409,
69
69
  'agent-already-registered': 409,
70
+ 'registry-read-only': 409,
70
71
  'agent-gone': 410,
71
72
  'flow-gone': 410,
72
73
  'policy-gone': 410,
@@ -160,6 +161,8 @@ export const ERROR_CODE_TO_STATUS: Readonly<Record<string, number>> = {
160
161
  // Admin plane — policies.
161
162
  'policy-not-found': 404,
162
163
  'policy-already-registered': 409,
164
+ 'policy-scope-taken': 409,
165
+ 'policy-scope-changed': 409,
163
166
  // Admin plane — eval suites.
164
167
  'block-not-found': 404,
165
168
  'block-already-registered': 409,
@@ -173,10 +176,33 @@ export const ERROR_CODE_TO_STATUS: Readonly<Record<string, number>> = {
173
176
  'judge-class-not-found': 404,
174
177
  'judge-class-name-taken': 409,
175
178
  'judge-class-not-applicable': 400,
179
+ // The judge class is restricted (`assertableBy`), and the caller isn't one who may assert it.
180
+ 'judge-class-not-allowed': 403,
181
+ // Live versions of agents and their promotions.
182
+ 'agent-version-not-found': 404,
183
+ 'promotion-not-found': 404,
184
+ 'nothing-to-roll-back': 409,
185
+ 'not-pinned': 409,
186
+ // The gate (evals step 4b).
187
+ 'gate-failed': 422,
188
+ 'promotion-superseded': 409,
189
+ // The binding can't record a gated promotion, so one a policy applies to is refused.
190
+ 'promotion-gate-unsupported': 501,
191
+ 'separate-approver-required': 403,
192
+ 'gate-policy-not-found': 404,
193
+ 'gate-policy-already-registered': 409,
194
+ 'gate-policy-scope-taken': 409,
195
+ 'gate-policy-scope-changed': 409,
196
+ 'gate-policy-scope-unpinned': 409,
197
+ 'gate-policy-needs-pin': 409,
198
+ /** Unregister: the version is live in a scope; move that pin first. */
199
+ 'agent-version-live': 409,
176
200
  'run-not-finished': 409,
177
201
  'item-not-found': 400,
178
202
  // The judgment binding can't list judged runs, so no test sets from judgments.
179
203
  'test-sets-not-supported': 501,
204
+ // Authorization is enforced, but a membership change can't be kept in step with it.
205
+ 'authz-membership-unsupported': 501,
180
206
  'eval-run-already-terminal': 409,
181
207
  'dispatcher-not-registered': 422,
182
208
  'dispatcher-input-invalid': 400,
@@ -31,6 +31,13 @@ export interface JudgedItemSummary extends JudgedItem {
31
31
  readonly yesWeight: number;
32
32
  /** The weight behind all judgments of the item. */
33
33
  readonly totalWeight: number;
34
+ /**
35
+ * The same two, counting only judgments recorded while their class was
36
+ * restricted (`Judgment.restricted`), for a comparison weighted
37
+ * `restricted-only`. Absent from a test set built before restrictions:
38
+ * no restricted evidence.
39
+ */
40
+ readonly restricted?: { readonly yesWeight: number; readonly totalWeight: number };
34
41
  /** The reasons given, newest first. */
35
42
  readonly reasons: readonly { readonly verdict: 'yes' | 'no'; readonly reason: string }[];
36
43
  }
@@ -2,8 +2,18 @@
2
2
  // Copyright (C) 2026 Kindgi Inc.
3
3
 
4
4
  import type { AgentId } from '@kindgi/agents';
5
+ import type { FlowVersionOverrides } from '@kindgi/flow';
5
6
  import type { Scope } from '@kindgi/platform';
6
- import type { Cursor, FlowId, ProjectId, RunId, Semver, TenantId, Timestamp } from '@kindgi/types';
7
+ import type {
8
+ Cursor,
9
+ FlowId,
10
+ ProjectId,
11
+ RunId,
12
+ ScopeSegment,
13
+ Semver,
14
+ TenantId,
15
+ Timestamp,
16
+ } from '@kindgi/types';
7
17
 
8
18
  import type { EvalKind } from './eval-suite-binding.js';
9
19
 
@@ -122,7 +132,8 @@ export interface EvalRunFilter {
122
132
  * What a comparison eval run compares the candidate against:
123
133
  * - `'recorded'`: the output each case recorded (what was judged);
124
134
  * - `{ agentId, version }`: that version, replayed under the same rules;
125
- * - `{ live }`: the version live in a scope (a project, segments).
135
+ * - `{ live }`: the version live in a scope: a project, and a segment
136
+ * path in it (coarse to fine, as live versions resolve it).
126
137
  */
127
138
  export type EvalBaseline =
128
139
  | 'recorded'
@@ -130,13 +141,22 @@ export type EvalBaseline =
130
141
  | {
131
142
  readonly live: {
132
143
  readonly projectId?: ProjectId;
133
- readonly segments?: Readonly<Record<string, string>>;
144
+ /** A segment path in `projectId`, coarse to fine. */
145
+ readonly segments?: readonly ScopeSegment[];
134
146
  };
135
147
  };
136
148
 
137
149
  /** Whether replayed reads use the past run's results when it has them, or run live. */
138
150
  export type EvalReads = 'recorded' | 'live';
139
151
 
152
+ /**
153
+ * Which judgments a comparison counts: every judgment at its class's
154
+ * weight (`as-recorded`, the default), or only those recorded while their
155
+ * class was restricted (`assertableBy`), the others weighing 0
156
+ * (`restricted-only`; T200).
157
+ */
158
+ export type EvalClassWeights = 'as-recorded' | 'restricted-only';
159
+
140
160
  /** How a comparison eval run runs its cases (absent: the run isn't a comparison). */
141
161
  export interface EvalComparison {
142
162
  readonly baseline: EvalBaseline;
@@ -145,6 +165,14 @@ export interface EvalComparison {
145
165
  readonly repetitions: number;
146
166
  /** How many ranked items `weightedPrecisionAtK` looks at (1–100). */
147
167
  readonly k: number;
168
+ /**
169
+ * For a flow candidate: agents and tools its replays run at other exact
170
+ * versions than the flow version's pins ("this flow, with `acme.scorer`
171
+ * at 0.4.0"), without publishing a new flow version.
172
+ */
173
+ readonly versions?: FlowVersionOverrides;
174
+ /** Which judgments count (absent: `as-recorded`). */
175
+ readonly classWeights?: EvalClassWeights;
148
176
  }
149
177
 
150
178
  export interface EvalRunStartInput {
@@ -4,6 +4,7 @@
4
4
  import { randomUUID } from 'node:crypto';
5
5
 
6
6
  import type { ReplayTurnReport } from '@kindgi/agents';
7
+ import type { FlowVersionOverrides } from '@kindgi/flow';
7
8
  import type { RunReplayRef } from '@kindgi/runtime';
8
9
  import type { ProjectId, RunId, TenantId, Timestamp } from '@kindgi/types';
9
10
 
@@ -63,6 +64,8 @@ export interface EvalRunSubjectInvokeInput {
63
64
  readonly replay?: RunReplayRef;
64
65
  /** The conversation before the past turn, oldest first: a replayed turn starts from it. */
65
66
  readonly history?: readonly unknown[];
67
+ /** A flow target's agents and tools at other exact versions (`EvalComparison.versions`). */
68
+ readonly versions?: FlowVersionOverrides;
66
69
  }
67
70
 
68
71
  export interface EvalRunSubjectInvokeOutcome {
@@ -6,6 +6,8 @@ import type { Flow } from '@kindgi/flow';
6
6
  import type { Scope } from '@kindgi/platform';
7
7
  import type { Cursor, FlowId, ProjectId, TenantId } from '@kindgi/types';
8
8
 
9
+ import type { RegistryReadOnly } from './registry-read-only.js';
10
+
9
11
  /**
10
12
  * Caller-plugged surface for the flow catalog. Mirrors
11
13
  * `AgentRegistryBinding` 1:1 — the API package does NOT own registry
@@ -26,6 +28,13 @@ import type { Cursor, FlowId, ProjectId, TenantId } from '@kindgi/types';
26
28
  * cursor round-trips as a string; it never inspects the payload.
27
29
  */
28
30
  export interface FlowRegistryBinding {
31
+ /**
32
+ * Set when this registry takes no writes (under `kindgi dev`, the
33
+ * pack's files are the source of its flows): every write is refused
34
+ * with `409 registry-read-only` and this reason, before the binding is
35
+ * called. See `RegistryReadOnly`.
36
+ */
37
+ readonly readOnly?: RegistryReadOnly;
29
38
  /**
30
39
  * Cursor-paginated list of flows (latest version per id, sorted by
31
40
  * flow id ascending). Optional `nameFilter` is a prefix match on the
package/src/flow-pins.ts CHANGED
@@ -11,6 +11,8 @@ import type { AgentRegistryBinding } from './agent-binding.js';
11
11
  import { type UnpinnableRef, activeToolVersions } from './agent-pins.js';
12
12
  import { type DeployedVersionOutcome, deployVersion } from './deploy-versions.js';
13
13
  import type { FlowRegistryBinding } from './flow-binding.js';
14
+ import type { LiveVersionBinding } from './live-version-binding.js';
15
+ import { PublishRefused } from './publish-refused.js';
14
16
  import type { ToolRegistryBinding } from './tool-binding.js';
15
17
 
16
18
  export type FlowPinsOutcome =
@@ -18,18 +20,32 @@ export type FlowPinsOutcome =
18
20
  | { readonly kind: 'unpinnable'; readonly issues: readonly UnpinnableRef[] };
19
21
 
20
22
  /**
21
- * Pin a flow version when it's published: each tool it runs, and each
22
- * agent it runs at no named version, to its latest active version now
23
- * (what a run would have bound), so every run of the version uses those.
24
- * A tool or agent with no published version makes the flow unpinnable:
25
- * the caller refuses the publish, naming each, rather than store a
26
- * partly pinned version.
23
+ * Where a flow version is published, so its agent pins are what a run
24
+ * there would get: the agents' live versions for the project.
25
+ */
26
+ export interface FlowPinsLive {
27
+ readonly binding: LiveVersionBinding;
28
+ readonly projectId: ProjectId;
29
+ }
30
+
31
+ /**
32
+ * Pin a flow version when it's published, to what a run would have bound
33
+ * then, so every run of the version uses those: each tool it runs to its
34
+ * latest active version, and each agent it runs at no named version to
35
+ * the version a run that names none gets in the flow's project. That's
36
+ * the agent's live version there (project, then its org, then the
37
+ * tenant), with `live`, else its latest. A gated agent's flow runs what
38
+ * its promotions let go live, not an unpromoted newer version. A tool or
39
+ * agent with no published version makes the flow unpinnable: the caller
40
+ * refuses the publish, naming each, rather than store a partly pinned
41
+ * version.
27
42
  */
28
43
  export async function resolveFlowPins(
29
44
  tools: ToolRegistryBinding,
30
45
  agents: AgentRegistryBinding,
31
46
  tenantId: TenantId,
32
47
  flow: Flow,
48
+ live?: FlowPinsLive,
33
49
  ): Promise<FlowPinsOutcome> {
34
50
  const refs = flowRefs(flow);
35
51
  const pinned: { tools: Record<string, string>; agents: Record<string, string> } = {
@@ -47,8 +63,8 @@ export async function resolveFlowPins(
47
63
  });
48
64
  }
49
65
  for (const id of refs.agents) {
50
- const latest = await agents.get({ tenantId, agentId: id as AgentId });
51
- if (latest !== null) pinned.agents[id] = latest.version as unknown as string;
66
+ const version = await implicitAgentVersion(agents, live, tenantId, id);
67
+ if (version !== undefined) pinned.agents[id] = version;
52
68
  else
53
69
  issues.push({
54
70
  path: '/nodes',
@@ -59,6 +75,29 @@ export async function resolveFlowPins(
59
75
  return { kind: 'ok', pins: pinned };
60
76
  }
61
77
 
78
+ /**
79
+ * The version a run of agent `id` that names none gets now: its live
80
+ * version for `live`'s project when one is pinned on the way up, else its
81
+ * latest; `undefined` when it has no published version.
82
+ */
83
+ async function implicitAgentVersion(
84
+ agents: AgentRegistryBinding,
85
+ live: FlowPinsLive | undefined,
86
+ tenantId: TenantId,
87
+ id: string,
88
+ ): Promise<string | undefined> {
89
+ if (live !== undefined) {
90
+ const resolved = await live.binding.resolve({
91
+ tenantId,
92
+ agentId: id,
93
+ projectId: live.projectId,
94
+ });
95
+ if (resolved !== null) return resolved.version as unknown as string;
96
+ }
97
+ const latest = await agents.get({ tenantId, agentId: id as AgentId });
98
+ return latest === null ? undefined : (latest.version as unknown as string);
99
+ }
100
+
62
101
  export interface PublishDeployedFlowInput {
63
102
  readonly flows: FlowRegistryBinding;
64
103
  readonly tenantId: TenantId;
@@ -83,7 +122,8 @@ export async function publishDeployedFlow(
83
122
  publish: async (version) => {
84
123
  const outcome = await flows.publish({ tenantId, projectId, flow: version, enqueueTuples });
85
124
  if (outcome.kind === 'ok') return 'ok';
86
- return outcome.kind === 'project-not-found' ? 'skipped' : 'taken';
125
+ if (outcome.kind === 'already-registered') return 'taken';
126
+ throw new PublishRefused('flow', `${flow.id}@${version.version}`, outcome.kind);
87
127
  },
88
128
  });
89
129
  }