@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.
Files changed (238) hide show
  1. package/dist/core/create-core-server.d.ts.map +1 -1
  2. package/dist/core/create-core-server.js +17 -3
  3. package/dist/core/create-core-server.js.map +1 -1
  4. package/dist/core/create-core-services.d.ts +3 -0
  5. package/dist/core/create-core-services.d.ts.map +1 -1
  6. package/dist/core/create-core-services.js +25 -1
  7. package/dist/core/create-core-services.js.map +1 -1
  8. package/dist/core/lifecycle.d.ts +12 -0
  9. package/dist/core/lifecycle.d.ts.map +1 -1
  10. package/dist/core/lifecycle.js +10 -0
  11. package/dist/core/lifecycle.js.map +1 -1
  12. package/dist/core-config.d.ts +16 -0
  13. package/dist/core-config.d.ts.map +1 -1
  14. package/dist/core-config.js +17 -0
  15. package/dist/core-config.js.map +1 -1
  16. package/dist/index.d.ts +3 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +13 -2
  19. package/dist/index.js.map +1 -1
  20. package/dist/modules/access/access.routes.d.ts.map +1 -1
  21. package/dist/modules/access/access.routes.js +4 -1
  22. package/dist/modules/access/access.routes.js.map +1 -1
  23. package/dist/modules/access/directory-sync-bot.d.ts.map +1 -1
  24. package/dist/modules/access/directory-sync-bot.js +7 -3
  25. package/dist/modules/access/directory-sync-bot.js.map +1 -1
  26. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +8 -1
  27. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
  28. package/dist/modules/agent-instructions/agent-instructions.routes.js +8 -2
  29. package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
  30. package/dist/modules/agent-instructions/compose.d.ts +38 -6
  31. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  32. package/dist/modules/agent-instructions/compose.js +39 -6
  33. package/dist/modules/agent-instructions/compose.js.map +1 -1
  34. package/dist/modules/agent-instructions/index.d.ts +2 -1
  35. package/dist/modules/agent-instructions/index.d.ts.map +1 -1
  36. package/dist/modules/agent-instructions/index.js +2 -1
  37. package/dist/modules/agent-instructions/index.js.map +1 -1
  38. package/dist/modules/agent-instructions/shared-file-rules.d.ts +115 -0
  39. package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -0
  40. package/dist/modules/agent-instructions/shared-file-rules.js +272 -0
  41. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -0
  42. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -1
  43. package/dist/modules/audit/agent-audit.service.js +4 -2
  44. package/dist/modules/audit/agent-audit.service.js.map +1 -1
  45. package/dist/modules/auth/account-erasure.service.d.ts.map +1 -1
  46. package/dist/modules/auth/account-erasure.service.js +57 -16
  47. package/dist/modules/auth/account-erasure.service.js.map +1 -1
  48. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  49. package/dist/modules/auth/auth.service.js +25 -15
  50. package/dist/modules/auth/auth.service.js.map +1 -1
  51. package/dist/modules/code-mode/code-mode.tool.d.ts +20 -2
  52. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  53. package/dist/modules/code-mode/code-mode.tool.js +66 -35
  54. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  55. package/dist/modules/database/connection.d.ts +16 -0
  56. package/dist/modules/database/connection.d.ts.map +1 -1
  57. package/dist/modules/database/connection.js +117 -0
  58. package/dist/modules/database/connection.js.map +1 -1
  59. package/dist/modules/database/core-schema.d.ts +296 -96
  60. package/dist/modules/database/core-schema.d.ts.map +1 -1
  61. package/dist/modules/database/core-schema.js +81 -31
  62. package/dist/modules/database/core-schema.js.map +1 -1
  63. package/dist/modules/database/migrate.d.ts +95 -1
  64. package/dist/modules/database/migrate.d.ts.map +1 -1
  65. package/dist/modules/database/migrate.js +390 -2
  66. package/dist/modules/database/migrate.js.map +1 -1
  67. package/dist/modules/kb-fs/locking-filesystem.d.ts +17 -0
  68. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -1
  69. package/dist/modules/kb-fs/locking-filesystem.js +29 -0
  70. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -1
  71. package/dist/modules/mcp/mcp.service.d.ts +8 -0
  72. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  73. package/dist/modules/mcp/mcp.service.js +38 -8
  74. package/dist/modules/mcp/mcp.service.js.map +1 -1
  75. package/dist/modules/plugins/join-request-records.store.d.ts.map +1 -1
  76. package/dist/modules/plugins/join-request-records.store.js +7 -4
  77. package/dist/modules/plugins/join-request-records.store.js.map +1 -1
  78. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  79. package/dist/modules/tool-auth/external-api-key.service.js +8 -2
  80. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  81. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  82. package/dist/modules/tool-manuals/tool-manuals.service.js +30 -0
  83. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  84. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  85. package/dist/modules/tool-manuals/tool-manuals.tools.js +42 -31
  86. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  87. package/dist/modules/tool-registry/description-length.d.ts +80 -0
  88. package/dist/modules/tool-registry/description-length.d.ts.map +1 -0
  89. package/dist/modules/tool-registry/description-length.js +108 -0
  90. package/dist/modules/tool-registry/description-length.js.map +1 -0
  91. package/dist/modules/workflow/git/git.service.d.ts +25 -0
  92. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  93. package/dist/modules/workflow/git/git.service.js +40 -1
  94. package/dist/modules/workflow/git/git.service.js.map +1 -1
  95. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  96. package/dist/modules/workflow/pending-commits.service.js +5 -1
  97. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  98. package/dist/modules/workflow/recovery-bot.d.ts.map +1 -1
  99. package/dist/modules/workflow/recovery-bot.js +7 -3
  100. package/dist/modules/workflow/recovery-bot.js.map +1 -1
  101. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  102. package/dist/modules/workflow/review-workflow/review-workflow.service.js +12 -3
  103. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  104. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  105. package/dist/modules/workflow/workflow.service.js +4 -1
  106. package/dist/modules/workflow/workflow.service.js.map +1 -1
  107. package/dist/modules/workspace/agent-upload.routes.d.ts +77 -0
  108. package/dist/modules/workspace/agent-upload.routes.d.ts.map +1 -0
  109. package/dist/modules/workspace/agent-upload.routes.js +210 -0
  110. package/dist/modules/workspace/agent-upload.routes.js.map +1 -0
  111. package/dist/modules/workspace/agent-upload.store.d.ts +284 -0
  112. package/dist/modules/workspace/agent-upload.store.d.ts.map +1 -0
  113. package/dist/modules/workspace/agent-upload.store.js +553 -0
  114. package/dist/modules/workspace/agent-upload.store.js.map +1 -0
  115. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  116. package/dist/modules/workspace/startup/steps/seed-tree.js +3 -3
  117. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  118. package/dist/modules/workspace/startup/steps/template-source.d.ts +40 -0
  119. package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
  120. package/dist/modules/workspace/startup/steps/template-source.js +46 -4
  121. package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
  122. package/dist/modules/workspace/upload-limits.d.ts +13 -0
  123. package/dist/modules/workspace/upload-limits.d.ts.map +1 -0
  124. package/dist/modules/workspace/upload-limits.js +13 -0
  125. package/dist/modules/workspace/upload-limits.js.map +1 -0
  126. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  127. package/dist/modules/workspace/workspace.routes.js +1 -1
  128. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  129. package/dist/modules/workspace/workspace.service.d.ts +13 -0
  130. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  131. package/dist/modules/workspace/workspace.service.js +61 -33
  132. package/dist/modules/workspace/workspace.service.js.map +1 -1
  133. package/dist/modules/workspace/workspace.tools.d.ts +11 -9
  134. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  135. package/dist/modules/workspace/workspace.tools.js +570 -134
  136. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  137. package/dist/modules/workspace/write-denial.d.ts +0 -6
  138. package/dist/modules/workspace/write-denial.d.ts.map +1 -1
  139. package/dist/modules/workspace/write-denial.js +0 -6
  140. package/dist/modules/workspace/write-denial.js.map +1 -1
  141. package/dist/modules/workspace/zip-entry-rules.d.ts +114 -0
  142. package/dist/modules/workspace/zip-entry-rules.d.ts.map +1 -0
  143. package/dist/modules/workspace/zip-entry-rules.js +154 -0
  144. package/dist/modules/workspace/zip-entry-rules.js.map +1 -0
  145. package/dist/shared/column-crypto.d.ts +194 -0
  146. package/dist/shared/column-crypto.d.ts.map +1 -0
  147. package/dist/shared/column-crypto.js +144 -0
  148. package/dist/shared/column-crypto.js.map +1 -0
  149. package/dist/shared/token-crypto.d.ts.map +1 -1
  150. package/dist/shared/token-crypto.js +25 -1
  151. package/dist/shared/token-crypto.js.map +1 -1
  152. package/dist/tenancy/static-tenant-source.d.ts.map +1 -1
  153. package/dist/tenancy/static-tenant-source.js +1 -0
  154. package/dist/tenancy/static-tenant-source.js.map +1 -1
  155. package/dist/tenancy/tenant-secrets.d.ts +5 -1
  156. package/dist/tenancy/tenant-secrets.d.ts.map +1 -1
  157. package/dist/tenancy/tenant-secrets.js +4 -0
  158. package/dist/tenancy/tenant-secrets.js.map +1 -1
  159. package/kb-template/AGENTS.md +42 -0
  160. package/migrations/0016_pii_encryption.sql +20 -0
  161. package/migrations/meta/0016_snapshot.json +2327 -0
  162. package/migrations/meta/_journal.json +7 -0
  163. package/package.json +3 -3
  164. package/src/core/__tests__/lifecycle.test.ts +71 -4
  165. package/src/core/create-core-server.ts +21 -3
  166. package/src/core/create-core-services.ts +27 -1
  167. package/src/core/lifecycle.ts +19 -0
  168. package/src/core-config.ts +18 -0
  169. package/src/index.ts +25 -0
  170. package/src/modules/access/__tests__/users-db-double.ts +21 -12
  171. package/src/modules/access/access.routes.ts +4 -1
  172. package/src/modules/access/directory-sync-bot.ts +7 -3
  173. package/src/modules/agent-instructions/__tests__/agent-instructions.route.test.ts +8 -4
  174. package/src/modules/agent-instructions/__tests__/compose.test.ts +19 -10
  175. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +238 -0
  176. package/src/modules/agent-instructions/agent-instructions.routes.ts +12 -2
  177. package/src/modules/agent-instructions/compose.ts +50 -7
  178. package/src/modules/agent-instructions/index.ts +12 -0
  179. package/src/modules/agent-instructions/shared-file-rules.ts +314 -0
  180. package/src/modules/audit/agent-audit.service.ts +4 -2
  181. package/src/modules/auth/__tests__/account-deactivation.test.ts +2 -1
  182. package/src/modules/auth/__tests__/account-erasure.approval-gate.test.ts +6 -2
  183. package/src/modules/auth/__tests__/account.routes.test.ts +6 -3
  184. package/src/modules/auth/__tests__/auth.service.test.ts +3 -1
  185. package/src/modules/auth/account-erasure.service.ts +69 -18
  186. package/src/modules/auth/auth.service.ts +33 -23
  187. package/src/modules/code-mode/__tests__/chain-runtime.e2e.test.ts +335 -0
  188. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +46 -2
  189. package/src/modules/code-mode/code-mode.tool.ts +80 -34
  190. package/src/modules/database/__tests__/connection.test.ts +12 -0
  191. package/src/modules/database/__tests__/pii-backfill.pg.test.ts +780 -0
  192. package/src/modules/database/connection.ts +117 -0
  193. package/src/modules/database/core-schema.ts +81 -31
  194. package/src/modules/database/migrate.ts +540 -2
  195. package/src/modules/kb-fs/__tests__/locking-filesystem.test.ts +93 -0
  196. package/src/modules/kb-fs/locking-filesystem.ts +37 -0
  197. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +4 -3
  198. package/src/modules/mcp/__tests__/mcp.service.test.ts +47 -7
  199. package/src/modules/mcp/mcp.service.ts +46 -7
  200. package/src/modules/plugins/join-request-records.store.ts +7 -4
  201. package/src/modules/tool-auth/external-api-key.service.ts +8 -2
  202. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +151 -0
  203. package/src/modules/tool-manuals/tool-manuals.service.ts +37 -0
  204. package/src/modules/tool-manuals/tool-manuals.tools.ts +44 -31
  205. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +378 -0
  206. package/src/modules/tool-registry/description-length.ts +111 -0
  207. package/src/modules/workflow/git/__tests__/git.service.prFetchFailure.test.ts +170 -0
  208. package/src/modules/workflow/git/git.service.ts +47 -1
  209. package/src/modules/workflow/pending-commits.service.ts +5 -1
  210. package/src/modules/workflow/recovery-bot.ts +7 -3
  211. package/src/modules/workflow/review-workflow/__tests__/carry-approvals-forward.test.ts +4 -0
  212. package/src/modules/workflow/review-workflow/__tests__/erase-approver.test.ts +25 -3
  213. package/src/modules/workflow/review-workflow/review-workflow.service.ts +12 -3
  214. package/src/modules/workflow/workflow.service.ts +4 -1
  215. package/src/modules/workspace/__tests__/agent-uploads.test.ts +1604 -0
  216. package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +24 -14
  217. package/src/modules/workspace/__tests__/workspace.service.any-workspace-credentials.test.ts +156 -0
  218. package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +77 -1
  219. package/src/modules/workspace/__tests__/workspace.service.test.ts +57 -0
  220. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +39 -36
  221. package/src/modules/workspace/__tests__/workspace.tools.test.ts +196 -46
  222. package/src/modules/workspace/agent-upload.routes.ts +214 -0
  223. package/src/modules/workspace/agent-upload.store.ts +668 -0
  224. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +9 -9
  225. package/src/modules/workspace/startup/steps/seed-tree.ts +3 -6
  226. package/src/modules/workspace/startup/steps/template-source.ts +53 -5
  227. package/src/modules/workspace/upload-limits.ts +12 -0
  228. package/src/modules/workspace/workspace.routes.ts +1 -2
  229. package/src/modules/workspace/workspace.service.ts +63 -37
  230. package/src/modules/workspace/workspace.tools.ts +647 -148
  231. package/src/modules/workspace/write-denial.ts +0 -8
  232. package/src/modules/workspace/zip-entry-rules.ts +173 -0
  233. package/src/shared/__tests__/column-crypto.test.ts +217 -0
  234. package/src/shared/column-crypto.ts +218 -0
  235. package/src/shared/token-crypto.ts +28 -1
  236. package/src/tenancy/__tests__/static-tenant-source.test.ts +4 -0
  237. package/src/tenancy/static-tenant-source.ts +1 -0
  238. 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.23.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-shared": "0.23.0",
56
- "@bevel-software/platform-mcp-core": "0.23.0"
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(['server.close', 'server.closeAllConnections', 'backgroundJobs.stopSweeping', 'backgroundJobs.drain']);
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(-3)).toEqual(['backgroundJobs.drain', 'commitWorker.stop', 'db.end']);
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(['backgroundJobs.stopSweeping', 'backgroundJobs.drain', 'commitWorker.stop', 'db.end']);
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(['backgroundJobs.stopSweeping', 'startupRetry.stop', 'backgroundJobs.drain', 'db.end']);
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
- if (jsonExemptPaths.has(req.path) || isSyncRawBodyPath(req.path)) return next();
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
- const db = getDb(config.databaseUrl, { schema: config.dbSchema });
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,
@@ -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);
@@ -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 literal bound into a drizzle condition, in order — for
7
- * `inArray(users.email, emails)` that is exactly `emails`.
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
- if (node instanceof Param) return typeof node.value === 'string' ? [node.value] : [];
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 emails bound into it are matched
34
- * against the roster the same way Postgres would — against the stored rows as
35
- * written, with no re-canonicalisation on the double's side. Real rows are
36
- * canonical (the auth service lowercases every address it writes), so a
37
- * caller that stopped lowering the emails it asks about would match nothing
38
- * here, exactly as it would match nothing in Postgres. A double that answered
39
- * with the whole roster could not tell those two apart.
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.email, emails));
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({ email: DIRECTORY_SYNC_BOT_EMAIL, name: DIRECTORY_SYNC_BOT_NAME })
30
- .onConflictDoNothing({ target: users.email })
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.email, DIRECTORY_SYNC_BOT_EMAIL))
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 { PLATFORM_HEADER, TOOL_PREFIX_LINE } from '../compose.js';
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(`${PLATFORM_HEADER}\n\nAcme builds solar farms.`);
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 header alone', async () => {
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(PLATFORM_HEADER);
91
+ expect(body.instructions).toBe(PLATFORM);
88
92
  expect(body.toolPrefix).toBe(TOOL_PREFIX_LINE);
89
93
  expect(body.preambleChars).toBe(0);
90
94
  });