@bevel-software/platform-core-backend 0.34.0 → 0.35.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 (179) hide show
  1. package/agent-guide/access-control.md +146 -300
  2. package/agent-guide/conventions.md +23 -25
  3. package/agent-guide/directory-structure.md +72 -120
  4. package/agent-guide/drafts-and-change-requests.md +92 -0
  5. package/agent-guide/for-git-users.md +47 -0
  6. package/agent-guide/glossary.md +36 -0
  7. package/agent-guide/how-agents-find-things.md +49 -0
  8. package/agent-guide/html-views.md +16 -11
  9. package/agent-guide/new-knowledge-base.md +9 -8
  10. package/agent-guide/skills.md +67 -42
  11. package/agent-guide/start-here.md +65 -0
  12. package/agent-guide/tool-manuals.md +132 -154
  13. package/agent-guide/tools.md +67 -0
  14. package/agent-guide/where-a-new-file-goes.md +45 -28
  15. package/dist/core/create-core-server.js +1 -1
  16. package/dist/core/create-core-server.js.map +1 -1
  17. package/dist/core/create-core-services.d.ts +6 -0
  18. package/dist/core/create-core-services.d.ts.map +1 -1
  19. package/dist/core/create-core-services.js +7 -4
  20. package/dist/core/create-core-services.js.map +1 -1
  21. package/dist/modules/access/access-control.interface.d.ts +22 -0
  22. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  23. package/dist/modules/access/access-control.service.d.ts +3 -1
  24. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  25. package/dist/modules/access/access-control.service.js +75 -0
  26. package/dist/modules/access/access-control.service.js.map +1 -1
  27. package/dist/modules/access/access.routes.d.ts.map +1 -1
  28. package/dist/modules/access/access.routes.js +23 -4
  29. package/dist/modules/access/access.routes.js.map +1 -1
  30. package/dist/modules/access/index.d.ts +1 -1
  31. package/dist/modules/access/index.d.ts.map +1 -1
  32. package/dist/modules/agent-guide/agent-guide.d.ts +47 -7
  33. package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -1
  34. package/dist/modules/agent-guide/agent-guide.js +103 -13
  35. package/dist/modules/agent-guide/agent-guide.js.map +1 -1
  36. package/dist/modules/agent-guide/agent-guide.tools.d.ts +6 -2
  37. package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -1
  38. package/dist/modules/agent-guide/agent-guide.tools.js +45 -31
  39. package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -1
  40. package/dist/modules/agent-guide/guide-budget.d.ts +48 -0
  41. package/dist/modules/agent-guide/guide-budget.d.ts.map +1 -0
  42. package/dist/modules/agent-guide/guide-budget.js +67 -0
  43. package/dist/modules/agent-guide/guide-budget.js.map +1 -0
  44. package/dist/modules/agent-guide/index.d.ts +2 -1
  45. package/dist/modules/agent-guide/index.d.ts.map +1 -1
  46. package/dist/modules/agent-guide/index.js +2 -1
  47. package/dist/modules/agent-guide/index.js.map +1 -1
  48. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  49. package/dist/modules/agent-instructions/compose.js +4 -0
  50. package/dist/modules/agent-instructions/compose.js.map +1 -1
  51. package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
  52. package/dist/modules/agent-instructions/shared-file-rules.js +10 -8
  53. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
  54. package/dist/modules/code-mode/code-mode.tool.js +2 -2
  55. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  56. package/dist/modules/connection-probe/connection-probe.service.d.ts +0 -12
  57. package/dist/modules/connection-probe/connection-probe.service.d.ts.map +1 -1
  58. package/dist/modules/connection-probe/connection-probe.service.js +1 -38
  59. package/dist/modules/connection-probe/connection-probe.service.js.map +1 -1
  60. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  61. package/dist/modules/mcp/mcp.service.js +6 -35
  62. package/dist/modules/mcp/mcp.service.js.map +1 -1
  63. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts +11 -1
  64. package/dist/modules/secrets-vault/db-secrets-vault.service.d.ts.map +1 -1
  65. package/dist/modules/secrets-vault/db-secrets-vault.service.js +159 -24
  66. package/dist/modules/secrets-vault/db-secrets-vault.service.js.map +1 -1
  67. package/dist/modules/secrets-vault/index.d.ts +1 -1
  68. package/dist/modules/secrets-vault/index.d.ts.map +1 -1
  69. package/dist/modules/secrets-vault/index.js +1 -1
  70. package/dist/modules/secrets-vault/index.js.map +1 -1
  71. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts +53 -11
  72. package/dist/modules/secrets-vault/secrets-vault.contract.d.ts.map +1 -1
  73. package/dist/modules/secrets-vault/secrets-vault.contract.js +27 -0
  74. package/dist/modules/secrets-vault/secrets-vault.contract.js.map +1 -1
  75. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  76. package/dist/modules/secrets-vault/secrets-vault.routes.js +85 -39
  77. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  78. package/dist/modules/settings/oidc-check.d.ts.map +1 -1
  79. package/dist/modules/settings/oidc-check.js +5 -17
  80. package/dist/modules/settings/oidc-check.js.map +1 -1
  81. package/dist/modules/skills/skills.tools.js +1 -1
  82. package/dist/modules/skills/skills.tools.js.map +1 -1
  83. package/dist/modules/tool-helpers/tool-def.js +1 -1
  84. package/dist/modules/tool-helpers/tool-def.js.map +1 -1
  85. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +7 -1
  86. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -1
  87. package/dist/modules/tool-manuals/mcp-json-discovery.js +69 -38
  88. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -1
  89. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +27 -0
  90. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  91. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +12 -8
  92. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  93. package/dist/modules/tool-manuals/tool-manuals.service.js +115 -44
  94. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  95. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +8 -2
  96. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  97. package/dist/modules/tool-manuals/tool-manuals.tools.js +31 -10
  98. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  99. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  100. package/dist/modules/workflow/agent-tools/workflow.tools.js +22 -18
  101. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  102. package/dist/modules/workspace/agent-access.gate.d.ts +1 -1
  103. package/dist/modules/workspace/agent-access.gate.d.ts.map +1 -1
  104. package/dist/modules/workspace/agent-access.gate.js +1 -1
  105. package/dist/modules/workspace/agent-access.gate.js.map +1 -1
  106. package/dist/modules/workspace/workspace.tools.d.ts +3 -2
  107. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  108. package/dist/modules/workspace/workspace.tools.js +71 -21
  109. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  110. package/dist/shared/oauth-token-error.d.ts +23 -0
  111. package/dist/shared/oauth-token-error.d.ts.map +1 -0
  112. package/dist/shared/oauth-token-error.js +47 -0
  113. package/dist/shared/oauth-token-error.js.map +1 -0
  114. package/dist/shared/read-capped.d.ts +19 -0
  115. package/dist/shared/read-capped.d.ts.map +1 -0
  116. package/dist/shared/read-capped.js +45 -0
  117. package/dist/shared/read-capped.js.map +1 -0
  118. package/package.json +4 -3
  119. package/src/__tests__/hexis-catalog.ts +144 -0
  120. package/src/__tests__/real-disk.ts +21 -0
  121. package/src/__tests__/tokens.ts +19 -0
  122. package/src/__tests__/tool-manuals.ts +32 -0
  123. package/src/__tests__/tool-wiring.ts +25 -0
  124. package/src/core/__tests__/oauth-sign-in-real-file.test.ts +179 -0
  125. package/src/core/create-core-server.ts +1 -1
  126. package/src/core/create-core-services.ts +13 -3
  127. package/src/modules/access/__tests__/access.routes.prospective.test.ts +52 -12
  128. package/src/modules/access/__tests__/holder-changes.test.ts +127 -0
  129. package/src/modules/access/access-control.interface.ts +26 -0
  130. package/src/modules/access/access-control.service.ts +92 -0
  131. package/src/modules/access/access.routes.ts +23 -4
  132. package/src/modules/access/index.ts +1 -1
  133. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +79 -25
  134. package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +38 -11
  135. package/src/modules/agent-guide/__tests__/guide-budget.test.ts +95 -0
  136. package/src/modules/agent-guide/__tests__/guide-eval.test.ts +182 -0
  137. package/src/modules/agent-guide/__tests__/guide-matches-code.test.ts +263 -0
  138. package/src/modules/agent-guide/__tests__/tool-surface.json +514 -0
  139. package/src/modules/agent-guide/__tests__/tool-surface.test.ts +75 -0
  140. package/src/modules/agent-guide/agent-guide.tools.ts +49 -31
  141. package/src/modules/agent-guide/agent-guide.ts +120 -13
  142. package/src/modules/agent-guide/guide-budget.ts +73 -0
  143. package/src/modules/agent-guide/index.ts +12 -0
  144. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +4 -4
  145. package/src/modules/agent-instructions/compose.ts +4 -0
  146. package/src/modules/agent-instructions/shared-file-rules.ts +10 -8
  147. package/src/modules/code-mode/code-mode.tool.ts +2 -2
  148. package/src/modules/connection-probe/__tests__/connection-probe.service.test.ts +5 -24
  149. package/src/modules/connection-probe/connection-probe.service.ts +1 -34
  150. package/src/modules/mcp/mcp.service.ts +5 -35
  151. package/src/modules/secrets-vault/__tests__/db-secrets-vault.oauth.test.ts +430 -3
  152. package/src/modules/secrets-vault/__tests__/oauth-live-declaration.route.test.ts +218 -0
  153. package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +39 -6
  154. package/src/modules/secrets-vault/db-secrets-vault.service.ts +170 -27
  155. package/src/modules/secrets-vault/index.ts +1 -1
  156. package/src/modules/secrets-vault/secrets-vault.contract.ts +61 -11
  157. package/src/modules/secrets-vault/secrets-vault.routes.ts +91 -39
  158. package/src/modules/settings/oidc-check.ts +5 -18
  159. package/src/modules/skills/skills.tools.ts +1 -1
  160. package/src/modules/tool-helpers/__tests__/agent-roles-write.test.ts +19 -11
  161. package/src/modules/tool-helpers/__tests__/route-tool-schemas.catalog.test.ts +38 -0
  162. package/src/modules/tool-helpers/tool-def.ts +1 -1
  163. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +197 -11
  164. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +96 -5
  165. package/src/modules/tool-manuals/mcp-json-discovery.ts +73 -34
  166. package/src/modules/tool-manuals/tool-manuals.contract.ts +22 -0
  167. package/src/modules/tool-manuals/tool-manuals.service.ts +138 -46
  168. package/src/modules/tool-manuals/tool-manuals.tools.ts +53 -14
  169. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +6 -113
  170. package/src/modules/workflow/agent-tools/workflow.tools.ts +22 -18
  171. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +8 -10
  172. package/src/modules/workspace/__tests__/workspace.tools.test.ts +156 -49
  173. package/src/modules/workspace/agent-access.gate.ts +1 -1
  174. package/src/modules/workspace/workspace.tools.ts +74 -21
  175. package/src/shared/__tests__/read-capped.test.ts +68 -0
  176. package/src/shared/oauth-token-error.ts +46 -0
  177. package/src/shared/read-capped.ts +40 -0
  178. package/agent-guide/finding-things.md +0 -7
  179. package/agent-guide/introduction.md +0 -27
@@ -142,6 +142,12 @@ async function keepFolderOf(fs, ctx, branch, removedPath, kbDirName) {
142
142
  }
143
143
  const int = (description) => ({ type: 'integer', description });
144
144
  const str = (description) => ({ type: 'string', description });
145
+ /**
146
+ * A write to the default branch is live at once, so the agent asks first. Said on the four
147
+ * tools behind 87 of the 91 writes there in the agent-guide benchmark (2026-10-26); `start-here`
148
+ * says it for the other write tools.
149
+ */
150
+ const ASK_BEFORE_DEFAULT_BRANCH = ' On the default branch a write is live for everyone at once: unless the person already said where, ask them first whether to write there or propose it on a draft.';
145
151
  /**
146
152
  * The upload route, named on every tool that takes content as a JSON string.
147
153
  *
@@ -829,8 +835,9 @@ changeGate,
829
835
  */
830
836
  uploads,
831
837
  /**
832
- * The platform's agent guide (see `modules/agent-guide`), which `read_file`
833
- * and `file_stat` answer at the guide's name in the repository root — after
838
+ * The start of the platform's agent guide and the list of its sections
839
+ * (`guideStartText` in `modules/agent-guide`), which `read_file` and
840
+ * `file_stat` answer at the guide's name in the repository root — after
834
841
  * the knowledge base's own file of that name, when it has one. Optional for
835
842
  * the harnesses that are about the file primitives; without it the two
836
843
  * tools read the disk and nothing else.
@@ -1636,13 +1643,13 @@ patternMatcher) {
1636
1643
  // transport hiccup on its first call as an unrecoverable start.
1637
1644
  const startSessionDef = toolDef({
1638
1645
  name: 'start_session',
1639
- description: 'Mint this conversation\'s id: the `sessionId` KnowledgeBase tools take. Call this ONCE, at the start of your work — minting a new id mid-run starts a second conversation as far as the server is concerned. The id is also a chat session in the app, so you can hand the SAME id to the `ask` tool: your reads and your questions are then one conversation. Pass the returned id as `sessionId` on every later KnowledgeBase tool call, inside `call_tool_chain` too. RETRYING IS SAFE: a call that fails created nothing, so retry it. If a retry lands after a success you hold two independent ids, which is harmless: keep passing the one you already used and ignore the other. Returns `{ sessionId }`, plus `firstRun` while the knowledge base is still empty: a note to act on.',
1646
+ description: 'Mint this conversation\'s id: the `sessionId` KnowledgeBase tools take. Call this ONCE, at the start of your work — minting a new id mid-run starts a second conversation as far as the server is concerned. Pass the returned id as `sessionId` to every tool whose arguments list it, inside `call_tool_chain` too; the others refuse it. Where the deployment offers an `ask` tool, hand it the SAME id: your reads and your questions are then one conversation. RETRYING IS SAFE: a call that fails created nothing, so retry it. If a retry lands after a success you hold two independent ids, which is harmless: keep passing the one you already used and ignore the other. Returns `{ sessionId }`, plus `firstRun` while the knowledge base is still empty: a note to act on.',
1640
1647
  path: '/api/agent/tools/start_session',
1641
1648
  inputs: { type: 'object', properties: {}, additionalProperties: false },
1642
1649
  outputs: {
1643
1650
  type: 'object',
1644
1651
  properties: {
1645
- sessionId: str('The minted session id — pass it as `sessionId` on subsequent KnowledgeBase tool calls and to `ask`.'),
1652
+ sessionId: str('The minted session id — pass it as `sessionId` to the tools that take one, and to `ask` where there is one.'),
1646
1653
  firstRun: str(`Present only while the knowledge base holds nothing but its starter guide (and a starter pack's untouched pages): what to offer the person (the guide's \`${FIRST_RUN_SECTION_ID}\` section says how).`),
1647
1654
  },
1648
1655
  required: ['sessionId'],
@@ -1843,7 +1850,7 @@ patternMatcher) {
1843
1850
  description: 'Read a workspace file as text. Returns `{ path, content, canWrite }`. Per the shared content rule: text as ' +
1844
1851
  'text, documents and email as extracted text, an image as the picture, anything else as a one-line ' +
1845
1852
  'description. Also reads a `__tool_chain_spill__/…` ref back from a truncated `call_tool_chain`: it ' +
1846
- 'belongs to no workspace, so `branch` is ignored and no `canWrite` comes back. `offset`/`limit` slice the ' +
1853
+ 'belongs to no workspace: any existing `branch` will do, and no `canWrite` comes back. `offset`/`limit` slice the ' +
1847
1854
  'content (characters; bytes for a spill ref; ignored for an image): page through large files and ' +
1848
1855
  'spills, never read multi-MB whole. `canWrite`: may you change this file directly on the default branch. ' +
1849
1856
  CAN_WRITE_CLAUSE,
@@ -1852,8 +1859,8 @@ patternMatcher) {
1852
1859
  properties: {
1853
1860
  branch: BRANCH_INPUT,
1854
1861
  path: str(`Path to read, under \`${kbDirName}/\` (e.g. \`${kbDirName}/KnowledgeBase/Foo.md\`), with or without a leading slash — a path without that prefix is placed under \`${kbDirName}/\` — or a \`__tool_chain_spill__/…\` ref from a truncated \`call_tool_chain\`, or a \`__tmp__/…\` ref to a file you staged in the workspace's tmp folder.`),
1855
- offset: int('Start character index (default 0).'),
1856
- limit: int('Max characters to return from `offset`.'),
1862
+ offset: int('Start character index (bytes for a spill ref; default 0).'),
1863
+ limit: int('Max characters to return from `offset` (bytes for a spill ref).'),
1857
1864
  sessionId: SESSION_ID_INPUT,
1858
1865
  },
1859
1866
  required: ['branch', 'path'],
@@ -2515,6 +2522,7 @@ patternMatcher) {
2515
2522
  gated: true,
2516
2523
  description: 'Write a workspace TEXT file. The change is committed + pushed as you. Returns `{ path, bytes, outcome }`, where `outcome` is ' +
2517
2524
  '`created`, `replaced` or `updated`.' +
2525
+ ASK_BEFORE_DEFAULT_BRANCH +
2518
2526
  UPLOAD_ROUTE_NOTE,
2519
2527
  inputs: {
2520
2528
  type: 'object',
@@ -2595,6 +2603,7 @@ patternMatcher) {
2595
2603
  '`created` / `replaced` / `updated` for a path it wrote, or `refused` with `error` (the code) and `message` (why) for a ' +
2596
2604
  'path it could not. `count` is how many were written. A path it refuses — the mode said no, or the file is not text — ' +
2597
2605
  'does not stop the others; read `files` to see what landed.' +
2606
+ ASK_BEFORE_DEFAULT_BRANCH +
2598
2607
  UPLOAD_ROUTE_NOTE,
2599
2608
  inputs: {
2600
2609
  type: 'object',
@@ -2632,7 +2641,7 @@ patternMatcher) {
2632
2641
  enum: ['created', 'replaced', 'updated', 'refused'],
2633
2642
  description: 'What happened at this path. `refused` means nothing was written there and the file is untouched.',
2634
2643
  },
2635
- error: str('Present when `outcome` is `refused`: the refusal code — `exists`, `missing` or `binary_not_writable`.'),
2644
+ error: str('Present when `outcome` is `refused`: the refusal code, such as `exists`, `missing`, `binary_not_writable` or a code of the deployment\'s own write check.'),
2636
2645
  message: str('Present when `outcome` is `refused`: the full refusal, the same one write_file would have given.'),
2637
2646
  },
2638
2647
  required: ['path', 'outcome'],
@@ -2761,6 +2770,7 @@ patternMatcher) {
2761
2770
  name: 'edit_file',
2762
2771
  gated: true,
2763
2772
  description: 'Replace an exact string in a workspace TEXT file. `old_string` must appear exactly once unless `replace_all`. Committed + pushed as you.' +
2773
+ ASK_BEFORE_DEFAULT_BRANCH +
2764
2774
  UPLOAD_ROUTE_NOTE,
2765
2775
  inputs: {
2766
2776
  type: 'object',
@@ -2839,7 +2849,8 @@ patternMatcher) {
2839
2849
  mount({
2840
2850
  name: 'delete_file',
2841
2851
  gated: true,
2842
- description: 'Delete ONE workspace file. Committed + pushed as you. Its folder stays, even when this was its last file. Files only: a folder is refused with a pointer to `delete_folder`.',
2852
+ description: 'Delete ONE workspace file. Committed + pushed as you. Its folder stays, even when this was its last file. Files only: a folder is refused with a pointer to `delete_folder`.' +
2853
+ ASK_BEFORE_DEFAULT_BRANCH,
2843
2854
  inputs: {
2844
2855
  type: 'object',
2845
2856
  properties: {
@@ -3061,10 +3072,11 @@ patternMatcher) {
3061
3072
  // the shared rules now, which are rebuilt from the layout where they live.
3062
3073
  description: 'Move or rename a workspace FILE or FOLDER; a folder moves recursively, with everything under it. `dest` is the full new path, not the folder to move into. Committed + pushed as you. ' +
3063
3074
  'The destination must not exist — a move never overwrites a file or merges into a folder. Access follows the ' +
3064
- 'DESTINATION folder, so a move can change what you (and others) may do with the file: the dry run answers ' +
3065
- '`{ src, dest, kind, descendants, access: { before, after }, accessChanges, allowed, reason? }`, where `access` is your ' +
3066
- 'own `{ read, write, download, owner }` at the source and at the destination AS IT WILL BE once the move has landed, ' +
3067
- 'with every `access.md` inside a moved folder counted at its new place, and a move whose `accessChanges` is true wants `confirm: true`. ' +
3075
+ 'DESTINATION folder, so a move can change who may open or edit the file. You must be able to read what moves and the folder it lands in, ' +
3076
+ 'or it is refused as read_file refuses. The dry run answers ' +
3077
+ '`{ src, dest, kind, descendants, access: { before, after }, accessChanges, holderChanges?, allowed, reason? }`: your own ' +
3078
+ 'access at both ends, and who else loses or gains it. A move that changes either wants `confirm: true`; tell the person ' +
3079
+ 'who loses and who gains before you confirm. ' +
3068
3080
  'Links are rewritten by default, in one commit with the move: those in the moved files and those in other markdown files pointing at them (`links` reports them). `rewriteLinks: false` turns that off. Path mentions in plain prose or code are never changed.',
3069
3081
  inputs: {
3070
3082
  type: 'object',
@@ -3073,7 +3085,7 @@ patternMatcher) {
3073
3085
  src: wsPath(kbDirName, 'Source path (file or folder)'),
3074
3086
  dest: wsPath(kbDirName, 'Destination path — the full new path; must not exist yet'),
3075
3087
  dryRun: { type: 'boolean', description: 'Answer with the impact and change nothing.' },
3076
- confirm: { type: 'boolean', description: 'Required when the move changes your access. Set it only after a dry run.' },
3088
+ confirm: { type: 'boolean', description: 'Required when the move changes your access or anyone else\'s. Set it only after a dry run.' },
3077
3089
  rewriteLinks: { type: 'boolean', description: 'Rewrite the links into, out of and between the moved files (default true). `false` moves without touching any link.' },
3078
3090
  sessionId: SESSION_ID_INPUT,
3079
3091
  },
@@ -3087,8 +3099,13 @@ patternMatcher) {
3087
3099
  dest: str('Destination path (echoes the input).'),
3088
3100
  kind: str('`file` or `folder`.'),
3089
3101
  descendants: int('Files that move: 1 for a file, the file count under a folder.'),
3090
- access: { type: 'object', description: 'Your `{ read, write, download, owner }` at the source (`before`) and at the destination once the move has landed (`after`).' },
3102
+ access: { type: 'object', description: 'Your `{ read, write, download, owner }` at the source (`before`) and at the destination once the move has landed (`after`), with every `access.md` inside a moved folder counted at its new place.' },
3091
3103
  accessChanges: { type: 'boolean', description: 'True when any of your verdicts differs between `before` and `after`.' },
3104
+ holderChanges: {
3105
+ type: 'object',
3106
+ description: 'Who loses and who gains opening (`read`) or editing (`write`) it once the move has landed: `{ lose, gain }`, each ' +
3107
+ '`[{ kind, name, email?, verb }]` with `kind` `group`, `role`, `plugin` or `person`. Absent when nobody\'s access changes.',
3108
+ },
3092
3109
  allowed: { type: 'boolean', description: 'Whether the move may run.' },
3093
3110
  reason: str('Why it may not, when `allowed` is false.'),
3094
3111
  dryRun: { type: 'boolean', description: 'True on a dry run.' },
@@ -3117,6 +3134,23 @@ patternMatcher) {
3117
3134
  // source already deleted.
3118
3135
  await assertAgentWriteAllowed(agentAccessGate, ctx, branch, src);
3119
3136
  await assertAgentWriteAllowed(agentAccessGate, ctx, branch, dest);
3137
+ // What moves and the folder it lands in must both be readable to the
3138
+ // caller, refused as read_file refuses them, on every branch: a move
3139
+ // preview reads both ends (the caller's own verdicts there, who else is
3140
+ // affected), and the folder's rules are what the names are made of. The
3141
+ // verdict comes BEFORE every other look, so neither end's existence is
3142
+ // told to someone who may not know it. The deployment's read hook is
3143
+ // asked first, as read_file asks it, and its refusal is the answer: a
3144
+ // move the hook would not let read is not previewed at all, since even
3145
+ // "this move asks for confirm" would tell what the comparison found. A
3146
+ // folder the caller may read moves with everything in it, files they
3147
+ // may not read included: those travel, they are not read.
3148
+ const destFolder = dest.slice(0, dest.lastIndexOf('/'));
3149
+ await notifyAgentRead(agentAccessGate, ctx, branch, src);
3150
+ await notifyAgentRead(agentAccessGate, ctx, branch, destFolder);
3151
+ const gate = readGateFor(branch, ctx);
3152
+ await assertCanRead(gate, src);
3153
+ await assertCanRead(gate, destFolder);
3120
3154
  const fs = await ctx.getFilesystem(branch);
3121
3155
  const root = await workspaceRoot(branch, ctx);
3122
3156
  // Before the kind check, which stats THROUGH a link: a dangling link at
@@ -3140,6 +3174,17 @@ patternMatcher) {
3140
3174
  accessAfter(branch, ctx, src, dest),
3141
3175
  ]);
3142
3176
  const accessChanges = verbsDiffer(before, after);
3177
+ // Who ELSE gains or loses opening or editing it: the lists the app's move
3178
+ // confirmation shows people. The caller may read both ends (the hook and
3179
+ // the rules, gated above, as the app's route gates), so the names go to
3180
+ // them, and a change asks for `confirm`. Outside the repository no rules
3181
+ // travel.
3182
+ const fromRel = toKbRelative(src, kbDirName);
3183
+ const toRel = toKbRelative(dest, kbDirName);
3184
+ const changes = fromRel !== null && toRel !== null
3185
+ ? await accessControl.prospectiveHolderChanges(workspaceIdForBranch(branch), fromRel, toRel)
3186
+ : undefined;
3187
+ const othersChange = changes !== undefined && (changes.lose.length > 0 || changes.gain.length > 0);
3143
3188
  // The placeholder moves with its folder, but it is never content.
3144
3189
  const descendants = srcFiles.filter((f) => !isFolderPlaceholder(f)).length;
3145
3190
  // Neither end may be the platform's own: a move neither takes a platform
@@ -3243,6 +3288,7 @@ patternMatcher) {
3243
3288
  descendants,
3244
3289
  access: { before, after },
3245
3290
  accessChanges,
3291
+ ...(othersChange ? { holderChanges: changes } : {}),
3246
3292
  allowed: moveReason === undefined,
3247
3293
  ...(moveReason !== undefined ? { reason: moveReason } : {}),
3248
3294
  ...(linkPlan ? { links: linkPlan.report } : {}),
@@ -3262,12 +3308,16 @@ patternMatcher) {
3262
3308
  throw new ToolError(reason, 409);
3263
3309
  if (linkPlan?.overCap !== undefined)
3264
3310
  throw new ToolError(linkPlan.overCap, 400);
3265
- if (accessChanges && a.confirm !== true) {
3311
+ if ((accessChanges || othersChange) && a.confirm !== true) {
3312
+ const why = [
3313
+ ...(accessChanges ? [`your access at "${dest}" differs from "${src}"`] : []),
3314
+ ...(othersChange ? ['it changes who else may open or edit it (`holderChanges`)'] : []),
3315
+ ];
3266
3316
  return {
3267
3317
  ...impact,
3268
3318
  confirmationRequired: true,
3269
3319
  moved: false,
3270
- message: `Nothing was moved: your access at "${dest}" differs from "${src}", so this move requires confirm: true.`,
3320
+ message: `Nothing was moved: ${why.join(', and ')}, so this move requires confirm: true.`,
3271
3321
  };
3272
3322
  }
3273
3323
  // The look above is what produces the sentence; the filesystem's own
@@ -3320,8 +3370,8 @@ patternMatcher) {
3320
3370
  name: 'copy_file',
3321
3371
  gated: true,
3322
3372
  description: 'Copy a workspace FILE to a new path. The destination must not exist — like a move, a copy never overwrites a file or a folder; to change what is in a file that already exists, write it. Committed + pushed as you. `src` may be a `__tmp__/<path>` ref to a file you staged in the workspace\'s tmp folder: the copy is how such a file is kept, as a normal repository file. ' +
3323
- 'Preflight first: `dryRun: true` changes nothing and answers `{ src, dest, kind, descendants, access: { before, after }, accessChanges, allowed, reason? }` — `access` is your own `{ read, write, download, owner }` at the source and at the destination AS IT WILL BE once the copy has landed, with every `access.md` inside a copied folder counted at its new place. ' +
3324
- 'One exception to that shape: when the destination is one you may not write, the answer is the refusal alone — `allowed: false` with `reason`, and no `kind` and no `descendants`, because nothing about the source is read before that verdict.',
3373
+ 'Preflight first: `dryRun: true` changes nothing and answers `{ src, dest, kind, descendants, access: { before, after }, accessChanges, allowed, reason? }` — `access` is your own `{ read, write, download, owner }` at the source and at the destination AS IT WILL BE once the copy has landed, with the copy\'s own rules counted at its new place. ' +
3374
+ 'One exception to that shape: when the destination is one you may not write, the answer is the refusal (`allowed: false` with `reason`, and the access figures) with no `kind` and no `descendants`, because nothing about the source is read before that verdict.',
3325
3375
  inputs: {
3326
3376
  type: 'object',
3327
3377
  properties: {
@@ -3741,7 +3791,7 @@ patternMatcher) {
3741
3791
  '`replaced` / `updated`, or `refused` with `error` (the code) and `message` (why). `count` is how many landed and ' +
3742
3792
  '`total` how many paths there were; `files` is cut to the first 25 unless you pass `all: true`. ' +
3743
3793
  'Every path is judged one by one — by your write access, the platform-file rules and what is already there — ' +
3744
- 'exactly as `write_file` judges it, and a refused path does not stop the others. ' +
3794
+ 'as `write_file` judges it, except that platform files are refused and binary content is fine; a refused path does not stop the others. ' +
3745
3795
  'The token is single-use: it is spent by the apply that lands it, and refused if you use it twice, let it ' +
3746
3796
  'expire, or present one issued to somebody else. `mode` means what it means on `write_file`.',
3747
3797
  inputs: {
@@ -3820,7 +3870,7 @@ patternMatcher) {
3820
3870
  'each one inside a folder too, is included only if you may read AND download that file. Returns ' +
3821
3871
  '`{ expiresAt, expiresInSeconds, files: [{ path, bytes, sha256, downloadUrl }], folders: [{ path, bytes, ' +
3822
3872
  'downloadUrl, files }], refused: [{ path, reason }] }`: a link per file, with its own content type, and per ' +
3823
- `folder a zip at full repository paths (\`apply_file_upload\` it at \`${kbDirName}/\` to put every file back). ` +
3873
+ `folder a zip at full repository paths (\`apply_file_upload\` it at \`${kbDirName}/\` to put them back). ` +
3824
3874
  'Fetch with any HTTP client (`curl -o <name> "<downloadUrl>"`, or the address without its last segment and an ' +
3825
3875
  '`x-download-token` header). Each link works ONCE, for 15 minutes, and serves the files as they are now. ' +
3826
3876
  'Refused: `not found` (missing or named-but-unreadable; folders omit unreadable files), ' +