@k2b/cloud 0.44.0 → 0.45.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/package.json +6 -2
- package/src/access/GroupCoverage.tsx +20 -5
- package/src/access/PermissionEditor.tsx +11 -2
- package/src/access/PrincipalAvatar.tsx +30 -21
- package/src/access/messages.ts +4 -4
- package/src/account/EntitySearch.tsx +12 -0
- package/src/ai/dictation-runtime.ts +6 -2
- package/src/ai/executor.ts +9 -3
- package/src/ai/index.ts +1 -0
- package/src/ai/project-tool.ts +4 -4
- package/src/ai/projects-authorization.ts +53 -0
- package/src/ai/projects-routes.ts +56 -46
- package/src/ai/projects.ts +100 -79
- package/src/ai/recent-resources.ts +3 -3
- package/src/ai/routes.ts +30 -30
- package/src/ai/skill-tool.ts +4 -4
- package/src/ai/skills-routes.ts +27 -26
- package/src/ai/skills.ts +120 -81
- package/src/api/accounts-entities.ts +6 -3
- package/src/api/admin-ai-skills.ts +4 -1
- package/src/api/auth.ts +9 -2
- package/src/browser/session.ts +39 -0
- package/src/browser/shared-text-history-validation.ts +151 -0
- package/src/browser/shared-text-history.ts +464 -0
- package/src/browser/shared-text-offline-authorized.ts +29 -0
- package/src/browser/shared-text-offline.ts +351 -0
- package/src/browser/shared-text-provider.ts +714 -0
- package/src/browser/shared-text-table-contract.ts +150 -0
- package/src/browser/shared-text.ts +19 -0
- package/src/contracts/shared.ts +2 -1
- package/src/server/api/respond.ts +10 -2
- package/src/server/index.ts +1 -0
- package/src/server/services/access.ts +23 -10
- package/src/server/services/index.ts +1 -0
- package/src/services/accounts/app.ts +30 -13
- package/src/services/accounts/entities.ts +29 -16
- package/src/services/accounts/group-sql.ts +23 -0
- package/src/services/accounts/groups.ts +16 -13
- package/src/services/accounts/identities.ts +8 -1
- package/src/services/accounts/identity-reconciliation.ts +3 -1
- package/src/services/accounts/local-groups.ts +69 -4
- package/src/services/accounts/users.ts +38 -8
- package/src/services/mandates/index.ts +2 -2
- package/src/services/mandates/policy.ts +1 -0
- package/src/services/session/index.ts +12 -3
- package/src/shared/email-html.ts +1 -0
- package/src/shared/markdown/extensions/katex.ts +8 -1
- package/src/shared/markdown/extensions/tables.ts +14 -1
- package/src/shared/markdown/index.ts +22 -11
- package/src/shared-text/anchors.ts +29 -0
- package/src/shared-text/awareness.ts +87 -0
- package/src/shared-text/changes.ts +27 -0
- package/src/shared-text/documents.ts +428 -77
- package/src/shared-text/events.ts +12 -2
- package/src/shared-text/index.ts +16 -1
- package/src/shared-text/log.ts +24 -5
- package/src/shared-text/snapshots.ts +69 -23
- package/src/shared-text/structural-history.ts +377 -0
- package/src/shared-text/table-actions.ts +432 -0
- package/src/shared-text/types.ts +24 -5
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/CloudShell.island.tsx +19 -3
- package/src/ssr/Layout.tsx +1 -1
- package/src/ssr/MobileProfileActions.tsx +2 -2
- package/src/ssr/ProfilePreferences.tsx +2 -1
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/admin-navigation.ts +1 -11
- package/src/ssr/app-launchpad.tsx +6 -1
- package/src/ssr/platform-messages.ts +2 -0
- package/src/ssr/profile-actions.ts +3 -6
- package/src/ssr/shell-data.tsx +1 -1
- package/src/workflows/language/expressions.ts +3 -2
- package/src/workflows/runtime/executor.ts +2 -1
package/src/ai/projects.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { type SQL, type SQLQuery, sql } from "bun";
|
|
2
2
|
import type { CloudResourceRef } from "../contracts/capabilities";
|
|
3
3
|
import { AuthenticatedPrincipalSchema, type ServiceAccountKind } from "../contracts/shared";
|
|
4
|
-
import type { AccessSubject } from "../server";
|
|
5
4
|
import {
|
|
6
5
|
buildAccessPrincipalCondition,
|
|
7
6
|
createAccess,
|
|
8
7
|
deleteAccess,
|
|
8
|
+
ensureManagerRemains,
|
|
9
9
|
hasPermission,
|
|
10
10
|
type PermissionLevel,
|
|
11
11
|
type Principal,
|
|
@@ -17,6 +17,13 @@ import { AiFileVersionConflict, AiFileWriteError, aiFileContentVersion } from ".
|
|
|
17
17
|
import { isAiImage } from "./file-media-type";
|
|
18
18
|
import { mountAiProjectFilePath } from "./file-mount";
|
|
19
19
|
import { pickStoredAiFilePath } from "./files-store";
|
|
20
|
+
import {
|
|
21
|
+
type AiResourceContext,
|
|
22
|
+
aiResourceAccessSubject,
|
|
23
|
+
aiResourcePermissionCap,
|
|
24
|
+
capAiResourcePermission,
|
|
25
|
+
requireAiResourceCreation,
|
|
26
|
+
} from "./projects-authorization";
|
|
20
27
|
import { withAiShortIdForDb } from "./short-id";
|
|
21
28
|
import type { AiProjectPromptSnapshot } from "./types";
|
|
22
29
|
|
|
@@ -86,7 +93,7 @@ export type AiProjectAccess = {
|
|
|
86
93
|
permission: AiProjectPermission;
|
|
87
94
|
displayName?: string;
|
|
88
95
|
avatarHash?: string | null;
|
|
89
|
-
/** Kind of a `service_account` principal
|
|
96
|
+
/** Kind of a `service_account` principal, including manager eligibility. */
|
|
90
97
|
serviceAccountKind?: ServiceAccountKind;
|
|
91
98
|
createdAt: string;
|
|
92
99
|
};
|
|
@@ -171,15 +178,21 @@ type AdminProjectRow = ProjectRow & {
|
|
|
171
178
|
};
|
|
172
179
|
|
|
173
180
|
const iso = (value: Date | string): string => (value instanceof Date ? value.toISOString() : new Date(value).toISOString());
|
|
174
|
-
const actorUserId = (
|
|
181
|
+
const actorUserId = (input: AiResourceContext | null): string | null => {
|
|
182
|
+
const subject = aiResourceAccessSubject(input);
|
|
183
|
+
return subject?.type === "user" ? subject.userId : null;
|
|
184
|
+
};
|
|
175
185
|
|
|
176
|
-
const principalForSubject = (
|
|
177
|
-
subject
|
|
186
|
+
const principalForSubject = (input: AiResourceContext): Principal => {
|
|
187
|
+
const subject = aiResourceAccessSubject(input)!;
|
|
188
|
+
return subject.type === "user"
|
|
178
189
|
? { type: "user", userId: subject.userId }
|
|
179
190
|
: { type: "service_account", serviceAccountId: subject.serviceAccountId };
|
|
191
|
+
};
|
|
180
192
|
|
|
181
|
-
const accessMatch = (
|
|
182
|
-
subject
|
|
193
|
+
const accessMatch = (input: AiResourceContext | null): SQLQuery => {
|
|
194
|
+
const subject = aiResourceAccessSubject(input);
|
|
195
|
+
return subject === null || aiResourcePermissionCap(input) === "none"
|
|
183
196
|
? sql`FALSE`
|
|
184
197
|
: buildAccessPrincipalCondition({
|
|
185
198
|
subject,
|
|
@@ -190,8 +203,9 @@ const accessMatch = (subject: AccessSubject | null): SQLQuery =>
|
|
|
190
203
|
authenticatedOnly: sql`access.authenticated_only`,
|
|
191
204
|
},
|
|
192
205
|
});
|
|
206
|
+
};
|
|
193
207
|
|
|
194
|
-
const permissionFor = async (row: ProjectRow, subject:
|
|
208
|
+
const permissionFor = async (row: ProjectRow, subject: AiResourceContext | null, db: SQL = sql): Promise<AiProjectPermission | "none"> => {
|
|
195
209
|
const match = accessMatch(subject);
|
|
196
210
|
const rows = await db<{ permission: AiProjectPermission }[]>`
|
|
197
211
|
SELECT access.permission
|
|
@@ -201,10 +215,10 @@ const permissionFor = async (row: ProjectRow, subject: AccessSubject | null, db:
|
|
|
201
215
|
ORDER BY CASE access.permission WHEN 'admin' THEN 3 WHEN 'write' THEN 2 WHEN 'read' THEN 1 ELSE 0 END DESC
|
|
202
216
|
LIMIT 1
|
|
203
217
|
`;
|
|
204
|
-
return rows[0]?.permission ?? "none";
|
|
218
|
+
return capAiResourcePermission(rows[0]?.permission ?? "none", subject);
|
|
205
219
|
};
|
|
206
220
|
|
|
207
|
-
const toProject = async (row: ProjectRow, subject:
|
|
221
|
+
const toProject = async (row: ProjectRow, subject: AiResourceContext | null, db: SQL = sql): Promise<AiProject | null> => {
|
|
208
222
|
const permission = await permissionFor(row, subject, db);
|
|
209
223
|
if (permission === "none") return null;
|
|
210
224
|
return {
|
|
@@ -331,41 +345,34 @@ const createProjectAccess = async (
|
|
|
331
345
|
};
|
|
332
346
|
|
|
333
347
|
const updateProjectAccess = async (projectId: string, accessId: string, permission: AiProjectPermission, db: SQL): Promise<boolean> => {
|
|
334
|
-
const [bound] = await db<{ id: string
|
|
335
|
-
SELECT access.id
|
|
348
|
+
const [bound] = await db<{ id: string }[]>`
|
|
349
|
+
SELECT access.id
|
|
336
350
|
FROM ai.project_access project_access
|
|
337
351
|
JOIN auth.access access ON access.id = project_access.access_id
|
|
338
352
|
WHERE project_access.project_id = ${projectId}::uuid AND project_access.short_id = ${accessId}
|
|
339
353
|
`;
|
|
340
354
|
if (!bound) return false;
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if ((admins?.count ?? 0) <= 1) throw new AiProjectLastAdminError();
|
|
348
|
-
}
|
|
355
|
+
const before = await listProjectAccess(projectId, db);
|
|
356
|
+
const guarded = ensureManagerRemains({
|
|
357
|
+
before,
|
|
358
|
+
after: before.map((entry) => (entry.shortId === accessId ? { ...entry, permission } : entry)),
|
|
359
|
+
});
|
|
360
|
+
if (!guarded.ok) throw new AiProjectLastAdminError();
|
|
349
361
|
const result = await db`UPDATE auth.access SET permission = ${permission}::auth.permission_level WHERE id = ${bound.id}::uuid`;
|
|
350
362
|
return result.count === 1;
|
|
351
363
|
};
|
|
352
364
|
|
|
353
365
|
const revokeProjectAccess = async (projectId: string, accessId: string, db: SQL): Promise<boolean> => {
|
|
354
|
-
const [bound] = await db<{ access_id: string
|
|
355
|
-
SELECT project_access.access_id
|
|
366
|
+
const [bound] = await db<{ access_id: string }[]>`
|
|
367
|
+
SELECT project_access.access_id
|
|
356
368
|
FROM ai.project_access project_access
|
|
357
369
|
JOIN auth.access access ON access.id = project_access.access_id
|
|
358
370
|
WHERE project_access.project_id = ${projectId}::uuid AND project_access.short_id = ${accessId}
|
|
359
371
|
`;
|
|
360
372
|
if (!bound) return false;
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
JOIN auth.access access ON access.id = project_access.access_id
|
|
365
|
-
WHERE project_access.project_id = ${projectId}::uuid AND access.permission = 'admin'
|
|
366
|
-
`;
|
|
367
|
-
if ((admins?.count ?? 0) <= 1) throw new AiProjectLastAdminError();
|
|
368
|
-
}
|
|
373
|
+
const before = await listProjectAccess(projectId, db);
|
|
374
|
+
const guarded = ensureManagerRemains({ before, after: before.filter((entry) => entry.shortId !== accessId) });
|
|
375
|
+
if (!guarded.ok) throw new AiProjectLastAdminError();
|
|
369
376
|
const deleted = await deleteAccess({ id: bound.access_id }, db);
|
|
370
377
|
if (!deleted.ok) throw new Error(deleted.error.message);
|
|
371
378
|
return true;
|
|
@@ -373,7 +380,7 @@ const revokeProjectAccess = async (projectId: string, accessId: string, db: SQL)
|
|
|
373
380
|
|
|
374
381
|
const requireProject = async (
|
|
375
382
|
projectId: string,
|
|
376
|
-
subject:
|
|
383
|
+
subject: AiResourceContext | null,
|
|
377
384
|
permission: AiProjectPermission = "read",
|
|
378
385
|
): Promise<AiProject | null> => {
|
|
379
386
|
const row = await getRow(projectId);
|
|
@@ -384,7 +391,7 @@ const requireProject = async (
|
|
|
384
391
|
|
|
385
392
|
const requireProjectByShortId = async (
|
|
386
393
|
shortId: string,
|
|
387
|
-
subject:
|
|
394
|
+
subject: AiResourceContext | null,
|
|
388
395
|
permission: AiProjectPermission = "read",
|
|
389
396
|
): Promise<AiProject | null> => {
|
|
390
397
|
const row = await getRowByShortId(shortId);
|
|
@@ -413,13 +420,14 @@ const projectSearchPattern = (value: string | undefined): string | null => {
|
|
|
413
420
|
|
|
414
421
|
export const aiProjects = {
|
|
415
422
|
async create(input: {
|
|
416
|
-
subject:
|
|
423
|
+
subject: AiResourceContext;
|
|
417
424
|
name: string;
|
|
418
425
|
description?: string;
|
|
419
426
|
icon?: string;
|
|
420
427
|
instructions?: string;
|
|
421
428
|
defaultModel?: AiModelRequest | null;
|
|
422
429
|
}): Promise<AiProject> {
|
|
430
|
+
requireAiResourceCreation(input.subject);
|
|
423
431
|
return sql.begin(async (tx) => {
|
|
424
432
|
const rows = await withAiShortIdForDb(
|
|
425
433
|
tx,
|
|
@@ -443,11 +451,11 @@ export const aiProjects = {
|
|
|
443
451
|
VALUES (${rows[0]!.id}::uuid, ${created.data.id}::uuid, ${shortId})
|
|
444
452
|
`,
|
|
445
453
|
);
|
|
446
|
-
return
|
|
454
|
+
return (await toProject(rows[0]!, input.subject, tx))!;
|
|
447
455
|
});
|
|
448
456
|
},
|
|
449
457
|
|
|
450
|
-
async list(subject:
|
|
458
|
+
async list(subject: AiResourceContext | null): Promise<AiProject[]> {
|
|
451
459
|
const match = accessMatch(subject);
|
|
452
460
|
const rows = await sql<ProjectRow[]>`
|
|
453
461
|
SELECT DISTINCT project.*, lower(project.name) AS sort_name
|
|
@@ -460,7 +468,7 @@ export const aiProjects = {
|
|
|
460
468
|
return (await Promise.all(rows.map((row) => toProject(row, subject)))).filter((project): project is AiProject => Boolean(project));
|
|
461
469
|
},
|
|
462
470
|
|
|
463
|
-
async resolveShortIds(projectIds: readonly string[], subject:
|
|
471
|
+
async resolveShortIds(projectIds: readonly string[], subject: AiResourceContext | null): Promise<Map<string, string>> {
|
|
464
472
|
const ids = [...new Set(projectIds)];
|
|
465
473
|
if (!ids.length) return new Map();
|
|
466
474
|
const match = accessMatch(subject);
|
|
@@ -475,7 +483,7 @@ export const aiProjects = {
|
|
|
475
483
|
return new Map(rows.map((row) => [row.id, row.short_id]));
|
|
476
484
|
},
|
|
477
485
|
|
|
478
|
-
async scopeVersion(subject:
|
|
486
|
+
async scopeVersion(subject: AiResourceContext | null): Promise<string> {
|
|
479
487
|
const projects = await this.list(subject);
|
|
480
488
|
return projects
|
|
481
489
|
.map((project) => `${project.shortId}:${project.permission}`)
|
|
@@ -488,30 +496,34 @@ export const aiProjects = {
|
|
|
488
496
|
|
|
489
497
|
async update(
|
|
490
498
|
projectId: string,
|
|
491
|
-
subject:
|
|
499
|
+
subject: AiResourceContext | null,
|
|
492
500
|
input: Partial<Pick<AiProject, "name" | "description" | "icon" | "instructions" | "defaultModel">>,
|
|
493
501
|
): Promise<AiProject | null> {
|
|
494
502
|
const changesTrustedInstructions = Object.hasOwn(input, "instructions") || Object.hasOwn(input, "defaultModel");
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
503
|
+
return sql.begin(async (tx) => {
|
|
504
|
+
const [project] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE`;
|
|
505
|
+
if (!project || !hasPermission(await permissionFor(project, subject, tx), changesTrustedInstructions ? "admin" : "write"))
|
|
506
|
+
return null;
|
|
507
|
+
const rows = await tx<ProjectRow[]>`
|
|
508
|
+
UPDATE ai.projects
|
|
509
|
+
SET name = COALESCE(${input.name?.trim() ?? null}, name),
|
|
510
|
+
description = COALESCE(${input.description?.trim() ?? null}, description),
|
|
511
|
+
icon = COALESCE(${input.icon?.trim() ?? null}, icon),
|
|
512
|
+
instructions = COALESCE(${input.instructions?.trim() ?? null}, instructions),
|
|
513
|
+
default_model = CASE
|
|
514
|
+
WHEN ${Object.hasOwn(input, "defaultModel")} THEN ${input.defaultModel ? JSON.stringify(AiModelRequestSchema.parse(input.defaultModel)) : null}::text::jsonb
|
|
515
|
+
ELSE default_model
|
|
516
|
+
END,
|
|
517
|
+
revision = revision + 1,
|
|
518
|
+
updated_at = now()
|
|
519
|
+
WHERE id = ${projectId}::uuid
|
|
520
|
+
RETURNING *
|
|
521
|
+
`;
|
|
522
|
+
return rows[0] ? toProject(rows[0], subject, tx) : null;
|
|
523
|
+
});
|
|
512
524
|
},
|
|
513
525
|
|
|
514
|
-
async delete(projectId: string, subject:
|
|
526
|
+
async delete(projectId: string, subject: AiResourceContext | null): Promise<boolean> {
|
|
515
527
|
return sql.begin(async (tx) => {
|
|
516
528
|
const [project] = await tx<ProjectRow[]>`
|
|
517
529
|
SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE
|
|
@@ -526,7 +538,7 @@ export const aiProjects = {
|
|
|
526
538
|
});
|
|
527
539
|
},
|
|
528
540
|
|
|
529
|
-
async snapshot(projectId: string, subject:
|
|
541
|
+
async snapshot(projectId: string, subject: AiResourceContext | null): Promise<AiProjectPromptSnapshot | null> {
|
|
530
542
|
return sql.begin(async (tx) => {
|
|
531
543
|
const [row] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR SHARE`;
|
|
532
544
|
if (!row || !hasPermission(await permissionFor(row, subject, tx), "read")) return null;
|
|
@@ -569,14 +581,14 @@ export const aiProjects = {
|
|
|
569
581
|
});
|
|
570
582
|
},
|
|
571
583
|
|
|
572
|
-
async listAccess(projectId: string, subject:
|
|
584
|
+
async listAccess(projectId: string, subject: AiResourceContext | null): Promise<AiProjectAccess[] | null> {
|
|
573
585
|
if (!(await requireProject(projectId, subject, "admin"))) return null;
|
|
574
586
|
return listProjectAccess(projectId);
|
|
575
587
|
},
|
|
576
588
|
|
|
577
589
|
async grantAccess(
|
|
578
590
|
projectId: string,
|
|
579
|
-
subject:
|
|
591
|
+
subject: AiResourceContext | null,
|
|
580
592
|
input: { principal: Principal; permission: AiProjectPermission },
|
|
581
593
|
): Promise<AiProjectAccess | null> {
|
|
582
594
|
return sql.begin(async (tx) => {
|
|
@@ -591,7 +603,7 @@ export const aiProjects = {
|
|
|
591
603
|
async updateAccess(
|
|
592
604
|
projectId: string,
|
|
593
605
|
accessId: string,
|
|
594
|
-
subject:
|
|
606
|
+
subject: AiResourceContext | null,
|
|
595
607
|
permission: AiProjectPermission,
|
|
596
608
|
): Promise<boolean> {
|
|
597
609
|
return sql.begin(async (tx) => {
|
|
@@ -603,7 +615,7 @@ export const aiProjects = {
|
|
|
603
615
|
});
|
|
604
616
|
},
|
|
605
617
|
|
|
606
|
-
async revokeAccess(projectId: string, accessId: string, subject:
|
|
618
|
+
async revokeAccess(projectId: string, accessId: string, subject: AiResourceContext | null): Promise<boolean> {
|
|
607
619
|
return sql.begin(async (tx) => {
|
|
608
620
|
const [project] = await tx<ProjectRow[]>`
|
|
609
621
|
SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE
|
|
@@ -727,7 +739,7 @@ export const aiProjects = {
|
|
|
727
739
|
},
|
|
728
740
|
},
|
|
729
741
|
|
|
730
|
-
async listKnowledge(projectId: string, subject:
|
|
742
|
+
async listKnowledge(projectId: string, subject: AiResourceContext | null, query?: string): Promise<AiProjectKnowledge[]> {
|
|
731
743
|
if (!(await requireProject(projectId, subject, "read"))) return [];
|
|
732
744
|
const q = query?.trim() || null;
|
|
733
745
|
const pattern = q ? `%${q.toLowerCase().replace(/[\\%_]/g, (char) => `\\${char}`)}%` : null;
|
|
@@ -742,7 +754,7 @@ export const aiProjects = {
|
|
|
742
754
|
return rows.map(toKnowledge);
|
|
743
755
|
},
|
|
744
756
|
|
|
745
|
-
async getKnowledgeByShortId(projectId: string, shortId: string, subject:
|
|
757
|
+
async getKnowledgeByShortId(projectId: string, shortId: string, subject: AiResourceContext | null): Promise<AiProjectKnowledge | null> {
|
|
746
758
|
if (!(await requireProject(projectId, subject, "read"))) return null;
|
|
747
759
|
const rows = await sql<KnowledgeRow[]>`
|
|
748
760
|
SELECT * FROM ai.project_knowledge
|
|
@@ -754,11 +766,12 @@ export const aiProjects = {
|
|
|
754
766
|
|
|
755
767
|
async createKnowledge(
|
|
756
768
|
projectId: string,
|
|
757
|
-
subject:
|
|
769
|
+
subject: AiResourceContext | null,
|
|
758
770
|
input: { title: string; content: string },
|
|
759
771
|
): Promise<AiProjectKnowledge | null> {
|
|
760
|
-
if (!(await requireProject(projectId, subject, "write"))) return null;
|
|
761
772
|
return sql.begin(async (tx) => {
|
|
773
|
+
const [project] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE`;
|
|
774
|
+
if (!project || !hasPermission(await permissionFor(project, subject, tx), "write")) return null;
|
|
762
775
|
const rows = await withAiShortIdForDb(
|
|
763
776
|
tx,
|
|
764
777
|
"idx_ai_project_knowledge_short_id",
|
|
@@ -776,11 +789,12 @@ export const aiProjects = {
|
|
|
776
789
|
async updateKnowledge(
|
|
777
790
|
projectId: string,
|
|
778
791
|
knowledgeId: string,
|
|
779
|
-
subject:
|
|
792
|
+
subject: AiResourceContext | null,
|
|
780
793
|
input: { title?: string; content?: string },
|
|
781
794
|
): Promise<AiProjectKnowledge | null> {
|
|
782
|
-
if (!(await requireProject(projectId, subject, "write"))) return null;
|
|
783
795
|
return sql.begin(async (tx) => {
|
|
796
|
+
const [project] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE`;
|
|
797
|
+
if (!project || !hasPermission(await permissionFor(project, subject, tx), "write")) return null;
|
|
784
798
|
const rows = await tx<KnowledgeRow[]>`
|
|
785
799
|
UPDATE ai.project_knowledge
|
|
786
800
|
SET title = COALESCE(${input.title?.trim() ?? null}, title), content = COALESCE(${input.content?.trim() ?? null}, content), updated_at = now()
|
|
@@ -792,9 +806,10 @@ export const aiProjects = {
|
|
|
792
806
|
});
|
|
793
807
|
},
|
|
794
808
|
|
|
795
|
-
async deleteKnowledge(projectId: string, knowledgeId: string, subject:
|
|
796
|
-
if (!(await requireProject(projectId, subject, "write"))) return false;
|
|
809
|
+
async deleteKnowledge(projectId: string, knowledgeId: string, subject: AiResourceContext | null): Promise<boolean> {
|
|
797
810
|
return sql.begin(async (tx) => {
|
|
811
|
+
const [project] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE`;
|
|
812
|
+
if (!project || !hasPermission(await permissionFor(project, subject, tx), "write")) return false;
|
|
798
813
|
const rows = await tx<{ id: string }[]>`
|
|
799
814
|
DELETE FROM ai.project_knowledge WHERE short_id = ${knowledgeId} AND project_id = ${projectId}::uuid RETURNING id
|
|
800
815
|
`;
|
|
@@ -803,7 +818,11 @@ export const aiProjects = {
|
|
|
803
818
|
});
|
|
804
819
|
},
|
|
805
820
|
|
|
806
|
-
async listFiles(
|
|
821
|
+
async listFiles(
|
|
822
|
+
projectId: string,
|
|
823
|
+
subject: AiResourceContext | null,
|
|
824
|
+
page?: { after?: string; limit: number },
|
|
825
|
+
): Promise<AiProjectFile[]> {
|
|
807
826
|
if (!(await requireProject(projectId, subject, "read"))) return [];
|
|
808
827
|
if (page && (!Number.isInteger(page.limit) || page.limit < 1 || page.limit > 1000)) throw new Error("Invalid file page limit");
|
|
809
828
|
return (
|
|
@@ -815,7 +834,7 @@ export const aiProjects = {
|
|
|
815
834
|
|
|
816
835
|
async writeFile(
|
|
817
836
|
projectId: string,
|
|
818
|
-
subject:
|
|
837
|
+
subject: AiResourceContext | null,
|
|
819
838
|
input: { path: string; mediaType: string; bytes: Uint8Array; expectedVersion?: string | null },
|
|
820
839
|
): Promise<AiProjectFile | null> {
|
|
821
840
|
if (!(await requireProject(projectId, subject, "write"))) return null;
|
|
@@ -854,7 +873,7 @@ export const aiProjects = {
|
|
|
854
873
|
async readFile(
|
|
855
874
|
projectId: string,
|
|
856
875
|
fileId: string,
|
|
857
|
-
subject:
|
|
876
|
+
subject: AiResourceContext | null,
|
|
858
877
|
): Promise<(AiProjectFile & { bytes: Uint8Array }) | null> {
|
|
859
878
|
if (!(await requireProject(projectId, subject, "read"))) return null;
|
|
860
879
|
const rows = await sql<(FileRow & { bytes: Uint8Array })[]>`
|
|
@@ -867,7 +886,7 @@ export const aiProjects = {
|
|
|
867
886
|
async readFileByPath(
|
|
868
887
|
projectId: string,
|
|
869
888
|
path: string,
|
|
870
|
-
subject:
|
|
889
|
+
subject: AiResourceContext | null,
|
|
871
890
|
): Promise<(AiProjectFile & { bytes: Uint8Array }) | null> {
|
|
872
891
|
if (!(await requireProject(projectId, subject, "read"))) return null;
|
|
873
892
|
const normalized = normalizeProjectPath(path);
|
|
@@ -884,7 +903,7 @@ export const aiProjects = {
|
|
|
884
903
|
return rows[0] ? { ...toFile(rows[0]), bytes: rows[0].bytes } : null;
|
|
885
904
|
},
|
|
886
905
|
|
|
887
|
-
async deleteFile(projectId: string, fileId: string, subject:
|
|
906
|
+
async deleteFile(projectId: string, fileId: string, subject: AiResourceContext | null): Promise<boolean> {
|
|
888
907
|
if (!(await requireProject(projectId, subject, "write"))) return false;
|
|
889
908
|
return sql.begin(async (tx) => {
|
|
890
909
|
const [row] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id=${projectId}::uuid FOR UPDATE`;
|
|
@@ -898,7 +917,7 @@ export const aiProjects = {
|
|
|
898
917
|
});
|
|
899
918
|
},
|
|
900
919
|
|
|
901
|
-
async listReferences(projectId: string, subject:
|
|
920
|
+
async listReferences(projectId: string, subject: AiResourceContext | null): Promise<AiProjectReference[]> {
|
|
902
921
|
if (!(await requireProject(projectId, subject, "read"))) return [];
|
|
903
922
|
return (
|
|
904
923
|
await sql<
|
|
@@ -909,11 +928,12 @@ export const aiProjects = {
|
|
|
909
928
|
|
|
910
929
|
async createReference(
|
|
911
930
|
projectId: string,
|
|
912
|
-
subject:
|
|
931
|
+
subject: AiResourceContext | null,
|
|
913
932
|
input: { ref: CloudResourceRef; label?: string },
|
|
914
933
|
): Promise<AiProjectReference | null> {
|
|
915
|
-
if (!(await requireProject(projectId, subject, "write"))) return null;
|
|
916
934
|
return sql.begin(async (tx) => {
|
|
935
|
+
const [project] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE`;
|
|
936
|
+
if (!project || !hasPermission(await permissionFor(project, subject, tx), "write")) return null;
|
|
917
937
|
const rows = await withAiShortIdForDb(
|
|
918
938
|
tx,
|
|
919
939
|
"idx_ai_project_resource_refs_short_id",
|
|
@@ -928,9 +948,10 @@ export const aiProjects = {
|
|
|
928
948
|
});
|
|
929
949
|
},
|
|
930
950
|
|
|
931
|
-
async deleteReference(projectId: string, referenceId: string, subject:
|
|
932
|
-
if (!(await requireProject(projectId, subject, "write"))) return false;
|
|
951
|
+
async deleteReference(projectId: string, referenceId: string, subject: AiResourceContext | null): Promise<boolean> {
|
|
933
952
|
return sql.begin(async (tx) => {
|
|
953
|
+
const [project] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE`;
|
|
954
|
+
if (!project || !hasPermission(await permissionFor(project, subject, tx), "write")) return false;
|
|
934
955
|
const rows = await tx<{ id: string }[]>`
|
|
935
956
|
DELETE FROM ai.project_resource_refs WHERE short_id = ${referenceId} AND project_id = ${projectId}::uuid RETURNING id
|
|
936
957
|
`;
|
|
@@ -123,9 +123,9 @@ export const listAiRecentResources = async (input: {
|
|
|
123
123
|
const skillIds = candidates.flatMap(({ ref }) => (ref.type === "core.ai.skill" ? [ref.id] : []));
|
|
124
124
|
// Exactly the sent Skills, so a large Skill catalog never pushes one out; failing, they are left out like a reader.
|
|
125
125
|
const skills = skillIds.length
|
|
126
|
-
? withinBudget(signal, () =>
|
|
127
|
-
(
|
|
128
|
-
)
|
|
126
|
+
? withinBudget(signal, () => (deps.skills ?? aiSkills).list(input.authority, { shortIds: skillIds, authority: input.authority })).catch(
|
|
127
|
+
() => [],
|
|
128
|
+
)
|
|
129
129
|
: Promise.resolve([]);
|
|
130
130
|
const registry = candidates.some(({ ref }) => ref.type !== "core.ai.skill" && ref.type !== "assistant.artifact")
|
|
131
131
|
? withinBudget(signal, async () =>
|