@kindgi/api 0.1.4-rc.0 → 0.1.4-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-binding.d.ts +20 -2
- package/dist/agent-binding.d.ts.map +1 -1
- package/dist/agent-pins.d.ts.map +1 -1
- package/dist/agent-pins.js +4 -1
- package/dist/agent-pins.js.map +1 -1
- package/dist/app.d.ts +8 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +30 -5
- package/dist/app.js.map +1 -1
- package/dist/deploy-versions.d.ts +10 -5
- package/dist/deploy-versions.d.ts.map +1 -1
- package/dist/deploy-versions.js +4 -10
- package/dist/deploy-versions.js.map +1 -1
- package/dist/derive-agent-version.d.ts +9 -1
- package/dist/derive-agent-version.d.ts.map +1 -1
- package/dist/derive-agent-version.js +11 -1
- package/dist/derive-agent-version.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +26 -0
- package/dist/errors.js.map +1 -1
- package/dist/eval-case-binding.d.ts +10 -0
- package/dist/eval-case-binding.d.ts.map +1 -1
- package/dist/eval-run-binding.d.ts +21 -3
- package/dist/eval-run-binding.d.ts.map +1 -1
- package/dist/eval-run-binding.js.map +1 -1
- package/dist/eval-run-dispatcher.d.ts +3 -0
- package/dist/eval-run-dispatcher.d.ts.map +1 -1
- package/dist/eval-run-dispatcher.js.map +1 -1
- package/dist/flow-binding.d.ts +8 -0
- package/dist/flow-binding.d.ts.map +1 -1
- package/dist/flow-pins.d.ts +20 -7
- package/dist/flow-pins.d.ts.map +1 -1
- package/dist/flow-pins.js +36 -11
- package/dist/flow-pins.js.map +1 -1
- package/dist/gate-policy-binding.d.ts +164 -0
- package/dist/gate-policy-binding.d.ts.map +1 -0
- package/dist/gate-policy-binding.js +12 -0
- package/dist/gate-policy-binding.js.map +1 -0
- package/dist/gate.d.ts +56 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +359 -0
- package/dist/gate.js.map +1 -0
- package/dist/guardrail-binding.d.ts +8 -0
- package/dist/guardrail-binding.d.ts.map +1 -1
- package/dist/handler-binding.d.ts +17 -2
- package/dist/handler-binding.d.ts.map +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/judged-dispatcher.d.ts +20 -1
- package/dist/judged-dispatcher.d.ts.map +1 -1
- package/dist/judged-dispatcher.js +57 -7
- package/dist/judged-dispatcher.js.map +1 -1
- package/dist/judgment-binding.d.ts +38 -0
- package/dist/judgment-binding.d.ts.map +1 -1
- package/dist/judgment-binding.js +19 -0
- package/dist/judgment-binding.js.map +1 -1
- package/dist/live-version-binding.d.ts +206 -0
- package/dist/live-version-binding.d.ts.map +1 -0
- package/dist/live-version-binding.js +4 -0
- package/dist/live-version-binding.js.map +1 -0
- package/dist/middleware/authorize.d.ts.map +1 -1
- package/dist/middleware/authorize.js +13 -2
- package/dist/middleware/authorize.js.map +1 -1
- package/dist/middleware/project-ref.d.ts +25 -0
- package/dist/middleware/project-ref.d.ts.map +1 -0
- package/dist/middleware/project-ref.js +72 -0
- package/dist/middleware/project-ref.js.map +1 -0
- package/dist/openapi/generate.d.ts.map +1 -1
- package/dist/openapi/generate.js +4 -0
- package/dist/openapi/generate.js.map +1 -1
- package/dist/openapi/operations.d.ts +6 -0
- package/dist/openapi/operations.d.ts.map +1 -1
- package/dist/openapi/operations.js +524 -27
- package/dist/openapi/operations.js.map +1 -1
- package/dist/openapi/schemas.d.ts +43 -0
- package/dist/openapi/schemas.d.ts.map +1 -1
- package/dist/openapi/schemas.js +1050 -7
- package/dist/openapi/schemas.js.map +1 -1
- package/dist/publish-refused.d.ts +18 -0
- package/dist/publish-refused.d.ts.map +1 -0
- package/dist/publish-refused.js +22 -0
- package/dist/publish-refused.js.map +1 -0
- package/dist/registry-read-only.d.ts +32 -0
- package/dist/registry-read-only.d.ts.map +1 -0
- package/dist/registry-read-only.js +22 -0
- package/dist/registry-read-only.js.map +1 -0
- package/dist/retention-binding.d.ts +29 -0
- package/dist/retention-binding.d.ts.map +1 -1
- package/dist/routes/agent-releases.d.ts +40 -0
- package/dist/routes/agent-releases.d.ts.map +1 -0
- package/dist/routes/agent-releases.js +462 -0
- package/dist/routes/agent-releases.js.map +1 -0
- package/dist/routes/agents.d.ts +7 -1
- package/dist/routes/agents.d.ts.map +1 -1
- package/dist/routes/agents.js +38 -4
- package/dist/routes/agents.js.map +1 -1
- package/dist/routes/approvals.d.ts.map +1 -1
- package/dist/routes/approvals.js +27 -9
- package/dist/routes/approvals.js.map +1 -1
- package/dist/routes/audit.d.ts.map +1 -1
- package/dist/routes/audit.js +7 -0
- package/dist/routes/audit.js.map +1 -1
- package/dist/routes/auth.js +1 -1
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/blocks.d.ts.map +1 -1
- package/dist/routes/blocks.js +37 -12
- package/dist/routes/blocks.js.map +1 -1
- package/dist/routes/conversations.d.ts +0 -7
- package/dist/routes/conversations.d.ts.map +1 -1
- package/dist/routes/conversations.js +19 -3
- package/dist/routes/conversations.js.map +1 -1
- package/dist/routes/deployments.d.ts +6 -0
- package/dist/routes/deployments.d.ts.map +1 -1
- package/dist/routes/deployments.js +49 -5
- package/dist/routes/deployments.js.map +1 -1
- package/dist/routes/env.d.ts.map +1 -1
- package/dist/routes/env.js +1 -0
- package/dist/routes/env.js.map +1 -1
- package/dist/routes/eval-comparison.d.ts +4 -2
- package/dist/routes/eval-comparison.d.ts.map +1 -1
- package/dist/routes/eval-comparison.js +59 -10
- package/dist/routes/eval-comparison.js.map +1 -1
- package/dist/routes/eval-runs.d.ts +7 -1
- package/dist/routes/eval-runs.d.ts.map +1 -1
- package/dist/routes/eval-runs.js +34 -3
- package/dist/routes/eval-runs.js.map +1 -1
- package/dist/routes/eval-versions.d.ts +25 -0
- package/dist/routes/eval-versions.d.ts.map +1 -0
- package/dist/routes/eval-versions.js +66 -0
- package/dist/routes/eval-versions.js.map +1 -0
- package/dist/routes/flows.d.ts +14 -7
- package/dist/routes/flows.d.ts.map +1 -1
- package/dist/routes/flows.js +14 -8
- package/dist/routes/flows.js.map +1 -1
- package/dist/routes/gate-policies.d.ts +19 -0
- package/dist/routes/gate-policies.d.ts.map +1 -0
- package/dist/routes/gate-policies.js +191 -0
- package/dist/routes/gate-policies.js.map +1 -0
- package/dist/routes/gate-policy-spec.d.ts +17 -0
- package/dist/routes/gate-policy-spec.d.ts.map +1 -0
- package/dist/routes/gate-policy-spec.js +192 -0
- package/dist/routes/gate-policy-spec.js.map +1 -0
- package/dist/routes/gate-policy-wire.d.ts +3 -0
- package/dist/routes/gate-policy-wire.d.ts.map +1 -0
- package/dist/routes/gate-policy-wire.js +18 -0
- package/dist/routes/gate-policy-wire.js.map +1 -0
- package/dist/routes/guardrails.d.ts.map +1 -1
- package/dist/routes/guardrails.js +4 -0
- package/dist/routes/guardrails.js.map +1 -1
- package/dist/routes/hierarchy-errors.d.ts +21 -0
- package/dist/routes/hierarchy-errors.d.ts.map +1 -1
- package/dist/routes/hierarchy-errors.js +21 -0
- package/dist/routes/hierarchy-errors.js.map +1 -1
- package/dist/routes/judged-suites.js +7 -0
- package/dist/routes/judged-suites.js.map +1 -1
- package/dist/routes/judgments.d.ts +4 -1
- package/dist/routes/judgments.d.ts.map +1 -1
- package/dist/routes/judgments.js +92 -19
- package/dist/routes/judgments.js.map +1 -1
- package/dist/routes/live-scope-wire.d.ts +32 -0
- package/dist/routes/live-scope-wire.d.ts.map +1 -0
- package/dist/routes/live-scope-wire.js +71 -0
- package/dist/routes/live-scope-wire.js.map +1 -0
- package/dist/routes/policies.d.ts +6 -3
- package/dist/routes/policies.d.ts.map +1 -1
- package/dist/routes/policies.js +55 -3
- package/dist/routes/policies.js.map +1 -1
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +59 -9
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/retention.d.ts.map +1 -1
- package/dist/routes/retention.js +18 -2
- package/dist/routes/retention.js.map +1 -1
- package/dist/routes/reviewers.d.ts +8 -1
- package/dist/routes/reviewers.d.ts.map +1 -1
- package/dist/routes/reviewers.js +20 -3
- package/dist/routes/reviewers.js.map +1 -1
- package/dist/routes/runs.d.ts.map +1 -1
- package/dist/routes/runs.js +23 -13
- package/dist/routes/runs.js.map +1 -1
- package/dist/routes/secrets.d.ts.map +1 -1
- package/dist/routes/secrets.js +2 -0
- package/dist/routes/secrets.js.map +1 -1
- package/dist/routes/segments.d.ts +17 -0
- package/dist/routes/segments.d.ts.map +1 -0
- package/dist/routes/segments.js +68 -0
- package/dist/routes/segments.js.map +1 -0
- package/dist/routes/teams.d.ts.map +1 -1
- package/dist/routes/teams.js +53 -8
- package/dist/routes/teams.js.map +1 -1
- package/dist/routes/tools.d.ts.map +1 -1
- package/dist/routes/tools.js +4 -0
- package/dist/routes/tools.js.map +1 -1
- package/dist/routes/uuid-param.d.ts +11 -0
- package/dist/routes/uuid-param.d.ts.map +1 -0
- package/dist/routes/uuid-param.js +19 -0
- package/dist/routes/uuid-param.js.map +1 -0
- package/dist/tool-binding.d.ts +8 -0
- package/dist/tool-binding.d.ts.map +1 -1
- package/openapi.json +6484 -2402
- package/package.json +21 -21
- package/src/agent-binding.ts +21 -2
- package/src/agent-pins.ts +3 -1
- package/src/app.ts +47 -4
- package/src/deploy-versions.ts +12 -15
- package/src/derive-agent-version.ts +12 -1
- package/src/errors.ts +26 -0
- package/src/eval-case-binding.ts +7 -0
- package/src/eval-run-binding.ts +31 -3
- package/src/eval-run-dispatcher.ts +3 -0
- package/src/flow-binding.ts +9 -0
- package/src/flow-pins.ts +49 -9
- package/src/gate-policy-binding.ts +171 -0
- package/src/gate.ts +467 -0
- package/src/guardrail-binding.ts +9 -0
- package/src/handler-binding.ts +17 -2
- package/src/index.ts +47 -1
- package/src/judged-dispatcher.ts +100 -9
- package/src/judgment-binding.ts +62 -0
- package/src/live-version-binding.ts +237 -0
- package/src/middleware/authorize.ts +18 -2
- package/src/middleware/project-ref.ts +88 -0
- package/src/openapi/generate.ts +5 -0
- package/src/openapi/operations.ts +664 -27
- package/src/openapi/schemas.ts +1175 -33
- package/src/publish-refused.ts +31 -0
- package/src/registry-read-only.ts +43 -0
- package/src/retention-binding.ts +30 -0
- package/src/routes/agent-releases.ts +581 -0
- package/src/routes/agents.ts +53 -3
- package/src/routes/approvals.ts +36 -8
- package/src/routes/audit.ts +10 -0
- package/src/routes/auth.ts +1 -1
- package/src/routes/blocks.ts +39 -14
- package/src/routes/conversations.ts +31 -3
- package/src/routes/deployments.ts +75 -5
- package/src/routes/env.ts +1 -0
- package/src/routes/eval-comparison.ts +59 -11
- package/src/routes/eval-runs.ts +46 -3
- package/src/routes/eval-versions.ts +110 -0
- package/src/routes/flows.ts +34 -11
- package/src/routes/gate-policies.ts +226 -0
- package/src/routes/gate-policy-spec.ts +229 -0
- package/src/routes/gate-policy-wire.ts +20 -0
- package/src/routes/guardrails.ts +7 -0
- package/src/routes/hierarchy-errors.ts +23 -0
- package/src/routes/judged-suites.ts +5 -0
- package/src/routes/judgments.ts +107 -23
- package/src/routes/live-scope-wire.ts +90 -0
- package/src/routes/policies.ts +61 -3
- package/src/routes/projects.ts +72 -9
- package/src/routes/retention.ts +21 -2
- package/src/routes/reviewers.ts +23 -4
- package/src/routes/runs.ts +34 -20
- package/src/routes/secrets.ts +2 -0
- package/src/routes/segments.ts +75 -0
- package/src/routes/teams.ts +66 -8
- package/src/routes/tools.ts +7 -0
- package/src/routes/uuid-param.ts +28 -0
- package/src/tool-binding.ts +9 -0
package/src/routes/projects.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
ProjectBinding,
|
|
11
11
|
ProjectMembership,
|
|
12
12
|
ProjectMembershipBinding,
|
|
13
|
+
ProjectMembershipUpdateRoleOutcome,
|
|
13
14
|
ProjectPatch,
|
|
14
15
|
ProjectRole,
|
|
15
16
|
ProjectSpec,
|
|
@@ -19,7 +20,12 @@ import type { Cursor, OrgId, ProjectId, TenantId, UserId } from '@kindgi/types';
|
|
|
19
20
|
import { statusFor, toWireError } from '../errors.js';
|
|
20
21
|
import type { Authorizer } from '../middleware/authorize.js';
|
|
21
22
|
import type { AppEnv } from '../types.js';
|
|
22
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
membershipNotKeptInStepError,
|
|
25
|
+
orgNotFoundError,
|
|
26
|
+
projectDefaultAlreadyExistsError,
|
|
27
|
+
slugConflictError,
|
|
28
|
+
} from './hierarchy-errors.js';
|
|
23
29
|
import { clampLimit } from './pagination.js';
|
|
24
30
|
|
|
25
31
|
/**
|
|
@@ -74,6 +80,39 @@ export function projectsRouter(
|
|
|
74
80
|
): Hono<AppEnv> {
|
|
75
81
|
const r = new Hono<AppEnv>();
|
|
76
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Change a member's role. With an authorizer, the row and its FGA tuple
|
|
85
|
+
* change together through the tenant-hierarchy binding, or the change
|
|
86
|
+
* is refused; without one, the membership binding alone.
|
|
87
|
+
*/
|
|
88
|
+
async function updateMemberRole(
|
|
89
|
+
tenantId: TenantId,
|
|
90
|
+
projectId: ProjectId,
|
|
91
|
+
userId: UserId,
|
|
92
|
+
role: ProjectRole,
|
|
93
|
+
): Promise<
|
|
94
|
+
| { readonly kind: 'done'; readonly outcome: ProjectMembershipUpdateRoleOutcome }
|
|
95
|
+
| { readonly kind: 'refused'; readonly error: ReturnType<typeof membershipNotKeptInStepError> }
|
|
96
|
+
> {
|
|
97
|
+
if (authorizer === undefined) {
|
|
98
|
+
return {
|
|
99
|
+
kind: 'done',
|
|
100
|
+
outcome: await membershipBinding.updateRole(tenantId, projectId, userId, role),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (tenantHierarchy.updateProjectMemberRole === undefined) {
|
|
104
|
+
return { kind: 'refused', error: membershipNotKeptInStepError('updateProjectMemberRole') };
|
|
105
|
+
}
|
|
106
|
+
const res = await tenantHierarchy.updateProjectMemberRole({
|
|
107
|
+
tenantId,
|
|
108
|
+
projectId,
|
|
109
|
+
userId,
|
|
110
|
+
role,
|
|
111
|
+
});
|
|
112
|
+
if (res.kind === 'err') throw new Error(res.error.message, { cause: res.error });
|
|
113
|
+
return { kind: 'done', outcome: res.value };
|
|
114
|
+
}
|
|
115
|
+
|
|
77
116
|
// ---------- GET /default (literal segment; must precede /:projectId) ----------
|
|
78
117
|
r.get('/default', async (c) => {
|
|
79
118
|
const requestId = c.get('requestId');
|
|
@@ -247,9 +286,11 @@ export function projectsRouter(
|
|
|
247
286
|
const error =
|
|
248
287
|
result.error.code === 'slug-conflict'
|
|
249
288
|
? slugConflictError('project', spec.slug)
|
|
250
|
-
: result.error.code === '
|
|
251
|
-
?
|
|
252
|
-
:
|
|
289
|
+
: result.error.code === 'org-not-found'
|
|
290
|
+
? orgNotFoundError(result.error.orgId as unknown as string)
|
|
291
|
+
: result.error.code === 'default-conflict'
|
|
292
|
+
? projectDefaultAlreadyExistsError()
|
|
293
|
+
: { code: 'internal-server-error', message: result.error.message };
|
|
253
294
|
c.status(statusFor(error.code) as never);
|
|
254
295
|
return c.json(toWireError(error, requestId));
|
|
255
296
|
}
|
|
@@ -263,7 +304,9 @@ export function projectsRouter(
|
|
|
263
304
|
const error =
|
|
264
305
|
outcome.kind === 'slug-conflict'
|
|
265
306
|
? slugConflictError('project', outcome.slug)
|
|
266
|
-
:
|
|
307
|
+
: outcome.kind === 'org-not-found'
|
|
308
|
+
? orgNotFoundError(outcome.orgId as unknown as string)
|
|
309
|
+
: projectDefaultAlreadyExistsError();
|
|
267
310
|
c.status(statusFor(error.code) as never);
|
|
268
311
|
return c.json(toWireError(error, requestId));
|
|
269
312
|
}
|
|
@@ -412,7 +455,19 @@ export function projectsRouter(
|
|
|
412
455
|
const tenantId = c.get('tenantId') as TenantId;
|
|
413
456
|
const projectId = c.req.param('projectId') as ProjectId;
|
|
414
457
|
const userId = c.req.param('userId') as UserId;
|
|
415
|
-
|
|
458
|
+
// With an authorizer, the row and its FGA tuple go together, or not
|
|
459
|
+
// at all: removing the row alone would leave the permission in place.
|
|
460
|
+
if (authorizer !== undefined) {
|
|
461
|
+
if (tenantHierarchy.removeProjectMember === undefined) {
|
|
462
|
+
const refusal = membershipNotKeptInStepError('removeProjectMember');
|
|
463
|
+
c.status(statusFor(refusal.code) as never);
|
|
464
|
+
return c.json(toWireError(refusal, c.get('requestId')));
|
|
465
|
+
}
|
|
466
|
+
const res = await tenantHierarchy.removeProjectMember({ tenantId, projectId, userId });
|
|
467
|
+
if (res.kind === 'err') throw new Error(res.error.message, { cause: res.error });
|
|
468
|
+
} else {
|
|
469
|
+
await membershipBinding.remove(tenantId, projectId, userId);
|
|
470
|
+
}
|
|
416
471
|
c.status(204);
|
|
417
472
|
return c.body(null);
|
|
418
473
|
});
|
|
@@ -452,7 +507,12 @@ export function projectsRouter(
|
|
|
452
507
|
),
|
|
453
508
|
);
|
|
454
509
|
}
|
|
455
|
-
const
|
|
510
|
+
const updated = await updateMemberRole(tenantId, projectId, userId, b.role);
|
|
511
|
+
if (updated.kind === 'refused') {
|
|
512
|
+
c.status(statusFor(updated.error.code) as never);
|
|
513
|
+
return c.json(toWireError(updated.error, requestId));
|
|
514
|
+
}
|
|
515
|
+
const { outcome } = updated;
|
|
456
516
|
if (outcome.kind === 'project-membership-not-found') {
|
|
457
517
|
c.status(statusFor('project-membership-not-found') as never);
|
|
458
518
|
return c.json(
|
|
@@ -596,8 +656,11 @@ export function projectsRouter(
|
|
|
596
656
|
),
|
|
597
657
|
);
|
|
598
658
|
}
|
|
599
|
-
if (outcome.kind === 'slug-conflict') {
|
|
600
|
-
const error =
|
|
659
|
+
if (outcome.kind === 'slug-conflict' || outcome.kind === 'org-not-found') {
|
|
660
|
+
const error =
|
|
661
|
+
outcome.kind === 'slug-conflict'
|
|
662
|
+
? slugConflictError('project', outcome.slug)
|
|
663
|
+
: orgNotFoundError(outcome.orgId as unknown as string);
|
|
601
664
|
c.status(statusFor(error.code) as never);
|
|
602
665
|
return c.json(toWireError(error, requestId));
|
|
603
666
|
}
|
package/src/routes/retention.ts
CHANGED
|
@@ -62,15 +62,21 @@ export function retentionRouter(binding: RetentionBinding, authorizer?: Authoriz
|
|
|
62
62
|
const limit = clampLimit(url.searchParams.get('limit') ?? undefined);
|
|
63
63
|
const pastGraceRaw = url.searchParams.get('pastGraceOnly');
|
|
64
64
|
const pastGraceOnly = pastGraceRaw === 'true';
|
|
65
|
+
const cursor = url.searchParams.get('cursor') ?? '';
|
|
65
66
|
|
|
66
|
-
const page = await binding.scheduled({
|
|
67
|
+
const { hasMore, nextCursor, ...page } = await binding.scheduled({
|
|
67
68
|
tenantId,
|
|
68
69
|
limit,
|
|
69
70
|
pastGraceOnly,
|
|
70
71
|
...(domain !== undefined && { domain }),
|
|
72
|
+
...(cursor !== '' && { cursor }),
|
|
71
73
|
});
|
|
72
74
|
|
|
73
|
-
return c.json(
|
|
75
|
+
return c.json({
|
|
76
|
+
...page,
|
|
77
|
+
hasMore: hasMore ?? someDomainFilled(page.data, limit),
|
|
78
|
+
...(nextCursor !== undefined && { nextCursor }),
|
|
79
|
+
});
|
|
74
80
|
});
|
|
75
81
|
|
|
76
82
|
r.post('/sweep', async (c) => {
|
|
@@ -153,6 +159,19 @@ export function retentionRouter(binding: RetentionBinding, authorizer?: Authoriz
|
|
|
153
159
|
return r;
|
|
154
160
|
}
|
|
155
161
|
|
|
162
|
+
/**
|
|
163
|
+
* A runtime that doesn't report `hasMore`: there may be more when some
|
|
164
|
+
* domain's rows fill `limit` (each domain is read up to `limit`).
|
|
165
|
+
*/
|
|
166
|
+
function someDomainFilled(
|
|
167
|
+
data: readonly { readonly domain: RetentionDomain }[],
|
|
168
|
+
limit: number,
|
|
169
|
+
): boolean {
|
|
170
|
+
const perDomain = new Map<RetentionDomain, number>();
|
|
171
|
+
for (const row of data) perDomain.set(row.domain, (perDomain.get(row.domain) ?? 0) + 1);
|
|
172
|
+
return [...perDomain.values()].some((n) => n >= limit);
|
|
173
|
+
}
|
|
174
|
+
|
|
156
175
|
function parseDomain(raw: string | null | undefined): RetentionDomain | 'invalid' | undefined {
|
|
157
176
|
if (raw === null || raw === undefined || raw === '') return undefined;
|
|
158
177
|
if ((RETENTION_DOMAINS as readonly string[]).includes(raw)) return raw as RetentionDomain;
|
package/src/routes/reviewers.ts
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
import { Hono } from 'hono';
|
|
5
5
|
|
|
6
|
-
import type
|
|
6
|
+
import { type ReviewerRole, ref } from '@kindgi/authz';
|
|
7
7
|
import type { Cursor, ReviewerId, TenantId, UserId } from '@kindgi/types';
|
|
8
8
|
|
|
9
9
|
import { statusFor, toWireError } from '../errors.js';
|
|
10
|
+
import type { Authorizer } from '../middleware/authorize.js';
|
|
10
11
|
import type { ReviewerRecord, ReviewerRegistryBinding } from '../reviewer-binding.js';
|
|
11
12
|
import type { AppEnv } from '../types.js';
|
|
12
13
|
import { clampLimit } from './pagination.js';
|
|
@@ -20,8 +21,9 @@ import { clampLimit } from './pagination.js';
|
|
|
20
21
|
* inside `approvalsRouter`: registering a reviewer is an admin surface
|
|
21
22
|
* action, not a reviewer-only one, so requiring a `reviewerRole` on the
|
|
22
23
|
* caller would be circular ("only reviewers can appoint reviewers").
|
|
23
|
-
*
|
|
24
|
-
*
|
|
24
|
+
* With authorization enforced, registering or unregistering needs
|
|
25
|
+
* `admin` on the tenant; reading the roster needs only a valid bearer
|
|
26
|
+
* token (the `/v1/*` auth chain upstream).
|
|
25
27
|
*
|
|
26
28
|
* Mounted BEFORE `approvalsRouter` on the parent `/v1` router so path
|
|
27
29
|
* matching resolves `/v1/approvals/reviewers/*` here rather than being
|
|
@@ -29,9 +31,26 @@ import { clampLimit } from './pagination.js';
|
|
|
29
31
|
*/
|
|
30
32
|
const ROLE_VALUES: ReadonlySet<ReviewerRole> = new Set(['standard', 'senior', 'admin']);
|
|
31
33
|
|
|
32
|
-
export function reviewersRouter(
|
|
34
|
+
export function reviewersRouter(
|
|
35
|
+
binding: ReviewerRegistryBinding,
|
|
36
|
+
/**
|
|
37
|
+
* When set, registering or unregistering a reviewer needs `admin` on
|
|
38
|
+
* the tenant: a reviewer (an `admin` one above all) decides approvals.
|
|
39
|
+
* Reading the list doesn't.
|
|
40
|
+
*/
|
|
41
|
+
authorizer?: Authorizer,
|
|
42
|
+
): Hono<AppEnv> {
|
|
33
43
|
const r = new Hono<AppEnv>();
|
|
34
44
|
|
|
45
|
+
if (authorizer !== undefined) {
|
|
46
|
+
r.use('*', async (c, next) => {
|
|
47
|
+
if (c.req.method === 'GET') return next();
|
|
48
|
+
const tenantId = c.get('tenantId') as TenantId;
|
|
49
|
+
const mw = authorizer.authorize('admin', () => ref('tenant', tenantId as unknown as string));
|
|
50
|
+
return mw(c, next);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
35
54
|
// ---------- GET / (list, cursor-paginated) ----------
|
|
36
55
|
r.get('/', async (c) => {
|
|
37
56
|
const requestId = c.get('requestId');
|
package/src/routes/runs.ts
CHANGED
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
// Copyright (C) 2026 Kindgi Inc.
|
|
3
3
|
|
|
4
4
|
import { Hono } from 'hono';
|
|
5
|
-
import type { MiddlewareHandler } from 'hono';
|
|
6
5
|
|
|
7
6
|
import type { AgentId } from '@kindgi/agents';
|
|
8
7
|
import type { KernelRunRecord, ListRunsInput, RunBinding } from '@kindgi/runtime';
|
|
9
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
FlowId,
|
|
10
|
+
ListScope,
|
|
11
|
+
ProjectId,
|
|
12
|
+
RunId,
|
|
13
|
+
ScopeSegment,
|
|
14
|
+
Semver,
|
|
15
|
+
TenantId,
|
|
16
|
+
} from '@kindgi/types';
|
|
10
17
|
|
|
11
18
|
import { ref } from '@kindgi/authz';
|
|
12
19
|
|
|
@@ -20,9 +27,11 @@ import type {
|
|
|
20
27
|
import type { Authorizer } from '../middleware/authorize.js';
|
|
21
28
|
import type { MintPublicRunTokenResult } from '../public-run-token.js';
|
|
22
29
|
import type { AppEnv } from '../types.js';
|
|
30
|
+
import { liveScopeToWire } from './live-scope-wire.js';
|
|
23
31
|
import type { DecodedCursor } from './pagination.js';
|
|
24
32
|
import { clampLimit, decodeCursor } from './pagination.js';
|
|
25
33
|
import { parseListScope } from './scope-params.js';
|
|
34
|
+
import { parseSegmentsBody } from './segments.js';
|
|
26
35
|
import {
|
|
27
36
|
formatSseFrame,
|
|
28
37
|
isTerminalWireKind,
|
|
@@ -30,6 +39,7 @@ import {
|
|
|
30
39
|
projectJournalEntry,
|
|
31
40
|
toRunProgressEvent,
|
|
32
41
|
} from './sse.js';
|
|
42
|
+
import { UUID_RE, refuseMalformedUuidParam } from './uuid-param.js';
|
|
33
43
|
|
|
34
44
|
const KERNEL_RUN_CHANNEL_PREFIX = 'kernel:run:';
|
|
35
45
|
|
|
@@ -61,23 +71,8 @@ const MAX_RUN_ANCESTRY = 16;
|
|
|
61
71
|
* `POST /:runId/cancel`, `POST /:runId/resume`, `GET /:runId/stream`
|
|
62
72
|
* (SSE), `GET /:runId/journal`.
|
|
63
73
|
*/
|
|
64
|
-
/** A run id
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* A `:runId` that isn't a run id is a 400, before it reaches a query (where
|
|
69
|
-
* Postgres's uuid cast would fail it as a 500).
|
|
70
|
-
*/
|
|
71
|
-
const refuseMalformedRunId: MiddlewareHandler<AppEnv> = async (c, next) => {
|
|
72
|
-
if (RUN_ID_RE.test(c.req.param('runId') ?? '')) return next();
|
|
73
|
-
c.status(statusFor('bad-input') as never);
|
|
74
|
-
return c.json(
|
|
75
|
-
toWireError(
|
|
76
|
-
{ code: 'bad-input', message: '`runId` must be a run id (a UUID)' },
|
|
77
|
-
c.get('requestId'),
|
|
78
|
-
),
|
|
79
|
-
);
|
|
80
|
-
};
|
|
74
|
+
/** A `:runId` that isn't a run id is a 400 (`uuid-param.ts`). */
|
|
75
|
+
const refuseMalformedRunId = refuseMalformedUuidParam('runId', 'a run id');
|
|
81
76
|
|
|
82
77
|
export function runsRouter(
|
|
83
78
|
binding: RunHandlerBinding,
|
|
@@ -634,6 +629,7 @@ function invokeFromBody(
|
|
|
634
629
|
tenantId,
|
|
635
630
|
...(body.projectId !== undefined && { projectId: body.projectId }),
|
|
636
631
|
input: body.input,
|
|
632
|
+
...(body.segments !== undefined && { segments: body.segments }),
|
|
637
633
|
...(body.dryRun !== undefined && { dryRun: body.dryRun }),
|
|
638
634
|
...(body.wait !== undefined && { wait: body.wait }),
|
|
639
635
|
};
|
|
@@ -681,10 +677,19 @@ function serializeRun(
|
|
|
681
677
|
id: row.agent.id,
|
|
682
678
|
version: row.agent.version,
|
|
683
679
|
conversationId: row.agent.conversationId as unknown as string,
|
|
680
|
+
...(row.agent.via !== undefined && { via: row.agent.via }),
|
|
681
|
+
...(row.agent.liveScope !== undefined && {
|
|
682
|
+
liveScope: liveScopeToWire(row.agent.liveScope),
|
|
683
|
+
}),
|
|
684
684
|
},
|
|
685
685
|
}),
|
|
686
686
|
...(row.replayOf != null && { replayOf: row.replayOf as unknown as string }),
|
|
687
687
|
...(row.evalRunId != null && { evalRunId: row.evalRunId }),
|
|
688
|
+
...(row.versions != null && { versions: row.versions }),
|
|
689
|
+
...(row.segments !== undefined &&
|
|
690
|
+
row.segments.length > 0 && {
|
|
691
|
+
segments: row.segments.map(({ key, value }) => ({ key, value })),
|
|
692
|
+
}),
|
|
688
693
|
};
|
|
689
694
|
}
|
|
690
695
|
|
|
@@ -776,7 +781,7 @@ function parseRunListFilter(
|
|
|
776
781
|
return { kind: 'err', message: '`topLevel` must be `true` or `false`' };
|
|
777
782
|
}
|
|
778
783
|
const topLevelOnly = topLevel === 'true';
|
|
779
|
-
if (parentRunId !== undefined && !
|
|
784
|
+
if (parentRunId !== undefined && !UUID_RE.test(parentRunId)) {
|
|
780
785
|
return { kind: 'err', message: '`parentRunId` must be a run id (a UUID)' };
|
|
781
786
|
}
|
|
782
787
|
if (parentRunId !== undefined && topLevelOnly) {
|
|
@@ -848,6 +853,7 @@ type ParsedStartRunBody =
|
|
|
848
853
|
readonly agentId: AgentId;
|
|
849
854
|
readonly agentVersion?: Semver;
|
|
850
855
|
readonly projectId?: ProjectId;
|
|
856
|
+
readonly segments?: readonly ScopeSegment[];
|
|
851
857
|
readonly input: unknown;
|
|
852
858
|
readonly dryRun?: boolean;
|
|
853
859
|
readonly wait?: boolean;
|
|
@@ -857,6 +863,7 @@ type ParsedStartRunBody =
|
|
|
857
863
|
readonly flowId: FlowId;
|
|
858
864
|
readonly flowVersion?: Semver;
|
|
859
865
|
readonly projectId?: ProjectId;
|
|
866
|
+
readonly segments?: readonly ScopeSegment[];
|
|
860
867
|
readonly input: unknown;
|
|
861
868
|
readonly dryRun?: boolean;
|
|
862
869
|
readonly wait?: boolean;
|
|
@@ -892,6 +899,11 @@ function parseStartRunBody(
|
|
|
892
899
|
const options = parseStartOptions(b.options);
|
|
893
900
|
if (options.kind === 'err') return options;
|
|
894
901
|
const { dryRun, wait } = options.value;
|
|
902
|
+
const parsedSegments = parseSegmentsBody(b.segments);
|
|
903
|
+
if (parsedSegments.kind === 'err') {
|
|
904
|
+
return { kind: 'err', error: { code: 'bad-input', message: parsedSegments.message } };
|
|
905
|
+
}
|
|
906
|
+
const { segments } = parsedSegments;
|
|
895
907
|
if (hasAgent) {
|
|
896
908
|
const agentVersion = b.agentVersion;
|
|
897
909
|
if (agentVersion !== undefined && typeof agentVersion !== 'string') {
|
|
@@ -914,6 +926,7 @@ function parseStartRunBody(
|
|
|
914
926
|
agentId: b.agent as AgentId,
|
|
915
927
|
...(agentVersion !== undefined && { agentVersion: agentVersion as Semver }),
|
|
916
928
|
...(agentProjectIdRaw !== undefined && { projectId: agentProjectIdRaw as ProjectId }),
|
|
929
|
+
...(segments !== undefined && { segments }),
|
|
917
930
|
input: b.input,
|
|
918
931
|
...(dryRun !== undefined && { dryRun }),
|
|
919
932
|
...(wait !== undefined && { wait }),
|
|
@@ -941,6 +954,7 @@ function parseStartRunBody(
|
|
|
941
954
|
flowId: b.flow as FlowId,
|
|
942
955
|
...(flowVersion !== undefined && { flowVersion: flowVersion as Semver }),
|
|
943
956
|
...(projectId !== undefined && { projectId: projectId as ProjectId }),
|
|
957
|
+
...(segments !== undefined && { segments }),
|
|
944
958
|
input: b.input,
|
|
945
959
|
...(dryRun !== undefined && { dryRun }),
|
|
946
960
|
...(wait !== undefined && { wait }),
|
package/src/routes/secrets.ts
CHANGED
|
@@ -160,6 +160,7 @@ export function secretsRouter(options: SecretsRouterOptions): Hono<AppEnv> {
|
|
|
160
160
|
});
|
|
161
161
|
return c.json({
|
|
162
162
|
data: page.data.map(serializeSecretRecord),
|
|
163
|
+
hasMore: page.nextCursor !== undefined,
|
|
163
164
|
...(page.nextCursor !== undefined && { nextCursor: page.nextCursor as unknown as string }),
|
|
164
165
|
});
|
|
165
166
|
});
|
|
@@ -232,6 +233,7 @@ export function secretsRouter(options: SecretsRouterOptions): Hono<AppEnv> {
|
|
|
232
233
|
});
|
|
233
234
|
return c.json({
|
|
234
235
|
data: page.data.map(serializeVersion),
|
|
236
|
+
hasMore: page.nextCursor !== undefined,
|
|
235
237
|
...(page.nextCursor !== undefined && { nextCursor: page.nextCursor as unknown as string }),
|
|
236
238
|
});
|
|
237
239
|
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { ScopeSegment } from '@kindgi/types';
|
|
5
|
+
|
|
6
|
+
/** A segment key: lowercase, like an identifier (`company`, `contact-role`). */
|
|
7
|
+
const SEGMENT_KEY_RE = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
8
|
+
const MAX_SEGMENTS = 8;
|
|
9
|
+
const MAX_VALUE_LENGTH = 256;
|
|
10
|
+
|
|
11
|
+
type Parsed =
|
|
12
|
+
| { readonly kind: 'ok'; readonly segments?: readonly ScopeSegment[] }
|
|
13
|
+
| { readonly kind: 'err'; readonly message: string };
|
|
14
|
+
|
|
15
|
+
function checked(segments: readonly ScopeSegment[], field: string): Parsed {
|
|
16
|
+
if (segments.length === 0) return { kind: 'ok' };
|
|
17
|
+
if (segments.length > MAX_SEGMENTS) {
|
|
18
|
+
return { kind: 'err', message: `\`${field}\` takes at most ${MAX_SEGMENTS} segments` };
|
|
19
|
+
}
|
|
20
|
+
const seen = new Set<string>();
|
|
21
|
+
for (const { key, value } of segments) {
|
|
22
|
+
if (!SEGMENT_KEY_RE.test(key)) {
|
|
23
|
+
return {
|
|
24
|
+
kind: 'err',
|
|
25
|
+
message: `\`${field}\`: a segment key is lowercase letters, digits, - and _ (starting with a letter); got "${key}"`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (seen.has(key))
|
|
29
|
+
return { kind: 'err', message: `\`${field}\`: the key "${key}" appears twice` };
|
|
30
|
+
seen.add(key);
|
|
31
|
+
if (value.length === 0 || value.length > MAX_VALUE_LENGTH) {
|
|
32
|
+
return {
|
|
33
|
+
kind: 'err',
|
|
34
|
+
message: `\`${field}\`: a segment value is 1–${MAX_VALUE_LENGTH} characters (key "${key}")`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { kind: 'ok', segments };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A run's or a pin's segment path from a JSON body: an ordered list of
|
|
43
|
+
* `{key, value}`, coarse to fine (e.g. `company` then `role`).
|
|
44
|
+
*/
|
|
45
|
+
export function parseSegmentsBody(raw: unknown, field = 'segments'): Parsed {
|
|
46
|
+
if (raw === undefined) return { kind: 'ok' };
|
|
47
|
+
if (!Array.isArray(raw)) {
|
|
48
|
+
return {
|
|
49
|
+
kind: 'err',
|
|
50
|
+
message: `\`${field}\` must be an array of {key, value}, coarse to fine`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const segments: ScopeSegment[] = [];
|
|
54
|
+
for (const item of raw) {
|
|
55
|
+
const { key, value } = (item ?? {}) as { key?: unknown; value?: unknown };
|
|
56
|
+
if (typeof key !== 'string' || typeof value !== 'string') {
|
|
57
|
+
return { kind: 'err', message: `\`${field}\`: each segment is {key: string, value: string}` };
|
|
58
|
+
}
|
|
59
|
+
segments.push({ key, value });
|
|
60
|
+
}
|
|
61
|
+
return checked(segments, field);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The same from a query: repeated `segment=key:value`, in order. */
|
|
65
|
+
export function parseSegmentsQuery(values: readonly string[]): Parsed {
|
|
66
|
+
const segments: ScopeSegment[] = [];
|
|
67
|
+
for (const raw of values) {
|
|
68
|
+
const colon = raw.indexOf(':');
|
|
69
|
+
if (colon <= 0) {
|
|
70
|
+
return { kind: 'err', message: `\`segment\` is key:value; got "${raw}"` };
|
|
71
|
+
}
|
|
72
|
+
segments.push({ key: raw.slice(0, colon), value: raw.slice(colon + 1) });
|
|
73
|
+
}
|
|
74
|
+
return checked(segments, 'segment');
|
|
75
|
+
}
|
package/src/routes/teams.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
TeamBinding,
|
|
11
11
|
TeamMembership,
|
|
12
12
|
TeamMembershipBinding,
|
|
13
|
+
TeamMembershipUpdateRoleOutcome,
|
|
13
14
|
TeamPatch,
|
|
14
15
|
TeamRole,
|
|
15
16
|
TeamSpec,
|
|
@@ -19,7 +20,11 @@ import type { Cursor, OrgId, TeamId, TenantId, UserId } from '@kindgi/types';
|
|
|
19
20
|
import { statusFor, toWireError } from '../errors.js';
|
|
20
21
|
import type { Authorizer } from '../middleware/authorize.js';
|
|
21
22
|
import type { AppEnv } from '../types.js';
|
|
22
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
membershipNotKeptInStepError,
|
|
25
|
+
orgNotFoundError,
|
|
26
|
+
slugConflictError,
|
|
27
|
+
} from './hierarchy-errors.js';
|
|
23
28
|
import { clampLimit } from './pagination.js';
|
|
24
29
|
|
|
25
30
|
/**
|
|
@@ -57,6 +62,34 @@ export function teamsRouter(
|
|
|
57
62
|
): Hono<AppEnv> {
|
|
58
63
|
const r = new Hono<AppEnv>();
|
|
59
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Change a member's role. With an authorizer, the row and its FGA tuple
|
|
67
|
+
* change together through the tenant-hierarchy binding, or the change
|
|
68
|
+
* is refused; without one, the membership binding alone.
|
|
69
|
+
*/
|
|
70
|
+
async function updateMemberRole(
|
|
71
|
+
tenantId: TenantId,
|
|
72
|
+
teamId: TeamId,
|
|
73
|
+
userId: UserId,
|
|
74
|
+
role: TeamRole,
|
|
75
|
+
): Promise<
|
|
76
|
+
| { readonly kind: 'done'; readonly outcome: TeamMembershipUpdateRoleOutcome }
|
|
77
|
+
| { readonly kind: 'refused'; readonly error: ReturnType<typeof membershipNotKeptInStepError> }
|
|
78
|
+
> {
|
|
79
|
+
if (authorizer === undefined) {
|
|
80
|
+
return {
|
|
81
|
+
kind: 'done',
|
|
82
|
+
outcome: await membershipBinding.updateRole(tenantId, teamId, userId, role),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (tenantHierarchy.updateTeamMemberRole === undefined) {
|
|
86
|
+
return { kind: 'refused', error: membershipNotKeptInStepError('updateTeamMemberRole') };
|
|
87
|
+
}
|
|
88
|
+
const res = await tenantHierarchy.updateTeamMemberRole({ tenantId, teamId, userId, role });
|
|
89
|
+
if (res.kind === 'err') throw new Error(res.error.message, { cause: res.error });
|
|
90
|
+
return { kind: 'done', outcome: res.value };
|
|
91
|
+
}
|
|
92
|
+
|
|
60
93
|
// ---------- Authorization middleware ----------
|
|
61
94
|
// Full PEP surface:
|
|
62
95
|
// POST / → admin on tenant
|
|
@@ -201,7 +234,9 @@ export function teamsRouter(
|
|
|
201
234
|
const error =
|
|
202
235
|
result.error.code === 'slug-conflict'
|
|
203
236
|
? slugConflictError('team', spec.slug)
|
|
204
|
-
:
|
|
237
|
+
: result.error.code === 'org-not-found'
|
|
238
|
+
? orgNotFoundError(result.error.orgId as unknown as string)
|
|
239
|
+
: { code: 'internal-server-error', message: result.error.message };
|
|
205
240
|
c.status(statusFor(error.code) as never);
|
|
206
241
|
return c.json(toWireError(error, requestId));
|
|
207
242
|
}
|
|
@@ -209,8 +244,11 @@ export function teamsRouter(
|
|
|
209
244
|
return c.json({ id: result.value.teamId as unknown as string });
|
|
210
245
|
}
|
|
211
246
|
const outcome = await binding.create(tenantId, spec);
|
|
212
|
-
if (outcome.kind === 'slug-conflict') {
|
|
213
|
-
const error =
|
|
247
|
+
if (outcome.kind === 'slug-conflict' || outcome.kind === 'org-not-found') {
|
|
248
|
+
const error =
|
|
249
|
+
outcome.kind === 'slug-conflict'
|
|
250
|
+
? slugConflictError('team', outcome.slug)
|
|
251
|
+
: orgNotFoundError(outcome.orgId as unknown as string);
|
|
214
252
|
c.status(statusFor(error.code) as never);
|
|
215
253
|
return c.json(toWireError(error, requestId));
|
|
216
254
|
}
|
|
@@ -358,7 +396,19 @@ export function teamsRouter(
|
|
|
358
396
|
const tenantId = c.get('tenantId') as TenantId;
|
|
359
397
|
const teamId = c.req.param('teamId') as TeamId;
|
|
360
398
|
const userId = c.req.param('userId') as UserId;
|
|
361
|
-
|
|
399
|
+
// With an authorizer, the row and its FGA tuple go together, or not
|
|
400
|
+
// at all: removing the row alone would leave the permission in place.
|
|
401
|
+
if (authorizer !== undefined) {
|
|
402
|
+
if (tenantHierarchy.removeTeamMember === undefined) {
|
|
403
|
+
const refusal = membershipNotKeptInStepError('removeTeamMember');
|
|
404
|
+
c.status(statusFor(refusal.code) as never);
|
|
405
|
+
return c.json(toWireError(refusal, c.get('requestId')));
|
|
406
|
+
}
|
|
407
|
+
const res = await tenantHierarchy.removeTeamMember({ tenantId, teamId, userId });
|
|
408
|
+
if (res.kind === 'err') throw new Error(res.error.message, { cause: res.error });
|
|
409
|
+
} else {
|
|
410
|
+
await membershipBinding.remove(tenantId, teamId, userId);
|
|
411
|
+
}
|
|
362
412
|
c.status(204);
|
|
363
413
|
return c.body(null);
|
|
364
414
|
});
|
|
@@ -398,7 +448,12 @@ export function teamsRouter(
|
|
|
398
448
|
),
|
|
399
449
|
);
|
|
400
450
|
}
|
|
401
|
-
const
|
|
451
|
+
const updated = await updateMemberRole(tenantId, teamId, userId, b.role);
|
|
452
|
+
if (updated.kind === 'refused') {
|
|
453
|
+
c.status(statusFor(updated.error.code) as never);
|
|
454
|
+
return c.json(toWireError(updated.error, requestId));
|
|
455
|
+
}
|
|
456
|
+
const { outcome } = updated;
|
|
402
457
|
if (outcome.kind === 'team-membership-not-found') {
|
|
403
458
|
c.status(statusFor('team-membership-not-found') as never);
|
|
404
459
|
return c.json(
|
|
@@ -542,8 +597,11 @@ export function teamsRouter(
|
|
|
542
597
|
),
|
|
543
598
|
);
|
|
544
599
|
}
|
|
545
|
-
if (outcome.kind === 'slug-conflict') {
|
|
546
|
-
const error =
|
|
600
|
+
if (outcome.kind === 'slug-conflict' || outcome.kind === 'org-not-found') {
|
|
601
|
+
const error =
|
|
602
|
+
outcome.kind === 'slug-conflict'
|
|
603
|
+
? slugConflictError('team', outcome.slug)
|
|
604
|
+
: orgNotFoundError(outcome.orgId as unknown as string);
|
|
547
605
|
c.status(statusFor(error.code) as never);
|
|
548
606
|
return c.json(toWireError(error, requestId));
|
|
549
607
|
}
|
package/src/routes/tools.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { Cursor, ProjectId, TenantId, ToolId, UserId } from '@kindgi/types'
|
|
|
9
9
|
|
|
10
10
|
import { statusFor, toWireError } from '../errors.js';
|
|
11
11
|
import type { Authorizer } from '../middleware/authorize.js';
|
|
12
|
+
import { refuseWritesWhenReadOnly } from '../registry-read-only.js';
|
|
12
13
|
import type { ToolRegistryBinding } from '../tool-binding.js';
|
|
13
14
|
import type { AppEnv } from '../types.js';
|
|
14
15
|
import { clampLimit } from './pagination.js';
|
|
@@ -47,6 +48,12 @@ export function toolsRouter(
|
|
|
47
48
|
onWrite?: ToolWriteHook,
|
|
48
49
|
): Hono<AppEnv> {
|
|
49
50
|
const r = new Hono<AppEnv>();
|
|
51
|
+
// A read-only registry (under `kindgi dev`, the pack's files) refuses
|
|
52
|
+
// every write before anything else runs.
|
|
53
|
+
r.use(
|
|
54
|
+
'*',
|
|
55
|
+
refuseWritesWhenReadOnly(() => binding.readOnly),
|
|
56
|
+
);
|
|
50
57
|
|
|
51
58
|
// Authorization (PEP) — mirrors agents. Cascade via `tool#parent@project`
|
|
52
59
|
// written in-tx by publish; direct check via `ref('tool', businessId)`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import type { MiddlewareHandler } from 'hono';
|
|
5
|
+
|
|
6
|
+
import { statusFor, toWireError } from '../errors.js';
|
|
7
|
+
import type { AppEnv } from '../types.js';
|
|
8
|
+
|
|
9
|
+
/** A UUID, the shape of a run's and a conversation's id. */
|
|
10
|
+
export const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A path parameter that must be a UUID: one that isn't is a 400 naming
|
|
14
|
+
* it, before it reaches a query (where Postgres's uuid cast would fail it
|
|
15
|
+
* as a 500). `what` names the id, e.g. `a run id`.
|
|
16
|
+
*/
|
|
17
|
+
export function refuseMalformedUuidParam(param: string, what: string): MiddlewareHandler<AppEnv> {
|
|
18
|
+
return async (c, next) => {
|
|
19
|
+
if (UUID_RE.test(c.req.param(param) ?? '')) return next();
|
|
20
|
+
c.status(statusFor('bad-input') as never);
|
|
21
|
+
return c.json(
|
|
22
|
+
toWireError(
|
|
23
|
+
{ code: 'bad-input', message: `\`${param}\` must be ${what} (a UUID)` },
|
|
24
|
+
c.get('requestId'),
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
}
|
package/src/tool-binding.ts
CHANGED
|
@@ -6,6 +6,8 @@ import type { Scope } from '@kindgi/platform';
|
|
|
6
6
|
import type { ToolManifest } from '@kindgi/tools';
|
|
7
7
|
import type { Cursor, ProjectId, Semver, TenantId, ToolId } from '@kindgi/types';
|
|
8
8
|
|
|
9
|
+
import type { RegistryReadOnly } from './registry-read-only.js';
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
12
|
* Caller-plugged surface for the tool catalog. Versioned CRUD mirroring
|
|
11
13
|
* `AgentRegistryBinding` — the API package does NOT own registry
|
|
@@ -30,6 +32,13 @@ import type { Cursor, ProjectId, Semver, TenantId, ToolId } from '@kindgi/types'
|
|
|
30
32
|
* not-found via `headExists`).
|
|
31
33
|
*/
|
|
32
34
|
export interface ToolRegistryBinding {
|
|
35
|
+
/**
|
|
36
|
+
* Set when this registry takes no writes (under `kindgi dev`, the
|
|
37
|
+
* pack's files are the source of its tools): every write is refused
|
|
38
|
+
* with `409 registry-read-only` and this reason, before the binding is
|
|
39
|
+
* called. See `RegistryReadOnly`.
|
|
40
|
+
*/
|
|
41
|
+
readonly readOnly?: RegistryReadOnly;
|
|
33
42
|
/**
|
|
34
43
|
* Cursor-paginated list of tools (latest active version per id).
|
|
35
44
|
* Optional `nameFilter` is a prefix match on the tool id — tools use
|