@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
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The error codes a token endpoint may answer with, as RFC 6749 §5.2 and OIDC
3
+ * Core §3.1.2.6 define them. A finite list, on purpose: a code is taken from
4
+ * a refusing provider's body only when it is exactly one of these, so no other
5
+ * string a provider puts in that field (a secret, a secret with a suffix, free
6
+ * text) reaches a message, a verdict or a log.
7
+ *
8
+ * Shared, not a module's own: the OIDC configuration check reads a provider's
9
+ * refusal through it and the secrets vault a token endpoint's, and one list is
10
+ * the only way the two cannot disagree about which codes are standard. Each
11
+ * caller keeps its own sentence per code (what a tool owner should do differs
12
+ * from what a sign-in administrator should do); the set of codes is one.
13
+ */
14
+ export const OAUTH_TOKEN_ERROR_CODES = Object.freeze([
15
+ 'invalid_request',
16
+ 'invalid_client',
17
+ 'invalid_grant',
18
+ 'unauthorized_client',
19
+ 'unsupported_grant_type',
20
+ 'invalid_scope',
21
+ 'access_denied',
22
+ 'server_error',
23
+ 'temporarily_unavailable',
24
+ ]);
25
+ /** Whether `code` is one of the standard token-endpoint error codes, and so safe to echo. */
26
+ export function isStandardTokenErrorCode(code) {
27
+ return typeof code === 'string' && OAUTH_TOKEN_ERROR_CODES.includes(code);
28
+ }
29
+ /**
30
+ * The standard `error` code in a refusing token endpoint's JSON body, or null:
31
+ * when the body is not JSON, not an object, carries no string `error`, or
32
+ * carries one that is not a standard code.
33
+ */
34
+ export function standardTokenErrorCode(bodyText) {
35
+ let json;
36
+ try {
37
+ json = JSON.parse(bodyText);
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ if (!json || typeof json !== 'object' || Array.isArray(json))
43
+ return null;
44
+ const { error } = json;
45
+ return isStandardTokenErrorCode(error) ? error : null;
46
+ }
47
+ //# sourceMappingURL=oauth-token-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-token-error.js","sourceRoot":"","sources":["../../src/shared/oauth-token-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAsB,MAAM,CAAC,MAAM,CAAC;IACtE,iBAAiB;IACjB,gBAAgB;IAChB,eAAe;IACf,qBAAqB;IACrB,wBAAwB;IACxB,eAAe;IACf,eAAe;IACf,cAAc;IACd,yBAAyB;CAC1B,CAAC,CAAC;AAEH,6FAA6F;AAC7F,MAAM,UAAU,wBAAwB,CAAC,IAAa;IACpD,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,uBAAuB,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AAC5E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CAAC,QAAgB;IACrD,IAAI,IAAa,CAAC;IAClB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1E,MAAM,EAAE,KAAK,EAAE,GAAG,IAA+B,CAAC;IAClD,OAAO,wBAAwB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AACxD,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * At most `limit` bytes of a response body, decoded as text, the rest of the
3
+ * stream cancelled.
4
+ *
5
+ * Each CHUNK is sliced to the remaining budget before being decoded, not just
6
+ * counted against it: a provider that answers in one 5MB chunk would otherwise
7
+ * put the whole thing through the decoder before the loop noticed it was over,
8
+ * which is the bound bypassed by the very party it is meant to bound. Never a
9
+ * byte past the limit: once the budget is spent the read stops without asking
10
+ * for more, so a body of exactly `limit` bytes on a stream that stays open
11
+ * returns at once. And the read never falls back to `res.text()` on a live
12
+ * stream: a cap applied after that read bounds nothing.
13
+ *
14
+ * Shared, not a module's own: the connection probe reads a provider's
15
+ * rejection with it and the secrets vault a token endpoint's refusal, and one
16
+ * reader is the only way the two cannot disagree about what "bounded" means.
17
+ */
18
+ export declare function readCapped(res: Response, limit: number): Promise<string>;
19
+ //# sourceMappingURL=read-capped.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-capped.d.ts","sourceRoot":"","sources":["../../src/shared/read-capped.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,UAAU,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAsB9E"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * At most `limit` bytes of a response body, decoded as text, the rest of the
3
+ * stream cancelled.
4
+ *
5
+ * Each CHUNK is sliced to the remaining budget before being decoded, not just
6
+ * counted against it: a provider that answers in one 5MB chunk would otherwise
7
+ * put the whole thing through the decoder before the loop noticed it was over,
8
+ * which is the bound bypassed by the very party it is meant to bound. Never a
9
+ * byte past the limit: once the budget is spent the read stops without asking
10
+ * for more, so a body of exactly `limit` bytes on a stream that stays open
11
+ * returns at once. And the read never falls back to `res.text()` on a live
12
+ * stream: a cap applied after that read bounds nothing.
13
+ *
14
+ * Shared, not a module's own: the connection probe reads a provider's
15
+ * rejection with it and the secrets vault a token endpoint's refusal, and one
16
+ * reader is the only way the two cannot disagree about what "bounded" means.
17
+ */
18
+ export async function readCapped(res, limit) {
19
+ const reader = res.body?.getReader();
20
+ // No stream (a mocked or already-consumed response): fall back to the whole
21
+ // body, which for those cases is ours and small.
22
+ if (!reader)
23
+ return (await res.text()).slice(0, limit);
24
+ const decoder = new TextDecoder();
25
+ let out = '';
26
+ let read = 0;
27
+ try {
28
+ while (read < limit) {
29
+ const { done, value } = await reader.read();
30
+ if (done)
31
+ break;
32
+ const remaining = limit - read;
33
+ const chunk = value.byteLength > remaining ? value.subarray(0, remaining) : value;
34
+ read += chunk.byteLength;
35
+ out += decoder.decode(chunk, { stream: true });
36
+ if (chunk.byteLength < value.byteLength)
37
+ break;
38
+ }
39
+ }
40
+ finally {
41
+ await reader.cancel().catch(() => { });
42
+ }
43
+ return out;
44
+ }
45
+ //# sourceMappingURL=read-capped.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-capped.js","sourceRoot":"","sources":["../../src/shared/read-capped.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,GAAa,EAAE,KAAa;IAC3D,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,CAAC;IACrC,4EAA4E;IAC5E,iDAAiD;IACjD,IAAI,CAAC,MAAM;QAAE,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IACvD,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,KAAK,EAAE,CAAC;YACpB,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YAC5C,IAAI,IAAI;gBAAE,MAAM;YAChB,MAAM,SAAS,GAAG,KAAK,GAAG,IAAI,CAAC;YAC/B,MAAM,KAAK,GAAG,KAAK,CAAC,UAAU,GAAG,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;YAClF,IAAI,IAAI,KAAK,CAAC,UAAU,CAAC;YACzB,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/C,IAAI,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU;gBAAE,MAAM;QACjD,CAAC;IACH,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACxC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bevel-software/platform-core-backend",
3
- "version": "0.34.0",
3
+ "version": "0.35.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",
@@ -55,8 +55,8 @@
55
55
  "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
56
56
  "yaml": "^2.9.0",
57
57
  "zod": "^3.24.0",
58
- "@bevel-software/platform-mcp-core": "0.34.0",
59
- "@bevel-software/platform-shared": "0.34.0"
58
+ "@bevel-software/platform-mcp-core": "0.35.0",
59
+ "@bevel-software/platform-shared": "0.35.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/adm-zip": "^0.5.8",
@@ -68,6 +68,7 @@
68
68
  "@types/pg": "^8.20.0",
69
69
  "@types/qs": "^6.15.1",
70
70
  "drizzle-kit": "^0.31.10",
71
+ "js-tiktoken": "^1.0.21",
71
72
  "tsx": "^4.19.0",
72
73
  "typescript": "^5.8.0",
73
74
  "vitest": "^3.2.6"
@@ -0,0 +1,144 @@
1
+ import express from 'express';
2
+ import { codeModeMetaTools } from '@bevel-software/platform-mcp-core';
3
+ import { testKbContext } from './kb-context.js';
4
+ import { EXTERNAL_KB_MANUAL_NAME, registerToolManualsTools } from '../modules/tool-manuals/index.js';
5
+ import { ToolRegistry, guideFirstDescription, type UtcpTool } from '../modules/tool-registry/index.js';
6
+ import type { ToolAuth } from '../modules/tool-auth/index.js';
7
+ import { RipgrepPatternMatcher, ToolDescriptionNotes } from '../modules/workspace/index.js';
8
+ import { registerPluginsTools } from '../modules/plugins/index.js';
9
+ import { registerSkillsTools } from '../modules/skills/index.js';
10
+ import { agentGuideSections, registerAgentGuideTool } from '../modules/agent-guide/index.js';
11
+ import {
12
+ RoutineWritePolicyService,
13
+ UuidSessionSink,
14
+ WorkflowHooks,
15
+ createToolHandlerFactory,
16
+ registerChangeRequestReadTools,
17
+ registerEmbedTools,
18
+ registerWorkflowTools,
19
+ registerWorkspaceTools,
20
+ } from './tool-wiring.js';
21
+
22
+ /**
23
+ * The tool catalog Hexis itself registers, built on stand-ins for the tests
24
+ * that read what an agent is TOLD: the description caps, the guide checks and
25
+ * the guide's budgets. Every registrar Hexis owns is mounted, because the
26
+ * definitions are the only thing under test: a stand-in that is never called
27
+ * is honest about that. Tools PROXIED from connected MCP servers are not here:
28
+ * their text is the other server's.
29
+ */
30
+
31
+ /** Dependencies the registrars take but never touch while they are only building defs. */
32
+ export const unused = <T,>(): T => ({}) as T;
33
+
34
+ /** A `.tool` catalog with nothing in it: the shortest honest answer for a listing. */
35
+ const emptyManuals = {
36
+ listLocalOnly: async () => [],
37
+ listAll: async () => [],
38
+ list: async () => [],
39
+ listInvalid: async () => [],
40
+ } as unknown as Parameters<typeof registerToolManualsTools>[4];
41
+
42
+ /** A skill catalog with nothing in it — the per-user skill line is then one fixed sentence. */
43
+ export const emptySkills = { listSkills: async () => [] } as unknown as Parameters<typeof registerSkillsTools>[4];
44
+
45
+ /**
46
+ * The meta-tools as the hosted endpoint builds them (`mcp.service.ts`):
47
+ * examples written against the namespace it registers the knowledge-base tools
48
+ * under and against the catalog it serves, and the chain without the rules it
49
+ * states only in the shared places. So the chain description measured here
50
+ * carries the worked call a client is really sent, which is its longest form.
51
+ */
52
+ export function servedMetaTools(external: readonly UtcpTool[]) {
53
+ return codeModeMetaTools(
54
+ EXTERNAL_KB_MANUAL_NAME,
55
+ external.map((t) => ({ utcpName: `${EXTERNAL_KB_MANUAL_NAME}.${t.name}`, inputSchema: t.inputs })),
56
+ { sharedRulesPointer: '' },
57
+ // As the hosted endpoint serves them: opening with the guide-first
58
+ // sentence, through the one helper the endpoint itself uses.
59
+ ).map((t) => ({ ...t, description: guideFirstDescription(t.description) }));
60
+ }
61
+
62
+ /**
63
+ * Every tool Hexis itself registers, by surface: what the in-process agent is
64
+ * handed, what the hosted endpoint lists to an MCP client, and the three
65
+ * meta-tools it lists beside them.
66
+ */
67
+ export async function hexisCatalog(
68
+ skills: Parameters<typeof registerSkillsTools>[4] = emptySkills,
69
+ ): Promise<{ internal: UtcpTool[]; external: UtcpTool[]; metaTools: ReturnType<typeof servedMetaTools> }> {
70
+ const registry = new ToolRegistry();
71
+ const router = express.Router();
72
+ const toolAuth = ((_req, _res, next) => next()) as unknown as ToolAuth;
73
+ const toolHandler = createToolHandlerFactory(unused());
74
+ const kb = testKbContext();
75
+ const gate = { recoveryBotEmail: 'bot@x', hooks: new WorkflowHooks(), notes: new ToolDescriptionNotes() };
76
+
77
+ // The change-request readers come first, as the root mounts them: they
78
+ // gate their whole payload on the caller's read access and take the access
79
+ // service and nothing else.
80
+ registerChangeRequestReadTools(registry, router, toolAuth, toolHandler, unused(), kb);
81
+ const workspaceTools = registerWorkspaceTools(
82
+ registry,
83
+ router,
84
+ toolAuth,
85
+ toolHandler,
86
+ unused(),
87
+ unused(),
88
+ unused(),
89
+ kb,
90
+ gate,
91
+ new RoutineWritePolicyService(),
92
+ new UuidSessionSink(),
93
+ undefined,
94
+ undefined,
95
+ // The two upload tools are mounted only when a store is supplied, and
96
+ // every real composition supplies one: without it they would be the two
97
+ // descriptions this suite never measured.
98
+ unused(),
99
+ // The guide reader, not needed to build defs; then the download store,
100
+ // mounted on the same terms as the upload one.
101
+ undefined,
102
+ unused(),
103
+ // Starter packs and the tree walk, not needed; then the grep matcher,
104
+ // mounted on the same terms (it spawns no worker until a search runs).
105
+ undefined,
106
+ undefined,
107
+ new RipgrepPatternMatcher(),
108
+ );
109
+ registerWorkflowTools(registry, router, toolAuth, toolHandler, kb, async () => true, { canRead: async () => true, canReadAtRef: async () => true, canReadBatch: async () => new Map(), canReadBatchAtRef: async () => new Map(), canWrite: async () => true } as never, { recoveryBotEmail: 'recovery@x', hooks: { runAgentRead: async () => undefined }, notes: { onChange: () => undefined } } as never);
110
+ registerPluginsTools(registry, kb);
111
+ registerSkillsTools(registry, router, toolAuth, toolHandler, skills);
112
+ registerToolManualsTools(registry, router, toolAuth, toolHandler, emptyManuals, {
113
+ accessControl: unused(),
114
+ variableStatus: unused(),
115
+ kb,
116
+ });
117
+ // `open_page`, on the file tools' router with `read_file`'s own read, as the
118
+ // root mounts it; its stand-ins answer nothing, since only its def is read.
119
+ registerEmbedTools(registry, router, toolAuth, toolHandler, {
120
+ embedService: unused(),
121
+ kb,
122
+ readForTool: workspaceTools.readForTool,
123
+ canBeReached: () => true,
124
+ appUrlFor: () => '',
125
+ notes: gate.notes,
126
+ });
127
+ // The guide's own tool, with the platform's sections: the one description
128
+ // that lists the sections is measured with the list it really carries.
129
+ registerAgentGuideTool(registry, router, toolAuth, toolHandler, () => agentGuideSections(kb.layout));
130
+
131
+ // The meta-tools as a client is served them (`mcp.service.ts` opens each
132
+ // with the guide-first sentence at the mount), so the catalog measured here
133
+ // is the catalog that goes out rather than the bare constant.
134
+ const external = (await registry.listExternal()) as UtcpTool[];
135
+ return { internal: (await registry.listInternal()) as UtcpTool[], external, metaTools: servedMetaTools(external) };
136
+ }
137
+
138
+ /** Every tool Hexis itself registers, on both surfaces, deduplicated by name. */
139
+ export async function hexisTools(skills: Parameters<typeof registerSkillsTools>[4] = emptySkills): Promise<UtcpTool[]> {
140
+ const { internal, external, metaTools } = await hexisCatalog(skills);
141
+ const byName = new Map<string, UtcpTool>();
142
+ for (const tool of [...internal, ...external, ...metaTools]) byName.set(tool.name, tool as UtcpTool);
143
+ return [...byName.values()];
144
+ }
@@ -0,0 +1,21 @@
1
+ import { NodeFs } from '../modules/kb-fs/node-fs.js';
2
+ import { KbPluginSource } from '../modules/plugins/index.js';
3
+ import type { KbContext } from '../shared/kb-context.js';
4
+
5
+ /**
6
+ * The real disk, and the plugin source that reads it, wired the way the
7
+ * composition root wires them (`core/create-core-services.ts`: one `NodeFs`,
8
+ * one `KbPluginSource` over it). A module's suite that drives its service over
9
+ * real files takes them from HERE rather than constructing another module's
10
+ * implementation itself: the root is the one place that knows those files
11
+ * (`NodeFs` is on no module entry, by design — a filesystem arrives by
12
+ * injection), and this fixture stands in for the root, as `kb-context.ts`
13
+ * does for the knowledge-base context.
14
+ */
15
+ export function realDisk(): NodeFs {
16
+ return new NodeFs();
17
+ }
18
+
19
+ export function realPluginSource(disk: NodeFs, kb: KbContext): KbPluginSource {
20
+ return new KbPluginSource(disk, kb);
21
+ }
@@ -0,0 +1,19 @@
1
+ import { Tiktoken } from 'js-tiktoken/lite';
2
+ import o200kBase from 'js-tiktoken/ranks/o200k_base';
3
+ import { GUIDE_TOKEN_ENCODING } from '../modules/agent-guide/index.js';
4
+
5
+ /**
6
+ * Token counts for the tests that hold agent-facing text to a budget
7
+ * (`modules/agent-guide/guide-budget.ts`): a real BPE tokenizer, offline and
8
+ * deterministic, so a count in CI is the count on a laptop. Built once: the
9
+ * rank table is large and the counts never change between calls.
10
+ */
11
+ const encoder = new Tiktoken(o200kBase);
12
+
13
+ if (GUIDE_TOKEN_ENCODING !== 'o200k_base') {
14
+ throw new Error(`The budgets name ${GUIDE_TOKEN_ENCODING}, but the counter is built on o200k_base.`);
15
+ }
16
+
17
+ export function countTokens(text: string): number {
18
+ return encoder.encode(text).length;
19
+ }
@@ -0,0 +1,32 @@
1
+ import { ToolManualService, type McpAuthDiscoveryPort } from '../modules/tool-manuals/tool-manuals.service.js';
2
+ import type { IAccessControl } from '../modules/access/index.js';
3
+ import type { WorkspaceService } from '../modules/workspace/index.js';
4
+ import type { KbContext } from '../shared/kb-context.js';
5
+ import { realDisk, realPluginSource } from './real-disk.js';
6
+
7
+ export type { McpAuthDiscoveryPort };
8
+
9
+ /**
10
+ * The tool-manual service over real files, wired the way the composition root
11
+ * wires it (`core/create-core-services.ts`): the real disk, the plugin source
12
+ * reading that same disk, and the discovery port set afterwards. A suite that
13
+ * drives the service over real tool files takes it from HERE and never
14
+ * constructs the class from its file — the class is on no module entry (a
15
+ * service arrives by injection), and this fixture stands in for the root, as
16
+ * `kb-context.ts` and `real-disk.ts` do for what they wire.
17
+ */
18
+ export function realToolManualService(
19
+ deps: {
20
+ workspaceService: WorkspaceService;
21
+ accessControl: IAccessControl;
22
+ kb: KbContext;
23
+ /** The disk the suite writes its files to; the service reads the same one. */
24
+ disk?: ReturnType<typeof realDisk>;
25
+ },
26
+ discovery?: McpAuthDiscoveryPort,
27
+ ): ToolManualService {
28
+ const disk = deps.disk ?? realDisk();
29
+ const svc = new ToolManualService(deps.workspaceService, deps.accessControl, deps.kb, disk, realPluginSource(disk, deps.kb));
30
+ if (discovery) svc.setMcpAuthDiscovery(discovery);
31
+ return svc;
32
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The pieces the composition root wires from module internals, for a suite
3
+ * that builds a tool surface the way `core/create-core-server.ts` does: the
4
+ * handler factory and context resolver, the tool-auth middleware, the two
5
+ * registrars that take their dependencies by hand, the session sink, the
6
+ * routine write policy, the access resolver and the workflow hooks.
7
+ *
8
+ * A module's entry holds its contracts, and a suite asserts through entries.
9
+ * These are not contracts: they are what the root composes, so a suite that
10
+ * needs them reaches them here, the one root fixture that imports past the
11
+ * entries, as the root itself does. A suite that imports one of these files
12
+ * directly is a second wiring that drifts from the root's.
13
+ */
14
+ export { createToolHandlerFactory } from '../modules/tool-helpers/tool-handler.js';
15
+ export { createToolContextResolver } from '../modules/tool-helpers/tool-context.js';
16
+ export { createToolAuthMiddleware } from '../modules/tool-auth/tool-auth.middleware.js';
17
+ export { InternalTokenService } from '../modules/tool-auth/internal-token.service.js';
18
+ export { registerWorkspaceTools } from '../modules/workspace/workspace.tools.js';
19
+ export { registerWorkflowTools } from '../modules/workflow/agent-tools/workflow.tools.js';
20
+ export { registerChangeRequestReadTools } from '../modules/workflow/agent-tools/change-request-read.tools.js';
21
+ export { registerEmbedTools } from '../modules/embed/embed.tools.js';
22
+ export { UuidSessionSink } from '../modules/workspace/session-sink.js';
23
+ export { RoutineWritePolicyService } from '../modules/workspace/routine-write-policy.js';
24
+ export { AccessControlService, loadActiveGroups } from '../modules/access/access-control.service.js';
25
+ export { WorkflowHooks, type AgentOperationContext } from '../modules/workflow/workflow-hooks.js';
@@ -0,0 +1,179 @@
1
+ import type { Server as HttpServer } from 'node:http';
2
+ import { mkdtemp, rm, mkdir, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import express from 'express';
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
+ import { DEFAULT_BRANCH } from '@bevel-software/platform-shared';
8
+ import { createSecretsVaultRoutes } from '../../modules/secrets-vault/index.js';
9
+ import { KbPluginSource } from '../../modules/plugins/index.js';
10
+ import type { WorkspaceService } from '../../modules/workspace/index.js';
11
+ import type { IAccessControl } from '../../modules/access/index.js';
12
+ // The two concrete services this suite wires, it wires the way the composition
13
+ // root does (`create-core-services.ts`): their modules publish them as types
14
+ // only, and the composition root is the one place that constructs them from
15
+ // their files. This suite stands in for that root — the routes of one module
16
+ // over the real parser of another — which is why it lives beside it.
17
+ import { ToolManualService } from '../../modules/tool-manuals/tool-manuals.service.js';
18
+ import { realDisk } from '../../__tests__/real-disk.js';
19
+ import { workspaceIdForBranch } from '../../shared/workspace-id.js';
20
+ import { testKbContext } from '../../__tests__/kb-context.js';
21
+
22
+ /**
23
+ * The sign-in start against REAL tool files: the routes are wired to the real
24
+ * `ToolManualService` over a workspace on disk, so what the start answers is
25
+ * what the parser makes of the file, not what a stub says it makes of it.
26
+ *
27
+ * The case this exists for (Local Testing, 2026-10-27): a declaration edited to
28
+ * `tokenUrl: http://127.0.0.1/token` is refused by the parser before the vault
29
+ * ever validates it — and the start answered "Tool or variable not found"
30
+ * instead of the validation's message.
31
+ */
32
+
33
+ const KB_DIR = 'knowledge-base';
34
+ const wsId = workspaceIdForBranch(DEFAULT_BRANCH);
35
+ const USER = 'owner@x.com';
36
+ const AUTHORIZE = 'https://auth.crm.example/authorize';
37
+ const TOKEN = 'https://auth.crm.example/token';
38
+
39
+ let root: string;
40
+ let httpServer: HttpServer | undefined;
41
+
42
+ beforeEach(async () => {
43
+ root = await mkdtemp(join(tmpdir(), 'signin-live-'));
44
+ await mkdir(join(root, wsId, KB_DIR, 'Plugins', 'GTM'), { recursive: true });
45
+ });
46
+ afterEach(async () => {
47
+ await new Promise<void>((r) => (httpServer ? httpServer.close(() => r()) : r()));
48
+ httpServer = undefined;
49
+ await rm(root, { recursive: true, force: true });
50
+ vi.restoreAllMocks();
51
+ });
52
+
53
+ const toolPath = () => join(root, wsId, KB_DIR, 'Plugins', 'crm.tool');
54
+ const writeTool = (oauth: Record<string, unknown>) =>
55
+ writeFile(
56
+ toolPath(),
57
+ JSON.stringify({
58
+ name: 'crm',
59
+ type: 'http',
60
+ url: 'https://api.crm.example/openapi.json',
61
+ variables: [{ name: 'SIGNIN', scope: 'user', oauth }],
62
+ }),
63
+ );
64
+ const writePlugin = (oauth: Record<string, unknown>) =>
65
+ Promise.all([
66
+ writeFile(
67
+ join(root, wsId, KB_DIR, 'Plugins', 'GTM', 'mcp.json'),
68
+ JSON.stringify({ mcpServers: { hubspot: { type: 'streamable-http', url: 'https://mcp.hubspot.example/mcp' } } }),
69
+ ),
70
+ writeFile(
71
+ join(root, wsId, KB_DIR, 'Plugins', 'GTM', 'plugin.json'),
72
+ JSON.stringify({
73
+ name: 'gtm',
74
+ extensions: {
75
+ 'software.bevel.hexis': {
76
+ mcpServers: {
77
+ hubspot: {
78
+ headers: { Authorization: 'Bearer ${HUBSPOT_TOKEN}' },
79
+ variables: [{ name: 'HUBSPOT_TOKEN', scope: 'user', oauth }],
80
+ },
81
+ },
82
+ },
83
+ },
84
+ }),
85
+ ),
86
+ ]);
87
+
88
+ async function serve() {
89
+ const disk = realDisk();
90
+ const kb = testKbContext({ kbDirName: KB_DIR });
91
+ const accessControl = {
92
+ canRead: async () => true,
93
+ canWrite: async () => true,
94
+ canReadBatch: async (_w: string, _e: string, paths: string[]) => new Map(paths.map((p) => [p, true])),
95
+ } as unknown as IAccessControl;
96
+ const workspaceService = {
97
+ getOrCreateForBranch: async () => ({ id: wsId }),
98
+ getWorkspacePath: async (id: string) => join(root, id),
99
+ } as unknown as WorkspaceService;
100
+ const toolManualService = new ToolManualService(workspaceService, accessControl, kb, disk, new KbPluginSource(disk, kb));
101
+ toolManualService.setMcpAuthDiscovery({ statusFor: async () => ({ status: 'open' as const }) });
102
+ const beginToolOAuthByKey = vi.fn(async () => ({ id: 'row-1', url: `${AUTHORIZE}?client_id=x` }));
103
+ const deps = {
104
+ secretsVault: { beginToolOAuthByKey },
105
+ toolManualService,
106
+ accessControl,
107
+ connectionProbe: {},
108
+ kb: { defaultWorkspaceId: () => wsId },
109
+ stateSecret: 'state-secret',
110
+ publicBackendUrl: 'http://localhost:3000',
111
+ publicFrontendUrl: 'http://localhost:5173',
112
+ } as unknown as Parameters<typeof createSecretsVaultRoutes>[0];
113
+ const app = express();
114
+ app.use(express.json());
115
+ app.use((req, _res, next) => {
116
+ req.userId = 'user-1';
117
+ req.userEmail = USER;
118
+ next();
119
+ });
120
+ app.use('/api', createSecretsVaultRoutes(deps));
121
+ httpServer = await new Promise<HttpServer>((resolve) => {
122
+ const s = app.listen(0, () => resolve(s));
123
+ });
124
+ const base = `http://127.0.0.1:${(httpServer.address() as { port: number }).port}/api/secrets/tools`;
125
+ const start = (slug: string, varName: string) => fetch(`${base}/${slug}/vars/${varName}/oauth/start`, { method: 'POST' });
126
+ return { toolManualService, beginToolOAuthByKey, start };
127
+ }
128
+
129
+ describe('POST …/oauth/start — through a real tool file', () => {
130
+ it('a client id edited in the .tool after the catalog was scanned is the one handed to the vault', async () => {
131
+ await writeTool({ clientId: 'client-A', authorizationUrl: AUTHORIZE, tokenUrl: TOKEN });
132
+ const s = await serve();
133
+ await s.toolManualService.listAccessible(USER); // the catalog caches client A
134
+ await writeTool({ clientId: 'client-B', authorizationUrl: AUTHORIZE, tokenUrl: TOKEN });
135
+
136
+ expect((await s.start('crm', 'SIGNIN')).status).toBe(200);
137
+ expect(s.beginToolOAuthByKey.mock.calls[0][0]).toMatchObject({
138
+ key: 'crm_SIGNIN',
139
+ provider: { clientId: 'client-B', authorizationUrl: AUTHORIZE, tokenUrl: TOKEN, pkce: true },
140
+ });
141
+ });
142
+
143
+ it.each([
144
+ ['an http:// token endpoint', 'http://127.0.0.1/token', '`variables[0].oauth.tokenUrl` must use https'],
145
+ ['a private token endpoint', 'https://10.0.0.5/token', '`variables[0].oauth.tokenUrl` host is not allowed'],
146
+ ])('a .tool naming %s is refused with the validation message, sending nothing', async (_n, tokenUrl, message) => {
147
+ await writeTool({ clientId: 'client-A', authorizationUrl: AUTHORIZE, tokenUrl: TOKEN });
148
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
149
+ const s = await serve();
150
+ await s.toolManualService.listAccessible(USER); // the catalog still holds the valid version
151
+ await writeTool({ clientId: 'client-A', authorizationUrl: AUTHORIZE, tokenUrl });
152
+ const fetchSpy = vi.spyOn(globalThis, 'fetch');
153
+
154
+ const res = await s.start('crm', 'SIGNIN');
155
+ expect(res.status).toBe(422);
156
+ expect(((await res.json()) as { error: string }).error).toBe(message);
157
+ expect(s.beginToolOAuthByKey).not.toHaveBeenCalled();
158
+ // The one fetch is the test's own call to the route: nothing went to a provider.
159
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
160
+ });
161
+
162
+ it('a plugin.json sign-in naming an http:// token endpoint is refused with the validation message, sending nothing', async () => {
163
+ await writePlugin({ clientId: 'owner-app', authorizationUrl: AUTHORIZE, tokenUrl: 'http://127.0.0.1/token' });
164
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
165
+ const s = await serve();
166
+ const fetchSpy = vi.spyOn(globalThis, 'fetch');
167
+
168
+ const res = await s.start('hubspot', 'HUBSPOT_TOKEN');
169
+ expect(res.status).toBe(422);
170
+ expect(((await res.json()) as { error: string }).error).toContain('`variables[0].oauth.tokenUrl` must use https');
171
+ expect(s.beginToolOAuthByKey).not.toHaveBeenCalled();
172
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
173
+ });
174
+
175
+ it('a slug no file declares is still not found', async () => {
176
+ const s = await serve();
177
+ expect((await s.start('nothing', 'SIGNIN')).status).toBe(404);
178
+ });
179
+ });
@@ -487,7 +487,7 @@ export async function createCoreServer(
487
487
  // because they are the only ones that gate their whole payload on the
488
488
  // caller's read access, so they take the access service and nothing else.
489
489
  registerChangeRequestReadTools(core.toolRegistry, toolsRouter, ta, th, core.accessControl, core.kb);
490
- const workspaceTools = registerWorkspaceTools(core.toolRegistry, toolsRouter, ta, th, core.spillStore, core.docExtractService, core.accessControl, core.kb, agentAccessGate, core.routineWritePolicy, core.sessionSink, allowedToolsChecker, core.changeGate, core.agentUploadStore, core.agentGuide, core.agentDownloadStore, core.starterPackService, core.disk, core.patternMatcher);
490
+ const workspaceTools = registerWorkspaceTools(core.toolRegistry, toolsRouter, ta, th, core.spillStore, core.docExtractService, core.accessControl, core.kb, agentAccessGate, core.routineWritePolicy, core.sessionSink, allowedToolsChecker, core.changeGate, core.agentUploadStore, core.agentGuideStart, core.agentDownloadStore, core.starterPackService, core.disk, core.patternMatcher);
491
491
  // `open_page` — the knowledge-base page shown inside a chat. Registered
492
492
  // here, on the same router as the file tools, because it answers with
493
493
  // `read_file`'s own read: the read hook, the access gate and the
@@ -171,6 +171,7 @@ import { AgentAuditService, retentionDaysFrom } from '../modules/audit/agent-aud
171
171
  import { readAgentPreamble, type AgentPreambleReader } from '../modules/agent-instructions/index.js';
172
172
  import {
173
173
  agentGuideSections,
174
+ guideStartText,
174
175
  joinGuideSections,
175
176
  type AgentGuideReader,
176
177
  type AgentGuideSectionsReader,
@@ -296,6 +297,12 @@ export interface CoreServices {
296
297
  * `read_file` of the guide's name answer. See modules/agent-guide.
297
298
  */
298
299
  agentGuide: AgentGuideReader;
300
+ /**
301
+ * The start of that guide and the list of its sections: what a `read_file`
302
+ * of the guide's name answers, the same as `get_agent_guide` with no
303
+ * `section`. See `guideStartText`.
304
+ */
305
+ agentGuideStart: AgentGuideReader;
299
306
  /** The same guide as its sections, each with its title — what `get_agent_guide` lists and serves one of. */
300
307
  agentGuideSections: AgentGuideSectionsReader;
301
308
  mcpServerEditService: McpServerEditService;
@@ -1376,11 +1383,13 @@ export async function createCoreServices(
1376
1383
  // serves the same composition to the local bridge and the frontend card.
1377
1384
  const readPreamble: AgentPreambleReader = () => readAgentPreamble(workspaceService, kb, disk);
1378
1385
  // The guide every agent is told to read first, composed when asked for —
1379
- // the layout is read per call, so a name the setup save applies lands
1380
- // without a restart, and the distribution's hook sees every composition.
1386
+ // the layout and the default branch are read per call, so a name the setup
1387
+ // save applies lands without a restart, and the distribution's hook sees
1388
+ // every composition.
1381
1389
  const agentGuideSectionsReader: AgentGuideSectionsReader = () =>
1382
- agentGuideSections(kb.layout, ports.agentGuide, { kbDirName });
1390
+ agentGuideSections(kb.layout, ports.agentGuide, { kbDirName, defaultBranch: kb.defaultBranch });
1383
1391
  const agentGuide: AgentGuideReader = async () => joinGuideSections(await agentGuideSectionsReader());
1392
+ const agentGuideStart: AgentGuideReader = async () => guideStartText(await agentGuideSectionsReader());
1384
1393
  // The Audit log. Records through the proxy below (every call an external
1385
1394
  // agent makes), reads keys through the key service so their shape is
1386
1395
  // defined once, and prunes past the retention setting — read per sweep, so
@@ -1723,6 +1732,7 @@ export async function createCoreServices(
1723
1732
  pendingToolsService,
1724
1733
  readAgentPreamble: readPreamble,
1725
1734
  agentGuide,
1735
+ agentGuideStart,
1726
1736
  agentGuideSections: agentGuideSectionsReader,
1727
1737
  pluginIndexService,
1728
1738
  pluginProvisionService,