@kindgi/api 0.1.3 → 0.1.4-rc.1

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 (222) hide show
  1. package/dist/agent-binding.d.ts +26 -4
  2. package/dist/agent-binding.d.ts.map +1 -1
  3. package/dist/agent-pins.d.ts +48 -0
  4. package/dist/agent-pins.d.ts.map +1 -0
  5. package/dist/agent-pins.js +102 -0
  6. package/dist/agent-pins.js.map +1 -0
  7. package/dist/app.d.ts +22 -0
  8. package/dist/app.d.ts.map +1 -1
  9. package/dist/app.js +27 -3
  10. package/dist/app.js.map +1 -1
  11. package/dist/block-binding.d.ts +132 -0
  12. package/dist/block-binding.d.ts.map +1 -0
  13. package/dist/block-binding.js +4 -0
  14. package/dist/block-binding.js.map +1 -0
  15. package/dist/block-pins.d.ts +22 -0
  16. package/dist/block-pins.d.ts.map +1 -0
  17. package/dist/block-pins.js +112 -0
  18. package/dist/block-pins.js.map +1 -0
  19. package/dist/cost-binding.d.ts +20 -1
  20. package/dist/cost-binding.d.ts.map +1 -1
  21. package/dist/cost-binding.js +4 -0
  22. package/dist/cost-binding.js.map +1 -1
  23. package/dist/deploy-versions.d.ts +60 -0
  24. package/dist/deploy-versions.d.ts.map +1 -0
  25. package/dist/deploy-versions.js +91 -0
  26. package/dist/deploy-versions.js.map +1 -0
  27. package/dist/deployment-binding.d.ts +24 -3
  28. package/dist/deployment-binding.d.ts.map +1 -1
  29. package/dist/derive-agent-version.d.ts +77 -0
  30. package/dist/derive-agent-version.d.ts.map +1 -0
  31. package/dist/derive-agent-version.js +149 -0
  32. package/dist/derive-agent-version.js.map +1 -0
  33. package/dist/errors.d.ts.map +1 -1
  34. package/dist/errors.js +18 -0
  35. package/dist/errors.js.map +1 -1
  36. package/dist/eval-case-binding.d.ts +65 -0
  37. package/dist/eval-case-binding.d.ts.map +1 -0
  38. package/dist/eval-case-binding.js +4 -0
  39. package/dist/eval-case-binding.js.map +1 -0
  40. package/dist/eval-run-binding.d.ts +37 -0
  41. package/dist/eval-run-binding.d.ts.map +1 -1
  42. package/dist/eval-run-binding.js.map +1 -1
  43. package/dist/eval-run-dispatcher.d.ts +41 -3
  44. package/dist/eval-run-dispatcher.d.ts.map +1 -1
  45. package/dist/eval-run-dispatcher.js +21 -15
  46. package/dist/eval-run-dispatcher.js.map +1 -1
  47. package/dist/eval-suite-binding.d.ts +1 -1
  48. package/dist/eval-suite-binding.d.ts.map +1 -1
  49. package/dist/eval-suite-binding.js +2 -0
  50. package/dist/eval-suite-binding.js.map +1 -1
  51. package/dist/flow-binding.d.ts +18 -4
  52. package/dist/flow-binding.d.ts.map +1 -1
  53. package/dist/flow-pins.d.ts +36 -0
  54. package/dist/flow-pins.d.ts.map +1 -0
  55. package/dist/flow-pins.js +81 -0
  56. package/dist/flow-pins.js.map +1 -0
  57. package/dist/guardrail-binding.d.ts +8 -0
  58. package/dist/guardrail-binding.d.ts.map +1 -1
  59. package/dist/handler-binding.d.ts +3 -0
  60. package/dist/handler-binding.d.ts.map +1 -1
  61. package/dist/index.d.ts +18 -6
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +8 -2
  64. package/dist/index.js.map +1 -1
  65. package/dist/judged-dispatcher.d.ts +138 -0
  66. package/dist/judged-dispatcher.d.ts.map +1 -0
  67. package/dist/judged-dispatcher.js +308 -0
  68. package/dist/judged-dispatcher.js.map +1 -0
  69. package/dist/judged-items.d.ts +86 -0
  70. package/dist/judged-items.d.ts.map +1 -0
  71. package/dist/judged-items.js +184 -0
  72. package/dist/judged-items.js.map +1 -0
  73. package/dist/judgment-binding.d.ts +316 -0
  74. package/dist/judgment-binding.d.ts.map +1 -0
  75. package/dist/judgment-binding.js +19 -0
  76. package/dist/judgment-binding.js.map +1 -0
  77. package/dist/openapi/generate.d.ts.map +1 -1
  78. package/dist/openapi/generate.js +4 -1
  79. package/dist/openapi/generate.js.map +1 -1
  80. package/dist/openapi/operations.d.ts.map +1 -1
  81. package/dist/openapi/operations.js +482 -13
  82. package/dist/openapi/operations.js.map +1 -1
  83. package/dist/openapi/schemas.d.ts +46 -0
  84. package/dist/openapi/schemas.d.ts.map +1 -1
  85. package/dist/openapi/schemas.js +1350 -175
  86. package/dist/openapi/schemas.js.map +1 -1
  87. package/dist/provider-binding.d.ts +12 -7
  88. package/dist/provider-binding.d.ts.map +1 -1
  89. package/dist/registry-read-only.d.ts +32 -0
  90. package/dist/registry-read-only.d.ts.map +1 -0
  91. package/dist/registry-read-only.js +22 -0
  92. package/dist/registry-read-only.js.map +1 -0
  93. package/dist/routes/agents.d.ts +9 -1
  94. package/dist/routes/agents.d.ts.map +1 -1
  95. package/dist/routes/agents.js +181 -11
  96. package/dist/routes/agents.js.map +1 -1
  97. package/dist/routes/blocks.d.ts +19 -0
  98. package/dist/routes/blocks.d.ts.map +1 -0
  99. package/dist/routes/blocks.js +306 -0
  100. package/dist/routes/blocks.js.map +1 -0
  101. package/dist/routes/cost.d.ts.map +1 -1
  102. package/dist/routes/cost.js +47 -2
  103. package/dist/routes/cost.js.map +1 -1
  104. package/dist/routes/deployments.d.ts +3 -0
  105. package/dist/routes/deployments.d.ts.map +1 -1
  106. package/dist/routes/deployments.js +224 -55
  107. package/dist/routes/deployments.js.map +1 -1
  108. package/dist/routes/eval-comparison.d.ts +15 -0
  109. package/dist/routes/eval-comparison.d.ts.map +1 -0
  110. package/dist/routes/eval-comparison.js +123 -0
  111. package/dist/routes/eval-comparison.js.map +1 -0
  112. package/dist/routes/eval-runs.d.ts +7 -1
  113. package/dist/routes/eval-runs.d.ts.map +1 -1
  114. package/dist/routes/eval-runs.js +42 -3
  115. package/dist/routes/eval-runs.js.map +1 -1
  116. package/dist/routes/eval-versions.d.ts +25 -0
  117. package/dist/routes/eval-versions.d.ts.map +1 -0
  118. package/dist/routes/eval-versions.js +66 -0
  119. package/dist/routes/eval-versions.js.map +1 -0
  120. package/dist/routes/flows.d.ts +13 -1
  121. package/dist/routes/flows.d.ts.map +1 -1
  122. package/dist/routes/flows.js +48 -3
  123. package/dist/routes/flows.js.map +1 -1
  124. package/dist/routes/guardrails.d.ts.map +1 -1
  125. package/dist/routes/guardrails.js +4 -0
  126. package/dist/routes/guardrails.js.map +1 -1
  127. package/dist/routes/hierarchy-errors.d.ts +45 -0
  128. package/dist/routes/hierarchy-errors.d.ts.map +1 -0
  129. package/dist/routes/hierarchy-errors.js +47 -0
  130. package/dist/routes/hierarchy-errors.js.map +1 -0
  131. package/dist/routes/judged-suites.d.ts +20 -0
  132. package/dist/routes/judged-suites.d.ts.map +1 -0
  133. package/dist/routes/judged-suites.js +272 -0
  134. package/dist/routes/judged-suites.js.map +1 -0
  135. package/dist/routes/judgment-context.d.ts +22 -0
  136. package/dist/routes/judgment-context.d.ts.map +1 -0
  137. package/dist/routes/judgment-context.js +88 -0
  138. package/dist/routes/judgment-context.js.map +1 -0
  139. package/dist/routes/judgment-flow-context.d.ts +32 -0
  140. package/dist/routes/judgment-flow-context.d.ts.map +1 -0
  141. package/dist/routes/judgment-flow-context.js +195 -0
  142. package/dist/routes/judgment-flow-context.js.map +1 -0
  143. package/dist/routes/judgments.d.ts +41 -0
  144. package/dist/routes/judgments.d.ts.map +1 -0
  145. package/dist/routes/judgments.js +566 -0
  146. package/dist/routes/judgments.js.map +1 -0
  147. package/dist/routes/orgs.d.ts +5 -2
  148. package/dist/routes/orgs.d.ts.map +1 -1
  149. package/dist/routes/orgs.js +38 -22
  150. package/dist/routes/orgs.js.map +1 -1
  151. package/dist/routes/policies.d.ts.map +1 -1
  152. package/dist/routes/policies.js +12 -1
  153. package/dist/routes/policies.js.map +1 -1
  154. package/dist/routes/projects.d.ts +10 -2
  155. package/dist/routes/projects.d.ts.map +1 -1
  156. package/dist/routes/projects.js +94 -80
  157. package/dist/routes/projects.js.map +1 -1
  158. package/dist/routes/providers.d.ts.map +1 -1
  159. package/dist/routes/providers.js +6 -1
  160. package/dist/routes/providers.js.map +1 -1
  161. package/dist/routes/runs.js +25 -3
  162. package/dist/routes/runs.js.map +1 -1
  163. package/dist/routes/teams.d.ts +6 -2
  164. package/dist/routes/teams.d.ts.map +1 -1
  165. package/dist/routes/teams.js +83 -73
  166. package/dist/routes/teams.js.map +1 -1
  167. package/dist/routes/tools.d.ts.map +1 -1
  168. package/dist/routes/tools.js +4 -0
  169. package/dist/routes/tools.js.map +1 -1
  170. package/dist/tool-binding.d.ts +8 -0
  171. package/dist/tool-binding.d.ts.map +1 -1
  172. package/openapi.json +14253 -10099
  173. package/package.json +21 -21
  174. package/src/agent-binding.ts +28 -4
  175. package/src/agent-pins.ts +147 -0
  176. package/src/app.ts +81 -3
  177. package/src/block-binding.ts +137 -0
  178. package/src/block-pins.ts +148 -0
  179. package/src/cost-binding.ts +21 -1
  180. package/src/deploy-versions.ts +157 -0
  181. package/src/deployment-binding.ts +27 -3
  182. package/src/derive-agent-version.ts +217 -0
  183. package/src/errors.ts +18 -0
  184. package/src/eval-case-binding.ts +71 -0
  185. package/src/eval-run-binding.ts +40 -0
  186. package/src/eval-run-dispatcher.ts +60 -16
  187. package/src/eval-suite-binding.ts +2 -0
  188. package/src/flow-binding.ts +20 -4
  189. package/src/flow-pins.ts +113 -0
  190. package/src/guardrail-binding.ts +9 -0
  191. package/src/handler-binding.ts +3 -0
  192. package/src/index.ts +86 -2
  193. package/src/judged-dispatcher.ts +530 -0
  194. package/src/judged-items.ts +263 -0
  195. package/src/judgment-binding.ts +349 -0
  196. package/src/openapi/generate.ts +7 -1
  197. package/src/openapi/operations.ts +579 -13
  198. package/src/openapi/schemas.ts +1408 -128
  199. package/src/provider-binding.ts +12 -7
  200. package/src/registry-read-only.ts +43 -0
  201. package/src/routes/agents.ts +252 -19
  202. package/src/routes/blocks.ts +387 -0
  203. package/src/routes/cost.ts +55 -1
  204. package/src/routes/deployments.ts +291 -56
  205. package/src/routes/eval-comparison.ts +135 -0
  206. package/src/routes/eval-runs.ts +57 -3
  207. package/src/routes/eval-versions.ts +110 -0
  208. package/src/routes/flows.ts +70 -5
  209. package/src/routes/guardrails.ts +7 -0
  210. package/src/routes/hierarchy-errors.ts +60 -0
  211. package/src/routes/judged-suites.ts +363 -0
  212. package/src/routes/judgment-context.ts +128 -0
  213. package/src/routes/judgment-flow-context.ts +245 -0
  214. package/src/routes/judgments.ts +743 -0
  215. package/src/routes/orgs.ts +44 -27
  216. package/src/routes/policies.ts +19 -0
  217. package/src/routes/projects.ts +118 -96
  218. package/src/routes/providers.ts +5 -0
  219. package/src/routes/runs.ts +29 -3
  220. package/src/routes/teams.ts +104 -90
  221. package/src/routes/tools.ts +7 -0
  222. package/src/tool-binding.ts +9 -0
@@ -18,7 +18,7 @@ import type { CapabilityDescriptor } from './capability-binding.js';
18
18
  * endpoints, credentials) cross the HTTP boundary. Secrets stay inside
19
19
  * the binding implementation; the wire surface returns only routing-
20
20
  * relevant metadata: provider-level `id` / `region` / `attributes` /
21
- * `capabilityKind` + `models[]` (each entry carries `name`,
21
+ * `capabilityKind` / `labels` + `models[]` (each entry carries `name`,
22
22
  * `contextWindow`, `features`, `cost`, optional `p95LatencyMs`,
23
23
  * `maxOutputTokens`, `description`).
24
24
  *
@@ -38,8 +38,8 @@ export interface ProviderRegistryBinding {
38
38
  */
39
39
  list(input: ProviderListInput): Promise<ProviderPage>;
40
40
  /**
41
- * Fetch a provider by id, or `null` when unknown. The route surfaces
42
- * `null` as `404 provider-not-found`.
41
+ * Fetch a provider by id, or `null` when unknown or unregistered. The
42
+ * route surfaces `null` as `404 provider-not-found`.
43
43
  */
44
44
  get(input: ProviderGetInput): Promise<ProviderMetadata | null>;
45
45
  /**
@@ -47,13 +47,18 @@ export interface ProviderRegistryBinding {
47
47
  * the wire shape via the `@kindgi/capabilities` runtime rules
48
48
  * before calling — the binding receives well-formed metadata.
49
49
  * Bindings MAY reject with `already-registered` when the same
50
- * `providerId` is re-registered; the route maps that to `409`.
50
+ * `providerId` is re-registered; the route maps that to `409`. An
51
+ * unregistered id is free: registering it again makes a new provider.
51
52
  */
52
53
  register(input: ProviderRegisterInput): Promise<ProviderRegisterOutcome>;
53
54
  /**
54
- * Remove a specific provider. Returns `{ unregistered: true }` on
55
- * success; `{ unregistered: false }` when the id was unknown — the
56
- * route flips the latter to `404`.
55
+ * Unregister a provider: a tombstone, not an erase. From then on the
56
+ * provider is gone from `list`, `get`, `capabilitiesFor` and
57
+ * `resolveForRuntime`, and the router never picks it; a retention
58
+ * policy on the `provider` domain purges the row. Returns
59
+ * `{ unregistered: true }` on success; `{ unregistered: false }` when
60
+ * the id was unknown or already unregistered — the route flips the
61
+ * latter to `404`.
57
62
  */
58
63
  unregister(input: ProviderUnregisterInput): Promise<ProviderUnregisterOutcome>;
59
64
  /**
@@ -0,0 +1,43 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { Context, MiddlewareHandler } from 'hono';
5
+
6
+ import { statusFor, toWireError } from './errors.js';
7
+ import type { AppEnv } from './types.js';
8
+
9
+ /**
10
+ * A registry that takes no writes: it serves its definitions from
11
+ * somewhere else (under `kindgi dev`, the pack's files). A binding that
12
+ * sets it has every write refused before the binding is called:
13
+ * publish, unregister and reinstate (and, for agents, deriving a
14
+ * version), and a deploy that would publish into it. The refusal is
15
+ * `409 registry-read-only` with `reason` as its message.
16
+ */
17
+ export interface RegistryReadOnly {
18
+ /** Why, and what to do instead, in plain words: the refusal's message. */
19
+ readonly reason: string;
20
+ }
21
+
22
+ /** The refusal of a write to a read-only registry. */
23
+ export function refuseReadOnly(c: Context<AppEnv>, readOnly: RegistryReadOnly) {
24
+ c.status(statusFor('registry-read-only') as never);
25
+ return c.json(
26
+ toWireError({ code: 'registry-read-only', message: readOnly.reason }, c.get('requestId')),
27
+ );
28
+ }
29
+
30
+ /**
31
+ * A router's writes (every method but GET and HEAD) are refused while
32
+ * its registry is read-only. Read at request time, so a binding can
33
+ * become writable without a restart.
34
+ */
35
+ export function refuseWritesWhenReadOnly(
36
+ readOnly: () => RegistryReadOnly | undefined,
37
+ ): MiddlewareHandler<AppEnv> {
38
+ return async (c, next) => {
39
+ const marker = readOnly();
40
+ if (marker === undefined || c.req.method === 'GET' || c.req.method === 'HEAD') return next();
41
+ return refuseReadOnly(c, marker);
42
+ };
43
+ }
@@ -1,15 +1,31 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (C) 2026 Kindgi Inc.
3
3
 
4
- import { Hono } from 'hono';
4
+ import { type Context, Hono } from 'hono';
5
5
 
6
- import { type Agent, type AgentId, type DefineAgentSpec, defineAgent } from '@kindgi/agents';
6
+ import {
7
+ type Agent,
8
+ type AgentId,
9
+ type DefineAgentSpec,
10
+ defineAgent,
11
+ pinsDigest,
12
+ } from '@kindgi/agents';
7
13
  import { type Principal, ref, tuplesForCreate } from '@kindgi/authz';
8
14
  import type { Cursor, ProjectId, Semver, TenantId, UserId } from '@kindgi/types';
9
15
 
10
- import type { AgentRegistryBinding } from '../agent-binding.js';
16
+ import type { AgentRegistryBinding, AgentVersionRecord } from '../agent-binding.js';
17
+ import { resolveAgentPins } from '../agent-pins.js';
18
+ import type { BlockRegistryBinding } from '../block-binding.js';
19
+ import {
20
+ type DeriveAgentVersionOutcome,
21
+ type PinSwaps,
22
+ deriveAgentVersion,
23
+ nextFreeAgentVersion,
24
+ } from '../derive-agent-version.js';
11
25
  import { statusFor, toWireError } from '../errors.js';
12
26
  import type { Authorizer } from '../middleware/authorize.js';
27
+ import { refuseWritesWhenReadOnly } from '../registry-read-only.js';
28
+ import type { ToolRegistryBinding } from '../tool-binding.js';
13
29
  import type { AppEnv } from '../types.js';
14
30
  import { clampLimit } from './pagination.js';
15
31
  import { parseScopeParams } from './scope-params.js';
@@ -24,9 +40,26 @@ import { parseScopeParams } from './scope-params.js';
24
40
  *
25
41
  * Publish is data-as-code: the body is a full `DefineAgentSpec` (the
26
42
  * same value `defineAgent(...)` accepts).
43
+ *
44
+ * With `toolRegistry`, a published version is pinned: each tool range
45
+ * resolves once, at publish, to the exact version every run of that
46
+ * version uses (`pins`, see `resolveAgentPins`), and a range that
47
+ * matches no published version refuses the publish. Without it,
48
+ * versions carry no pins and resolve their ranges per run.
27
49
  */
28
- export function agentsRouter(binding: AgentRegistryBinding, authorizer?: Authorizer): Hono<AppEnv> {
50
+ export function agentsRouter(
51
+ binding: AgentRegistryBinding,
52
+ authorizer?: Authorizer,
53
+ toolRegistry?: ToolRegistryBinding,
54
+ blockRegistry?: BlockRegistryBinding,
55
+ ): Hono<AppEnv> {
29
56
  const r = new Hono<AppEnv>();
57
+ // A read-only registry (under `kindgi dev`, the pack's files) refuses
58
+ // every write before anything else runs.
59
+ r.use(
60
+ '*',
61
+ refuseWritesWhenReadOnly(() => binding.readOnly),
62
+ );
30
63
 
31
64
  // Authorization — check the resource directly (ref('agent', businessId));
32
65
  // permissions cascade from the parent project because the
@@ -35,6 +68,7 @@ export function agentsRouter(binding: AgentRegistryBinding, authorizer?: Authori
35
68
  //
36
69
  // POST / → admin on body.projectId
37
70
  // GET /:agentId, GET /:agentId/versions[/…] → read on the agent
71
+ // POST /:agentId/versions → publish on the agent (derive)
38
72
  // POST /:agentId/versions/:version/unregister → admin on the agent
39
73
  // POST /:agentId/versions/:version/reinstate → admin on the agent
40
74
  // GET / (list) → tenant-scoped fetch;
@@ -73,7 +107,10 @@ export function agentsRouter(binding: AgentRegistryBinding, authorizer?: Authori
73
107
  return mw(c, next);
74
108
  });
75
109
  r.use('/:agentId/*', async (c, next) => {
76
- const action = c.req.method === 'GET' ? 'read' : 'admin';
110
+ // Deriving a version (POST …/versions) is `publish` on the agent;
111
+ // unregister and reinstate are `admin`.
112
+ const action =
113
+ c.req.method === 'GET' ? 'read' : c.req.path.endsWith('/versions') ? 'publish' : 'admin';
77
114
  const agentId = c.req.param('agentId') ?? '';
78
115
  const mw = authorizer.authorize(action, () => ref('agent', agentId));
79
116
  return mw(c, next);
@@ -281,6 +318,28 @@ export function agentsRouter(binding: AgentRegistryBinding, authorizer?: Authori
281
318
  );
282
319
  }
283
320
 
321
+ // Pin the version: every tool range resolves now, once, to the
322
+ // version its runs use. A range nothing satisfies refuses the
323
+ // publish rather than store a partly pinned version.
324
+ let agent: Agent = defined.value;
325
+ if (toolRegistry !== undefined) {
326
+ const resolved = await resolveAgentPins(toolRegistry, tenantId, defined.value, blockRegistry);
327
+ if (resolved.kind === 'unpinnable') {
328
+ c.status(statusFor('invalid-agent') as never);
329
+ return c.json(
330
+ toWireError(
331
+ {
332
+ code: 'validation-failed',
333
+ message: `Agent "${defined.value.id as unknown as string}" uses tool or data-block versions it can't pin (${resolved.issues.length} issue${resolved.issues.length === 1 ? '' : 's'})`,
334
+ issues: resolved.issues as unknown as Record<string, unknown>[],
335
+ },
336
+ requestId,
337
+ ),
338
+ );
339
+ }
340
+ agent = { ...agent, pins: resolved.pins, pinsDigest: pinsDigest(resolved.pins) };
341
+ }
342
+
284
343
  const principal = c.get('principal') as Principal | undefined;
285
344
  const creatorUserId =
286
345
  principal?.actor.kind === 'user'
@@ -289,7 +348,7 @@ export function agentsRouter(binding: AgentRegistryBinding, authorizer?: Authori
289
348
  const outcome = await binding.publish({
290
349
  tenantId,
291
350
  projectId,
292
- agent: defined.value,
351
+ agent,
293
352
  // Write the authorization tuples in the same transaction as the
294
353
  // registry row. The binding calls this with the business
295
354
  // `agentId`.
@@ -300,18 +359,7 @@ export function agentsRouter(binding: AgentRegistryBinding, authorizer?: Authori
300
359
  ),
301
360
  });
302
361
  if (outcome.kind === 'already-registered') {
303
- c.status(statusFor('agent-already-registered') as never);
304
- return c.json(
305
- toWireError(
306
- {
307
- code: 'agent-already-registered',
308
- message: `Agent "${outcome.agentId as unknown as string}" version "${outcome.version as unknown as string}" is already registered`,
309
- agentId: outcome.agentId as unknown as string,
310
- version: outcome.version as unknown as string,
311
- },
312
- requestId,
313
- ),
314
- );
362
+ return alreadyRegistered(c, binding, tenantId, outcome.agentId, outcome.version);
315
363
  }
316
364
  if (outcome.kind === 'project-not-found') {
317
365
  // Caller supplied a `projectId` that does not resolve within
@@ -337,6 +385,47 @@ export function agentsRouter(binding: AgentRegistryBinding, authorizer?: Authori
337
385
  });
338
386
  });
339
387
 
388
+ // ---------- POST /:agentId/versions (derive a version) ----------
389
+ r.post('/:agentId/versions', async (c) => {
390
+ const requestId = c.get('requestId');
391
+ const tenantId = c.get('tenantId') as TenantId;
392
+ const agentId = c.req.param('agentId') as AgentId;
393
+ const body = await jsonObject(c);
394
+ if (body === undefined) {
395
+ c.status(statusFor('bad-input') as never);
396
+ return c.json(
397
+ toWireError(
398
+ { code: 'bad-input', message: 'Request body must be a JSON object' },
399
+ requestId,
400
+ ),
401
+ );
402
+ }
403
+ const parsed = deriveBody(body);
404
+ if (typeof parsed === 'string') {
405
+ c.status(statusFor('bad-input') as never);
406
+ return c.json(toWireError({ code: 'bad-input', message: parsed }, requestId));
407
+ }
408
+ const principal = c.get('principal') as Principal | undefined;
409
+ const userId = principal?.actor.kind === 'user' ? (principal.actor.id as UserId) : undefined;
410
+ const outcome = await deriveAgentVersion({
411
+ agents: binding,
412
+ blocks: blockRegistry,
413
+ tenantId,
414
+ agentId,
415
+ from: parsed.from,
416
+ swaps: parsed.pins,
417
+ ...(parsed.label !== undefined && { label: parsed.label }),
418
+ ...(userId !== undefined && { by: `user:${userId as unknown as string}` }),
419
+ ...(parsed.projectId !== undefined && { projectId: parsed.projectId }),
420
+ tuplesFor: (projectId) => (id) =>
421
+ tuplesForCreate(
422
+ { kind: 'agent', id: id as AgentId, tenantId, projectId },
423
+ userId ?? ('00000000-0000-0000-0000-000000000000' as UserId),
424
+ ),
425
+ });
426
+ return derived(c, agentId, parsed.from, outcome);
427
+ });
428
+
340
429
  // ---------- POST /:agentId/versions/:version/unregister ----------
341
430
  r.post('/:agentId/versions/:version/unregister', async (c) => {
342
431
  const requestId = c.get('requestId');
@@ -403,7 +492,145 @@ export function agentsRouter(binding: AgentRegistryBinding, authorizer?: Authori
403
492
  return r;
404
493
  }
405
494
 
406
- function serializeAgent(a: Agent): Record<string, unknown> {
495
+ /**
496
+ * The 409 for a publish whose number is taken. Versions never change, so
497
+ * it names the next free one (an expert's derived version may hold it).
498
+ */
499
+ async function alreadyRegistered(
500
+ c: Context<AppEnv>,
501
+ binding: AgentRegistryBinding,
502
+ tenantId: TenantId,
503
+ agentId: AgentId,
504
+ version: Semver,
505
+ ) {
506
+ const next = await nextFreeAgentVersion(binding, tenantId, agentId, version as unknown as string);
507
+ const suggestion = next === undefined ? '' : `; publish it as ${next}, the next free version`;
508
+ c.status(statusFor('agent-already-registered') as never);
509
+ return c.json(
510
+ toWireError(
511
+ {
512
+ code: 'agent-already-registered',
513
+ message: `Agent "${agentId as unknown as string}" version "${version as unknown as string}" is already registered, and versions never change${suggestion}`,
514
+ agentId: agentId as unknown as string,
515
+ version: version as unknown as string,
516
+ ...(next !== undefined && { nextFreeVersion: next }),
517
+ },
518
+ c.get('requestId'),
519
+ ),
520
+ );
521
+ }
522
+
523
+ /** The request body as an object; undefined when it isn't JSON or isn't an object. */
524
+ async function jsonObject(c: Context<AppEnv>): Promise<Record<string, unknown> | undefined> {
525
+ try {
526
+ const body = await c.req.json();
527
+ return body !== null && typeof body === 'object' && !Array.isArray(body)
528
+ ? (body as Record<string, unknown>)
529
+ : undefined;
530
+ } catch {
531
+ return undefined;
532
+ }
533
+ }
534
+
535
+ interface DeriveBody {
536
+ readonly from: string;
537
+ readonly pins: PinSwaps;
538
+ readonly label?: string;
539
+ readonly projectId?: ProjectId;
540
+ }
541
+
542
+ /** `{ from, pins: { prompts?, settings? }, label?, projectId? }`, or what's wrong with it. */
543
+ function deriveBody(b: Record<string, unknown>): DeriveBody | string {
544
+ if (typeof b.from !== 'string' || b.from.length === 0) {
545
+ return '`from` (the version to derive from) is required';
546
+ }
547
+ const pins = pinSwapsOf(b.pins);
548
+ if (typeof pins === 'string') return pins;
549
+ for (const key of ['label', 'projectId'] as const) {
550
+ if (b[key] !== undefined && typeof b[key] !== 'string') return `\`${key}\` must be a string`;
551
+ }
552
+ return {
553
+ from: b.from,
554
+ pins,
555
+ ...(typeof b.label === 'string' && { label: b.label }),
556
+ ...(typeof b.projectId === 'string' && { projectId: b.projectId as ProjectId }),
557
+ };
558
+ }
559
+
560
+ /** `pins`: prompt and settings pins only, each block id → exact version; or what's wrong with it. */
561
+ function pinSwapsOf(pins: unknown): PinSwaps | string {
562
+ if (pins === null || typeof pins !== 'object' || Array.isArray(pins)) {
563
+ return '`pins` must be { prompts?, settings? }';
564
+ }
565
+ for (const [kind, map] of Object.entries(pins)) {
566
+ if (kind !== 'prompts' && kind !== 'settings') {
567
+ return `\`pins.${kind}\` can't be swapped: only prompt and settings pins (tool pins come from code)`;
568
+ }
569
+ if (!isStringMap(map)) return `\`pins.${kind}\` must map block ids to exact versions`;
570
+ }
571
+ return pins as PinSwaps;
572
+ }
573
+
574
+ function isStringMap(value: unknown): boolean {
575
+ return (
576
+ value !== null &&
577
+ typeof value === 'object' &&
578
+ !Array.isArray(value) &&
579
+ Object.values(value).every((v) => typeof v === 'string')
580
+ );
581
+ }
582
+
583
+ /** The response to a derive. */
584
+ function derived(
585
+ c: Context<AppEnv>,
586
+ agentId: AgentId,
587
+ from: string,
588
+ outcome: DeriveAgentVersionOutcome,
589
+ ) {
590
+ const requestId = c.get('requestId');
591
+ const fail = (
592
+ status: string,
593
+ error: { code: string; message: string } & Record<string, unknown>,
594
+ ) => {
595
+ c.status(statusFor(status) as never);
596
+ return c.json(toWireError(error, requestId));
597
+ };
598
+ switch (outcome.kind) {
599
+ case 'ok':
600
+ c.status(201);
601
+ return c.json(serializeAgent(outcome.agent));
602
+ case 'reused':
603
+ return c.json(serializeAgent(outcome.agent));
604
+ case 'not-found':
605
+ return fail('agent-not-found', {
606
+ code: 'agent-not-found',
607
+ message: `No agent "${agentId as unknown as string}" at version "${from}"`,
608
+ });
609
+ case 'unpinned':
610
+ return fail('validation-failed', {
611
+ code: 'validation-failed',
612
+ message: `Agent "${agentId as unknown as string}" version ${from} has no pins (it was published before pins); publish it again to pin it, then derive`,
613
+ });
614
+ case 'invalid':
615
+ return fail('validation-failed', {
616
+ code: 'validation-failed',
617
+ message: `Can't derive from version ${from} (${outcome.issues.length} issue${outcome.issues.length === 1 ? '' : 's'})`,
618
+ issues: outcome.issues as unknown as Record<string, unknown>[],
619
+ });
620
+ case 'no-project':
621
+ return fail('bad-input', {
622
+ code: 'bad-input',
623
+ message: "`projectId` is required: this runtime doesn't record the version's project",
624
+ });
625
+ case 'project-not-found':
626
+ return fail('bad-input', {
627
+ code: 'bad-input',
628
+ message: `\`projectId\` "${outcome.projectId as unknown as string}" does not resolve to a project in this tenant`,
629
+ });
630
+ }
631
+ }
632
+
633
+ function serializeAgent(a: AgentVersionRecord): Record<string, unknown> {
407
634
  return {
408
635
  id: a.id as unknown as string,
409
636
  version: a.version as unknown as string,
@@ -411,6 +638,8 @@ function serializeAgent(a: Agent): Record<string, unknown> {
411
638
  ...(a.description !== undefined && { description: a.description }),
412
639
  instructions: a.instructions,
413
640
  ...(a.parameters !== undefined && { parameters: a.parameters }),
641
+ ...(a.settings !== undefined && { settings: a.settings }),
642
+ ...(a.modelSettings !== undefined && { modelSettings: a.modelSettings }),
414
643
  capabilities: a.capabilities,
415
644
  tools: a.tools,
416
645
  retrieval: a.retrieval,
@@ -420,5 +649,9 @@ function serializeAgent(a: Agent): Record<string, unknown> {
420
649
  ...(a.tags !== undefined && { tags: a.tags }),
421
650
  ...(a.preferredProvider !== undefined && { preferredProvider: a.preferredProvider }),
422
651
  ...(a.preferredModel !== undefined && { preferredModel: a.preferredModel }),
652
+ ...(a.pins !== undefined && { pins: a.pins }),
653
+ ...(a.pinsDigest !== undefined && { pinsDigest: a.pinsDigest }),
654
+ ...(a.derivedFrom !== undefined && { derivedFrom: a.derivedFrom }),
655
+ ...(a.unregisteredAt !== undefined && { unregisteredAt: a.unregisteredAt }),
423
656
  };
424
657
  }