@kindgi/api 0.1.2 → 0.1.4-rc.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 (250) hide show
  1. package/dist/agent-binding.d.ts +18 -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 +22 -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 +100 -5
  20. package/dist/cost-binding.d.ts.map +1 -1
  21. package/dist/cost-binding.js +10 -0
  22. package/dist/cost-binding.js.map +1 -1
  23. package/dist/deploy-versions.d.ts +58 -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 +69 -0
  30. package/dist/derive-agent-version.d.ts.map +1 -0
  31. package/dist/derive-agent-version.js +139 -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 +17 -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 +30 -0
  41. package/dist/eval-run-binding.d.ts.map +1 -1
  42. package/dist/eval-run-dispatcher.d.ts +38 -3
  43. package/dist/eval-run-dispatcher.d.ts.map +1 -1
  44. package/dist/eval-run-dispatcher.js +21 -15
  45. package/dist/eval-run-dispatcher.js.map +1 -1
  46. package/dist/eval-suite-binding.d.ts +1 -1
  47. package/dist/eval-suite-binding.d.ts.map +1 -1
  48. package/dist/eval-suite-binding.js +2 -0
  49. package/dist/eval-suite-binding.js.map +1 -1
  50. package/dist/flow-binding.d.ts +10 -4
  51. package/dist/flow-binding.d.ts.map +1 -1
  52. package/dist/flow-pins.d.ts +36 -0
  53. package/dist/flow-pins.d.ts.map +1 -0
  54. package/dist/flow-pins.js +81 -0
  55. package/dist/flow-pins.js.map +1 -0
  56. package/dist/hitl-binding.d.ts +20 -5
  57. package/dist/hitl-binding.d.ts.map +1 -1
  58. package/dist/index.d.ts +19 -8
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +8 -2
  61. package/dist/index.js.map +1 -1
  62. package/dist/judged-dispatcher.d.ts +134 -0
  63. package/dist/judged-dispatcher.d.ts.map +1 -0
  64. package/dist/judged-dispatcher.js +297 -0
  65. package/dist/judged-dispatcher.js.map +1 -0
  66. package/dist/judged-items.d.ts +86 -0
  67. package/dist/judged-items.d.ts.map +1 -0
  68. package/dist/judged-items.js +184 -0
  69. package/dist/judged-items.js.map +1 -0
  70. package/dist/judgment-binding.d.ts +316 -0
  71. package/dist/judgment-binding.d.ts.map +1 -0
  72. package/dist/judgment-binding.js +19 -0
  73. package/dist/judgment-binding.js.map +1 -0
  74. package/dist/middleware/auth.d.ts +3 -2
  75. package/dist/middleware/auth.d.ts.map +1 -1
  76. package/dist/middleware/auth.js.map +1 -1
  77. package/dist/middleware/idempotency.d.ts +5 -1
  78. package/dist/middleware/idempotency.d.ts.map +1 -1
  79. package/dist/middleware/idempotency.js +8 -1
  80. package/dist/middleware/idempotency.js.map +1 -1
  81. package/dist/openapi/generate.d.ts.map +1 -1
  82. package/dist/openapi/generate.js +4 -1
  83. package/dist/openapi/generate.js.map +1 -1
  84. package/dist/openapi/operations.d.ts.map +1 -1
  85. package/dist/openapi/operations.js +543 -24
  86. package/dist/openapi/operations.js.map +1 -1
  87. package/dist/openapi/schemas.d.ts +58 -0
  88. package/dist/openapi/schemas.d.ts.map +1 -1
  89. package/dist/openapi/schemas.js +1058 -27
  90. package/dist/openapi/schemas.js.map +1 -1
  91. package/dist/provenance-binding.d.ts +27 -1
  92. package/dist/provenance-binding.d.ts.map +1 -1
  93. package/dist/provenance-binding.js.map +1 -1
  94. package/dist/provider-binding.d.ts +12 -7
  95. package/dist/provider-binding.d.ts.map +1 -1
  96. package/dist/reviewer-binding.d.ts +10 -2
  97. package/dist/reviewer-binding.d.ts.map +1 -1
  98. package/dist/reviewer-role.d.ts +13 -0
  99. package/dist/reviewer-role.d.ts.map +1 -0
  100. package/dist/reviewer-role.js +27 -0
  101. package/dist/reviewer-role.js.map +1 -0
  102. package/dist/routes/agents.d.ts +9 -1
  103. package/dist/routes/agents.d.ts.map +1 -1
  104. package/dist/routes/agents.js +175 -11
  105. package/dist/routes/agents.js.map +1 -1
  106. package/dist/routes/approvals.d.ts +5 -4
  107. package/dist/routes/approvals.d.ts.map +1 -1
  108. package/dist/routes/approvals.js +52 -16
  109. package/dist/routes/approvals.js.map +1 -1
  110. package/dist/routes/blocks.d.ts +19 -0
  111. package/dist/routes/blocks.d.ts.map +1 -0
  112. package/dist/routes/blocks.js +281 -0
  113. package/dist/routes/blocks.js.map +1 -0
  114. package/dist/routes/conversations.d.ts +7 -1
  115. package/dist/routes/conversations.d.ts.map +1 -1
  116. package/dist/routes/conversations.js +41 -1
  117. package/dist/routes/conversations.js.map +1 -1
  118. package/dist/routes/cost.d.ts.map +1 -1
  119. package/dist/routes/cost.js +142 -11
  120. package/dist/routes/cost.js.map +1 -1
  121. package/dist/routes/deployments.d.ts +3 -0
  122. package/dist/routes/deployments.d.ts.map +1 -1
  123. package/dist/routes/deployments.js +208 -55
  124. package/dist/routes/deployments.js.map +1 -1
  125. package/dist/routes/eval-comparison.d.ts +14 -0
  126. package/dist/routes/eval-comparison.d.ts.map +1 -0
  127. package/dist/routes/eval-comparison.js +87 -0
  128. package/dist/routes/eval-comparison.js.map +1 -0
  129. package/dist/routes/eval-runs.d.ts.map +1 -1
  130. package/dist/routes/eval-runs.js +8 -0
  131. package/dist/routes/eval-runs.js.map +1 -1
  132. package/dist/routes/flows.d.ts +13 -1
  133. package/dist/routes/flows.d.ts.map +1 -1
  134. package/dist/routes/flows.js +44 -3
  135. package/dist/routes/flows.js.map +1 -1
  136. package/dist/routes/hierarchy-errors.d.ts +35 -0
  137. package/dist/routes/hierarchy-errors.d.ts.map +1 -0
  138. package/dist/routes/hierarchy-errors.js +39 -0
  139. package/dist/routes/hierarchy-errors.js.map +1 -0
  140. package/dist/routes/identity.d.ts +7 -0
  141. package/dist/routes/identity.d.ts.map +1 -1
  142. package/dist/routes/identity.js +3 -2
  143. package/dist/routes/identity.js.map +1 -1
  144. package/dist/routes/judged-suites.d.ts +20 -0
  145. package/dist/routes/judged-suites.d.ts.map +1 -0
  146. package/dist/routes/judged-suites.js +272 -0
  147. package/dist/routes/judged-suites.js.map +1 -0
  148. package/dist/routes/judgment-context.d.ts +22 -0
  149. package/dist/routes/judgment-context.d.ts.map +1 -0
  150. package/dist/routes/judgment-context.js +88 -0
  151. package/dist/routes/judgment-context.js.map +1 -0
  152. package/dist/routes/judgment-flow-context.d.ts +32 -0
  153. package/dist/routes/judgment-flow-context.d.ts.map +1 -0
  154. package/dist/routes/judgment-flow-context.js +195 -0
  155. package/dist/routes/judgment-flow-context.js.map +1 -0
  156. package/dist/routes/judgments.d.ts +41 -0
  157. package/dist/routes/judgments.d.ts.map +1 -0
  158. package/dist/routes/judgments.js +566 -0
  159. package/dist/routes/judgments.js.map +1 -0
  160. package/dist/routes/orgs.d.ts +5 -2
  161. package/dist/routes/orgs.d.ts.map +1 -1
  162. package/dist/routes/orgs.js +38 -22
  163. package/dist/routes/orgs.js.map +1 -1
  164. package/dist/routes/policies.d.ts.map +1 -1
  165. package/dist/routes/policies.js +12 -1
  166. package/dist/routes/policies.js.map +1 -1
  167. package/dist/routes/projects.d.ts +10 -2
  168. package/dist/routes/projects.d.ts.map +1 -1
  169. package/dist/routes/projects.js +87 -79
  170. package/dist/routes/projects.js.map +1 -1
  171. package/dist/routes/provenance.d.ts.map +1 -1
  172. package/dist/routes/provenance.js +32 -1
  173. package/dist/routes/provenance.js.map +1 -1
  174. package/dist/routes/providers.d.ts.map +1 -1
  175. package/dist/routes/providers.js +6 -1
  176. package/dist/routes/providers.js.map +1 -1
  177. package/dist/routes/runs.d.ts +0 -7
  178. package/dist/routes/runs.d.ts.map +1 -1
  179. package/dist/routes/runs.js +65 -20
  180. package/dist/routes/runs.js.map +1 -1
  181. package/dist/routes/scope-params.d.ts +16 -1
  182. package/dist/routes/scope-params.d.ts.map +1 -1
  183. package/dist/routes/scope-params.js +28 -0
  184. package/dist/routes/scope-params.js.map +1 -1
  185. package/dist/routes/teams.d.ts +6 -2
  186. package/dist/routes/teams.d.ts.map +1 -1
  187. package/dist/routes/teams.js +77 -73
  188. package/dist/routes/teams.js.map +1 -1
  189. package/dist/types.d.ts +4 -3
  190. package/dist/types.d.ts.map +1 -1
  191. package/dist/webhook-endpoint-binding.d.ts +11 -0
  192. package/dist/webhook-endpoint-binding.d.ts.map +1 -1
  193. package/dist/webhook-endpoint-binding.js.map +1 -1
  194. package/openapi.json +13316 -9299
  195. package/package.json +21 -21
  196. package/src/agent-binding.ts +19 -4
  197. package/src/agent-pins.ts +147 -0
  198. package/src/app.ts +76 -3
  199. package/src/block-binding.ts +137 -0
  200. package/src/block-pins.ts +148 -0
  201. package/src/cost-binding.ts +116 -5
  202. package/src/deploy-versions.ts +157 -0
  203. package/src/deployment-binding.ts +27 -3
  204. package/src/derive-agent-version.ts +206 -0
  205. package/src/errors.ts +17 -0
  206. package/src/eval-case-binding.ts +71 -0
  207. package/src/eval-run-binding.ts +33 -0
  208. package/src/eval-run-dispatcher.ts +57 -16
  209. package/src/eval-suite-binding.ts +2 -0
  210. package/src/flow-binding.ts +11 -4
  211. package/src/flow-pins.ts +113 -0
  212. package/src/hitl-binding.ts +20 -4
  213. package/src/index.ts +88 -2
  214. package/src/judged-dispatcher.ts +507 -0
  215. package/src/judged-items.ts +263 -0
  216. package/src/judgment-binding.ts +349 -0
  217. package/src/middleware/auth.ts +3 -2
  218. package/src/middleware/idempotency.ts +7 -1
  219. package/src/openapi/generate.ts +7 -1
  220. package/src/openapi/operations.ts +615 -24
  221. package/src/openapi/schemas.ts +1157 -22
  222. package/src/provenance-binding.ts +42 -1
  223. package/src/provider-binding.ts +12 -7
  224. package/src/reviewer-binding.ts +10 -2
  225. package/src/reviewer-role.ts +35 -0
  226. package/src/routes/agents.ts +243 -19
  227. package/src/routes/approvals.ts +70 -19
  228. package/src/routes/blocks.ts +362 -0
  229. package/src/routes/conversations.ts +57 -1
  230. package/src/routes/cost.ts +159 -21
  231. package/src/routes/deployments.ts +266 -56
  232. package/src/routes/eval-comparison.ts +101 -0
  233. package/src/routes/eval-runs.ts +11 -0
  234. package/src/routes/flows.ts +63 -5
  235. package/src/routes/hierarchy-errors.ts +51 -0
  236. package/src/routes/identity.ts +10 -2
  237. package/src/routes/judged-suites.ts +363 -0
  238. package/src/routes/judgment-context.ts +128 -0
  239. package/src/routes/judgment-flow-context.ts +245 -0
  240. package/src/routes/judgments.ts +743 -0
  241. package/src/routes/orgs.ts +44 -27
  242. package/src/routes/policies.ts +19 -0
  243. package/src/routes/projects.ts +106 -95
  244. package/src/routes/provenance.ts +48 -1
  245. package/src/routes/providers.ts +5 -0
  246. package/src/routes/runs.ts +79 -22
  247. package/src/routes/scope-params.ts +35 -1
  248. package/src/routes/teams.ts +96 -90
  249. package/src/types.ts +4 -3
  250. package/src/webhook-endpoint-binding.ts +11 -0
@@ -8,7 +8,16 @@
8
8
  // touches provenance storage directly.
9
9
  //
10
10
 
11
- import type { FlowId, ProvenanceId, Result, RunId, TenantId, Timestamp } from '@kindgi/types';
11
+ import type {
12
+ FlowId,
13
+ ListScope,
14
+ ProjectId,
15
+ ProvenanceId,
16
+ Result,
17
+ RunId,
18
+ TenantId,
19
+ Timestamp,
20
+ } from '@kindgi/types';
12
21
 
13
22
  // ---------- domain type surface ----------
14
23
 
@@ -98,6 +107,8 @@ export interface ProvenanceListCursor {
98
107
  export interface ListProvenanceRecordsInput {
99
108
  readonly tenantId: TenantId;
100
109
  readonly limit: number;
110
+ /** Only one project's records, or every project's in an org. Absent: the tenant's. */
111
+ readonly scope?: ListScope;
101
112
  readonly runId?: RunId;
102
113
  readonly agentId?: string;
103
114
  readonly createdAfter?: Date;
@@ -117,6 +128,8 @@ export interface ProvenanceRecordSummary {
117
128
  readonly createdAt: Timestamp;
118
129
  readonly flowRef?: { readonly id: FlowId; readonly version: string };
119
130
  readonly signed: boolean;
131
+ /** The project of the record's run; absent on records from before it was stored. */
132
+ readonly projectId?: ProjectId;
120
133
  }
121
134
 
122
135
  export interface ListProvenanceRecordsResult {
@@ -166,4 +179,32 @@ export interface ProvenanceBinding {
166
179
  * tenant/run pair.
167
180
  */
168
181
  getByRunId(tenantId: TenantId, runId: RunId): Promise<Result<Provenance, ProvenanceBindingError>>;
182
+
183
+ /**
184
+ * The cost ledger's usage of each model call in the run's provenance,
185
+ * by the `callId` in its `model-call` node's attributes. Optional: a
186
+ * deployment without a cost ledger omits it. The routes add it to a
187
+ * read as `callUsage`, outside the signed DAG; a signed export
188
+ * includes it as it stood when signed.
189
+ */
190
+ getCallUsage?(
191
+ tenantId: TenantId,
192
+ runId: RunId,
193
+ ): Promise<Result<CallUsageByCallId, ProvenanceBindingError>>;
169
194
  }
195
+
196
+ /** One model call's usage, as the cost ledger has it. */
197
+ export interface CallUsage {
198
+ readonly usage: {
199
+ readonly promptTokens: number;
200
+ readonly completionTokens: number;
201
+ readonly cacheReadTokens?: number;
202
+ readonly cacheWriteTokens?: number;
203
+ readonly reasoningTokens?: number;
204
+ };
205
+ readonly costUsd?: number;
206
+ readonly durationMs?: number;
207
+ readonly servedModel?: string;
208
+ }
209
+
210
+ export type CallUsageByCallId = Readonly<Record<string, CallUsage>>;
@@ -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
  /**
@@ -6,16 +6,24 @@ import type { Cursor, ReviewerId, TenantId, Timestamp, UserId } from '@kindgi/ty
6
6
 
7
7
  /**
8
8
  * Translates the bearer-token identity (`UserId`) into the `ReviewerId`
9
- * that `HitlBinding.submitReview` needs. The API package intentionally does
9
+ * that `HitlBinding.submitReview` needs, and the reviewer role the
10
+ * approvals surface is scoped by. The API package intentionally does
10
11
  * NOT own the `user ↔ reviewer` mapping — deployments plug that in via
11
12
  * this binding, same pattern as `TokenResolver` (for auth read) and
12
13
  * `TokenAdmin` (for mint/revoke).
13
14
  *
14
- * Return `null` when the user has no registered reviewer row for the
15
+ * Both return `null` when the user has no active reviewer row for the
15
16
  * tenant (leads to `403 permission-denied` from the approvals route).
16
17
  */
17
18
  export interface ReviewerBinding {
18
19
  resolveReviewer(input: ReviewerLookupInput): Promise<ReviewerId | null>;
20
+ /**
21
+ * The user's reviewer role in the tenant. Consulted for a token that
22
+ * carries no `reviewerRole` of its own (a session or API key of a
23
+ * registered reviewer), so the roster, not the token, says who reviews.
24
+ * A binding without it: only a token that carries its role reviews.
25
+ */
26
+ resolveReviewerRole?(input: ReviewerLookupInput): Promise<ReviewerRole | null>;
19
27
  }
20
28
 
21
29
  export interface ReviewerLookupInput {
@@ -0,0 +1,35 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { ReviewerRole } from '@kindgi/authz';
5
+ import type { TenantId, UserId } from '@kindgi/types';
6
+ import type { Context } from 'hono';
7
+
8
+ import type { ReviewerBinding } from './reviewer-binding.js';
9
+ import type { AppEnv } from './types.js';
10
+
11
+ /**
12
+ * The caller's reviewer role: the one its token carries, else the one the
13
+ * roster gives its user (`ReviewerBinding.resolveReviewerRole`, when the
14
+ * binding has it), kept on the request. `undefined` for a caller that
15
+ * isn't a reviewer: a token with neither a role nor a user, or a user the
16
+ * roster doesn't know.
17
+ */
18
+ export async function callerReviewerRole(
19
+ c: Context<AppEnv>,
20
+ reviewerBinding: ReviewerBinding | undefined,
21
+ ): Promise<ReviewerRole | undefined> {
22
+ const carried = c.get('reviewerRole');
23
+ if (carried !== undefined) return carried;
24
+ const userId = c.get('userId') as UserId | undefined;
25
+ if (userId === undefined || reviewerBinding?.resolveReviewerRole === undefined) {
26
+ return undefined;
27
+ }
28
+ const role = await reviewerBinding.resolveReviewerRole({
29
+ tenantId: c.get('tenantId') as TenantId,
30
+ userId,
31
+ });
32
+ if (role === null) return undefined;
33
+ c.set('reviewerRole', role);
34
+ return role;
35
+ }
@@ -1,15 +1,30 @@
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 type { ToolRegistryBinding } from '../tool-binding.js';
13
28
  import type { AppEnv } from '../types.js';
14
29
  import { clampLimit } from './pagination.js';
15
30
  import { parseScopeParams } from './scope-params.js';
@@ -24,8 +39,19 @@ import { parseScopeParams } from './scope-params.js';
24
39
  *
25
40
  * Publish is data-as-code: the body is a full `DefineAgentSpec` (the
26
41
  * same value `defineAgent(...)` accepts).
42
+ *
43
+ * With `toolRegistry`, a published version is pinned: each tool range
44
+ * resolves once, at publish, to the exact version every run of that
45
+ * version uses (`pins`, see `resolveAgentPins`), and a range that
46
+ * matches no published version refuses the publish. Without it,
47
+ * versions carry no pins and resolve their ranges per run.
27
48
  */
28
- export function agentsRouter(binding: AgentRegistryBinding, authorizer?: Authorizer): Hono<AppEnv> {
49
+ export function agentsRouter(
50
+ binding: AgentRegistryBinding,
51
+ authorizer?: Authorizer,
52
+ toolRegistry?: ToolRegistryBinding,
53
+ blockRegistry?: BlockRegistryBinding,
54
+ ): Hono<AppEnv> {
29
55
  const r = new Hono<AppEnv>();
30
56
 
31
57
  // Authorization — check the resource directly (ref('agent', businessId));
@@ -35,6 +61,7 @@ export function agentsRouter(binding: AgentRegistryBinding, authorizer?: Authori
35
61
  //
36
62
  // POST / → admin on body.projectId
37
63
  // GET /:agentId, GET /:agentId/versions[/…] → read on the agent
64
+ // POST /:agentId/versions → publish on the agent (derive)
38
65
  // POST /:agentId/versions/:version/unregister → admin on the agent
39
66
  // POST /:agentId/versions/:version/reinstate → admin on the agent
40
67
  // GET / (list) → tenant-scoped fetch;
@@ -73,7 +100,10 @@ export function agentsRouter(binding: AgentRegistryBinding, authorizer?: Authori
73
100
  return mw(c, next);
74
101
  });
75
102
  r.use('/:agentId/*', async (c, next) => {
76
- const action = c.req.method === 'GET' ? 'read' : 'admin';
103
+ // Deriving a version (POST …/versions) is `publish` on the agent;
104
+ // unregister and reinstate are `admin`.
105
+ const action =
106
+ c.req.method === 'GET' ? 'read' : c.req.path.endsWith('/versions') ? 'publish' : 'admin';
77
107
  const agentId = c.req.param('agentId') ?? '';
78
108
  const mw = authorizer.authorize(action, () => ref('agent', agentId));
79
109
  return mw(c, next);
@@ -281,6 +311,28 @@ export function agentsRouter(binding: AgentRegistryBinding, authorizer?: Authori
281
311
  );
282
312
  }
283
313
 
314
+ // Pin the version: every tool range resolves now, once, to the
315
+ // version its runs use. A range nothing satisfies refuses the
316
+ // publish rather than store a partly pinned version.
317
+ let agent: Agent = defined.value;
318
+ if (toolRegistry !== undefined) {
319
+ const resolved = await resolveAgentPins(toolRegistry, tenantId, defined.value, blockRegistry);
320
+ if (resolved.kind === 'unpinnable') {
321
+ c.status(statusFor('invalid-agent') as never);
322
+ return c.json(
323
+ toWireError(
324
+ {
325
+ code: 'validation-failed',
326
+ message: `Agent "${defined.value.id as unknown as string}" uses tool versions that aren't published (${resolved.issues.length} issue${resolved.issues.length === 1 ? '' : 's'})`,
327
+ issues: resolved.issues as unknown as Record<string, unknown>[],
328
+ },
329
+ requestId,
330
+ ),
331
+ );
332
+ }
333
+ agent = { ...agent, pins: resolved.pins, pinsDigest: pinsDigest(resolved.pins) };
334
+ }
335
+
284
336
  const principal = c.get('principal') as Principal | undefined;
285
337
  const creatorUserId =
286
338
  principal?.actor.kind === 'user'
@@ -289,7 +341,7 @@ export function agentsRouter(binding: AgentRegistryBinding, authorizer?: Authori
289
341
  const outcome = await binding.publish({
290
342
  tenantId,
291
343
  projectId,
292
- agent: defined.value,
344
+ agent,
293
345
  // Write the authorization tuples in the same transaction as the
294
346
  // registry row. The binding calls this with the business
295
347
  // `agentId`.
@@ -300,18 +352,7 @@ export function agentsRouter(binding: AgentRegistryBinding, authorizer?: Authori
300
352
  ),
301
353
  });
302
354
  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
- );
355
+ return alreadyRegistered(c, binding, tenantId, outcome.agentId, outcome.version);
315
356
  }
316
357
  if (outcome.kind === 'project-not-found') {
317
358
  // Caller supplied a `projectId` that does not resolve within
@@ -337,6 +378,47 @@ export function agentsRouter(binding: AgentRegistryBinding, authorizer?: Authori
337
378
  });
338
379
  });
339
380
 
381
+ // ---------- POST /:agentId/versions (derive a version) ----------
382
+ r.post('/:agentId/versions', async (c) => {
383
+ const requestId = c.get('requestId');
384
+ const tenantId = c.get('tenantId') as TenantId;
385
+ const agentId = c.req.param('agentId') as AgentId;
386
+ const body = await jsonObject(c);
387
+ if (body === undefined) {
388
+ c.status(statusFor('bad-input') as never);
389
+ return c.json(
390
+ toWireError(
391
+ { code: 'bad-input', message: 'Request body must be a JSON object' },
392
+ requestId,
393
+ ),
394
+ );
395
+ }
396
+ const parsed = deriveBody(body);
397
+ if (typeof parsed === 'string') {
398
+ c.status(statusFor('bad-input') as never);
399
+ return c.json(toWireError({ code: 'bad-input', message: parsed }, requestId));
400
+ }
401
+ const principal = c.get('principal') as Principal | undefined;
402
+ const userId = principal?.actor.kind === 'user' ? (principal.actor.id as UserId) : undefined;
403
+ const outcome = await deriveAgentVersion({
404
+ agents: binding,
405
+ blocks: blockRegistry,
406
+ tenantId,
407
+ agentId,
408
+ from: parsed.from,
409
+ swaps: parsed.pins,
410
+ ...(parsed.label !== undefined && { label: parsed.label }),
411
+ ...(userId !== undefined && { by: `user:${userId as unknown as string}` }),
412
+ ...(parsed.projectId !== undefined && { projectId: parsed.projectId }),
413
+ tuplesFor: (projectId) => (id) =>
414
+ tuplesForCreate(
415
+ { kind: 'agent', id: id as AgentId, tenantId, projectId },
416
+ userId ?? ('00000000-0000-0000-0000-000000000000' as UserId),
417
+ ),
418
+ });
419
+ return derived(c, agentId, parsed.from, outcome);
420
+ });
421
+
340
422
  // ---------- POST /:agentId/versions/:version/unregister ----------
341
423
  r.post('/:agentId/versions/:version/unregister', async (c) => {
342
424
  const requestId = c.get('requestId');
@@ -403,7 +485,143 @@ export function agentsRouter(binding: AgentRegistryBinding, authorizer?: Authori
403
485
  return r;
404
486
  }
405
487
 
406
- function serializeAgent(a: Agent): Record<string, unknown> {
488
+ /**
489
+ * The 409 for a publish whose number is taken. Versions never change, so
490
+ * it names the next free one (an expert's derived version may hold it).
491
+ */
492
+ async function alreadyRegistered(
493
+ c: Context<AppEnv>,
494
+ binding: AgentRegistryBinding,
495
+ tenantId: TenantId,
496
+ agentId: AgentId,
497
+ version: Semver,
498
+ ) {
499
+ const next = await nextFreeAgentVersion(binding, tenantId, agentId, version as unknown as string);
500
+ const suggestion = next === undefined ? '' : `; publish it as ${next}, the next free version`;
501
+ c.status(statusFor('agent-already-registered') as never);
502
+ return c.json(
503
+ toWireError(
504
+ {
505
+ code: 'agent-already-registered',
506
+ message: `Agent "${agentId as unknown as string}" version "${version as unknown as string}" is already registered, and versions never change${suggestion}`,
507
+ agentId: agentId as unknown as string,
508
+ version: version as unknown as string,
509
+ ...(next !== undefined && { nextFreeVersion: next }),
510
+ },
511
+ c.get('requestId'),
512
+ ),
513
+ );
514
+ }
515
+
516
+ /** The request body as an object; undefined when it isn't JSON or isn't an object. */
517
+ async function jsonObject(c: Context<AppEnv>): Promise<Record<string, unknown> | undefined> {
518
+ try {
519
+ const body = await c.req.json();
520
+ return body !== null && typeof body === 'object' && !Array.isArray(body)
521
+ ? (body as Record<string, unknown>)
522
+ : undefined;
523
+ } catch {
524
+ return undefined;
525
+ }
526
+ }
527
+
528
+ interface DeriveBody {
529
+ readonly from: string;
530
+ readonly pins: PinSwaps;
531
+ readonly label?: string;
532
+ readonly projectId?: ProjectId;
533
+ }
534
+
535
+ /** `{ from, pins: { prompts?, settings? }, label?, projectId? }`, or what's wrong with it. */
536
+ function deriveBody(b: Record<string, unknown>): DeriveBody | string {
537
+ if (typeof b.from !== 'string' || b.from.length === 0) {
538
+ return '`from` (the version to derive from) is required';
539
+ }
540
+ const pins = pinSwapsOf(b.pins);
541
+ if (typeof pins === 'string') return pins;
542
+ for (const key of ['label', 'projectId'] as const) {
543
+ if (b[key] !== undefined && typeof b[key] !== 'string') return `\`${key}\` must be a string`;
544
+ }
545
+ return {
546
+ from: b.from,
547
+ pins,
548
+ ...(typeof b.label === 'string' && { label: b.label }),
549
+ ...(typeof b.projectId === 'string' && { projectId: b.projectId as ProjectId }),
550
+ };
551
+ }
552
+
553
+ /** `pins`: prompt and settings pins only, each block id → exact version; or what's wrong with it. */
554
+ function pinSwapsOf(pins: unknown): PinSwaps | string {
555
+ if (pins === null || typeof pins !== 'object' || Array.isArray(pins)) {
556
+ return '`pins` must be { prompts?, settings? }';
557
+ }
558
+ for (const [kind, map] of Object.entries(pins)) {
559
+ if (kind !== 'prompts' && kind !== 'settings') {
560
+ return `\`pins.${kind}\` can't be swapped: only prompt and settings pins (tool pins come from code)`;
561
+ }
562
+ if (!isStringMap(map)) return `\`pins.${kind}\` must map block ids to exact versions`;
563
+ }
564
+ return pins as PinSwaps;
565
+ }
566
+
567
+ function isStringMap(value: unknown): boolean {
568
+ return (
569
+ value !== null &&
570
+ typeof value === 'object' &&
571
+ !Array.isArray(value) &&
572
+ Object.values(value).every((v) => typeof v === 'string')
573
+ );
574
+ }
575
+
576
+ /** The response to a derive. */
577
+ function derived(
578
+ c: Context<AppEnv>,
579
+ agentId: AgentId,
580
+ from: string,
581
+ outcome: DeriveAgentVersionOutcome,
582
+ ) {
583
+ const requestId = c.get('requestId');
584
+ const fail = (
585
+ status: string,
586
+ error: { code: string; message: string } & Record<string, unknown>,
587
+ ) => {
588
+ c.status(statusFor(status) as never);
589
+ return c.json(toWireError(error, requestId));
590
+ };
591
+ switch (outcome.kind) {
592
+ case 'ok':
593
+ c.status(201);
594
+ return c.json(serializeAgent(outcome.agent));
595
+ case 'not-found':
596
+ return fail('agent-not-found', {
597
+ code: 'agent-not-found',
598
+ message: `No agent "${agentId as unknown as string}" at version "${from}"`,
599
+ });
600
+ case 'unpinned':
601
+ return fail('validation-failed', {
602
+ code: 'validation-failed',
603
+ 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`,
604
+ });
605
+ case 'invalid':
606
+ return fail('validation-failed', {
607
+ code: 'validation-failed',
608
+ message: `Can't derive from version ${from} (${outcome.issues.length} issue${outcome.issues.length === 1 ? '' : 's'})`,
609
+ issues: outcome.issues as unknown as Record<string, unknown>[],
610
+ });
611
+ case 'no-project':
612
+ return fail('bad-input', {
613
+ code: 'bad-input',
614
+ message: "`projectId` is required: this runtime doesn't record the version's project",
615
+ });
616
+ case 'project-not-found':
617
+ return fail('bad-input', {
618
+ code: 'bad-input',
619
+ message: `\`projectId\` "${outcome.projectId as unknown as string}" does not resolve to a project in this tenant`,
620
+ });
621
+ }
622
+ }
623
+
624
+ function serializeAgent(a: AgentVersionRecord): Record<string, unknown> {
407
625
  return {
408
626
  id: a.id as unknown as string,
409
627
  version: a.version as unknown as string,
@@ -411,6 +629,8 @@ function serializeAgent(a: Agent): Record<string, unknown> {
411
629
  ...(a.description !== undefined && { description: a.description }),
412
630
  instructions: a.instructions,
413
631
  ...(a.parameters !== undefined && { parameters: a.parameters }),
632
+ ...(a.settings !== undefined && { settings: a.settings }),
633
+ ...(a.modelSettings !== undefined && { modelSettings: a.modelSettings }),
414
634
  capabilities: a.capabilities,
415
635
  tools: a.tools,
416
636
  retrieval: a.retrieval,
@@ -420,5 +640,9 @@ function serializeAgent(a: Agent): Record<string, unknown> {
420
640
  ...(a.tags !== undefined && { tags: a.tags }),
421
641
  ...(a.preferredProvider !== undefined && { preferredProvider: a.preferredProvider }),
422
642
  ...(a.preferredModel !== undefined && { preferredModel: a.preferredModel }),
643
+ ...(a.pins !== undefined && { pins: a.pins }),
644
+ ...(a.pinsDigest !== undefined && { pinsDigest: a.pinsDigest }),
645
+ ...(a.derivedFrom !== undefined && { derivedFrom: a.derivedFrom }),
646
+ ...(a.unregisteredAt !== undefined && { unregisteredAt: a.unregisteredAt }),
423
647
  };
424
648
  }