@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.
- package/dist/agent-binding.d.ts +18 -4
- package/dist/agent-binding.d.ts.map +1 -1
- package/dist/agent-pins.d.ts +48 -0
- package/dist/agent-pins.d.ts.map +1 -0
- package/dist/agent-pins.js +102 -0
- package/dist/agent-pins.js.map +1 -0
- package/dist/app.d.ts +22 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +22 -3
- package/dist/app.js.map +1 -1
- package/dist/block-binding.d.ts +132 -0
- package/dist/block-binding.d.ts.map +1 -0
- package/dist/block-binding.js +4 -0
- package/dist/block-binding.js.map +1 -0
- package/dist/block-pins.d.ts +22 -0
- package/dist/block-pins.d.ts.map +1 -0
- package/dist/block-pins.js +112 -0
- package/dist/block-pins.js.map +1 -0
- package/dist/cost-binding.d.ts +100 -5
- package/dist/cost-binding.d.ts.map +1 -1
- package/dist/cost-binding.js +10 -0
- package/dist/cost-binding.js.map +1 -1
- package/dist/deploy-versions.d.ts +58 -0
- package/dist/deploy-versions.d.ts.map +1 -0
- package/dist/deploy-versions.js +91 -0
- package/dist/deploy-versions.js.map +1 -0
- package/dist/deployment-binding.d.ts +24 -3
- package/dist/deployment-binding.d.ts.map +1 -1
- package/dist/derive-agent-version.d.ts +69 -0
- package/dist/derive-agent-version.d.ts.map +1 -0
- package/dist/derive-agent-version.js +139 -0
- package/dist/derive-agent-version.js.map +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +17 -0
- package/dist/errors.js.map +1 -1
- package/dist/eval-case-binding.d.ts +65 -0
- package/dist/eval-case-binding.d.ts.map +1 -0
- package/dist/eval-case-binding.js +4 -0
- package/dist/eval-case-binding.js.map +1 -0
- package/dist/eval-run-binding.d.ts +30 -0
- package/dist/eval-run-binding.d.ts.map +1 -1
- package/dist/eval-run-dispatcher.d.ts +38 -3
- package/dist/eval-run-dispatcher.d.ts.map +1 -1
- package/dist/eval-run-dispatcher.js +21 -15
- package/dist/eval-run-dispatcher.js.map +1 -1
- package/dist/eval-suite-binding.d.ts +1 -1
- package/dist/eval-suite-binding.d.ts.map +1 -1
- package/dist/eval-suite-binding.js +2 -0
- package/dist/eval-suite-binding.js.map +1 -1
- package/dist/flow-binding.d.ts +10 -4
- package/dist/flow-binding.d.ts.map +1 -1
- package/dist/flow-pins.d.ts +36 -0
- package/dist/flow-pins.d.ts.map +1 -0
- package/dist/flow-pins.js +81 -0
- package/dist/flow-pins.js.map +1 -0
- package/dist/hitl-binding.d.ts +20 -5
- package/dist/hitl-binding.d.ts.map +1 -1
- package/dist/index.d.ts +19 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/judged-dispatcher.d.ts +134 -0
- package/dist/judged-dispatcher.d.ts.map +1 -0
- package/dist/judged-dispatcher.js +297 -0
- package/dist/judged-dispatcher.js.map +1 -0
- package/dist/judged-items.d.ts +86 -0
- package/dist/judged-items.d.ts.map +1 -0
- package/dist/judged-items.js +184 -0
- package/dist/judged-items.js.map +1 -0
- package/dist/judgment-binding.d.ts +316 -0
- package/dist/judgment-binding.d.ts.map +1 -0
- package/dist/judgment-binding.js +19 -0
- package/dist/judgment-binding.js.map +1 -0
- package/dist/middleware/auth.d.ts +3 -2
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/idempotency.d.ts +5 -1
- package/dist/middleware/idempotency.d.ts.map +1 -1
- package/dist/middleware/idempotency.js +8 -1
- package/dist/middleware/idempotency.js.map +1 -1
- package/dist/openapi/generate.d.ts.map +1 -1
- package/dist/openapi/generate.js +4 -1
- package/dist/openapi/generate.js.map +1 -1
- package/dist/openapi/operations.d.ts.map +1 -1
- package/dist/openapi/operations.js +543 -24
- package/dist/openapi/operations.js.map +1 -1
- package/dist/openapi/schemas.d.ts +58 -0
- package/dist/openapi/schemas.d.ts.map +1 -1
- package/dist/openapi/schemas.js +1058 -27
- package/dist/openapi/schemas.js.map +1 -1
- package/dist/provenance-binding.d.ts +27 -1
- package/dist/provenance-binding.d.ts.map +1 -1
- package/dist/provenance-binding.js.map +1 -1
- package/dist/provider-binding.d.ts +12 -7
- package/dist/provider-binding.d.ts.map +1 -1
- package/dist/reviewer-binding.d.ts +10 -2
- package/dist/reviewer-binding.d.ts.map +1 -1
- package/dist/reviewer-role.d.ts +13 -0
- package/dist/reviewer-role.d.ts.map +1 -0
- package/dist/reviewer-role.js +27 -0
- package/dist/reviewer-role.js.map +1 -0
- package/dist/routes/agents.d.ts +9 -1
- package/dist/routes/agents.d.ts.map +1 -1
- package/dist/routes/agents.js +175 -11
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/approvals.d.ts +5 -4
- package/dist/routes/approvals.d.ts.map +1 -1
- package/dist/routes/approvals.js +52 -16
- package/dist/routes/approvals.js.map +1 -1
- package/dist/routes/blocks.d.ts +19 -0
- package/dist/routes/blocks.d.ts.map +1 -0
- package/dist/routes/blocks.js +281 -0
- package/dist/routes/blocks.js.map +1 -0
- package/dist/routes/conversations.d.ts +7 -1
- package/dist/routes/conversations.d.ts.map +1 -1
- package/dist/routes/conversations.js +41 -1
- package/dist/routes/conversations.js.map +1 -1
- package/dist/routes/cost.d.ts.map +1 -1
- package/dist/routes/cost.js +142 -11
- package/dist/routes/cost.js.map +1 -1
- package/dist/routes/deployments.d.ts +3 -0
- package/dist/routes/deployments.d.ts.map +1 -1
- package/dist/routes/deployments.js +208 -55
- package/dist/routes/deployments.js.map +1 -1
- package/dist/routes/eval-comparison.d.ts +14 -0
- package/dist/routes/eval-comparison.d.ts.map +1 -0
- package/dist/routes/eval-comparison.js +87 -0
- package/dist/routes/eval-comparison.js.map +1 -0
- package/dist/routes/eval-runs.d.ts.map +1 -1
- package/dist/routes/eval-runs.js +8 -0
- package/dist/routes/eval-runs.js.map +1 -1
- package/dist/routes/flows.d.ts +13 -1
- package/dist/routes/flows.d.ts.map +1 -1
- package/dist/routes/flows.js +44 -3
- package/dist/routes/flows.js.map +1 -1
- package/dist/routes/hierarchy-errors.d.ts +35 -0
- package/dist/routes/hierarchy-errors.d.ts.map +1 -0
- package/dist/routes/hierarchy-errors.js +39 -0
- package/dist/routes/hierarchy-errors.js.map +1 -0
- package/dist/routes/identity.d.ts +7 -0
- package/dist/routes/identity.d.ts.map +1 -1
- package/dist/routes/identity.js +3 -2
- package/dist/routes/identity.js.map +1 -1
- package/dist/routes/judged-suites.d.ts +20 -0
- package/dist/routes/judged-suites.d.ts.map +1 -0
- package/dist/routes/judged-suites.js +272 -0
- package/dist/routes/judged-suites.js.map +1 -0
- package/dist/routes/judgment-context.d.ts +22 -0
- package/dist/routes/judgment-context.d.ts.map +1 -0
- package/dist/routes/judgment-context.js +88 -0
- package/dist/routes/judgment-context.js.map +1 -0
- package/dist/routes/judgment-flow-context.d.ts +32 -0
- package/dist/routes/judgment-flow-context.d.ts.map +1 -0
- package/dist/routes/judgment-flow-context.js +195 -0
- package/dist/routes/judgment-flow-context.js.map +1 -0
- package/dist/routes/judgments.d.ts +41 -0
- package/dist/routes/judgments.d.ts.map +1 -0
- package/dist/routes/judgments.js +566 -0
- package/dist/routes/judgments.js.map +1 -0
- package/dist/routes/orgs.d.ts +5 -2
- package/dist/routes/orgs.d.ts.map +1 -1
- package/dist/routes/orgs.js +38 -22
- package/dist/routes/orgs.js.map +1 -1
- package/dist/routes/policies.d.ts.map +1 -1
- package/dist/routes/policies.js +12 -1
- package/dist/routes/policies.js.map +1 -1
- package/dist/routes/projects.d.ts +10 -2
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +87 -79
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/provenance.d.ts.map +1 -1
- package/dist/routes/provenance.js +32 -1
- package/dist/routes/provenance.js.map +1 -1
- package/dist/routes/providers.d.ts.map +1 -1
- package/dist/routes/providers.js +6 -1
- package/dist/routes/providers.js.map +1 -1
- package/dist/routes/runs.d.ts +0 -7
- package/dist/routes/runs.d.ts.map +1 -1
- package/dist/routes/runs.js +65 -20
- package/dist/routes/runs.js.map +1 -1
- package/dist/routes/scope-params.d.ts +16 -1
- package/dist/routes/scope-params.d.ts.map +1 -1
- package/dist/routes/scope-params.js +28 -0
- package/dist/routes/scope-params.js.map +1 -1
- package/dist/routes/teams.d.ts +6 -2
- package/dist/routes/teams.d.ts.map +1 -1
- package/dist/routes/teams.js +77 -73
- package/dist/routes/teams.js.map +1 -1
- package/dist/types.d.ts +4 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/webhook-endpoint-binding.d.ts +11 -0
- package/dist/webhook-endpoint-binding.d.ts.map +1 -1
- package/dist/webhook-endpoint-binding.js.map +1 -1
- package/openapi.json +13316 -9299
- package/package.json +21 -21
- package/src/agent-binding.ts +19 -4
- package/src/agent-pins.ts +147 -0
- package/src/app.ts +76 -3
- package/src/block-binding.ts +137 -0
- package/src/block-pins.ts +148 -0
- package/src/cost-binding.ts +116 -5
- package/src/deploy-versions.ts +157 -0
- package/src/deployment-binding.ts +27 -3
- package/src/derive-agent-version.ts +206 -0
- package/src/errors.ts +17 -0
- package/src/eval-case-binding.ts +71 -0
- package/src/eval-run-binding.ts +33 -0
- package/src/eval-run-dispatcher.ts +57 -16
- package/src/eval-suite-binding.ts +2 -0
- package/src/flow-binding.ts +11 -4
- package/src/flow-pins.ts +113 -0
- package/src/hitl-binding.ts +20 -4
- package/src/index.ts +88 -2
- package/src/judged-dispatcher.ts +507 -0
- package/src/judged-items.ts +263 -0
- package/src/judgment-binding.ts +349 -0
- package/src/middleware/auth.ts +3 -2
- package/src/middleware/idempotency.ts +7 -1
- package/src/openapi/generate.ts +7 -1
- package/src/openapi/operations.ts +615 -24
- package/src/openapi/schemas.ts +1157 -22
- package/src/provenance-binding.ts +42 -1
- package/src/provider-binding.ts +12 -7
- package/src/reviewer-binding.ts +10 -2
- package/src/reviewer-role.ts +35 -0
- package/src/routes/agents.ts +243 -19
- package/src/routes/approvals.ts +70 -19
- package/src/routes/blocks.ts +362 -0
- package/src/routes/conversations.ts +57 -1
- package/src/routes/cost.ts +159 -21
- package/src/routes/deployments.ts +266 -56
- package/src/routes/eval-comparison.ts +101 -0
- package/src/routes/eval-runs.ts +11 -0
- package/src/routes/flows.ts +63 -5
- package/src/routes/hierarchy-errors.ts +51 -0
- package/src/routes/identity.ts +10 -2
- package/src/routes/judged-suites.ts +363 -0
- package/src/routes/judgment-context.ts +128 -0
- package/src/routes/judgment-flow-context.ts +245 -0
- package/src/routes/judgments.ts +743 -0
- package/src/routes/orgs.ts +44 -27
- package/src/routes/policies.ts +19 -0
- package/src/routes/projects.ts +106 -95
- package/src/routes/provenance.ts +48 -1
- package/src/routes/providers.ts +5 -0
- package/src/routes/runs.ts +79 -22
- package/src/routes/scope-params.ts +35 -1
- package/src/routes/teams.ts +96 -90
- package/src/types.ts +4 -3
- package/src/webhook-endpoint-binding.ts +11 -0
package/src/routes/approvals.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
import { Hono } from 'hono';
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import { AGENT_GATE_SUBJECTS, TOOL_CALL_GATE_SUBJECT } from '@kindgi/agents';
|
|
7
|
+
import type { ConversationBinding, GateDecisionValue } from '@kindgi/agents';
|
|
7
8
|
import { REVIEWER_ROLE_RANK, type ReviewerRole } from '@kindgi/authz';
|
|
8
9
|
import { serializePublicKeyPem, signEd25519 } from '@kindgi/crypto';
|
|
9
10
|
import type { SigningKeyBinding } from '@kindgi/crypto';
|
|
@@ -23,10 +24,18 @@ import type {
|
|
|
23
24
|
|
|
24
25
|
import { statusFor, toWireError } from '../errors.js';
|
|
25
26
|
import type { RunHandlerBinding } from '../handler-binding.js';
|
|
26
|
-
import type {
|
|
27
|
+
import type {
|
|
28
|
+
Approval,
|
|
29
|
+
ApprovalStatus,
|
|
30
|
+
HitlBinding,
|
|
31
|
+
ReviewDecisionKind,
|
|
32
|
+
ReviewDecisionRecord,
|
|
33
|
+
} from '../hitl-binding.js';
|
|
27
34
|
import type { ReviewerBinding } from '../reviewer-binding.js';
|
|
35
|
+
import { callerReviewerRole } from '../reviewer-role.js';
|
|
28
36
|
import type { AppEnv } from '../types.js';
|
|
29
37
|
import { clampLimit } from './pagination.js';
|
|
38
|
+
import { parseListScope } from './scope-params.js';
|
|
30
39
|
|
|
31
40
|
const APPROVAL_STATUSES: ReadonlySet<ApprovalStatus> = new Set([
|
|
32
41
|
'pending',
|
|
@@ -80,10 +89,11 @@ export interface ApprovalsRouterOptions {
|
|
|
80
89
|
/**
|
|
81
90
|
* Approvals resource routes.
|
|
82
91
|
*
|
|
83
|
-
* Reviewer-role scoping: routes require
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* approvals route — this surface is
|
|
92
|
+
* Reviewer-role scoping: routes require a reviewer role — the token's
|
|
93
|
+
* (`TokenResolution.reviewerRole`), or its user's in the reviewer roster
|
|
94
|
+
* (`ReviewerBinding.resolveReviewerRole`). A caller with neither gets
|
|
95
|
+
* `403 permission-denied` on every approvals route — this surface is
|
|
96
|
+
* reviewer-only.
|
|
87
97
|
*
|
|
88
98
|
* Visibility filter: a caller with role `R` sees approvals whose
|
|
89
99
|
* `requiredRole` rank ≤ their rank (standard < senior < admin). The
|
|
@@ -103,9 +113,11 @@ export function approvalsRouter(
|
|
|
103
113
|
const runHandler = options.runHandler;
|
|
104
114
|
|
|
105
115
|
// ---------- role gate for the whole resource ----------
|
|
116
|
+
// A reviewer: a token that carries a role, or whose user the roster
|
|
117
|
+
// names (a session or API key of a registered reviewer).
|
|
106
118
|
r.use('*', async (c, next) => {
|
|
107
119
|
const requestId = c.get('requestId');
|
|
108
|
-
const role = c
|
|
120
|
+
const role = await callerReviewerRole(c, reviewerBinding);
|
|
109
121
|
if (role === undefined) {
|
|
110
122
|
c.status(statusFor('permission-denied') as never);
|
|
111
123
|
return c.json(
|
|
@@ -113,7 +125,7 @@ export function approvalsRouter(
|
|
|
113
125
|
{
|
|
114
126
|
code: 'permission-denied',
|
|
115
127
|
message:
|
|
116
|
-
'
|
|
128
|
+
"The caller isn't a reviewer: its token carries no reviewer role, and its user isn't registered as one (`kindgi reviewers register`). The approvals surface is reviewer-only.",
|
|
117
129
|
},
|
|
118
130
|
requestId,
|
|
119
131
|
),
|
|
@@ -130,6 +142,14 @@ export function approvalsRouter(
|
|
|
130
142
|
const role = c.get('reviewerRole') as ReviewerRole;
|
|
131
143
|
const limit = clampLimit(c.req.query('limit'));
|
|
132
144
|
|
|
145
|
+
const scopeParsed = parseListScope(c.req.query(), { tenantId });
|
|
146
|
+
if (scopeParsed.kind === 'err') {
|
|
147
|
+
c.status(statusFor('scope-invalid') as never);
|
|
148
|
+
return c.json(
|
|
149
|
+
toWireError({ code: 'scope-invalid', message: scopeParsed.message }, requestId),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
133
153
|
const statusRaw = c.req.query('status');
|
|
134
154
|
let statusFilter: ApprovalStatus | undefined;
|
|
135
155
|
if (statusRaw !== undefined && statusRaw.length > 0) {
|
|
@@ -213,6 +233,7 @@ export function approvalsRouter(
|
|
|
213
233
|
const listInput = {
|
|
214
234
|
tenantId,
|
|
215
235
|
limit: HITL_LIMIT_CAP,
|
|
236
|
+
...(scopeParsed.scope !== undefined && { scope: scopeParsed.scope }),
|
|
216
237
|
...(statusFilter !== undefined && { status: statusFilter }),
|
|
217
238
|
...(requiredRoleFilter !== undefined && { requiredRole: requiredRoleFilter }),
|
|
218
239
|
...(createdAfterIso !== undefined && { since: createdAfterIso as unknown as Timestamp }),
|
|
@@ -327,6 +348,22 @@ export function approvalsRouter(
|
|
|
327
348
|
);
|
|
328
349
|
}
|
|
329
350
|
|
|
351
|
+
// An agent's tool-call and session gates resume on the decision alone
|
|
352
|
+
// (`readGateDecision` fails closed on anything else), so a `value`
|
|
353
|
+
// can't stand in for it: refused before anything is recorded.
|
|
354
|
+
if (parsed.value.value !== undefined && AGENT_GATE_SUBJECTS.has(approval.subjectKind)) {
|
|
355
|
+
c.status(statusFor('bad-input') as never);
|
|
356
|
+
return c.json(
|
|
357
|
+
toWireError(
|
|
358
|
+
{
|
|
359
|
+
code: 'bad-input',
|
|
360
|
+
message: `An approval of an agent's ${approval.subjectKind === TOOL_CALL_GATE_SUBJECT ? 'tool call' : 'session'} takes a decision (approve or reject) and an optional rationale, not a \`value\`: the turn resumes on the decision alone.`,
|
|
361
|
+
},
|
|
362
|
+
requestId,
|
|
363
|
+
),
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
330
367
|
const reviewerId = await reviewerBinding.resolveReviewer({
|
|
331
368
|
tenantId,
|
|
332
369
|
userId: userId as UserId,
|
|
@@ -362,23 +399,25 @@ export function approvalsRouter(
|
|
|
362
399
|
// stays suspended until the new (escalated) approval terminates,
|
|
363
400
|
// or the run is cancelled explicitly.
|
|
364
401
|
//
|
|
365
|
-
// Resume value shape: `{ decided, rationale
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
//
|
|
402
|
+
// Resume value shape: `GateDecisionValue`, `{ decided, rationale?,
|
|
403
|
+
// decidedBy, approvalId }`, the shape the agent's approval gate
|
|
404
|
+
// consumes; the run's journal keeps who decided which approval. For
|
|
405
|
+
// another subject, the caller CAN supply an explicit `value` to
|
|
406
|
+
// override it, when the resume payload must carry more than the
|
|
407
|
+
// decision (an agent gate refuses one, above).
|
|
369
408
|
let waitpointResolved = false;
|
|
370
409
|
if (
|
|
371
410
|
approval.waitTokenId !== undefined &&
|
|
372
411
|
approval.provenanceRef?.runId !== undefined &&
|
|
373
412
|
(parsed.value.decision === 'approve' || parsed.value.decision === 'reject')
|
|
374
413
|
) {
|
|
375
|
-
const
|
|
376
|
-
parsed.value.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
414
|
+
const decided: GateDecisionValue = {
|
|
415
|
+
decided: parsed.value.decision,
|
|
416
|
+
...(parsed.value.rationale !== undefined && { rationale: parsed.value.rationale }),
|
|
417
|
+
decidedBy: `user:${userId as unknown as string}`,
|
|
418
|
+
approvalId: approval.id as unknown as string,
|
|
419
|
+
};
|
|
420
|
+
const resumeValue = parsed.value.value !== undefined ? parsed.value.value : decided;
|
|
382
421
|
const resolved = await runBinding.completeToken(
|
|
383
422
|
tenantId,
|
|
384
423
|
approval.provenanceRef.runId as RunId,
|
|
@@ -661,6 +700,18 @@ function serializeApproval(a: Approval): Record<string, unknown> {
|
|
|
661
700
|
updatedAt: a.updatedAt,
|
|
662
701
|
decidedAt: a.decidedAt,
|
|
663
702
|
expiresAt: a.expiresAt,
|
|
703
|
+
decision: a.decision === undefined ? undefined : serializeApprovalDecision(a.decision),
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function serializeApprovalDecision(d: ReviewDecisionRecord): Record<string, unknown> {
|
|
708
|
+
return {
|
|
709
|
+
decision: d.decision,
|
|
710
|
+
...(d.decidedBy !== undefined && { decidedBy: d.decidedBy }),
|
|
711
|
+
reviewerId: d.reviewerId,
|
|
712
|
+
reviewerRoleAtDecision: d.reviewerRoleAtDecision,
|
|
713
|
+
decidedAt: d.decidedAt,
|
|
714
|
+
...(d.rationale !== undefined && { rationale: d.rationale }),
|
|
664
715
|
};
|
|
665
716
|
}
|
|
666
717
|
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import { type Context, Hono } from 'hono';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
BLOCK_KINDS,
|
|
8
|
+
type BlockDefinition,
|
|
9
|
+
type BlockKind,
|
|
10
|
+
settingsSchemaIssues,
|
|
11
|
+
validateBlock,
|
|
12
|
+
} from '@kindgi/agents';
|
|
13
|
+
import { ref } from '@kindgi/authz';
|
|
14
|
+
import type { Cursor, ProjectId, TenantId } from '@kindgi/types';
|
|
15
|
+
|
|
16
|
+
import type { BlockPublishOutcome, BlockRecord, BlockRegistryBinding } from '../block-binding.js';
|
|
17
|
+
import { statusFor, toWireError } from '../errors.js';
|
|
18
|
+
import type { Authorizer } from '../middleware/authorize.js';
|
|
19
|
+
import type { AppEnv } from '../types.js';
|
|
20
|
+
import { clampLimit } from './pagination.js';
|
|
21
|
+
import { parseScopeParams } from './scope-params.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Data blocks: versioned prompts and settings that agent versions pin
|
|
25
|
+
* (`/v1/blocks`).
|
|
26
|
+
*
|
|
27
|
+
* A block belongs to one project and is authorized through it: reading
|
|
28
|
+
* is `read` on the project, publishing, unregistering and reinstating
|
|
29
|
+
* are `write` on it. A block the caller can't read answers 404, as one
|
|
30
|
+
* that doesn't exist.
|
|
31
|
+
*
|
|
32
|
+
* A version never changes, nor does a block's kind. A settings block's
|
|
33
|
+
* values must satisfy the latest version's schema as well as their
|
|
34
|
+
* own, so whatever reads them sees a stable shape.
|
|
35
|
+
*/
|
|
36
|
+
export function blocksRouter(binding: BlockRegistryBinding, authorizer?: Authorizer): Hono<AppEnv> {
|
|
37
|
+
const r = new Hono<AppEnv>();
|
|
38
|
+
|
|
39
|
+
async function canProject(
|
|
40
|
+
c: Context<AppEnv>,
|
|
41
|
+
action: 'read' | 'write',
|
|
42
|
+
projectId: ProjectId,
|
|
43
|
+
): Promise<boolean> {
|
|
44
|
+
return authorizer === undefined || authorizer.can(c, action, ref('project', projectId));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function notFound(c: Context<AppEnv>, blockId: string, version?: string) {
|
|
48
|
+
c.status(statusFor('block-not-found') as never);
|
|
49
|
+
return c.json(
|
|
50
|
+
toWireError(
|
|
51
|
+
{
|
|
52
|
+
code: 'block-not-found',
|
|
53
|
+
message: `No block "${blockId}"${version !== undefined ? ` at version "${version}"` : ''}`,
|
|
54
|
+
blockId,
|
|
55
|
+
...(version !== undefined && { version }),
|
|
56
|
+
},
|
|
57
|
+
c.get('requestId'),
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function badInput(c: Context<AppEnv>, message: string) {
|
|
63
|
+
c.status(statusFor('bad-input') as never);
|
|
64
|
+
return c.json(toWireError({ code: 'bad-input', message }, c.get('requestId')));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------- GET / (each block's latest version) ----------
|
|
68
|
+
r.get('/', async (c) => {
|
|
69
|
+
const tenantId = c.get('tenantId') as TenantId;
|
|
70
|
+
const kind = c.req.query('kind');
|
|
71
|
+
if (kind !== undefined && kind.length > 0 && !isBlockKind(kind)) {
|
|
72
|
+
return badInput(
|
|
73
|
+
c,
|
|
74
|
+
`Unknown block kind "${kind}". Expected one of: ${BLOCK_KINDS.join(', ')}.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
const scope = parseScopeParams(c.req.query(), { tenantId });
|
|
78
|
+
if (scope.kind === 'err') {
|
|
79
|
+
c.status(statusFor('scope-invalid') as never);
|
|
80
|
+
return c.json(
|
|
81
|
+
toWireError({ code: 'scope-invalid', message: scope.message }, c.get('requestId')),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const cursor = c.req.query('cursor');
|
|
85
|
+
const name = c.req.query('name');
|
|
86
|
+
const page = await binding.list({
|
|
87
|
+
tenantId,
|
|
88
|
+
limit: clampLimit(c.req.query('limit')),
|
|
89
|
+
...(cursor !== undefined && cursor.length > 0 && { cursor: cursor as Cursor }),
|
|
90
|
+
...(kind !== undefined && kind.length > 0 && { blockKind: kind as BlockKind }),
|
|
91
|
+
...(name !== undefined && name.length > 0 && { nameFilter: name }),
|
|
92
|
+
...(scope.scope !== undefined && { scope: scope.scope }),
|
|
93
|
+
});
|
|
94
|
+
const visible =
|
|
95
|
+
authorizer === undefined
|
|
96
|
+
? page.data
|
|
97
|
+
: await authorizer.filterByCan(c, 'read', page.data, (b) => ref('project', b.projectId));
|
|
98
|
+
return c.json({
|
|
99
|
+
data: visible.map(serializeBlock),
|
|
100
|
+
hasMore: page.nextCursor !== undefined,
|
|
101
|
+
...(page.nextCursor !== undefined && { nextCursor: page.nextCursor as unknown as string }),
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ---------- GET /:blockId (latest version) ----------
|
|
106
|
+
r.get('/:blockId', async (c) => {
|
|
107
|
+
const blockId = c.req.param('blockId');
|
|
108
|
+
const block = await binding.get({ tenantId: c.get('tenantId') as TenantId, blockId });
|
|
109
|
+
if (block === null || !(await canProject(c, 'read', block.projectId))) {
|
|
110
|
+
return notFound(c, blockId);
|
|
111
|
+
}
|
|
112
|
+
return c.json(serializeBlock(block));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ---------- GET /:blockId/versions ----------
|
|
116
|
+
r.get('/:blockId/versions', async (c) => {
|
|
117
|
+
const tenantId = c.get('tenantId') as TenantId;
|
|
118
|
+
const blockId = c.req.param('blockId');
|
|
119
|
+
const cursor = c.req.query('cursor');
|
|
120
|
+
const page = await binding.listVersions({
|
|
121
|
+
tenantId,
|
|
122
|
+
blockId,
|
|
123
|
+
limit: clampLimit(c.req.query('limit')),
|
|
124
|
+
includeTombstoned: c.req.query('includeTombstoned') === 'true',
|
|
125
|
+
...(cursor !== undefined && cursor.length > 0 && { cursor: cursor as Cursor }),
|
|
126
|
+
});
|
|
127
|
+
const owner = page.data[0] ?? (await anyVersion(tenantId, blockId));
|
|
128
|
+
if (owner === null || !(await canProject(c, 'read', owner.projectId))) {
|
|
129
|
+
return notFound(c, blockId);
|
|
130
|
+
}
|
|
131
|
+
return c.json({
|
|
132
|
+
data: page.data.map(serializeBlock),
|
|
133
|
+
hasMore: page.nextCursor !== undefined,
|
|
134
|
+
...(page.nextCursor !== undefined && { nextCursor: page.nextCursor as unknown as string }),
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ---------- GET /:blockId/versions/:version ----------
|
|
139
|
+
r.get('/:blockId/versions/:version', async (c) => {
|
|
140
|
+
const blockId = c.req.param('blockId');
|
|
141
|
+
const version = c.req.param('version');
|
|
142
|
+
const block = await binding.getVersion({
|
|
143
|
+
tenantId: c.get('tenantId') as TenantId,
|
|
144
|
+
blockId,
|
|
145
|
+
version,
|
|
146
|
+
});
|
|
147
|
+
if (block === null || !(await canProject(c, 'read', block.projectId))) {
|
|
148
|
+
return notFound(c, blockId, version);
|
|
149
|
+
}
|
|
150
|
+
return c.json(serializeBlock(block));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ---------- POST / (publish a version) ----------
|
|
154
|
+
r.post('/', async (c) => {
|
|
155
|
+
const requestId = c.get('requestId');
|
|
156
|
+
const tenantId = c.get('tenantId') as TenantId;
|
|
157
|
+
let body: unknown;
|
|
158
|
+
try {
|
|
159
|
+
body = await c.req.json();
|
|
160
|
+
} catch {
|
|
161
|
+
return badInput(c, 'Request body must be valid JSON');
|
|
162
|
+
}
|
|
163
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body)) {
|
|
164
|
+
return badInput(c, 'Request body must be an object');
|
|
165
|
+
}
|
|
166
|
+
const { projectId, ...definition } = body as Record<string, unknown>;
|
|
167
|
+
if (typeof projectId !== 'string' || projectId.length === 0) {
|
|
168
|
+
return badInput(c, '`projectId` is required');
|
|
169
|
+
}
|
|
170
|
+
if (!(await canProject(c, 'write', projectId as ProjectId))) {
|
|
171
|
+
c.status(statusFor('permission-denied') as never);
|
|
172
|
+
return c.json(
|
|
173
|
+
toWireError(
|
|
174
|
+
{
|
|
175
|
+
code: 'permission-denied',
|
|
176
|
+
message: `Publishing a block needs write on project "${projectId}"`,
|
|
177
|
+
},
|
|
178
|
+
requestId,
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const validated = validateBlock(definition);
|
|
183
|
+
if (validated.kind === 'err')
|
|
184
|
+
return validationFailed(c, validated.error.message, validated.error.issues);
|
|
185
|
+
const block = validated.value;
|
|
186
|
+
|
|
187
|
+
// A block keeps its kind, and a settings block's latest schema holds.
|
|
188
|
+
const latest = await binding.get({ tenantId, blockId: block.id });
|
|
189
|
+
const continuity = continuityIssues(latest, block);
|
|
190
|
+
if (continuity.length > 0) {
|
|
191
|
+
return validationFailed(c, `Block "${block.id}" can't take this version`, continuity);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const outcome = await binding.publish({ tenantId, projectId: projectId as ProjectId, block });
|
|
195
|
+
return published(c, outcome);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ---------- POST /:blockId/versions/:version/unregister ----------
|
|
199
|
+
r.post('/:blockId/versions/:version/unregister', async (c) => {
|
|
200
|
+
const tenantId = c.get('tenantId') as TenantId;
|
|
201
|
+
const blockId = c.req.param('blockId');
|
|
202
|
+
const version = c.req.param('version');
|
|
203
|
+
const block = await binding.getVersion({ tenantId, blockId, version });
|
|
204
|
+
if (block === null || !(await canProject(c, 'read', block.projectId))) {
|
|
205
|
+
return notFound(c, blockId, version);
|
|
206
|
+
}
|
|
207
|
+
if (!(await canProject(c, 'write', block.projectId))) return denied(c, block.projectId);
|
|
208
|
+
const outcome = await binding.unregister({ tenantId, blockId, version });
|
|
209
|
+
if (!outcome.unregistered) return notFound(c, blockId, version);
|
|
210
|
+
return c.json({ blockId, version, unregistered: true });
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ---------- POST /:blockId/versions/:version/reinstate ----------
|
|
214
|
+
r.post('/:blockId/versions/:version/reinstate', async (c) => {
|
|
215
|
+
const tenantId = c.get('tenantId') as TenantId;
|
|
216
|
+
const blockId = c.req.param('blockId');
|
|
217
|
+
const version = c.req.param('version');
|
|
218
|
+
const block = await binding.getVersion({ tenantId, blockId, version });
|
|
219
|
+
if (block === null || !(await canProject(c, 'read', block.projectId))) {
|
|
220
|
+
return notFound(c, blockId, version);
|
|
221
|
+
}
|
|
222
|
+
if (!(await canProject(c, 'write', block.projectId))) return denied(c, block.projectId);
|
|
223
|
+
const outcome = await binding.reinstateVersion({ tenantId, blockId, version });
|
|
224
|
+
if (outcome.kind === 'not-found') return notFound(c, blockId, version);
|
|
225
|
+
return c.json({ blockId, version, wasTombstoned: outcome.wasTombstoned });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
/** Any version of a block, unregistered ones included: whose project it is. */
|
|
229
|
+
async function anyVersion(tenantId: TenantId, blockId: string): Promise<BlockRecord | null> {
|
|
230
|
+
const page = await binding.listVersions({
|
|
231
|
+
tenantId,
|
|
232
|
+
blockId,
|
|
233
|
+
limit: 1,
|
|
234
|
+
includeTombstoned: true,
|
|
235
|
+
});
|
|
236
|
+
return page.data[0] ?? null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return r;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function isBlockKind(value: string): value is BlockKind {
|
|
243
|
+
return (BLOCK_KINDS as readonly string[]).includes(value);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** A new version against the block's latest: same kind, and a settings block's schema holds. */
|
|
247
|
+
function continuityIssues(
|
|
248
|
+
latest: BlockRecord | null,
|
|
249
|
+
block: BlockDefinition,
|
|
250
|
+
): { path: string; message: string }[] {
|
|
251
|
+
if (latest === null) return [];
|
|
252
|
+
if (latest.kind !== block.kind) {
|
|
253
|
+
return [
|
|
254
|
+
{
|
|
255
|
+
path: '/kind',
|
|
256
|
+
message: `block "${block.id}" is a ${latest.kind} block; a version can't change its kind`,
|
|
257
|
+
},
|
|
258
|
+
];
|
|
259
|
+
}
|
|
260
|
+
if (
|
|
261
|
+
latest.kind === 'settings' &&
|
|
262
|
+
block.kind === 'settings' &&
|
|
263
|
+
latest.content.schema !== undefined
|
|
264
|
+
) {
|
|
265
|
+
return settingsSchemaIssues(block.content.values, latest.content.schema).map((i) => ({
|
|
266
|
+
...i,
|
|
267
|
+
message: `${i.message} (the schema of version ${latest.version})`,
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** The response to a publish outcome. */
|
|
274
|
+
function published(c: Context<AppEnv>, outcome: BlockPublishOutcome) {
|
|
275
|
+
const requestId = c.get('requestId');
|
|
276
|
+
switch (outcome.kind) {
|
|
277
|
+
case 'ok':
|
|
278
|
+
c.status(201);
|
|
279
|
+
return c.json({ blockId: outcome.blockId, version: outcome.version });
|
|
280
|
+
case 'already-registered':
|
|
281
|
+
c.status(statusFor('block-already-registered') as never);
|
|
282
|
+
return c.json(
|
|
283
|
+
toWireError(
|
|
284
|
+
{
|
|
285
|
+
code: 'block-already-registered',
|
|
286
|
+
message: `Block "${outcome.blockId}" version "${outcome.version}" is already published; versions never change, so publish a new one`,
|
|
287
|
+
blockId: outcome.blockId,
|
|
288
|
+
version: outcome.version,
|
|
289
|
+
},
|
|
290
|
+
requestId,
|
|
291
|
+
),
|
|
292
|
+
);
|
|
293
|
+
case 'project-mismatch':
|
|
294
|
+
c.status(statusFor('block-project-mismatch') as never);
|
|
295
|
+
return c.json(
|
|
296
|
+
toWireError(
|
|
297
|
+
{
|
|
298
|
+
code: 'block-project-mismatch',
|
|
299
|
+
message: `Block "${outcome.blockId}" belongs to project "${outcome.projectId as unknown as string}"; publish its versions there`,
|
|
300
|
+
blockId: outcome.blockId,
|
|
301
|
+
projectId: outcome.projectId as unknown as string,
|
|
302
|
+
},
|
|
303
|
+
requestId,
|
|
304
|
+
),
|
|
305
|
+
);
|
|
306
|
+
case 'project-not-found':
|
|
307
|
+
c.status(statusFor('bad-input') as never);
|
|
308
|
+
return c.json(
|
|
309
|
+
toWireError(
|
|
310
|
+
{
|
|
311
|
+
code: 'bad-input',
|
|
312
|
+
message: `\`projectId\` "${outcome.projectId as unknown as string}" does not resolve to a project in this tenant`,
|
|
313
|
+
},
|
|
314
|
+
requestId,
|
|
315
|
+
),
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function validationFailed(
|
|
321
|
+
c: Context<AppEnv>,
|
|
322
|
+
message: string,
|
|
323
|
+
issues: readonly { readonly path: string; readonly message: string }[],
|
|
324
|
+
) {
|
|
325
|
+
c.status(statusFor('validation-failed') as never);
|
|
326
|
+
return c.json(
|
|
327
|
+
toWireError(
|
|
328
|
+
{
|
|
329
|
+
code: 'validation-failed',
|
|
330
|
+
message,
|
|
331
|
+
issues: issues as unknown as Record<string, unknown>[],
|
|
332
|
+
},
|
|
333
|
+
c.get('requestId'),
|
|
334
|
+
),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function denied(c: Context<AppEnv>, projectId: ProjectId) {
|
|
339
|
+
c.status(statusFor('permission-denied') as never);
|
|
340
|
+
return c.json(
|
|
341
|
+
toWireError(
|
|
342
|
+
{
|
|
343
|
+
code: 'permission-denied',
|
|
344
|
+
message: `This needs write on the block's project "${projectId as unknown as string}"`,
|
|
345
|
+
},
|
|
346
|
+
c.get('requestId'),
|
|
347
|
+
),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function serializeBlock(b: BlockRecord): Record<string, unknown> {
|
|
352
|
+
return {
|
|
353
|
+
id: b.id,
|
|
354
|
+
version: b.version,
|
|
355
|
+
kind: b.kind,
|
|
356
|
+
...(b.description !== undefined && { description: b.description }),
|
|
357
|
+
content: b.content,
|
|
358
|
+
projectId: b.projectId as unknown as string,
|
|
359
|
+
publishedAt: b.publishedAt,
|
|
360
|
+
...(b.unregisteredAt !== undefined && { unregisteredAt: b.unregisteredAt }),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
@@ -10,12 +10,16 @@ import type {
|
|
|
10
10
|
ConversationMessage,
|
|
11
11
|
ConversationPageCursor,
|
|
12
12
|
} from '@kindgi/agents';
|
|
13
|
+
import type { ProjectBinding } from '@kindgi/platform';
|
|
13
14
|
import type { RunBinding } from '@kindgi/runtime';
|
|
14
|
-
import type { AgentId, Semver, TenantId } from '@kindgi/types';
|
|
15
|
+
import type { AgentId, ProjectId, Semver, TenantId } from '@kindgi/types';
|
|
15
16
|
|
|
16
17
|
import { statusFor, toWireError } from '../errors.js';
|
|
17
18
|
import type { AppEnv } from '../types.js';
|
|
18
19
|
import { clampLimit, decodeCursor, encodeCursor } from './pagination.js';
|
|
20
|
+
import { parseListScope } from './scope-params.js';
|
|
21
|
+
|
|
22
|
+
const PROJECT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
25
|
* Conversations resource routes. All storage access goes through the
|
|
@@ -27,6 +31,11 @@ import { clampLimit, decodeCursor, encodeCursor } from './pagination.js';
|
|
|
27
31
|
export function conversationsRouter(
|
|
28
32
|
conversationBinding: ConversationBinding,
|
|
29
33
|
runBinding: RunBinding,
|
|
34
|
+
/**
|
|
35
|
+
* When wired, a `projectId` given to `POST /` must be one of the
|
|
36
|
+
* tenant's projects, and an omitted one is the tenant's Default project.
|
|
37
|
+
*/
|
|
38
|
+
projectBinding?: ProjectBinding,
|
|
30
39
|
): Hono<AppEnv> {
|
|
31
40
|
const r = new Hono<AppEnv>();
|
|
32
41
|
|
|
@@ -53,6 +62,14 @@ export function conversationsRouter(
|
|
|
53
62
|
|
|
54
63
|
const agentIdRaw = c.req.query('agentId');
|
|
55
64
|
|
|
65
|
+
const scopeParsed = parseListScope(c.req.query(), { tenantId });
|
|
66
|
+
if (scopeParsed.kind === 'err') {
|
|
67
|
+
c.status(statusFor('scope-invalid') as never);
|
|
68
|
+
return c.json(
|
|
69
|
+
toWireError({ code: 'scope-invalid', message: scopeParsed.message }, requestId),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
56
73
|
let before: ConversationPageCursor | undefined;
|
|
57
74
|
const rawCursor = c.req.query('cursor');
|
|
58
75
|
if (rawCursor !== undefined && rawCursor.length > 0) {
|
|
@@ -68,6 +85,7 @@ export function conversationsRouter(
|
|
|
68
85
|
|
|
69
86
|
const listResult = await conversationBinding.listConversationsPage({
|
|
70
87
|
tenantId,
|
|
88
|
+
...(scopeParsed.scope !== undefined && { scope: scopeParsed.scope }),
|
|
71
89
|
...(agentIdRaw !== undefined && agentIdRaw.length > 0 && { agentId: agentIdRaw as AgentId }),
|
|
72
90
|
...(statusFilter !== undefined && { status: statusFilter }),
|
|
73
91
|
...(before !== undefined && { before }),
|
|
@@ -126,9 +144,30 @@ export function conversationsRouter(
|
|
|
126
144
|
c.status(statusFor(parsed.error.code) as never);
|
|
127
145
|
return c.json(toWireError(parsed.error, requestId));
|
|
128
146
|
}
|
|
147
|
+
const supplied = parsed.value.projectId;
|
|
148
|
+
if (
|
|
149
|
+
supplied !== undefined &&
|
|
150
|
+
projectBinding !== undefined &&
|
|
151
|
+
(await projectBinding.get(tenantId, supplied)) === undefined
|
|
152
|
+
) {
|
|
153
|
+
c.status(statusFor('bad-input') as never);
|
|
154
|
+
return c.json(
|
|
155
|
+
toWireError(
|
|
156
|
+
{
|
|
157
|
+
code: 'bad-input',
|
|
158
|
+
message: `\`projectId\` "${supplied as unknown as string}" does not resolve to a project in this tenant`,
|
|
159
|
+
},
|
|
160
|
+
requestId,
|
|
161
|
+
),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
// Omitted: the tenant's Default project, as for a run. No Default (or
|
|
165
|
+
// no project binding): no project, never a refusal.
|
|
166
|
+
const projectId = supplied ?? (await projectBinding?.getDefault(tenantId))?.id;
|
|
129
167
|
|
|
130
168
|
const opened = await conversationBinding.openConversation({
|
|
131
169
|
tenantId,
|
|
170
|
+
...(projectId !== undefined && { projectId }),
|
|
132
171
|
agentId: parsed.value.agentId,
|
|
133
172
|
agentVersion: parsed.value.agentVersion,
|
|
134
173
|
title: parsed.value.title,
|
|
@@ -232,6 +271,7 @@ function serializeConversation(c: Conversation): Record<string, unknown> {
|
|
|
232
271
|
agentVersion: c.agentVersion as unknown as string,
|
|
233
272
|
title: c.title,
|
|
234
273
|
...(c.participantId !== undefined && { participantId: c.participantId }),
|
|
274
|
+
...(c.projectId !== undefined && { projectId: c.projectId as unknown as string }),
|
|
235
275
|
scope: c.scope,
|
|
236
276
|
status: (c.closedAt === undefined ? 'open' : 'closed') as 'open' | 'closed',
|
|
237
277
|
openedAt: c.openedAt as unknown as string,
|
|
@@ -283,6 +323,7 @@ interface ParsedOpenBody {
|
|
|
283
323
|
readonly agentVersion: Semver;
|
|
284
324
|
readonly title: string;
|
|
285
325
|
readonly scope: Readonly<Record<string, unknown>>;
|
|
326
|
+
readonly projectId?: ProjectId;
|
|
286
327
|
readonly participantId?: string;
|
|
287
328
|
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
288
329
|
}
|
|
@@ -335,6 +376,20 @@ function parseOpenBody(
|
|
|
335
376
|
scope = scopeRaw as Readonly<Record<string, unknown>>;
|
|
336
377
|
}
|
|
337
378
|
|
|
379
|
+
const projectIdRaw = b.projectId;
|
|
380
|
+
if (
|
|
381
|
+
projectIdRaw !== undefined &&
|
|
382
|
+
(typeof projectIdRaw !== 'string' || !PROJECT_ID_RE.test(projectIdRaw))
|
|
383
|
+
) {
|
|
384
|
+
return {
|
|
385
|
+
kind: 'err',
|
|
386
|
+
error: {
|
|
387
|
+
code: 'bad-input',
|
|
388
|
+
message: '`projectId` must be a project id (a UUID) when supplied',
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
338
393
|
const participantIdRaw = b.participantId;
|
|
339
394
|
if (participantIdRaw !== undefined && typeof participantIdRaw !== 'string') {
|
|
340
395
|
return {
|
|
@@ -362,6 +417,7 @@ function parseOpenBody(
|
|
|
362
417
|
agentVersion: agentVersion as Semver,
|
|
363
418
|
title,
|
|
364
419
|
scope,
|
|
420
|
+
...(projectIdRaw !== undefined && { projectId: projectIdRaw as ProjectId }),
|
|
365
421
|
...(participantIdRaw !== undefined && { participantId: participantIdRaw }),
|
|
366
422
|
...(metadata !== undefined && { metadata }),
|
|
367
423
|
},
|