@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
@@ -18,12 +18,9 @@ import { mergeGroupsIntoRoles, parseRolesYaml } from '../../../../access-model/a
18
18
  import { TemplateFilesStep } from '../template-files.step.js';
19
19
  import { buildSeedTree } from '../seed-tree.js';
20
20
  import { defaultKbTemplateDir } from '../../../../../assets.js';
21
- import {
22
- DEFAULT_KB_LAYOUT,
23
- agentsFilePointerSentence,
24
- renderKbLayoutPlaceholders,
25
- } from '@bevel-software/platform-shared';
26
- import { PLATFORM_HEADER, TOOL_PREFIX_LINE, composeAgentInstructions } from '../../../../agent-instructions/index.js';
21
+ import { DEFAULT_KB_LAYOUT, agentsFilePointerSentence } from '@bevel-software/platform-shared';
22
+ import { renderTemplateText } from '../template-source.js';
23
+ import { TOOL_PREFIX_LINE, composeAgentInstructions, platformInstructions } from '../../../../agent-instructions/index.js';
27
24
  import { testKbContext } from '../../../../../__tests__/kb-context.js';
28
25
 
29
26
  const execFileAsync = promisify(execFile);
@@ -148,7 +145,10 @@ const PREAMBLE_IGNORE_BLOCK =
148
145
 
149
146
  /** The template as the step writes it under the default layout — placeholders rendered. */
150
147
  async function template(name: string): Promise<string> {
151
- return renderKbLayoutPlaceholders(await fs.readFile(path.join(TEMPLATE_DIR, name), 'utf8'), DEFAULT_KB_LAYOUT);
148
+ // `renderTemplateText`, not the layout renderer alone: the managed guide also
149
+ // asks for the shared file rules, and a fixture rendered with half the
150
+ // placeholders filled would read as drifted on every boot.
151
+ return renderTemplateText(await fs.readFile(path.join(TEMPLATE_DIR, name), 'utf8'), DEFAULT_KB_LAYOUT);
152
152
  }
153
153
 
154
154
  /** Every required file + reserved root already present, from the real template. */
@@ -695,7 +695,7 @@ describe('TemplateFilesStep', () => {
695
695
  expect((await git(dir, ['rev-list', '--count', 'HEAD'])).trim()).toBe('1');
696
696
  });
697
697
 
698
- it('ships a template that composes to the platform header alone: one comment, nothing broadcast', async () => {
698
+ it('ships a template that composes to the platform text alone: one comment, nothing broadcast', async () => {
699
699
  const shipped = await template('mcp-description.md');
700
700
  expect(shipped.trimStart().startsWith('<!--')).toBe(true);
701
701
  expect(shipped.trimEnd().endsWith('-->')).toBe(true);
@@ -705,7 +705,7 @@ describe('TemplateFilesStep', () => {
705
705
  expect(shipped).toContain('first paragraph under about 220');
706
706
  expect(shipped).not.toContain('{{');
707
707
  const composed = composeAgentInstructions(shipped);
708
- expect(composed.instructions).toBe(PLATFORM_HEADER);
708
+ expect(composed.instructions).toBe(platformInstructions(DEFAULT_KB_LAYOUT));
709
709
  expect(composed.toolPrefix).toBe(TOOL_PREFIX_LINE);
710
710
  expect(composed.unterminatedComment).toBe(false);
711
711
  expect(composed.preambleChars).toBe(0);
@@ -1,14 +1,11 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import {
4
- LEGACY_AGENTS_FILE,
5
- renderKbLayoutPlaceholders,
6
- } from '@bevel-software/platform-shared';
3
+ import { LEGACY_AGENTS_FILE } from '@bevel-software/platform-shared';
7
4
  import type { IFsProbe, ITreeWalker } from '../../../../shared/fs.contract.js';
8
5
  import type { KbContext } from '../../../../shared/kb-context.js';
9
6
  import { renderRolesYaml } from '../../../access-model/render-roles-yaml.js';
10
7
  import { reservedRootDirs } from './template-files.step.js';
11
- import { TEMPLATE_SOURCE_FALLBACKS, TemplateSource } from './template-source.js';
8
+ import { TEMPLATE_SOURCE_FALLBACKS, TemplateSource, renderTemplateText } from './template-source.js';
12
9
  import { assertNotGitInternals, hasGitInternalsSegment } from '../../../../shared/git-internals.js';
13
10
  import { GitInternalsError } from '../../../../shared/domain-errors.js';
14
11
 
@@ -192,7 +189,7 @@ class KbSeedTree {
192
189
  if (text === null) {
193
190
  await fs.copyFile(from, to);
194
191
  } else {
195
- await fs.writeFile(to, renderKbLayoutPlaceholders(text, this.kb.layout), 'utf8');
192
+ await fs.writeFile(to, renderTemplateText(text, this.kb.layout), 'utf8');
196
193
  }
197
194
  await fs.chmod(to, (await fs.stat(from)).mode & 0o777);
198
195
  }
@@ -1,8 +1,10 @@
1
1
  import path from 'node:path';
2
- import { gitignoreLiteral, renderKbLayoutPlaceholders } from '@bevel-software/platform-shared';
2
+ import { gitignoreLiteral, renderKbLayoutPlaceholders, type KbLayout } from '@bevel-software/platform-shared';
3
3
  import { IGNORE_FILENAME, isAbsence, type EntryStat, type IFsProbe } from '../../../../shared/fs.contract.js';
4
4
  import type { KbContext } from '../../../../shared/kb-context.js';
5
+
5
6
  import { PREAMBLE_FILE } from '../../../agent-instructions/compose.js';
7
+ import { sharedFileRulesSection } from '../../../agent-instructions/shared-file-rules.js';
6
8
  import { defaultKbTemplateDir } from '../../../../assets.js';
7
9
  import { logger } from '../../../../shared/logging.js';
8
10
 
@@ -37,6 +39,42 @@ export const TEMPLATE_SOURCE_FALLBACKS: ReadonlyMap<string, string> = new Map([
37
39
  ['.gitignore', 'gitignore.template'],
38
40
  ]);
39
41
 
42
+ /**
43
+ * Where the managed guide asks for the rules every file tool shares. Not a
44
+ * layout placeholder: the layout renderer lives in `platform-shared`, which
45
+ * the frontend also reads, and the rules are backend text.
46
+ */
47
+ export const SHARED_FILE_RULES_PLACEHOLDER = '{{sharedFileRules}}';
48
+
49
+ /**
50
+ * Every placeholder a template file carries, filled: the layout's names on the
51
+ * text an author wrote, THEN the shared file rules dropped into the hole they
52
+ * left. ONE function, because the two writers of template content — the top-up
53
+ * step through {@link TemplateSource.read} and the empty-remote seeder, which
54
+ * streams files itself — must render identically or a freshly seeded knowledge
55
+ * base would carry a guide with a literal `{{sharedFileRules}}` in it.
56
+ *
57
+ * THAT ORDER, not the other one: the rules are already rendered for this
58
+ * layout (`sharedFileRulesSection` takes it), so running the layout renderer
59
+ * over them again would only reach text they STATE rather than contain — a
60
+ * folder a deployment really named `{{skillsDir}}` would come back as some
61
+ * other folder's name, and the rule would then point at a folder that is not
62
+ * the one it refuses a move out of. Injecting last leaves the section byte for
63
+ * byte what both channels carry, and costs nothing:
64
+ * `renderKbLayoutPlaceholders` touches only its own four tokens, so the
65
+ * placeholder for the rules survives the first pass untouched.
66
+ *
67
+ * A replacer FUNCTION for the rules: they carry dollar sequences (a `${VAR}`
68
+ * a template writes) that a string replacement would read as capture-group
69
+ * syntax.
70
+ */
71
+ export function renderTemplateText(text: string, layout: KbLayout): string {
72
+ return renderKbLayoutPlaceholders(text, layout).replaceAll(
73
+ SHARED_FILE_RULES_PLACEHOLDER,
74
+ () => sharedFileRulesSection(layout),
75
+ );
76
+ }
77
+
40
78
  /**
41
79
  * ONE template directory, read through ONE disk port — what both readers of
42
80
  * the KB template share.
@@ -112,6 +150,16 @@ export class TemplateSource {
112
150
  * placeholders the template carries (`{{pluginsDir}}` …) are filled with
113
151
  * the names in effect. Every required file is text; a template without
114
152
  * placeholders passes through unchanged.
153
+ *
154
+ * `{{sharedFileRules}}` is filled from the ONE source text the MCP handshake
155
+ * also sends (see `shared-file-rules.ts`), so the rules every file tool
156
+ * shares are stated in the guide and in the instructions without being
157
+ * written twice. Filled AFTER the layout pass (see
158
+ * {@link renderTemplateText}): the rules come back already rendered for this
159
+ * layout, so a layout pass over them could only misread a name they STATE as
160
+ * a placeholder. The two passes do not collide — the layout renderer touches
161
+ * only its own four tokens, so `{{sharedFileRules}}` reaches the second pass
162
+ * untouched.
115
163
  */
116
164
  async read(relPath: string): Promise<string> {
117
165
  let raw: string;
@@ -134,13 +182,13 @@ export class TemplateSource {
134
182
  // reads as syntax (`#Guide.md`, `!Guide.md`, brackets) would hide nothing
135
183
  // written bare. Escaped there, and only there: everywhere else the
136
184
  // placeholder is prose. ONE render, with the escaped name as the layout's
137
- // — a second pass over the rendered text would read a name that happens
138
- // to contain a placeholder as one.
185
+ // — rendering twice over text already rendered would read a name that
186
+ // happens to contain a placeholder as one.
139
187
  const layout = this.kb.layout;
140
188
  if (relPath === IGNORE_FILENAME) {
141
- return renderKbLayoutPlaceholders(raw, { ...layout, agentsFile: gitignoreLiteral(layout.agentsFile) });
189
+ return renderTemplateText(raw, { ...layout, agentsFile: gitignoreLiteral(layout.agentsFile) });
142
190
  }
143
- return renderKbLayoutPlaceholders(raw, layout);
191
+ return renderTemplateText(raw, layout);
144
192
  }
145
193
 
146
194
  /**
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The deployment's own upload ceiling, in bytes.
3
+ *
4
+ * ONE number for every surface that accepts bytes: the app's
5
+ * `POST /workspace/:id/upload` (a person dropping a file in the explorer) and
6
+ * the agent upload route (`POST /agent/uploads/:token`). They used to be the
7
+ * same number written twice, which is the shape a limit drifts in — an agent
8
+ * refused at 50 MB by one route and accepted by the other would have no way to
9
+ * read which limit applied. The refusal NAMES this value, so a caller can size
10
+ * its next attempt from the answer rather than by bisection.
11
+ */
12
+ export const MAX_UPLOAD_BYTES = 50 * 1024 * 1024; // 50 MB
@@ -38,8 +38,7 @@ import { createGitInternalsRouteGuard } from './git-internals.middleware.js';
38
38
  import { removeEmptyDirs } from './empty-dirs.js';
39
39
  import '../auth/auth.middleware.js'; // Express Request augmentation
40
40
  import type { SkillSaveCheck } from './workspace.tools.js';
41
-
42
- const MAX_UPLOAD_BYTES = 50 * 1024 * 1024; // 50 MB
41
+ import { MAX_UPLOAD_BYTES } from './upload-limits.js';
43
42
 
44
43
  /**
45
44
  * One file identity from one request field, or `null` when the caller sent
@@ -8,7 +8,6 @@ import type { AuthUser, IWorkspaceService, WorkspaceInfo, FileTreeEntry } from '
8
8
  import { AsyncLocalStorage } from 'node:async_hooks';
9
9
  import {
10
10
  validateRelativePath,
11
- validateFilename,
12
11
  FOLDER_PLACEHOLDER,
13
12
  isFolderPlaceholder,
14
13
  entryExistsMessage,
@@ -26,6 +25,7 @@ import { NodeGitRunner } from '../workflow/git/node-git-runner.js';
26
25
  import { assertWithinDirectory } from '../../shared/path-containment.js';
27
26
  import { assertRepoRootNameFree, normalizeWorkspacePath } from '../kb-fs/repo-path.js';
28
27
  import { assertNoGitInternalsSegment, assertNotGitInternals, hasGitInternalsSegment } from '../../shared/git-internals.js';
28
+ import { isZipNoiseEntry, readZipEntry, zipEntryName, zipEntryNameRefusal, zipEntrySegments } from './zip-entry-rules.js';
29
29
  import {
30
30
  GitInternalsError,
31
31
  PathNotFoundError,
@@ -495,6 +495,19 @@ export class WorkspaceService implements IWorkspaceService {
495
495
  * a conventional default branch isn't on the remote) just to run a global op.
496
496
  * Scans disk rather than the in-memory `branchDirs` map so clones that
497
497
  * survived a process restart are still found.
498
+ *
499
+ * INTERCHANGEABLE ONLY ONCE IT CAN REACH THE REMOTE. The clone handed back
500
+ * is one this process may never have opened, and opening a branch is the
501
+ * only other place a clone's credential helper is brought into line with the
502
+ * deployment's (`getOrCreateForBranch`). A clone made before the helper was
503
+ * persisted, or before a token was configured, has none — and it is exactly
504
+ * the kind that sorts first on disk, because it is old. Every repo-global
505
+ * operation then ran its fetches in a working copy that could not
506
+ * authenticate: the fetch failed, quietly, and each change request whose
507
+ * branch that clone had never seen was reported as `unknown branch`, list
508
+ * after list, while the branches sat on the remote. So the helper is stamped
509
+ * here too, before the clone is offered. A no-op for a clone already stamped
510
+ * in this process, and it never throws (see `stampCredentialHelper`).
498
511
  */
499
512
  async findAnyWorkspaceId(): Promise<string | null> {
500
513
  let entries: Array<{ name: string; isDirectory: () => boolean }>;
@@ -505,12 +518,15 @@ export class WorkspaceService implements IWorkspaceService {
505
518
  }
506
519
  for (const entry of entries) {
507
520
  if (!entry.isDirectory()) continue;
521
+ const repoDir = path.join(this.workspacesRoot, entry.name, this.kbDirName);
508
522
  try {
509
- await fs.access(path.join(this.workspacesRoot, entry.name, this.kbDirName, '.git'));
510
- return entry.name;
523
+ await fs.access(path.join(repoDir, '.git'));
511
524
  } catch {
512
525
  // No `.git` — half-built or unrelated dir; keep looking.
526
+ continue;
513
527
  }
528
+ await this.refreshCredentialHelperIfStale(branchForWorkspaceId(entry.name), repoDir);
529
+ return entry.name;
514
530
  }
515
531
  return null;
516
532
  }
@@ -688,6 +704,15 @@ export class WorkspaceService implements IWorkspaceService {
688
704
  const startedMeanwhile = this.inFlightSetAsides.get(branch);
689
705
  if (startedMeanwhile) {
690
706
  await startedMeanwhile;
707
+ } else if (this.discardsOf(id) !== discardsAtAdoption) {
708
+ // The copy whose address this call read is no longer there: a move
709
+ // began AND ended while it was being read. An in-flight entry is
710
+ // cleared the moment its move ends, so a caller that comes back after
711
+ // that finds nobody to wait for — and its answer is about a copy that
712
+ // has already been set aside. Acting on it would set aside whatever is
713
+ // at this path NOW, which is the fresh clone the first caller is in
714
+ // the middle of making. Nothing is left to move; the bootstrap below
715
+ // waits for that clone.
691
716
  } else {
692
717
  const kept = path.join(setAsideRootFor(this.workspacesRoot, this.setAsideRoot), setAsideStamp(), id);
693
718
  // What surrounds the move may wait, and the startup phase may set
@@ -698,6 +723,11 @@ export class WorkspaceService implements IWorkspaceService {
698
723
  if (this.discardsOf(id) !== discardsAtAdoption) return;
699
724
  await setAsideClone(repoDir, kept);
700
725
  moved = true;
726
+ // Said the way the startup phase says it: the copy at this path was
727
+ // taken away. That is what the check above reads, so every caller
728
+ // that looked at the old copy before this line knows its answer is
729
+ // stale, however late it comes back.
730
+ this.forgetClone(id);
701
731
  }).finally(() => {
702
732
  this.inFlightSetAsides.delete(branch);
703
733
  });
@@ -1997,43 +2027,22 @@ export class WorkspaceService implements IWorkspaceService {
1997
2027
  continue;
1998
2028
  }
1999
2029
  processedCount++;
2000
- const rawName = entry.entryName.replace(/\\/g, '/');
2001
-
2002
- if (
2003
- rawName.startsWith('__MACOSX/') ||
2004
- rawName === '__MACOSX' ||
2005
- rawName.endsWith('/.DS_Store') ||
2006
- rawName === '.DS_Store' ||
2007
- /(^|\/)\._/.test(rawName)
2008
- ) {
2009
- continue;
2010
- }
2030
+ const rawName = zipEntryName(entry.entryName);
2011
2031
 
2012
- if (!rawName || rawName.startsWith('/') || /(^|\/)\.\.($|\/)/.test(rawName)) {
2013
- skipped.push({ path: rawName || '(empty)', reason: 'Invalid path' });
2014
- continue;
2015
- }
2032
+ if (isZipNoiseEntry(rawName)) continue;
2016
2033
 
2017
- // An archive never writes into the git folder, whatever the entry's spelling.
2018
- if (hasGitInternalsSegment(rawName)) {
2019
- skipped.push({ path: rawName, reason: new GitInternalsError().message });
2034
+ // What an entry's NAME may be — invalid path, the git folder in any
2035
+ // spelling, a segment no filesystem keeps intact — is `zip-entry-rules`,
2036
+ // so `apply_file_upload` judges the same names by the same rules rather
2037
+ // than by a second copy of them.
2038
+ const nameRefusal = zipEntryNameRefusal(rawName);
2039
+ if (nameRefusal !== null) {
2040
+ skipped.push({ path: rawName || '(empty)', reason: nameRefusal });
2020
2041
  continue;
2021
2042
  }
2022
2043
 
2023
2044
  const trimmed = rawName.replace(/\/+$/, '');
2024
- const segments = trimmed.split('/').filter((s) => s.length > 0);
2025
- let invalidReason: string | null = null;
2026
- for (const segment of segments) {
2027
- const reason = validateFilename(segment);
2028
- if (reason) {
2029
- invalidReason = reason;
2030
- break;
2031
- }
2032
- }
2033
- if (invalidReason) {
2034
- skipped.push({ path: rawName, reason: invalidReason });
2035
- continue;
2036
- }
2045
+ const segments = zipEntrySegments(rawName);
2037
2046
 
2038
2047
  const targetAbsolute = path.resolve(destAbsolute, ...segments);
2039
2048
  const destRoot = path.resolve(destAbsolute);
@@ -2114,14 +2123,31 @@ export class WorkspaceService implements IWorkspaceService {
2114
2123
  continue;
2115
2124
  }
2116
2125
 
2117
- const data = entry.getData();
2118
- if (data.byteLength > UNZIP_MAX_ENTRY_BYTES) {
2126
+ // Read through the one bounded reader (`readZipEntry`), never with the
2127
+ // declared size above as the only bound: an entry that declares ZERO is
2128
+ // inflated with no cap at all by the archive reader, so a few kilobytes
2129
+ // could expand to whatever memory there is before the checks below ran.
2130
+ // Capped at what is left of both limits, and a read that fails is this
2131
+ // entry's refusal rather than the whole archive's.
2132
+ const read = readZipEntry(
2133
+ entry,
2134
+ Math.min(UNZIP_MAX_ENTRY_BYTES, UNZIP_MAX_TOTAL_BYTES - totalUncompressed),
2135
+ );
2136
+ if (!read.ok && read.reason === 'unreadable') {
2137
+ skipped.push({ path: rawName, reason: `Entry could not be read: ${read.detail}` });
2138
+ continue;
2139
+ }
2140
+ if (!read.ok) {
2119
2141
  skipped.push({
2120
2142
  path: rawName,
2121
- reason: `Entry exceeds ${UNZIP_MAX_ENTRY_BYTES} byte per-file limit`,
2143
+ reason:
2144
+ totalUncompressed + UNZIP_MAX_ENTRY_BYTES > UNZIP_MAX_TOTAL_BYTES
2145
+ ? `Archive exceeds ${UNZIP_MAX_TOTAL_BYTES} byte total uncompressed limit`
2146
+ : `Entry exceeds ${UNZIP_MAX_ENTRY_BYTES} byte per-file limit`,
2122
2147
  });
2123
2148
  continue;
2124
2149
  }
2150
+ const data = read.data;
2125
2151
  if (totalUncompressed + data.byteLength > UNZIP_MAX_TOTAL_BYTES) {
2126
2152
  skipped.push({
2127
2153
  path: rawName,