@bevel-software/platform-core-backend 0.23.0 → 0.25.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/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +17 -3
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +3 -0
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +25 -1
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +12 -0
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +10 -0
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/core-config.d.ts +16 -0
- package/dist/core-config.d.ts.map +1 -1
- package/dist/core-config.js +17 -0
- package/dist/core-config.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -2
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +4 -1
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/access/directory-sync-bot.d.ts.map +1 -1
- package/dist/modules/access/directory-sync-bot.js +7 -3
- package/dist/modules/access/directory-sync-bot.js.map +1 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +8 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.js +8 -2
- package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
- package/dist/modules/agent-instructions/compose.d.ts +38 -6
- package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
- package/dist/modules/agent-instructions/compose.js +39 -6
- package/dist/modules/agent-instructions/compose.js.map +1 -1
- package/dist/modules/agent-instructions/index.d.ts +2 -1
- package/dist/modules/agent-instructions/index.d.ts.map +1 -1
- package/dist/modules/agent-instructions/index.js +2 -1
- package/dist/modules/agent-instructions/index.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts +115 -0
- package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -0
- package/dist/modules/agent-instructions/shared-file-rules.js +272 -0
- package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -0
- package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
- package/dist/modules/audit/agent-audit.service.js +4 -2
- package/dist/modules/audit/agent-audit.service.js.map +1 -1
- package/dist/modules/auth/account-erasure.service.d.ts.map +1 -1
- package/dist/modules/auth/account-erasure.service.js +57 -16
- package/dist/modules/auth/account-erasure.service.js.map +1 -1
- package/dist/modules/auth/auth.service.d.ts.map +1 -1
- package/dist/modules/auth/auth.service.js +25 -15
- package/dist/modules/auth/auth.service.js.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.d.ts +20 -2
- package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
- package/dist/modules/code-mode/code-mode.tool.js +66 -35
- package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
- package/dist/modules/database/connection.d.ts +16 -0
- package/dist/modules/database/connection.d.ts.map +1 -1
- package/dist/modules/database/connection.js +117 -0
- package/dist/modules/database/connection.js.map +1 -1
- package/dist/modules/database/core-schema.d.ts +296 -96
- package/dist/modules/database/core-schema.d.ts.map +1 -1
- package/dist/modules/database/core-schema.js +81 -31
- package/dist/modules/database/core-schema.js.map +1 -1
- package/dist/modules/database/migrate.d.ts +95 -1
- package/dist/modules/database/migrate.d.ts.map +1 -1
- package/dist/modules/database/migrate.js +390 -2
- package/dist/modules/database/migrate.js.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.d.ts +17 -0
- package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
- package/dist/modules/kb-fs/locking-filesystem.js +29 -0
- package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +8 -0
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +38 -8
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/plugins/join-request-records.store.d.ts.map +1 -1
- package/dist/modules/plugins/join-request-records.store.js +7 -4
- package/dist/modules/plugins/join-request-records.store.js.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
- package/dist/modules/tool-auth/external-api-key.service.js +8 -2
- package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +30 -0
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +42 -31
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/tool-registry/description-length.d.ts +80 -0
- package/dist/modules/tool-registry/description-length.d.ts.map +1 -0
- package/dist/modules/tool-registry/description-length.js +108 -0
- package/dist/modules/tool-registry/description-length.js.map +1 -0
- package/dist/modules/workflow/git/git.service.d.ts +25 -0
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +40 -1
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
- package/dist/modules/workflow/pending-commits.service.js +5 -1
- package/dist/modules/workflow/pending-commits.service.js.map +1 -1
- package/dist/modules/workflow/recovery-bot.d.ts.map +1 -1
- package/dist/modules/workflow/recovery-bot.js +7 -3
- package/dist/modules/workflow/recovery-bot.js.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js +12 -3
- package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +4 -1
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/agent-upload.routes.d.ts +77 -0
- package/dist/modules/workspace/agent-upload.routes.d.ts.map +1 -0
- package/dist/modules/workspace/agent-upload.routes.js +210 -0
- package/dist/modules/workspace/agent-upload.routes.js.map +1 -0
- package/dist/modules/workspace/agent-upload.store.d.ts +284 -0
- package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -0
- package/dist/modules/workspace/agent-upload.store.js +553 -0
- package/dist/modules/workspace/agent-upload.store.js.map +1 -0
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.js +3 -3
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.d.ts +40 -0
- package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.js +46 -4
- package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
- package/dist/modules/workspace/upload-limits.d.ts +13 -0
- package/dist/modules/workspace/upload-limits.d.ts.map +1 -0
- package/dist/modules/workspace/upload-limits.js +13 -0
- package/dist/modules/workspace/upload-limits.js.map +1 -0
- package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.routes.js +1 -1
- package/dist/modules/workspace/workspace.routes.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +13 -0
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +61 -33
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +11 -9
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +570 -134
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/modules/workspace/write-denial.d.ts +0 -6
- package/dist/modules/workspace/write-denial.d.ts.map +1 -1
- package/dist/modules/workspace/write-denial.js +0 -6
- package/dist/modules/workspace/write-denial.js.map +1 -1
- package/dist/modules/workspace/zip-entry-rules.d.ts +114 -0
- package/dist/modules/workspace/zip-entry-rules.d.ts.map +1 -0
- package/dist/modules/workspace/zip-entry-rules.js +154 -0
- package/dist/modules/workspace/zip-entry-rules.js.map +1 -0
- package/dist/shared/column-crypto.d.ts +194 -0
- package/dist/shared/column-crypto.d.ts.map +1 -0
- package/dist/shared/column-crypto.js +144 -0
- package/dist/shared/column-crypto.js.map +1 -0
- package/dist/shared/token-crypto.d.ts.map +1 -1
- package/dist/shared/token-crypto.js +25 -1
- package/dist/shared/token-crypto.js.map +1 -1
- package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
- package/dist/tenancy/static-tenant-source.js +1 -0
- package/dist/tenancy/static-tenant-source.js.map +1 -1
- package/dist/tenancy/tenant-secrets.d.ts +5 -1
- package/dist/tenancy/tenant-secrets.d.ts.map +1 -1
- package/dist/tenancy/tenant-secrets.js +4 -0
- package/dist/tenancy/tenant-secrets.js.map +1 -1
- package/kb-template/AGENTS.md +42 -0
- package/migrations/0016_pii_encryption.sql +20 -0
- package/migrations/meta/0016_snapshot.json +2327 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +3 -3
- package/src/core/__tests__/lifecycle.test.ts +71 -4
- package/src/core/create-core-server.ts +21 -3
- package/src/core/create-core-services.ts +27 -1
- package/src/core/lifecycle.ts +19 -0
- package/src/core-config.ts +18 -0
- package/src/index.ts +25 -0
- package/src/modules/access/__tests__/users-db-double.ts +21 -12
- package/src/modules/access/access.routes.ts +4 -1
- package/src/modules/access/directory-sync-bot.ts +7 -3
- package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +8 -4
- package/src/modules/agent-instructions/__tests__/compose.test.ts +19 -10
- package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +238 -0
- package/src/modules/agent-instructions/agent-instructions.routes.ts +12 -2
- package/src/modules/agent-instructions/compose.ts +50 -7
- package/src/modules/agent-instructions/index.ts +12 -0
- package/src/modules/agent-instructions/shared-file-rules.ts +314 -0
- package/src/modules/audit/agent-audit.service.ts +4 -2
- package/src/modules/auth/__tests__/account-deactivation.test.ts +2 -1
- package/src/modules/auth/__tests__/account-erasure.approval-gate.test.ts +6 -2
- package/src/modules/auth/__tests__/account.routes.test.ts +6 -3
- package/src/modules/auth/__tests__/auth.service.test.ts +3 -1
- package/src/modules/auth/account-erasure.service.ts +69 -18
- package/src/modules/auth/auth.service.ts +33 -23
- package/src/modules/code-mode/__tests__/chain-runtime.e2e.test.ts +335 -0
- package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +46 -2
- package/src/modules/code-mode/code-mode.tool.ts +80 -34
- package/src/modules/database/__tests__/connection.test.ts +12 -0
- package/src/modules/database/__tests__/pii-backfill.pg.test.ts +780 -0
- package/src/modules/database/connection.ts +117 -0
- package/src/modules/database/core-schema.ts +81 -31
- package/src/modules/database/migrate.ts +540 -2
- package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +93 -0
- package/src/modules/kb-fs/locking-filesystem.ts +37 -0
- package/src/modules/mcp/__tests__/mcp.e2e.test.ts +4 -3
- package/src/modules/mcp/__tests__/mcp.service.test.ts +47 -7
- package/src/modules/mcp/mcp.service.ts +46 -7
- package/src/modules/plugins/join-request-records.store.ts +7 -4
- package/src/modules/tool-auth/external-api-key.service.ts +8 -2
- package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +151 -0
- package/src/modules/tool-manuals/tool-manuals.service.ts +37 -0
- package/src/modules/tool-manuals/tool-manuals.tools.ts +44 -31
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +378 -0
- package/src/modules/tool-registry/description-length.ts +111 -0
- package/src/modules/workflow/git/__tests__/git.service.prFetchFailure.test.ts +170 -0
- package/src/modules/workflow/git/git.service.ts +47 -1
- package/src/modules/workflow/pending-commits.service.ts +5 -1
- package/src/modules/workflow/recovery-bot.ts +7 -3
- package/src/modules/workflow/review-workflow/__tests__/carry-approvals-forward.test.ts +4 -0
- package/src/modules/workflow/review-workflow/__tests__/erase-approver.test.ts +25 -3
- package/src/modules/workflow/review-workflow/review-workflow.service.ts +12 -3
- package/src/modules/workflow/workflow.service.ts +4 -1
- package/src/modules/workspace/__tests__/agent-uploads.test.ts +1604 -0
- package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +24 -14
- package/src/modules/workspace/__tests__/workspace.service.any-workspace-credentials.test.ts +156 -0
- package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +77 -1
- package/src/modules/workspace/__tests__/workspace.service.test.ts +57 -0
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +39 -36
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +196 -46
- package/src/modules/workspace/agent-upload.routes.ts +214 -0
- package/src/modules/workspace/agent-upload.store.ts +668 -0
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +9 -9
- package/src/modules/workspace/startup/steps/seed-tree.ts +3 -6
- package/src/modules/workspace/startup/steps/template-source.ts +53 -5
- package/src/modules/workspace/upload-limits.ts +12 -0
- package/src/modules/workspace/workspace.routes.ts +1 -2
- package/src/modules/workspace/workspace.service.ts +63 -37
- package/src/modules/workspace/workspace.tools.ts +647 -148
- package/src/modules/workspace/write-denial.ts +0 -8
- package/src/modules/workspace/zip-entry-rules.ts +173 -0
- package/src/shared/__tests__/column-crypto.test.ts +217 -0
- package/src/shared/column-crypto.ts +218 -0
- package/src/shared/token-crypto.ts +28 -1
- package/src/tenancy/__tests__/static-tenant-source.test.ts +4 -0
- package/src/tenancy/static-tenant-source.ts +1 -0
- package/src/tenancy/tenant-secrets.ts +5 -1
|
@@ -113,6 +113,13 @@
|
|
|
113
113
|
"when": 1790892582327,
|
|
114
114
|
"tag": "0015_account_deactivation",
|
|
115
115
|
"breakpoints": true
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"idx": 16,
|
|
119
|
+
"version": "7",
|
|
120
|
+
"when": 1790933605784,
|
|
121
|
+
"tag": "0016_pii_encryption",
|
|
122
|
+
"breakpoints": true
|
|
116
123
|
}
|
|
117
124
|
]
|
|
118
125
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bevel-software/platform-core-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Open-source core backend of the Bevel platform: git-backed knowledge workspace, workflow (branches/change requests/locks/SSE), skills, tools, secrets vault, access control and the remote MCP surface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
|
53
53
|
"yaml": "^2.9.0",
|
|
54
54
|
"zod": "^3.24.0",
|
|
55
|
-
"@bevel-software/platform-
|
|
56
|
-
"@bevel-software/platform-
|
|
55
|
+
"@bevel-software/platform-mcp-core": "0.25.0",
|
|
56
|
+
"@bevel-software/platform-shared": "0.25.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/adm-zip": "^0.5.8",
|
|
@@ -262,6 +262,14 @@ describe('createShutdown', () => {
|
|
|
262
262
|
order.push('backgroundJobs.drain');
|
|
263
263
|
},
|
|
264
264
|
},
|
|
265
|
+
agentUploadStore: {
|
|
266
|
+
stopSweeping() {
|
|
267
|
+
order.push('agentUploadStore.stopSweeping');
|
|
268
|
+
},
|
|
269
|
+
async drainSweep() {
|
|
270
|
+
order.push('agentUploadStore.drainSweep');
|
|
271
|
+
},
|
|
272
|
+
},
|
|
265
273
|
db: {
|
|
266
274
|
$client: {
|
|
267
275
|
async end() {
|
|
@@ -289,7 +297,12 @@ describe('createShutdown', () => {
|
|
|
289
297
|
'server.close',
|
|
290
298
|
'server.closeAllConnections',
|
|
291
299
|
'backgroundJobs.stopSweeping',
|
|
300
|
+
// The upload sweep is stopped in the same breath as the other timers,
|
|
301
|
+
// and drained before the pool goes: a graph that is stopping must not
|
|
302
|
+
// leave a sweep walking the staging root its replacement will stage into.
|
|
303
|
+
'agentUploadStore.stopSweeping',
|
|
292
304
|
'backgroundJobs.drain',
|
|
305
|
+
'agentUploadStore.drainSweep',
|
|
293
306
|
'commitWorker.stop',
|
|
294
307
|
'db.end',
|
|
295
308
|
]);
|
|
@@ -310,7 +323,13 @@ describe('createShutdown', () => {
|
|
|
310
323
|
d.finishClose();
|
|
311
324
|
await settle();
|
|
312
325
|
// The jobs are still running: nothing after them has run.
|
|
313
|
-
expect(d.order).toEqual([
|
|
326
|
+
expect(d.order).toEqual([
|
|
327
|
+
'server.close',
|
|
328
|
+
'server.closeAllConnections',
|
|
329
|
+
'backgroundJobs.stopSweeping',
|
|
330
|
+
'agentUploadStore.stopSweeping',
|
|
331
|
+
'backgroundJobs.drain',
|
|
332
|
+
]);
|
|
314
333
|
release();
|
|
315
334
|
await done;
|
|
316
335
|
expect(d.order.slice(-2)).toEqual(['commitWorker.stop', 'db.end']);
|
|
@@ -339,6 +358,7 @@ describe('createShutdown', () => {
|
|
|
339
358
|
const d = deps();
|
|
340
359
|
// A stop that lands mid-boot: the services exist, the jobs do not.
|
|
341
360
|
delete (d.deps as { backgroundJobs?: unknown }).backgroundJobs;
|
|
361
|
+
delete (d.deps as { agentUploadStore?: unknown }).agentUploadStore;
|
|
342
362
|
const shutdown = createShutdown(d.deps as never);
|
|
343
363
|
const done = shutdown('SIGTERM');
|
|
344
364
|
await settle();
|
|
@@ -363,7 +383,12 @@ describe('createShutdown', () => {
|
|
|
363
383
|
await settle();
|
|
364
384
|
d.finishClose();
|
|
365
385
|
await done;
|
|
366
|
-
expect(d.order.slice(-
|
|
386
|
+
expect(d.order.slice(-4)).toEqual([
|
|
387
|
+
'backgroundJobs.drain',
|
|
388
|
+
'agentUploadStore.drainSweep',
|
|
389
|
+
'commitWorker.stop',
|
|
390
|
+
'db.end',
|
|
391
|
+
]);
|
|
367
392
|
});
|
|
368
393
|
|
|
369
394
|
it('does not hang on a step that never finishes, and still runs the rest', async () => {
|
|
@@ -376,7 +401,9 @@ describe('createShutdown', () => {
|
|
|
376
401
|
'server.close',
|
|
377
402
|
'server.closeAllConnections',
|
|
378
403
|
'backgroundJobs.stopSweeping',
|
|
404
|
+
'agentUploadStore.stopSweeping',
|
|
379
405
|
'backgroundJobs.drain',
|
|
406
|
+
'agentUploadStore.drainSweep',
|
|
380
407
|
'commitWorker.stop',
|
|
381
408
|
'db.end',
|
|
382
409
|
]);
|
|
@@ -413,6 +440,14 @@ describe('stopCore', () => {
|
|
|
413
440
|
order.push('backgroundJobs.drain');
|
|
414
441
|
},
|
|
415
442
|
},
|
|
443
|
+
agentUploadStore: {
|
|
444
|
+
stopSweeping() {
|
|
445
|
+
order.push('agentUploadStore.stopSweeping');
|
|
446
|
+
},
|
|
447
|
+
async drainSweep() {
|
|
448
|
+
order.push('agentUploadStore.drainSweep');
|
|
449
|
+
},
|
|
450
|
+
},
|
|
416
451
|
startupRetry: {
|
|
417
452
|
stop() {
|
|
418
453
|
order.push('startupRetry.stop');
|
|
@@ -436,13 +471,31 @@ describe('stopCore', () => {
|
|
|
436
471
|
await stopCore(g.core as never);
|
|
437
472
|
expect(g.order).toEqual([
|
|
438
473
|
'backgroundJobs.stopSweeping',
|
|
474
|
+
'agentUploadStore.stopSweeping',
|
|
439
475
|
'startupRetry.stop',
|
|
440
476
|
'backgroundJobs.drain',
|
|
477
|
+
'agentUploadStore.drainSweep',
|
|
441
478
|
'commitWorker.stop',
|
|
442
479
|
'db.end',
|
|
443
480
|
]);
|
|
444
481
|
});
|
|
445
482
|
|
|
483
|
+
it('stops the upload sweep, so an evicted tenant cannot sweep its own replacement', async () => {
|
|
484
|
+
const g = graph();
|
|
485
|
+
await stopCore(g.core as never);
|
|
486
|
+
// This is the eviction path: the next activation of the same tenant builds
|
|
487
|
+
// a new store over the SAME staging directory with an empty record map, so
|
|
488
|
+
// a sweep left running from this graph would read the replacement's files
|
|
489
|
+
// as orphans and delete them under an apply about to read them. Both
|
|
490
|
+
// halves, in order — the timer cleared, then the sweep in flight awaited
|
|
491
|
+
// before anything else lets go.
|
|
492
|
+
const stopped = g.order.indexOf('agentUploadStore.stopSweeping');
|
|
493
|
+
const drained = g.order.indexOf('agentUploadStore.drainSweep');
|
|
494
|
+
expect(stopped).toBeGreaterThanOrEqual(0);
|
|
495
|
+
expect(stopped).toBeLessThan(drained);
|
|
496
|
+
expect(drained).toBeLessThan(g.order.indexOf('db.end'));
|
|
497
|
+
});
|
|
498
|
+
|
|
446
499
|
it('forgets the graph\'s secrets scope, so a descriptor that outlives it resolves nothing', async () => {
|
|
447
500
|
const g = graph();
|
|
448
501
|
registerBevelSecretsVariableLoader(
|
|
@@ -459,14 +512,28 @@ describe('stopCore', () => {
|
|
|
459
512
|
const g = graph();
|
|
460
513
|
const core = { ...g.core, startupRetry: null, secretsScope: undefined };
|
|
461
514
|
await stopCore(core as never);
|
|
462
|
-
expect(g.order).toEqual([
|
|
515
|
+
expect(g.order).toEqual([
|
|
516
|
+
'backgroundJobs.stopSweeping',
|
|
517
|
+
'agentUploadStore.stopSweeping',
|
|
518
|
+
'backgroundJobs.drain',
|
|
519
|
+
'agentUploadStore.drainSweep',
|
|
520
|
+
'commitWorker.stop',
|
|
521
|
+
'db.end',
|
|
522
|
+
]);
|
|
463
523
|
});
|
|
464
524
|
|
|
465
525
|
it('does not wait past the budget for a worker that never stops, and still ends the pool', async () => {
|
|
466
526
|
const g = graph();
|
|
467
527
|
const core = { ...g.core, commitWorker: { stop: () => new Promise<void>(() => undefined) } };
|
|
468
528
|
await stopCore(core as never, { deadlineMs: 50 });
|
|
469
|
-
expect(g.order).toEqual([
|
|
529
|
+
expect(g.order).toEqual([
|
|
530
|
+
'backgroundJobs.stopSweeping',
|
|
531
|
+
'agentUploadStore.stopSweeping',
|
|
532
|
+
'startupRetry.stop',
|
|
533
|
+
'backgroundJobs.drain',
|
|
534
|
+
'agentUploadStore.drainSweep',
|
|
535
|
+
'db.end',
|
|
536
|
+
]);
|
|
470
537
|
});
|
|
471
538
|
});
|
|
472
539
|
|
|
@@ -4,6 +4,10 @@ import path from 'node:path';
|
|
|
4
4
|
import type { Router, RequestHandler } from 'express';
|
|
5
5
|
import { createAuthRoutes } from '../modules/auth/auth.routes.js';
|
|
6
6
|
import { createWorkspaceRoutes } from '../modules/workspace/workspace.routes.js';
|
|
7
|
+
import {
|
|
8
|
+
createAgentUploadRoutes,
|
|
9
|
+
isAgentUploadRawBodyPath,
|
|
10
|
+
} from '../modules/workspace/agent-upload.routes.js';
|
|
7
11
|
import { createGitInternalsRouteGuard } from '../modules/workspace/git-internals.middleware.js';
|
|
8
12
|
import { startCore } from './lifecycle.js';
|
|
9
13
|
import { createDiffRoutes } from '../modules/diff/diff.routes.js';
|
|
@@ -210,7 +214,14 @@ export async function createCoreServer(
|
|
|
210
214
|
// refused with its own 403, never parsed first (and answered 400 or 413).
|
|
211
215
|
app.use(createWriteGateMiddleware(core.writeAccess));
|
|
212
216
|
app.use((req, res, next) => {
|
|
213
|
-
|
|
217
|
+
// The agent upload route is exempt for the same reason `/api/sync` is:
|
|
218
|
+
// whoever needs the EXACT bytes has to see them before any parser can
|
|
219
|
+
// drain the stream. `curl --data-binary @file.zip` with a JSON
|
|
220
|
+
// content-type is a request an agent can make, and the parser would
|
|
221
|
+
// otherwise leave the handler nothing to store.
|
|
222
|
+
if (jsonExemptPaths.has(req.path) || isSyncRawBodyPath(req.path) || isAgentUploadRawBodyPath(req.path)) {
|
|
223
|
+
return next();
|
|
224
|
+
}
|
|
214
225
|
return globalJson(req, res, next);
|
|
215
226
|
});
|
|
216
227
|
|
|
@@ -445,7 +456,14 @@ export async function createCoreServer(
|
|
|
445
456
|
// `get_skill`. Warnings only; it never refuses a save.
|
|
446
457
|
const allowedToolsChecker = new AllowedToolsChecker(core.toolRegistry, core.toolManualService, core.kb);
|
|
447
458
|
registerWorkflowTools(core.toolRegistry, toolsRouter, ta, th, core.kb);
|
|
448
|
-
registerWorkspaceTools(core.toolRegistry, toolsRouter, ta, th, core.spillStore, core.docExtractService, core.accessControl, core.kb, agentAccessGate, core.routineWritePolicy, core.sessionSink, allowedToolsChecker, core.changeGate);
|
|
459
|
+
registerWorkspaceTools(core.toolRegistry, toolsRouter, ta, th, core.spillStore, core.docExtractService, core.accessControl, core.kb, agentAccessGate, core.routineWritePolicy, core.sessionSink, allowedToolsChecker, core.changeGate, core.agentUploadStore);
|
|
460
|
+
// The agent upload route, on the same router as the tool endpoints so it
|
|
461
|
+
// mounts ahead of the JWT `/api` mounts below — but WITHOUT `toolAuth`: its
|
|
462
|
+
// whole credential is the single-use token in its path, which is the point
|
|
463
|
+
// (an agent's `curl` carries no session and no connection key). It resolves
|
|
464
|
+
// no workspace and writes into none; every access, platform-file and branch
|
|
465
|
+
// rule is applied later by `apply_file_upload`.
|
|
466
|
+
toolsRouter.use(createAgentUploadRoutes({ uploads: core.agentUploadStore }));
|
|
449
467
|
registerSkillsTools(core.toolRegistry, toolsRouter, ta, th, core.skillService, allowedToolsChecker);
|
|
450
468
|
// Definitions only: the endpoints they describe are the app's own plugin
|
|
451
469
|
// creation routes, mounted below behind the key-or-session gate.
|
|
@@ -489,7 +507,7 @@ export async function createCoreServer(
|
|
|
489
507
|
// What every connected agent is told at session start, as the hosted proxy
|
|
490
508
|
// composes it: read by the local `hexis-mcp` bridge at startup and by the
|
|
491
509
|
// External agent access card. Same `manualAuth`, same router, as `all-tools`.
|
|
492
|
-
toolsRouter.use(createAgentInstructionsRoutes(core.manualAuthMiddleware, core.readAgentPreamble));
|
|
510
|
+
toolsRouter.use(createAgentInstructionsRoutes(core.manualAuthMiddleware, core.readAgentPreamble, () => core.kb.layout));
|
|
493
511
|
// The fingerprint of the caller's released catalog. The local `hexis-mcp`
|
|
494
512
|
// server polls it to learn that a manual or a skill changed under a
|
|
495
513
|
// connection it cannot be pushed to; nothing else consults it. Same
|
|
@@ -54,6 +54,7 @@ function parseDomainList(raw: string): string[] {
|
|
|
54
54
|
.filter((d) => d.length > 0);
|
|
55
55
|
}
|
|
56
56
|
import { SpillStore } from '../modules/workspace/spill-store.js';
|
|
57
|
+
import { AgentUploadStore, assertUploadsRootOutsideWorkspaces } from '../modules/workspace/agent-upload.store.js';
|
|
57
58
|
import { DocExtractService } from '../modules/workspace/file-readers/doc-extract.service.js';
|
|
58
59
|
import { UuidSessionSink, type ISessionSink } from '../modules/workspace/session-sink.js';
|
|
59
60
|
import { AuthService } from '../modules/auth/auth.service.js';
|
|
@@ -220,6 +221,8 @@ export interface CoreServices {
|
|
|
220
221
|
/** The scope this graph's vault is registered under with the UTCP variable loader. */
|
|
221
222
|
secretsScope: string;
|
|
222
223
|
spillStore: SpillStore;
|
|
224
|
+
/** The bytes an agent uploaded, held until `apply_file_upload` lands them or their token expires. */
|
|
225
|
+
agentUploadStore: AgentUploadStore;
|
|
223
226
|
docExtractService: DocExtractService;
|
|
224
227
|
accessControl: AccessControlService;
|
|
225
228
|
creatorAccess: CreatorAccessService;
|
|
@@ -357,7 +360,11 @@ export async function createCoreServices(
|
|
|
357
360
|
// the default schema, so a single-tenant deployment keeps the lock ids and
|
|
358
361
|
// the ledger it always had.
|
|
359
362
|
const tenantKey = config.dbSchema === DEFAULT_DB_SCHEMA ? '' : config.dbSchema;
|
|
360
|
-
|
|
363
|
+
// The handle holds this knowledge base's key for its personal-data columns:
|
|
364
|
+
// the same `SECRETS_ENC_KEY` that seals its stored credentials — its own
|
|
365
|
+
// derived one, for a tenant — so everything in the schema opens with one
|
|
366
|
+
// key and nothing in it opens with another tenant's.
|
|
367
|
+
const db = getDb(config.databaseUrl, { schema: config.dbSchema, piiKey: config.secretsEncKey });
|
|
361
368
|
// A schema of its own is created on first use, so a tenant's first
|
|
362
369
|
// activation needs nothing done by hand; `public` always exists. Then the
|
|
363
370
|
// server is asked whether the connections really search that schema: a
|
|
@@ -377,6 +384,10 @@ export async function createCoreServices(
|
|
|
377
384
|
// package runs its own squashed idempotent CORE history from the packaged
|
|
378
385
|
// `migrations/` folder, tracked in `__drizzle_migrations_core`. An
|
|
379
386
|
// enterprise overlay runs its own history AFTER this (see migrate.ts).
|
|
387
|
+
// `runCoreMigrations` also seals any pre-encryption plaintext PII rows and
|
|
388
|
+
// swaps the unique constraints onto the blind-index columns (see
|
|
389
|
+
// migrate.ts), under the same lock, before any service reads or writes a
|
|
390
|
+
// PII column.
|
|
380
391
|
await runCoreMigrations(db, coreMigrationsDir());
|
|
381
392
|
|
|
382
393
|
// Deployment settings come next, before ANY service is built: the KB remote
|
|
@@ -652,6 +663,17 @@ export async function createCoreServices(
|
|
|
652
663
|
// Shared, workspace-independent store for oversized `call_tool_chain` results,
|
|
653
664
|
// read back via `read_file`. Sibling of `workspacesRoot`, never committed.
|
|
654
665
|
const spillStore = new SpillStore(config.spillRoot);
|
|
666
|
+
// The upload route an agent lands files by, so their content never passes
|
|
667
|
+
// through the model. Bytes live BESIDE the workspaces root (never inside
|
|
668
|
+
// one) until the apply commits them or the token expires — checked here
|
|
669
|
+
// rather than assumed, because a root configured inside a workspace would
|
|
670
|
+
// put bytes no gate has judged where the file tools read.
|
|
671
|
+
await assertUploadsRootOutsideWorkspaces(config.agentUploadsRoot, config.workspacesRoot);
|
|
672
|
+
const agentUploadStore = new AgentUploadStore({
|
|
673
|
+
root: config.agentUploadsRoot,
|
|
674
|
+
publicBaseUrl: config.publicBackendUrl,
|
|
675
|
+
tokenPrefix: config.uploadTokenPrefix,
|
|
676
|
+
});
|
|
655
677
|
// Office-document/PDF text extraction for `read_file`/`grep`, cached by
|
|
656
678
|
// content hash beside the workspaces root (see `DocExtractionCache`).
|
|
657
679
|
const docExtractService = new DocExtractService(config.docExtractCacheRoot);
|
|
@@ -1167,6 +1189,9 @@ export async function createCoreServices(
|
|
|
1167
1189
|
// For the needs-authorization setup link surfaced to external agents.
|
|
1168
1190
|
publicFrontendUrl: config.publicFrontendUrl,
|
|
1169
1191
|
readAgentPreamble: readPreamble,
|
|
1192
|
+
// The guide's name the shared file rules spell, read per request so a name
|
|
1193
|
+
// the setup save applies lands without a restart.
|
|
1194
|
+
kbLayout: () => kb.layout,
|
|
1170
1195
|
secretsScope,
|
|
1171
1196
|
},
|
|
1172
1197
|
// Pre-dispatch per-user credential check: the vault answers "has this caller
|
|
@@ -1404,6 +1429,7 @@ export async function createCoreServices(
|
|
|
1404
1429
|
kb,
|
|
1405
1430
|
kbDirName,
|
|
1406
1431
|
spillStore,
|
|
1432
|
+
agentUploadStore,
|
|
1407
1433
|
docExtractService,
|
|
1408
1434
|
accessControl,
|
|
1409
1435
|
creatorAccess,
|
package/src/core/lifecycle.ts
CHANGED
|
@@ -428,6 +428,15 @@ export interface CoreStopDeps {
|
|
|
428
428
|
* to it. Optional: a caller that has not built them yet passes nothing.
|
|
429
429
|
*/
|
|
430
430
|
backgroundJobs?: { stopSweeping(): void; drain(): Promise<void> };
|
|
431
|
+
/**
|
|
432
|
+
* The agent upload store, whose own sweep deletes staged bytes nobody
|
|
433
|
+
* applied. Stopped with this graph because the next graph for the same
|
|
434
|
+
* tenant stages into the SAME directory with an empty record map: a sweep
|
|
435
|
+
* left running from a store that is gone would read the replacement's files
|
|
436
|
+
* as orphans and delete them under an apply that is about to read them.
|
|
437
|
+
* Optional: a caller that has not built one passes nothing.
|
|
438
|
+
*/
|
|
439
|
+
agentUploadStore?: { stopSweeping(): void; drainSweep(): Promise<void> };
|
|
431
440
|
/** The startup phase's retry, if the boot left one asking — see {@link BootableCore.startupRetry}. */
|
|
432
441
|
startupRetry?: { stop(): void } | null;
|
|
433
442
|
/** The database — its pool is ended last, once nothing above can still need it. */
|
|
@@ -511,6 +520,10 @@ async function releaseCore(deps: CoreStopDeps, remaining: () => number, log: (m:
|
|
|
511
520
|
// earliest point the interval is dead weight. Synchronous and unfailing
|
|
512
521
|
// — it just clears an interval — so it needs no budget of its own.
|
|
513
522
|
deps.backgroundJobs?.stopSweeping();
|
|
523
|
+
// And the upload store's sweep, for the same reason and in the same breath:
|
|
524
|
+
// synchronous, and it only clears an interval and tells a sweep already
|
|
525
|
+
// running to stop deleting.
|
|
526
|
+
deps.agentUploadStore?.stopSweeping();
|
|
514
527
|
// A startup phase still asking for an unreachable remote stops asking:
|
|
515
528
|
// synchronous, and nothing after it must be able to clone into a
|
|
516
529
|
// workspaces folder that is about to belong to nobody.
|
|
@@ -524,6 +537,12 @@ async function releaseCore(deps: CoreStopDeps, remaining: () => number, log: (m:
|
|
|
524
537
|
if (deps.backgroundJobs) {
|
|
525
538
|
await bounded(deps.backgroundJobs.drain(), remaining(), 'finishing the background jobs', log);
|
|
526
539
|
}
|
|
540
|
+
// The sweep in flight when the interval was cleared: awaited so the next
|
|
541
|
+
// graph for this tenant cannot start staging bytes while a store it has
|
|
542
|
+
// never heard of is still walking the same directory.
|
|
543
|
+
if (deps.agentUploadStore) {
|
|
544
|
+
await bounded(deps.agentUploadStore.drainSweep(), remaining(), 'finishing the upload sweep', log);
|
|
545
|
+
}
|
|
527
546
|
await bounded(deps.commitWorker.stop(), remaining(), 'stopping the commit worker', log);
|
|
528
547
|
if (deps.secretsScope !== undefined) unregisterBevelSecretsVariableLoader(deps.secretsScope);
|
|
529
548
|
await bounded(closeDb(deps.db), remaining(), 'ending the database pool', log);
|
package/src/core-config.ts
CHANGED
|
@@ -88,6 +88,7 @@ export interface TenantConfig {
|
|
|
88
88
|
readonly workspacesRoot: string;
|
|
89
89
|
readonly backupsRoot: string;
|
|
90
90
|
readonly spillRoot: string;
|
|
91
|
+
readonly agentUploadsRoot: string;
|
|
91
92
|
readonly docExtractCacheRoot: string;
|
|
92
93
|
readonly jwtSecret: string;
|
|
93
94
|
readonly adminEmail: string;
|
|
@@ -184,6 +185,21 @@ export class CoreConfig implements TenantConfig, ProcessConfig {
|
|
|
184
185
|
* never committed, best-effort GC'd on write.
|
|
185
186
|
*/
|
|
186
187
|
readonly spillRoot: string;
|
|
188
|
+
/**
|
|
189
|
+
* Sibling-to-`workspacesRoot` location for the bytes an agent uploads with
|
|
190
|
+
* `request_file_upload`, held until `apply_file_upload` lands them (or their
|
|
191
|
+
* token expires, whichever comes first). BESIDE the workspaces, never inside
|
|
192
|
+
* one, and that placement is the point: an upload is content the server has
|
|
193
|
+
* accepted but not yet judged, so no file tool may name it — every workspace
|
|
194
|
+
* path resolves against a branch's checkout, and there is no spelling of one
|
|
195
|
+
* that reaches in here. Ephemeral and never committed.
|
|
196
|
+
*
|
|
197
|
+
* `AGENT_UPLOADS_ROOT` overrides it, and the placement is CHECKED at boot
|
|
198
|
+
* (`assertUploadsRootOutsideWorkspaces`): a value inside `workspacesRoot` —
|
|
199
|
+
* or one that resolves there through a link — refuses to start rather than
|
|
200
|
+
* quietly staging unjudged bytes where the file tools read.
|
|
201
|
+
*/
|
|
202
|
+
readonly agentUploadsRoot: string;
|
|
187
203
|
/**
|
|
188
204
|
* Sibling-to-`workspacesRoot` location for the document-extraction cache —
|
|
189
205
|
* text extracted from office documents/PDFs (`read_file`/`grep`), keyed by
|
|
@@ -380,6 +396,8 @@ export class CoreConfig implements TenantConfig, ProcessConfig {
|
|
|
380
396
|
this.workspacesRoot = process.env.WORKSPACES_ROOT || path.resolve(process.cwd(), 'workspaces');
|
|
381
397
|
this.backupsRoot = process.env.BACKUPS_ROOT || path.resolve(this.workspacesRoot, '..', 'backups');
|
|
382
398
|
this.spillRoot = process.env.SPILL_ROOT || path.resolve(this.workspacesRoot, '..', 'tool-chain-spills');
|
|
399
|
+
this.agentUploadsRoot =
|
|
400
|
+
process.env.AGENT_UPLOADS_ROOT || path.resolve(this.workspacesRoot, '..', 'agent-uploads');
|
|
383
401
|
this.docExtractCacheRoot =
|
|
384
402
|
process.env.DOC_EXTRACT_CACHE_ROOT || path.resolve(this.workspacesRoot, '..', 'doc-extract-cache');
|
|
385
403
|
// Required, and checked BEFORE the auth routes it signs for are ever
|
package/src/index.ts
CHANGED
|
@@ -52,18 +52,43 @@ export { coreMigrationsDir, defaultKbTemplateDir } from './assets.js';
|
|
|
52
52
|
export {
|
|
53
53
|
runCoreMigrations,
|
|
54
54
|
runEnterpriseMigrations,
|
|
55
|
+
runPiiEncryptionBackfill,
|
|
56
|
+
// The same backfill for an overlay's own sealed columns: its spec goes to
|
|
57
|
+
// `runEnterpriseMigrations`, or to `runPiiBackfill` under a lock of its own.
|
|
58
|
+
runPiiBackfill,
|
|
59
|
+
type PiiBackfillExecutor,
|
|
60
|
+
type PiiBackfillSpec,
|
|
61
|
+
type PiiBackfillTable,
|
|
62
|
+
type PiiBlindIndex,
|
|
55
63
|
} from './modules/database/migrate.js';
|
|
56
64
|
export {
|
|
57
65
|
getDb,
|
|
58
66
|
createDb,
|
|
59
67
|
closeDb,
|
|
60
68
|
dbSchemaOf,
|
|
69
|
+
piiKeysOf,
|
|
61
70
|
assertSchemaName,
|
|
62
71
|
DEFAULT_DB_SCHEMA,
|
|
63
72
|
type Database,
|
|
64
73
|
type DbOptions,
|
|
65
74
|
} from './modules/database/connection.js';
|
|
66
75
|
|
|
76
|
+
// PII column encryption (see shared/column-crypto.ts). The key belongs to the
|
|
77
|
+
// database handle (`createDb(url, { piiKey })`), so an overlay seals its own
|
|
78
|
+
// schema's columns by declaring them `encryptedText` / `blindIndexText` and
|
|
79
|
+
// running on the handle the composition root built. `derivePiiKeys` and
|
|
80
|
+
// `piiKeysOf` are for code that handles the stored form itself: SQL written
|
|
81
|
+
// by hand, or a process that reads a knowledge base's tables without a handle
|
|
82
|
+
// of this package's.
|
|
83
|
+
export {
|
|
84
|
+
encryptedText,
|
|
85
|
+
blindIndexText,
|
|
86
|
+
derivePiiKeys,
|
|
87
|
+
isEncryptedBlob,
|
|
88
|
+
PII_CIPHERTEXT_PREFIX,
|
|
89
|
+
type PiiKeys,
|
|
90
|
+
} from './shared/column-crypto.js';
|
|
91
|
+
|
|
67
92
|
// Build identity surfaced by GET /api/health.
|
|
68
93
|
export { GIT_SHA, resolveGitSha } from './version.js';
|
|
69
94
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Param } from 'drizzle-orm';
|
|
2
2
|
|
|
3
3
|
import type { Database } from '../../database/connection.js';
|
|
4
|
+
import { users } from '../../database/schema.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* Every
|
|
7
|
-
* `inArray(users.
|
|
7
|
+
* Every address a drizzle condition binds to the users' blind index, in
|
|
8
|
+
* order — for `inArray(users.emailBidx, emails)` that is exactly `emails`.
|
|
8
9
|
*
|
|
9
10
|
* A drizzle `SQL` is a tree of chunks; the bound values sit in it as `Param`
|
|
10
11
|
* nodes (an `inArray` puts them in a nested array chunk). Reading them back is
|
|
@@ -14,7 +15,12 @@ import type { Database } from '../../database/connection.js';
|
|
|
14
15
|
*/
|
|
15
16
|
function boundValues(node: unknown, depth = 0): string[] {
|
|
16
17
|
if (depth > 8 || node === null || node === undefined) return [];
|
|
17
|
-
|
|
18
|
+
// Only what is bound THROUGH THE INDEX COLUMN counts: an address compared
|
|
19
|
+
// with the encrypted `email` column matches nothing in Postgres (the
|
|
20
|
+
// ciphertext is randomized), so it matches nothing here.
|
|
21
|
+
if (node instanceof Param) {
|
|
22
|
+
return node.encoder === users.emailBidx && typeof node.value === 'string' ? [node.value] : [];
|
|
23
|
+
}
|
|
18
24
|
if (Array.isArray(node)) return node.flatMap((c) => boundValues(c, depth + 1));
|
|
19
25
|
const chunks = (node as { queryChunks?: unknown }).queryChunks;
|
|
20
26
|
return Array.isArray(chunks) ? chunks.flatMap((c) => boundValues(c, depth + 1)) : [];
|
|
@@ -30,14 +36,17 @@ function boundValues(node: unknown, depth = 0): string[] {
|
|
|
30
36
|
* routes ask about comes back without one, which is what makes the share
|
|
31
37
|
* dialog label it "hasn't signed in yet".
|
|
32
38
|
*
|
|
33
|
-
* The `where` clause IS evaluated: the
|
|
34
|
-
* against the roster the same way Postgres would
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
39
|
+
* The `where` clause IS evaluated: the values bound into it are matched
|
|
40
|
+
* against the roster the same way Postgres would. The email column is
|
|
41
|
+
* randomized ciphertext, so the routes compare its blind index
|
|
42
|
+
* (`email_bidx`), which they bind the ADDRESSES to — the database handle
|
|
43
|
+
* turns each into its index, over the canonical (trimmed, lowercased)
|
|
44
|
+
* address. The double does what the index does: a caller that stopped
|
|
45
|
+
* lowering the emails it asks about still matches here, as it does in
|
|
46
|
+
* Postgres.
|
|
40
47
|
*/
|
|
48
|
+
const canonical = (email: string) => email.trim().toLowerCase();
|
|
49
|
+
|
|
41
50
|
export function usersDbDouble(
|
|
42
51
|
accounts: readonly (string | { email: string; name?: string })[] = [],
|
|
43
52
|
): Database {
|
|
@@ -54,8 +63,8 @@ export function usersDbDouble(
|
|
|
54
63
|
where: (condition: unknown) => Promise<typeof rows>;
|
|
55
64
|
};
|
|
56
65
|
query.where = async (condition: unknown) => {
|
|
57
|
-
const asked = new Set(boundValues(condition));
|
|
58
|
-
return rows.filter((r) => asked.has(r.email));
|
|
66
|
+
const asked = new Set(boundValues(condition).map(canonical));
|
|
67
|
+
return rows.filter((r) => asked.has(canonical(r.email)));
|
|
59
68
|
};
|
|
60
69
|
return query;
|
|
61
70
|
};
|
|
@@ -724,10 +724,13 @@ export function createAccessRoutes(
|
|
|
724
724
|
async function accountsAmong(emails: string[]): Promise<Set<string>> {
|
|
725
725
|
// `inArray` refuses an empty list, and there is nothing to ask anyway.
|
|
726
726
|
if (emails.length === 0) return new Set();
|
|
727
|
+
// Through the blind index: `email` is randomized ciphertext, and the
|
|
728
|
+
// index is what the unique constraint and every lookup key on. It is
|
|
729
|
+
// compared with the addresses themselves.
|
|
727
730
|
const rows = await db
|
|
728
731
|
.select({ email: users.email })
|
|
729
732
|
.from(users)
|
|
730
|
-
.where(inArray(users.
|
|
733
|
+
.where(inArray(users.emailBidx, emails));
|
|
731
734
|
return new Set(rows.map((r) => r.email.trim().toLowerCase()));
|
|
732
735
|
}
|
|
733
736
|
|
|
@@ -26,8 +26,12 @@ export const DIRECTORY_SYNC_BOT_NAME = 'Directory Sync Bot';
|
|
|
26
26
|
export async function ensureDirectorySyncBot(db: Database): Promise<AuthUser> {
|
|
27
27
|
const inserted = await db
|
|
28
28
|
.insert(users)
|
|
29
|
-
.values({
|
|
30
|
-
|
|
29
|
+
.values({
|
|
30
|
+
email: DIRECTORY_SYNC_BOT_EMAIL,
|
|
31
|
+
emailBidx: DIRECTORY_SYNC_BOT_EMAIL,
|
|
32
|
+
name: DIRECTORY_SYNC_BOT_NAME,
|
|
33
|
+
})
|
|
34
|
+
.onConflictDoNothing({ target: users.emailBidx })
|
|
31
35
|
.returning();
|
|
32
36
|
if (inserted.length > 0) {
|
|
33
37
|
const row = inserted[0];
|
|
@@ -37,7 +41,7 @@ export async function ensureDirectorySyncBot(db: Database): Promise<AuthUser> {
|
|
|
37
41
|
const [row] = await db
|
|
38
42
|
.select()
|
|
39
43
|
.from(users)
|
|
40
|
-
.where(eq(users.
|
|
44
|
+
.where(eq(users.emailBidx, DIRECTORY_SYNC_BOT_EMAIL))
|
|
41
45
|
.limit(1);
|
|
42
46
|
if (!row) {
|
|
43
47
|
throw new Error(
|
|
@@ -6,7 +6,11 @@ import type { IExternalApiKeyService } from '../../tool-auth/external-api-key.in
|
|
|
6
6
|
import type { InternalTokenService } from '../../tool-auth/internal-token.service.js';
|
|
7
7
|
import type { AuthService } from '../../auth/auth.service.js';
|
|
8
8
|
import { createAgentInstructionsRoutes } from '../agent-instructions.routes.js';
|
|
9
|
-
import {
|
|
9
|
+
import { DEFAULT_KB_LAYOUT } from '@bevel-software/platform-shared';
|
|
10
|
+
import { TOOL_PREFIX_LINE, platformInstructions } from '../compose.js';
|
|
11
|
+
|
|
12
|
+
/** The platform-owned text the route leads with: header + the shared file rules. */
|
|
13
|
+
const PLATFORM = platformInstructions(DEFAULT_KB_LAYOUT);
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
16
|
* `GET /agent/instructions` behind the REAL manual-auth gate, with the three
|
|
@@ -61,7 +65,7 @@ describe('GET /agent/instructions', () => {
|
|
|
61
65
|
expect(res.status, bearer).toBe(200);
|
|
62
66
|
expect(res.headers.get('cache-control')).toBe('no-store');
|
|
63
67
|
const body = (await res.json()) as Record<string, unknown>;
|
|
64
|
-
expect(body.instructions).toBe(`${
|
|
68
|
+
expect(body.instructions).toBe(`${PLATFORM}\n\nAcme builds solar farms.`);
|
|
65
69
|
expect(body.toolPrefix).toBe(`${TOOL_PREFIX_LINE} Acme builds solar farms.`);
|
|
66
70
|
expect(body).toMatchObject({
|
|
67
71
|
truncated: false,
|
|
@@ -79,12 +83,12 @@ describe('GET /agent/instructions', () => {
|
|
|
79
83
|
expect((await fetch(url, { headers: { Authorization: 'Bearer not-a-session' } })).status).toBe(401);
|
|
80
84
|
});
|
|
81
85
|
|
|
82
|
-
it('a missing file yields the
|
|
86
|
+
it('a missing file yields the platform text alone', async () => {
|
|
83
87
|
const url = await serve(async () => null);
|
|
84
88
|
const res = await fetch(url, { headers: { Authorization: `Bearer ${CONNECTION_KEY}` } });
|
|
85
89
|
expect(res.status).toBe(200);
|
|
86
90
|
const body = (await res.json()) as { instructions: string; toolPrefix: string; preambleChars: number };
|
|
87
|
-
expect(body.instructions).toBe(
|
|
91
|
+
expect(body.instructions).toBe(PLATFORM);
|
|
88
92
|
expect(body.toolPrefix).toBe(TOOL_PREFIX_LINE);
|
|
89
93
|
expect(body.preambleChars).toBe(0);
|
|
90
94
|
});
|