@bevel-software/platform-core-backend 0.25.0 → 0.26.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 (254) hide show
  1. package/agent-guide/access-control.md +234 -0
  2. package/agent-guide/conventions.md +27 -0
  3. package/agent-guide/directory-structure.md +145 -0
  4. package/agent-guide/finding-things.md +7 -0
  5. package/agent-guide/introduction.md +27 -0
  6. package/agent-guide/skills.md +47 -0
  7. package/agent-guide/tool-manuals.md +217 -0
  8. package/agent-guide/where-a-new-file-goes.md +36 -0
  9. package/dist/assets.d.ts +7 -0
  10. package/dist/assets.d.ts.map +1 -1
  11. package/dist/assets.js +9 -0
  12. package/dist/assets.js.map +1 -1
  13. package/dist/core/core-ports.d.ts +11 -0
  14. package/dist/core/core-ports.d.ts.map +1 -1
  15. package/dist/core/core-ports.js.map +1 -1
  16. package/dist/core/create-core-server.d.ts.map +1 -1
  17. package/dist/core/create-core-server.js +13 -2
  18. package/dist/core/create-core-server.js.map +1 -1
  19. package/dist/core/create-core-services.d.ts +9 -0
  20. package/dist/core/create-core-services.d.ts.map +1 -1
  21. package/dist/core/create-core-services.js +14 -4
  22. package/dist/core/create-core-services.js.map +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +2 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/modules/access/access-control.interface.d.ts +9 -0
  28. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  29. package/dist/modules/access/access-control.service.d.ts +1 -0
  30. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  31. package/dist/modules/access/access-control.service.js +16 -0
  32. package/dist/modules/access/access-control.service.js.map +1 -1
  33. package/dist/modules/agent-guide/agent-guide.d.ts +139 -0
  34. package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -0
  35. package/dist/modules/agent-guide/agent-guide.js +191 -0
  36. package/dist/modules/agent-guide/agent-guide.js.map +1 -0
  37. package/dist/modules/agent-guide/agent-guide.tools.d.ts +24 -0
  38. package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -0
  39. package/dist/modules/agent-guide/agent-guide.tools.js +100 -0
  40. package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -0
  41. package/dist/modules/agent-guide/index.d.ts +4 -0
  42. package/dist/modules/agent-guide/index.d.ts.map +1 -0
  43. package/dist/modules/agent-guide/index.js +4 -0
  44. package/dist/modules/agent-guide/index.js.map +1 -0
  45. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +3 -2
  46. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
  47. package/dist/modules/agent-instructions/agent-instructions.routes.js +3 -2
  48. package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
  49. package/dist/modules/agent-instructions/compose.d.ts +9 -6
  50. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  51. package/dist/modules/agent-instructions/compose.js +9 -6
  52. package/dist/modules/agent-instructions/compose.js.map +1 -1
  53. package/dist/modules/agent-instructions/index.d.ts +1 -1
  54. package/dist/modules/agent-instructions/index.d.ts.map +1 -1
  55. package/dist/modules/agent-instructions/index.js +1 -1
  56. package/dist/modules/agent-instructions/index.js.map +1 -1
  57. package/dist/modules/agent-instructions/shared-file-rules.d.ts +10 -50
  58. package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
  59. package/dist/modules/agent-instructions/shared-file-rules.js +32 -85
  60. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
  61. package/dist/modules/mcp/mcp.service.d.ts +29 -2
  62. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  63. package/dist/modules/mcp/mcp.service.js +113 -16
  64. package/dist/modules/mcp/mcp.service.js.map +1 -1
  65. package/dist/modules/mcp/tool-schema-guard.d.ts +105 -0
  66. package/dist/modules/mcp/tool-schema-guard.d.ts.map +1 -0
  67. package/dist/modules/mcp/tool-schema-guard.js +171 -0
  68. package/dist/modules/mcp/tool-schema-guard.js.map +1 -0
  69. package/dist/modules/plugins/plugins.tools.d.ts +36 -2
  70. package/dist/modules/plugins/plugins.tools.d.ts.map +1 -1
  71. package/dist/modules/plugins/plugins.tools.js +71 -14
  72. package/dist/modules/plugins/plugins.tools.js.map +1 -1
  73. package/dist/modules/settings/deployment-settings.service.d.ts +0 -7
  74. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  75. package/dist/modules/settings/deployment-settings.service.js +14 -53
  76. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  77. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  78. package/dist/modules/settings/setup.routes.js +3 -6
  79. package/dist/modules/settings/setup.routes.js.map +1 -1
  80. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  81. package/dist/modules/skills/skills.tools.js +58 -16
  82. package/dist/modules/skills/skills.tools.js.map +1 -1
  83. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +23 -4
  84. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  85. package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -1
  86. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +4 -0
  87. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  88. package/dist/modules/tool-manuals/tool-manuals.service.js +14 -0
  89. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  90. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +7 -0
  91. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  92. package/dist/modules/tool-manuals/tool-manuals.tools.js +66 -36
  93. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  94. package/dist/modules/tool-registry/description-length.d.ts +14 -14
  95. package/dist/modules/tool-registry/description-length.d.ts.map +1 -1
  96. package/dist/modules/tool-registry/description-length.js +24 -26
  97. package/dist/modules/tool-registry/description-length.js.map +1 -1
  98. package/dist/modules/tool-registry/guide-first.d.ts +23 -0
  99. package/dist/modules/tool-registry/guide-first.d.ts.map +1 -0
  100. package/dist/modules/tool-registry/guide-first.js +32 -0
  101. package/dist/modules/tool-registry/guide-first.js.map +1 -0
  102. package/dist/modules/tool-registry/tool-registry.d.ts +6 -0
  103. package/dist/modules/tool-registry/tool-registry.d.ts.map +1 -1
  104. package/dist/modules/tool-registry/tool-registry.js +9 -2
  105. package/dist/modules/tool-registry/tool-registry.js.map +1 -1
  106. package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts +449 -0
  107. package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts.map +1 -0
  108. package/dist/modules/workflow/agent-tools/change-request-read-shape.js +481 -0
  109. package/dist/modules/workflow/agent-tools/change-request-read-shape.js.map +1 -0
  110. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts +73 -0
  111. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -0
  112. package/dist/modules/workflow/agent-tools/change-request-read.tools.js +582 -0
  113. package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -0
  114. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +12 -1
  115. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -1
  116. package/dist/modules/workflow/agent-tools/change-request-summary.js +5 -1
  117. package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -1
  118. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  119. package/dist/modules/workflow/agent-tools/workflow.tools.js +9 -0
  120. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  121. package/dist/modules/workflow/git/git.service.d.ts +210 -13
  122. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  123. package/dist/modules/workflow/git/git.service.js +456 -91
  124. package/dist/modules/workflow/git/git.service.js.map +1 -1
  125. package/dist/modules/workflow/git/merge-commit.d.ts +73 -0
  126. package/dist/modules/workflow/git/merge-commit.d.ts.map +1 -0
  127. package/dist/modules/workflow/git/merge-commit.js +89 -0
  128. package/dist/modules/workflow/git/merge-commit.js.map +1 -0
  129. package/dist/modules/workflow/git/pull-request.service.d.ts +94 -1
  130. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  131. package/dist/modules/workflow/git/pull-request.service.js +332 -37
  132. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  133. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +35 -0
  134. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  135. package/dist/modules/workflow/review-workflow/review-workflow.service.js +178 -12
  136. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  137. package/dist/modules/workflow/workflow.routes.d.ts +6 -2
  138. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  139. package/dist/modules/workflow/workflow.routes.js +7 -2
  140. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  141. package/dist/modules/workflow/workflow.service.d.ts +4 -0
  142. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  143. package/dist/modules/workflow/workflow.service.js +3 -0
  144. package/dist/modules/workflow/workflow.service.js.map +1 -1
  145. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +70 -0
  146. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  147. package/dist/modules/workspace/startup/kb-startup-runner.js +213 -20
  148. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  149. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  150. package/dist/modules/workspace/startup/steps/seed-tree.js +22 -27
  151. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  152. package/dist/modules/workspace/startup/steps/template-files.step.d.ts +58 -52
  153. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  154. package/dist/modules/workspace/startup/steps/template-files.step.js +209 -223
  155. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  156. package/dist/modules/workspace/startup/steps/template-source.d.ts +5 -3
  157. package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
  158. package/dist/modules/workspace/startup/steps/template-source.js +5 -3
  159. package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
  160. package/dist/modules/workspace/workspace.tools.d.ts +10 -1
  161. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  162. package/dist/modules/workspace/workspace.tools.js +211 -18
  163. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  164. package/dist/shared/domain-errors.d.ts +11 -0
  165. package/dist/shared/domain-errors.d.ts.map +1 -1
  166. package/dist/shared/domain-errors.js +14 -0
  167. package/dist/shared/domain-errors.js.map +1 -1
  168. package/dist/shared/hidden-tools.d.ts +44 -0
  169. package/dist/shared/hidden-tools.d.ts.map +1 -0
  170. package/dist/shared/hidden-tools.js +13 -0
  171. package/dist/shared/hidden-tools.js.map +1 -0
  172. package/kb-template/.bevelignore +0 -5
  173. package/package.json +4 -3
  174. package/src/__tests__/kb-layout-config.test.ts +10 -100
  175. package/src/__tests__/packaged-assets-ship.test.ts +54 -0
  176. package/src/assets.ts +10 -0
  177. package/src/core/core-ports.ts +11 -0
  178. package/src/core/create-core-server.ts +13 -2
  179. package/src/core/create-core-services.ts +28 -4
  180. package/src/index.ts +2 -2
  181. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +58 -0
  182. package/src/modules/access/__tests__/access-control.platform-restore.test.ts +8 -7
  183. package/src/modules/access/__tests__/access-personal-plugin.test.ts +1 -18
  184. package/src/modules/access/access-control.interface.ts +15 -0
  185. package/src/modules/access/access-control.service.ts +21 -0
  186. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +328 -0
  187. package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +189 -0
  188. package/src/modules/agent-guide/agent-guide.tools.ts +122 -0
  189. package/src/modules/agent-guide/agent-guide.ts +291 -0
  190. package/src/modules/agent-guide/index.ts +21 -0
  191. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +28 -121
  192. package/src/modules/agent-instructions/agent-instructions.routes.ts +3 -2
  193. package/src/modules/agent-instructions/compose.ts +9 -6
  194. package/src/modules/agent-instructions/index.ts +0 -3
  195. package/src/modules/agent-instructions/shared-file-rules.ts +31 -93
  196. package/src/modules/mcp/__tests__/fake-downstream-mcp-server.ts +14 -3
  197. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +250 -0
  198. package/src/modules/mcp/__tests__/mcp.service.test.ts +31 -23
  199. package/src/modules/mcp/__tests__/tool-schema-guard.test.ts +266 -0
  200. package/src/modules/mcp/mcp.service.ts +137 -19
  201. package/src/modules/mcp/tool-schema-guard.ts +196 -0
  202. package/src/modules/plugins/__tests__/plugins.tools.test.ts +154 -4
  203. package/src/modules/plugins/plugins.tools.ts +75 -15
  204. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +26 -55
  205. package/src/modules/settings/deployment-settings.service.ts +13 -54
  206. package/src/modules/settings/setup.routes.ts +3 -6
  207. package/src/modules/skills/__tests__/skills.tools.description.test.ts +91 -0
  208. package/src/modules/skills/skills.tools.ts +62 -16
  209. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +57 -0
  210. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +73 -4
  211. package/src/modules/tool-manuals/tool-manuals.contract.ts +24 -4
  212. package/src/modules/tool-manuals/tool-manuals.service.ts +17 -0
  213. package/src/modules/tool-manuals/tool-manuals.tools.ts +74 -36
  214. package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +160 -0
  215. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +61 -59
  216. package/src/modules/tool-registry/description-length.ts +24 -26
  217. package/src/modules/tool-registry/guide-first.ts +34 -0
  218. package/src/modules/tool-registry/tool-registry.ts +9 -2
  219. package/src/modules/workflow/__tests__/apply-failure.test.ts +6 -1
  220. package/src/modules/workflow/agent-tools/__tests__/change-request-read-shape.test.ts +705 -0
  221. package/src/modules/workflow/agent-tools/__tests__/change-request-read.tools.test.ts +1518 -0
  222. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +23 -2
  223. package/src/modules/workflow/agent-tools/change-request-read-shape.ts +712 -0
  224. package/src/modules/workflow/agent-tools/change-request-read.tools.ts +724 -0
  225. package/src/modules/workflow/agent-tools/change-request-summary.ts +5 -1
  226. package/src/modules/workflow/agent-tools/workflow.tools.ts +8 -0
  227. package/src/modules/workflow/git/__tests__/git.service.appliedChange.test.ts +285 -0
  228. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +124 -0
  229. package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +334 -0
  230. package/src/modules/workflow/git/__tests__/pull-request.service.list-fetch.test.ts +72 -2
  231. package/src/modules/workflow/git/__tests__/pull-request.service.placeholder.test.ts +24 -2
  232. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +620 -1
  233. package/src/modules/workflow/git/git.service.ts +537 -94
  234. package/src/modules/workflow/git/merge-commit.ts +88 -0
  235. package/src/modules/workflow/git/pull-request.service.ts +380 -54
  236. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +7 -1
  237. package/src/modules/workflow/review-workflow/__tests__/merge-records-own-commit.test.ts +407 -0
  238. package/src/modules/workflow/review-workflow/review-workflow.service.ts +189 -11
  239. package/src/modules/workflow/workflow.routes.ts +7 -2
  240. package/src/modules/workflow/workflow.service.ts +7 -0
  241. package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +4 -3
  242. package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +21 -10
  243. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +33 -55
  244. package/src/modules/workspace/__tests__/workspace.tools.test.ts +255 -22
  245. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +231 -1
  246. package/src/modules/workspace/startup/kb-startup-runner.ts +216 -19
  247. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +191 -489
  248. package/src/modules/workspace/startup/steps/seed-tree.ts +21 -27
  249. package/src/modules/workspace/startup/steps/template-files.step.ts +217 -249
  250. package/src/modules/workspace/startup/steps/template-source.ts +5 -3
  251. package/src/modules/workspace/workspace.tools.ts +226 -16
  252. package/src/shared/domain-errors.ts +15 -0
  253. package/src/shared/hidden-tools.ts +45 -0
  254. package/kb-template/AGENTS.md +0 -730
@@ -1,730 +0,0 @@
1
- ---
2
- # This file's own access rule: readable by every signed-in person and their
3
- # agents, whatever the root access.md says. Agents are told to read this file
4
- # before their first action, and the root rules grant read to nobody by
5
- # default — without this line a non-admin's agent would fail on step one.
6
- read:
7
- - everyone
8
- ---
9
- # Knowledge base
10
-
11
- This is a git-backed knowledge base. You are the primary agent responsible for
12
- maintaining it.
13
-
14
- > **This file is managed by the platform.** It lives at the repository root as
15
- > `{{agentsFile}}`, and every server restart replaces it with the current
16
- > template, so edits made here are overwritten. Deployment- or team-specific
17
- > conventions belong in files of your own — anywhere under
18
- > `{{knowledgeBaseDir}}/`, linked from wherever they are needed.
19
-
20
- **Read `mcp-description.md` at the repository root first.** It says what this
21
- knowledge base contains and when to consult it. Agents connected over MCP
22
- receive the default branch's copy inline at the start of every session; a
23
- clone like this one reads the copy on its own branch.
24
-
25
- **There is no required format for knowledge.** Write markdown the way the
26
- subject wants to be written: prose, tables, checklists, diagrams, whatever
27
- serves the reader. Nothing here parses your files into a schema or rejects a
28
- document for having the wrong shape. If a deployment layers a structured
29
- knowledge graph on top, it brings its own conventions and its own guide; this
30
- one describes the platform underneath, which stores files and controls who may
31
- change them.
32
-
33
- ## Directory Structure
34
-
35
- ```text
36
- knowledge-base/
37
- ├── {{knowledgeBaseDir}}/ ← the knowledge itself; organise it however suits you
38
- ├── {{skillsDir}}/ ← shared skills, organised by who owns them
39
- ├── {{pluginsDir}}/ ← one folder per plugin: its tools, and links to skills
40
- ├── roles.yaml ← identity → role mapping (Admin-only edits)
41
- └── access.md ← repo-root access-control rules
42
- ```
43
-
44
- (The three root names above are this deployment's own — a deployment may
45
- rename them in its setup screen, and this guide is rendered with the names in
46
- effect each time it is written.)
47
-
48
- Tool paths are workspace-relative, and the workspace root holds this
49
- repository as the `knowledge-base/` folder: a file in it is
50
- `knowledge-base/{{knowledgeBaseDir}}/Foo.md`. Write the prefix where you can —
51
- it is the path every tool reports back — but a path without it is PLACED under
52
- `knowledge-base/` rather than refused, so `{{knowledgeBaseDir}}/Foo.md` names that
53
- same file, and so does the root-anchored `/knowledge-base/{{knowledgeBaseDir}}/Foo.md`
54
- the app's Copy path gives you. Nothing you send can land beside the repository,
55
- where git would never see it. `.` or `..` segments, backslashes and every other
56
- absolute path are refused.
57
-
58
- Only those three folders are structural. `{{skillsDir}}/` holds shared skills at any
59
- depth — the folder that holds a `SKILL.md` is the skill, and everything above
60
- it is ownership (`{{skillsDir}}/<scope>/…/<skill>/SKILL.md`, with an `access.md` in
61
- any scope folder that needs its own rules). `{{pluginsDir}}/` has a layout the
62
- platform reads:
63
-
64
- ```text
65
- {{pluginsDir}}/<Plugin>/plugin.json the manifest (Agent Plugins) — what makes the folder a plugin; its `name` is the plugin's identity
66
- {{pluginsDir}}/<Plugin>/skills/<skill>/SKILL.md a skill that lives inside the plugin
67
- {{pluginsDir}}/<Plugin>/mcp.json MCP servers (authoritative)
68
- {{pluginsDir}}/<Plugin>/software.bevel.hexis/tools/ `.tool` manuals
69
- {{pluginsDir}}/<Plugin>/access.md who can read/write the plugin
70
- {{pluginsDir}}/personal-<user-id>/… one per person: private
71
- ```
72
-
73
- **The manifest's `name` is the plugin.** It is a kebab-case identifier
74
- (`sales-team`), and it is what every grant spells (`plugin/sales-team/read`),
75
- what the URLs and the catalog key on, and what the compiled marketplace
76
- publishes the plugin as. `displayName` is what people see it called ("Sales
77
- Team"); absent, the folder name is shown. Rename a plugin from its page in the
78
- app: an identifier change rewrites every grant that names it, in one commit —
79
- editing `name` by hand leaves those grants pointing at a plugin that no longer
80
- exists.
81
-
82
- **A plugin LINKS shared skills rather than containing them.** Its manifest
83
- lists skill paths under `extensions["software.bevel.hexis"].skills` — each
84
- entry is one skill folder or a folder of skills under `{{skillsDir}}/`:
85
-
86
- ```json
87
- { "extensions": { "software.bevel.hexis": { "skills": ["{{skillsDir}}/Engineering/deploy", "{{skillsDir}}/Sales"] } } }
88
- ```
89
-
90
- One skill, stored once, can be listed by many plugins. A plugin's effective
91
- skills are the ones inside its folder plus everything its links resolve to.
92
- Do not edit that list by hand: linking is done from the plugin's page in the
93
- app, because it is two edits at once — the manifest entry AND a grant on the
94
- skill (see *Access control* below). A manifest entry without the grant lists
95
- a skill the plugin's members cannot read; the app shows such a link as
96
- needing setup and offers Repair.
97
-
98
- **Ownership decides who may read a skill, never the plugin.** A shared
99
- skill's readability comes from the `access.md` rules on its own folder and
100
- the scopes above it. A plugin that links a skill someone cannot read simply
101
- does not show it to them.
102
-
103
- **Symlinks are not supported anywhere under `{{pluginsDir}}/`.** Access control
104
- resolves rules by path, and a symlink is a second path to the same content —
105
- the two can disagree about who may read what. The platform never creates
106
- them and ignores any it finds (they can only arrive via a direct git push).
107
-
108
- **A plugin follows the [Agent Plugins](https://agent-plugins.org) specification**
109
- (v1.0.0), so another conformant client can load one: it reads `plugin.json`, the
110
- skills under `skills/`, and the servers in `mcp.json`, and ignores everything
111
- else. Two things here are ours and sit outside that portable core. `access.md`
112
- stays at the plugin root because access resolution walks root → file, so the
113
- same rules one level down would govern only that subtree. And `http`/`inline` `.tool`
114
- manuals live under the reverse-DNS `software.bevel.hexis/` namespace, because
115
- the specification describes MCP servers only and has no way to express them.
116
-
117
- **MCP servers belong in `mcp.json` — do not write `.tool` files for them.**
118
- Each `mcpServers` key is the server's identity: it is the namespace its vault
119
- secrets bind to (`<name>_<VAR>`), so renaming a key unbinds every configured
120
- secret and sign-in. The portable entry carries only where the server is
121
- (`type`, `url`, literal headers). Anything this platform needs beyond that —
122
- auth headers carrying `${VAR}` vault references, `variables` declarations,
123
- a `description`, or `local: true` for a server only reachable from a user's
124
- machine — goes in `plugin.json` under
125
- `extensions["software.bevel.hexis"].mcpServers[<name>]`, which other clients
126
- ignore by design. A `type: "stdio"` entry (a command run on the user's own
127
- machine) is always local: the hosted endpoint never spawns it; the local
128
- `hexis-mcp` server fetches the plugin's files to a local directory and runs it
129
- per the Agent Plugins runtime contract (`PLUGIN_ROOT`/`PLUGIN_DATA`, `./`
130
- commands contained to the plugin). A stdio server SHOULD exit when its stdin
131
- reaches EOF — the client also terminates it on shutdown, but a server that
132
- ignores EOF outlives crashes as an orphan whose working directory blocks the
133
- plugin folder from ever refreshing.
134
-
135
- **Secrets are never written into a plugin's portable files.** The specification
136
- defines no portable credential mechanism on purpose: authorization and
137
- credential storage are the client's business, header and `env` values are
138
- "visible package data", and a client must not expand anything except
139
- `${PLUGIN_ROOT}` and `${PLUGIN_DATA}`. So the Secrets Vault IS this platform's
140
- answer to that — and `mcp.json` carries only where a server is, never a
141
- `${VAR}` reference to how to authenticate with it. Those live in `plugin.json`
142
- under `extensions["software.bevel.hexis"].mcpServers[<name>]`, which is ours
143
- to interpret and which other clients ignore by design.
144
-
145
- **Plugin folders are made through the platform, not by writing files.** A
146
- folder is a plugin exactly when it carries a `plugin.json` (the platform
147
- writes one into every legacy plugin folder at startup), and it is LISTED only
148
- when it also carries an `access.md` — a bare directory under `{{pluginsDir}}/` is
149
- neither. Plugins may sit at any depth under `{{pluginsDir}}/`; a folder that holds
150
- plugins deeper down is a grouping folder, not a plugin. A new plugin needs an
151
- `access.md` naming who runs it, and the write gate refuses a plain write
152
- into an unused name there — so do not try to create a plugin by writing a
153
- skill into `{{pluginsDir}}/<new-name>/…`; it will be denied. Use the two tools
154
- instead:
155
-
156
- - `my_plugin` — your user's own private space, created on first use:
157
- `{{pluginsDir}}/personal-<id>/`. Readable only by its owner — not even
158
- admins — and never listed as a plugin. Their personal skills go under its `skills/`,
159
- each in its own folder with a `SKILL.md`; write there with the file tools.
160
- - `create_plugin` — a shared plugin, named, optionally inside a grouping
161
- folder under `{{pluginsDir}}/` (`parent`). The caller runs it; others join
162
- through the app or are granted in its `access.md`.
163
-
164
- The app's **New plugin** button and `POST /api/plugins` do the same. A skill
165
- moves from a personal space into a plugin by moving its folder.
166
-
167
- Everything under `{{knowledgeBaseDir}}/` is yours to arrange. Subfolders, naming,
168
- whether a topic is one file or twenty — all of it is a judgement call about
169
- what the next reader needs, not a rule the platform enforces.
170
-
171
- A deployment may reserve further root folders of its own — `Data/`, `Agents/`
172
- and `Pipelines/` scaffold an agentic execution layer in some installations.
173
- They are not part of this template and are not created here; where they exist,
174
- each carries its own `README.md` describing what belongs in it.
175
-
176
- ## Where a new file goes
177
-
178
- Decide by what the file IS, not by which folder you already hold rights in.
179
- Write access is not evidence that a file belongs somewhere.
180
-
181
- - **Any document goes under `{{knowledgeBaseDir}}/`.** Knowledge, notes,
182
- reports, tickets, specifications, plans, meeting minutes — anything written
183
- to be read by a person. That is what the root is for, and its shape inside
184
- is yours to choose.
185
- - **A shared skill goes under `{{skillsDir}}/`**, or under
186
- `{{pluginsDir}}/<Plugin>/skills/<skill>/SKILL.md` when it belongs to one
187
- plugin alone. A person's private skill goes in their own space (`my_plugin`).
188
- - **Tool manuals, MCP server declarations and manifests go inside a plugin:**
189
- `.tool` manuals under `{{pluginsDir}}/<Plugin>/software.bevel.hexis/tools/`,
190
- servers in that plugin's `mcp.json`, and `plugin.json` at its root.
191
- - **A plugin folder never holds a document.** `{{pluginsDir}}/` carries
192
- machinery — manifests, tool manuals, server declarations, access rules, and
193
- the skills a plugin owns. A ticket or a report written there is filed where
194
- nobody will look for it, under rules written for tools.
195
- - **When the place named does not exist, or nothing fits, ask.** If the user
196
- names a folder that is not there, or the file is of a kind this deployment
197
- has made no home for, say so and ask where it should go. Do not settle for a
198
- folder you happen to be able to write to; a wrong guess is discovered much
199
- later than a question.
200
-
201
- {{sharedFileRules}}
202
-
203
- ## Access control
204
-
205
- Access to any path — reading it as much as writing it — is governed by
206
- `roles.yaml` (who has which role), `groups.yaml` (who is in which group) and
207
- `access.md` files (who may do what, where).
208
-
209
- - **Roles** in `roles.yaml` map a role name to a list of members: emails, and
210
- `group:<Name>` entries that give the role to a whole group (see *Giving a
211
- role to a group* below). Role names are
212
- case- and whitespace-insensitive (`Admin` = `admin` = `ADMIN`; `Product Team`
213
- = `product team`). The reserved name `deny` cannot be used, and neither can
214
- names starting with `role/` or `plugin/` — those spellings are tokens in
215
- access entries (below). One exception to the file's authority: the
216
- **deployment admin** — the address the server configuration sets as
217
- `ADMIN_EMAIL` — is **always an Admin**, whether or not `roles.yaml` lists
218
- it, and taking it out of the file does not change that. It is the rescue
219
- path for a `roles.yaml` that has lost its last Admin. The App roles page
220
- shows that account under Admin as a fixed member that cannot be added or
221
- removed there; every other Admin membership is exactly what the file says,
222
- and removing one takes effect on that person's next request.
223
- - **Plugins are grantable principals.** `plugin/<name>/read`,
224
- `plugin/<name>/write` and `plugin/<name>/owner` in any access file mean
225
- everyone who currently holds that verb on the plugin whose manifest `name`
226
- is `<name>`, derived live from the plugin's own `access.md`. Any spelling
227
- folds to the identifier (`plugin/GTM/read` and `plugin/gtm/read` are one
228
- principal). This is how a shared skill is made visible to a plugin's
229
- members: `read: plugin/gtm/read` on the skill's folder.
230
- Adding or removing someone on the plugin changes what they can read
231
- everywhere the token is granted, with no copying.
232
- - **Access rules** live in `access.md` files, which carry **two blocks with two
233
- scopes**: the BODY (below the closing `---`) declares the rules for the
234
- folder the file sits in, and the FRONTMATTER declares who may read and
235
- write that `access.md` itself. Each block names verbs (`read`, `write`,
236
- `download`, `owner`) whose entries are either grants (a bare principal) or
237
- denials (the lowercase word `deny`, a space, then the principal).
238
- Capitalised forms like `Deny` are *not* triggers; they are treated as part
239
- of a name.
240
- - **Principals** are a role name from `roles.yaml`, a group name from
241
- `groups.yaml`, a person as `Name <email>`, a plugin token (above), or
242
- **`everyone`** — the built-in org-wide principal: every signed-in person and
243
- their agents. `read: everyone` in a folder's BODY opens that folder to the
244
- whole organisation; it is how an organisation-wide skill or plugin is
245
- shared. The same line in a file's FRONTMATTER only makes that one file
246
- visible — a plugin's `access.md` ships with `read: everyone` in its
247
- frontmatter so the plugin can be found and joined, and that admits nobody
248
- to the plugin itself. A person's own space (`{{pluginsDir}}/personal-<id>/`)
249
- denies `everyone` outright, so opening a parent folder never opens it.
250
- When a group and a role share a name, the bare name means the GROUP;
251
- `role/<Name>` (for example `deny role/Reviewer`) always means the role.
252
- - **Keep an `access.md` body pure YAML**, with any explanation in `#` comments.
253
- A body that does not parse as YAML naming at least one verb is read in the
254
- older format instead, where the FRONTMATTER carried the folder's rules — so a
255
- stray line of prose silently changes which block governs the folder.
256
- - **The verbs nest.** `owner` sits over `write` and `download`; `write` and
257
- `download` each sit over `read` — anyone who may edit a node, or save a copy
258
- of it, may also view it. `write` and `download` say nothing about each other.
259
- The nesting is GRANT-ONLY: a grant of a higher verb confers the lower ones,
260
- but `deny write` or `deny download` says nothing about `read` and never
261
- strips a separate read grant. So `download: Ana <ana@x.io>` alone lets Ana
262
- open the node as well as download it, and a `deny download` beside an
263
- inherited read leaves her able to open it but not save it.
264
- - **You can only change what you can read.** Nothing is created, changed,
265
- moved into or removed from a place the caller cannot read — on every
266
- branch, drafts included, whatever `write:` rules say. A write tool refused
267
- for this says so (`write-denied`, naming the unreadable folder), and
268
- proposing is not offered either: a proposal into a folder its author cannot
269
- see would vanish from them the moment it landed. Two exceptions. A NEW
270
- FOLDER directly under `{{knowledgeBaseDir}}/`, `{{skillsDir}}/` or
271
- `{{pluginsDir}}/`: anyone may start one, whatever the root's rules grant
272
- them, and the new folder's `access.md` is seeded with the creator's own
273
- `read:` grant so what they put there is visible to them (a loose FILE
274
- directly at a root has no folder to carry that grant and is not excepted).
275
- And an Admin — or the deployment owner — may change the files directly in
276
- the repository root (`roles.yaml`, `access.md`, `groups.yaml`, `{{agentsFile}}`,
277
- …) even when the root grants read to nobody: the same rescue the write
278
- floor gives them, so a tree whose root rules lock everyone out stays
279
- repairable from inside the app. That rescue stops at the root; a subfolder
280
- an admin cannot read is closed to them like to anyone else.
281
- - **Resolution** walks repo root → file directory, accumulating per-principal
282
- state. User-level entries trump role-level entries. A role denial removes
283
- only that role's contribution; it does not undo grants from other roles.
284
- - **`roles.yaml` is editable only by Admin** — hard-coded in the resolver,
285
- never overridable by an `access.md`.
286
- - **`access.md` files are picked up at any depth**, so a folder can tighten or
287
- widen what it inherited from its parent.
288
- - **Per-file rules exist for Markdown notes only.** A note (`.md`, lowercase)
289
- may name verbs in its own frontmatter, and those rules apply to that one
290
- note. (A `.tool` definition keeps the access verbs in its own YAML the same
291
- way.) Every other file (a PDF, a presentation, a spreadsheet, an image, any
292
- binary, a `.markdown` or `.MD` file, or binary content saved as `.md`)
293
- takes its folder's rules: sharing it on its own is refused with
294
- `folder-governs-access`, naming the folder. To change who
295
- can open such a file, change its folder's `access.md`, or move the file to a
296
- folder whose rules fit.
297
-
298
- Rules are enforced at runtime; a malformed `roles.yaml` or `access.md` surfaces
299
- when access is resolved.
300
-
301
- ### Roles are pre-set — a "new role" is usually a group
302
-
303
- **What a role is.** A role in `roles.yaml` is an app role: a capability the
304
- platform defines and acts on (`Admin` is one), listed with the people who hold
305
- it. The set of roles is pre-set by the platform. A role is not a way to name a
306
- team.
307
-
308
- **Agents never create roles.** Add people to a role that already exists, or
309
- remove them, and nothing more: never add a role name to `roles.yaml`, and never
310
- rename one — a rename is a delete plus a create. Such a write is refused with a
311
- 422 that names the role and says: app roles are pre-set — add people to
312
- existing roles, and use a GROUP for a task- or team-scoped set of people.
313
- Relay that refusal to your user as it stands; do not look for another way to
314
- write the file.
315
-
316
- **Is it really a group?** When someone asks for a "new role", it almost always
317
- is. It is a group when any of these hold:
318
-
319
- - the name says who the people are — a team, a project, a customer, a
320
- committee — rather than a capability the platform already has;
321
- - it would change or disappear when the project ends or the team reshuffles;
322
- - its purpose is to give those people access to some folders or files.
323
-
324
- A request that matches a role that already exists is membership, not a new
325
- role.
326
-
327
- **What to do instead.**
328
-
329
- 1. If an existing role already carries the capability, add the people to it.
330
- 2. Otherwise make it a group: add or extend the group in `groups.yaml` (or
331
- point your user at the app's Groups page), then grant the group in the
332
- `access.md` of the folders it should reach.
333
- 3. If your user still needs a role the platform does not have, that is not an
334
- edit you can make — say so, and leave the decision to an admin.
335
-
336
- ### Giving a role to a group
337
-
338
- A role's member list takes a group as well as individual emails. Write the
339
- entry as `- group:<Name>`, where `<Name>` is a group in the active group
340
- source — `synced-groups.yaml` when the deployment syncs groups from an
341
- identity provider, `groups.yaml` otherwise. Here a `Reviewer` role the
342
- deployment already has goes to a whole group:
343
-
344
- ```yaml
345
- roles:
346
- Admin:
347
- - dana@example.com
348
- Reviewer:
349
- - lee@example.com
350
- - group:Platform Team
351
- ```
352
-
353
- - **Matching.** The name is matched case- and whitespace-insensitively against
354
- the active group source, like role names: `group:platform team` and
355
- `group:Platform Team` are the same entry as `group:Platform Team`.
356
- - **Unknown groups are refused.** An entry naming a group the active source
357
- does not declare is a validation error: the write is refused with a 422
358
- that names the entry and its role (`'- group:Platfrom Team' under role
359
- 'Reviewer'`), and nothing is saved. Create the group first, or fix the name.
360
- - **A group under `Admin` makes every member a full admin** — including anyone
361
- added to the group later, and including the right to edit `roles.yaml`
362
- itself. Only make that edit when your user is an Admin and explicitly asks
363
- for exactly that, and say so in the commit summary; for anyone else, tell
364
- them what it would mean and who can do it (below). `Admin` must also always keep at least one
365
- direct email member; a group entry alone is not enough, so a broken
366
- directory can never leave the deployment without an admin.
367
- - **With direct emails.** Group entries and emails add up: the role's members
368
- are everyone listed by email plus everyone currently in each listed group.
369
- A person in both is simply a member; adding or removing someone from the
370
- group changes the role with no edit to `roles.yaml`.
371
- - **With denials.** Group members hold the role's grants exactly as if they
372
- were listed by email. A denial of the role in an `access.md`
373
- (`deny role/Reviewer`) therefore removes the role's contribution for
374
- everyone in the group, as it does for the emails. Write the `role/` form:
375
- a bare `deny Reviewer` would deny a group named `Reviewer` instead, if one
376
- exists. The nearest `access.md` that says
377
- anything about the person decides: a person granted by name
378
- (`Name <email>`) in the SAME `access.md` as the denial keeps that access,
379
- because within one file a person's own entry beats a role entry. A grant by
380
- name in a folder further up does not survive a role denial closer to the
381
- file.
382
-
383
- **Only an Admin changes `roles.yaml`, and only on the default branch.** A
384
- change request cannot carry the edit: when a request is merged, `roles.yaml`
385
- is restored to what the default branch has, so a role edit drafted on a
386
- branch is dropped at the merge without a word. Do not propose one. If your
387
- user is an Admin, `edit_file` the file on the default branch directly — for
388
- example, to give the Reviewer role to a group, add the entry under the
389
- existing role:
390
-
391
- ```yaml
392
- roles:
393
- Admin:
394
- - dana@example.com
395
- Reviewer:
396
- - lee@example.com
397
- - group:Platform Team # added
398
- ```
399
-
400
- If your user is not an Admin, tell them who is (the `Admin` entries in
401
- `roles.yaml`) and that the change is made in the app's Roles page or by an
402
- admin editing the file; do not open a change request for it.
403
-
404
- ### Direct writes vs change requests
405
-
406
- File-level write access decides how a change lands on the default branch:
407
-
408
- - A user — or an agent acting as that user — whose access resolution grants
409
- **write or owner on every file the change touches** may commit **directly**
410
- to the default branch.
411
- - Without that access, the change goes through a **branch + change request**,
412
- approved by an owner / write-access holder of the affected files — every
413
- affected file with an eligible approver needs that approval, whatever its
414
- type (notes, binary files, files without an extension).
415
- - Agents carry exactly their user's access, never more. Before writing to the
416
- default branch, **ask the user** whether to write directly or go through the
417
- review flow — and prefer a change request when in doubt, when the change is
418
- large, or when it touches content the user does not own.
419
-
420
- ### An agent proposes and syncs; a person merges
421
-
422
- - **Propose** with `open_change_request`, then give the user the request's
423
- `url`. Reviewing, approving and merging a change request happen in the app,
424
- by a person — no agent tool approves a file, bypasses approval, or merges a
425
- request. `merge_change_request` no longer exists.
426
- - **Sync** a draft with `merge_branch`, `source` = the branch the request
427
- targets, `target` = the draft. This is allowed while the draft's request is
428
- open, and is how you bring it up to date or surface conflicts to resolve on
429
- the draft.
430
- - `merge_branch` refuses to merge a draft into the branch its open change
431
- request targets — it names the request; ask the user to review it in the
432
- app. Into a protected branch it merges only what you could commit there
433
- directly, under the rule above — and never a change to `roles.yaml`, whoever
434
- you are: roles are changed in the app, not merged in from a draft.
435
-
436
- ## Skills (`{{skillsDir}}/<scope>/…/<skill>/SKILL.md`, or `{{pluginsDir}}/<Plugin>/skills/<skill>/SKILL.md`)
437
-
438
- A skill is a folder holding a `SKILL.md` and whatever files it needs. Shared
439
- skills live under `{{skillsDir}}/`, organised by ownership; a skill that belongs to
440
- exactly one plugin may live inside that plugin's `skills/` folder instead.
441
- Skill names are unique across the whole catalog, whichever home they have.
442
- The frontmatter names it, declares which tools it may use, and may carry a
443
- version:
444
-
445
- ```yaml
446
- ---
447
- name: weekly-newsletter
448
- description: Drafts the Friday newsletter for review.
449
- allowed-tools: [slack_post_message]
450
- metadata:
451
- version: "1.4.0"
452
- ---
453
- ```
454
-
455
- The body is the instructions, in plain markdown. `allowed-tools` entries are
456
- tool names from the `.tool` manuals and MCP servers of the plugins that hold
457
- the skill. `metadata.version` is semver; `list_skills` reports it, and
458
- `get_skill` with a `version` loads the skill as it was when it last declared
459
- that version (omit `version` for the latest). Any other `metadata` keys are
460
- the author's own notes — the catalog carries the file as it is and acts on
461
- none of them.
462
-
463
- A `SKILL.md` committed on the default branch is listed and loadable from the
464
- very next `list_skills` or `get_skill`, on the connection you already have:
465
- skills are read from the workspace on every request, on either connection.
466
- See *A released tool or skill is live within ten seconds* under **Tool
467
- Manuals** for the one caveat (an MCP client that caches the prompt list it
468
- was given at connect time must re-list — the prompt-list-changed notification
469
- that tells it to arrives with the connection's next catalog check, which is
470
- within ten seconds on a connection in use and at its next use on an idle
471
- one).
472
-
473
- **How skills reach agents.** Through the MCP server (`list_skills`,
474
- `get_skill`), or as native plugins: every user can clone a git remote from
475
- the app's external-agent page that holds a plugin marketplace compiled from
476
- exactly the skills they may read — one plugin per plugin here, a
477
- `skills-and-knowledge` plugin for the rest plus this knowledge base's MCP
478
- server, and `hexis-all`, one plugin holding every skill they may read and
479
- the MCP server, for a single install.
480
-
481
- ## Tool Manuals (`{{pluginsDir}}/<Plugin>/software.bevel.hexis/tools/*.tool`)
482
-
483
- Each plugin folder holds `*.tool` files — reusable **tool manuals** that let agents call external APIs. They are **not part of the knowledge graph** (never modelled as nodes) and are access-controlled like any other file via `access.md`. Any user who can *read* a `.tool` can use its tools; anyone who can *write* it sets its shared (admin) secrets (see below). Put each manual in the plugin's `software.bevel.hexis/tools/` directory, beside
484
- the skills that use it. The same integration may exist in several plugins as
485
- separate files (`Everyone/…/serper.tool` and `Finance/…/serper.tool`), each
486
- with its own credentials and access rule — a plugin is a folder, not a registry
487
- of unique names. Remember: `.tool` files are for `http` and `inline` manuals
488
- only; MCP servers belong in `mcp.json`.
489
-
490
- A `.tool` file is JSON or YAML. Its `type` decides how tools are discovered:
491
-
492
- - **`inline`** — the tools are embedded in the file (no network round-trip to list them).
493
- - **`http`** — `url` points to an endpoint that returns a UTCP manual.
494
-
495
- (`type: mcp` is the LEGACY spelling of an MCP server as a `.tool`. The boot
496
- migration converts such files into `mcp.json` entries; do not write new ones.)
497
-
498
- **The tool is the frontmatter.** A `.tool` is one `---` YAML block holding *everything* — its `id`, its access verbs (`read:`/`write:`/`owner:`/`download:`), and its config (`type`/`url`/`variables`/…) — all in the same object. Anything after the closing `---` is free-form notes the parser ignores (like a `SKILL.md` body):
499
-
500
- ```yaml
501
- ---
502
- id: my_tool
503
- write:
504
- - Product Team
505
- owner:
506
- - Jane Doe <jane@x.com>
507
- type: http
508
- url: https://api.example.com/utcp
509
- ---
510
- ```
511
-
512
- (A file with no `---` fence is the legacy form — the whole file is the object, so a bare JSON `.tool` still works.)
513
-
514
- **`id` = variable namespace.** The `id` is the manual's stable identity: it's the UTCP namespace secrets bind to (`<id>_<VAR>`) and its route slug. It must be lowercase `snake_case` and **unique** across all `.tool` files. Resolution is `id` → `name` → the file name (so a `name:` alone works, same as the id system uses for every file). If two files collide, the one saved most recently through the app is auto-suffixed (`my_tool` → `my_tool2`). **Access** declared here gates who can use and edit that tool, exactly like a node's own frontmatter (most specific; overrides the folder `access.md`).
515
-
516
- **Frontmatter `id` = address.** This is generic, not tool-specific: ANY `.md` or `.tool` file whose frontmatter declares an `id` (or a lowercase snake_case/kebab `name`) is addressable at `/workspace/<branch>/<id>` in the app, exactly like a knowledge node — tools, skills (`SKILL.md`), and plain notes alike. Graph nodes win an id collision; files without frontmatter stay path-addressed.
517
-
518
- **Remote vs local (`remote`).** A tool is available to remote agents by default. Add `remote: false` for a tool that only works on the user's own machine (e.g. an `http` manual whose `url` is on `localhost`): the hosted remote MCP endpoint cannot reach it, so it skips the tool and advertises it through the `list_local_tools` tool instead. (An MCP server that is local-only declares `local: true` in the plugin.json extensions block instead — see above.)
519
-
520
- To actually USE those tools, run the workspace as a local MCP server:
521
-
522
- ```
523
- npx @bevel-software/hexis-mcp --url <workspace-url> --key <connection-key>
524
- ```
525
-
526
- It serves everything the hosted endpoint serves **plus** the local-only tools, because it runs on the machine where they exist. Remote tools still execute on the server, so their shared keys and OAuth sign-ins keep working untouched; a local-only tool's own `${VAR}`s come from the environment of whatever launched the command (your MCP client's config), since the Secrets Vault never leaves the server. Reading the `.tool` and wiring the server into your client by hand still works and is the fallback when the command is unavailable.
527
-
528
- ### Referencing secrets — `${VAR}` and the `variables` block
529
-
530
- Anywhere a `.tool` needs a credential (an API key, a token) write a placeholder like `${API_KEY}`. At call time it is filled from the **Secrets Vault** under the key `<id>_<VAR>`, where `<id>` is the manual's resolved id (the same `id` → `name` → file-name resolution described above) — so a manual whose id is `weather` referencing `${API_KEY}` reads the secret `weather_API_KEY`. A secret is therefore bound to exactly one manual; another manual cannot read it.
531
-
532
- Declare who provisions each variable with an optional top-level `variables` array. Each entry is `{ name, scope, label? }`:
533
-
534
- - **`scope: admin`** (the **default**) — set **once by a writer** of this `.tool` file; the same value is shared by everyone who uses the tool. Prefer this: keep as much as possible owned by the tool author.
535
- - **`scope: user`** — set by **each end user** for themselves (their own value, never shared).
536
-
537
- `name` must match `[A-Za-z0-9_]+`. A referenced `${VAR}` that you don't declare defaults to `admin` — and it still SURFACES automatically: the app detects every `${VAR}` the file actually references and shows it in the secrets UI, so the `variables` block is only needed to change a variable's scope to `user`, give it a label, or declare an OAuth sign-in. Values are entered in the Secrets Vault UI (or the `.tool` editor's sidebar), never in the file itself. A malformed `variables` entry makes the whole file fail to load, so it is never silently mis-scoped.
538
-
539
- ### Declaring an OAuth sign-in — the `oauth` block
540
-
541
- A `user`-scoped variable can be filled by **signing in** instead of by a typed value: add an `oauth` block and each member authorizes with the provider; the token then rides in whatever header references `${VAR}`. The block carries PUBLIC config only:
542
-
543
- | field | | |
544
- |---|---|---|
545
- | `clientId` | required | the OAuth app's client id — the tool owner registers the app with the provider, using the redirect URI `<backend>/api/secrets/oauth/callback` |
546
- | `authorizationUrl`, `tokenUrl` | **optional on an `mcp.json` server**, required in a `.tool` | leave both out on an MCP server: they are discovered from the server's own OAuth metadata. Give both or neither. |
547
- | `scopes` | optional | `string[]`, requested at sign-in and required back from the token |
548
- | `pkce` | optional, default **on** | PKCE S256 — MCP servers require it; providers without it ignore it. Only `false` is meaningful. |
549
- | `resource` | optional | RFC 8707 resource indicator (the MCP server URL); discovered on an `mcp.json` server |
550
- | `authParams` | optional | extra static authorize params, e.g. Google's `access_type: offline` |
551
-
552
- **Never** a `clientSecret` — a `.tool` carrying one fails to load, and an `mcp.json` server whose plugin.json entry carries one is dropped from the catalog. The secret is pasted once by a tool writer on the tool's page, then every member signs in on the Connect page.
553
-
554
- For an `mcp.json` server the declaration lives in `plugin.json`, in the same extensions entry as the auth header that uses it:
555
-
556
- ```json
557
- {
558
- "extensions": {
559
- "software.bevel.hexis": {
560
- "mcpServers": {
561
- "hubspot": {
562
- "headers": { "Authorization": "Bearer ${HUBSPOT_TOKEN}" },
563
- "variables": [
564
- { "name": "HUBSPOT_TOKEN", "scope": "user", "label": "HubSpot sign-in",
565
- "oauth": { "clientId": "<the app's client id>" } }
566
- ]
567
- }
568
- }
569
- }
570
- }
571
- }
572
- ```
573
-
574
- That is the whole declaration: endpoints, PKCE and the resource indicator come from the server. Add `authorizationUrl`/`tokenUrl` only when `list_tool_setup` reports in `setup.reason` that they could not be discovered.
575
-
576
- ### Calling a Google API as a service account: `auth_type: google_service_account`
577
-
578
- Some Google APIs (Google Ads, Tag Manager, BigQuery, Sheets, …) are called as a **service account**: one shared identity, no sign-in per person. Google does not accept the service account's key on a call. It accepts a short-lived token that has to be minted from the key, so a header holding `${VAR}` cannot do it. Name the key in an `auth` block on an inline tool's `tool_call_template` instead, and the platform mints the token at call time, keeps it until shortly before it expires, and sends it as `Authorization: Bearer <token>`:
579
-
580
- ```yaml
581
- ---
582
- id: google_ads
583
- type: inline
584
- variables:
585
- - { name: GOOGLE_SA_KEY, scope: admin, label: "Service-account key JSON" }
586
- - { name: DEVELOPER_TOKEN, scope: admin, label: "Google Ads developer token" }
587
- tools:
588
- - name: list_accessible_customers
589
- description: List the Google Ads customers the service account can reach.
590
- inputs: { type: object, properties: {} }
591
- outputs: { type: object, properties: {} }
592
- tool_call_template:
593
- call_template_type: http
594
- http_method: GET
595
- url: https://googleads.googleapis.com/v22/customers:listAccessibleCustomers
596
- headers: { developer-token: "${DEVELOPER_TOKEN}" }
597
- auth:
598
- auth_type: google_service_account
599
- credentials: ${GOOGLE_SA_KEY}
600
- scopes: https://www.googleapis.com/auth/adwords
601
- ---
602
- ```
603
-
604
- | field | requirement | notes |
605
- |---|---|---|
606
- | `credentials` | required | always a `${VAR}`: the vault variable holding the key JSON Google issued for the service account (the whole file, or the file in base64). Admin-scoped, so a writer of the `.tool` stores it once on the tool's page. Never the key itself. |
607
- | `scopes` | required | the OAuth scopes the API needs: one scope, a space-separated list, or a list |
608
- | `subject` | optional | a user's email to act as, for a service account granted domain-wide delegation |
609
-
610
- The token always comes from Google's own token endpoint; a `token_uri` inside the key is ignored. Give the service account access in the Google product itself (for example add its email as a user of the Google Ads account or the Tag Manager container), or that product refuses every call with an error of its own. When Google refuses the key itself (a revoked key, a scope the account may not have, a subject without delegation), the error names the service account and Google's reason, never the key.
611
-
612
- The block works in one place: an inline tool's `tool_call_template` with `call_template_type: http`. Anywhere else (an `sse`, `streamable_http` or `mcp` template, or a `type: http` / `type: mcp` tool that discovers its tools from a `url`) no token would be sent, so the `.tool` is refused and `list_tool_setup` names it under `invalid`, saying where the block was found. It works the same for a `remote: false` tool run by the local `hexis-mcp` server, which mints the token on the machine it runs on.
613
-
614
- A service account is one shared identity. To have each person call Google as themselves instead, do not use this block: declare a sign-in variable (`oauth`, above) and send it as `Authorization: Bearer ${VAR}`.
615
-
616
- ### Examples
617
-
618
- An `http` manual that authenticates with a shared org key and a per-user key:
619
-
620
- ```yaml
621
- name: weather
622
- type: http
623
- url: https://api.weather.example/utcp
624
- headers:
625
- Authorization: Bearer ${ORG_KEY}
626
- X-User-Key: ${USER_KEY}
627
- variables:
628
- - { name: ORG_KEY, scope: admin, label: "Org-wide weather.com key" }
629
- - { name: USER_KEY, scope: user, label: "Your personal weather.com key" }
630
- ```
631
-
632
- An `inline` manual with one tool:
633
-
634
- ```json
635
- {
636
- "name": "billing",
637
- "type": "inline",
638
- "variables": [{ "name": "BILLING_KEY", "scope": "admin" }],
639
- "tools": [
640
- {
641
- "name": "create_invoice",
642
- "description": "Create an invoice.",
643
- "inputs": { "type": "object", "properties": {} },
644
- "outputs": { "type": "object", "properties": {} },
645
- "tool_call_template": {
646
- "call_template_type": "http",
647
- "http_method": "POST",
648
- "url": "https://api.billing.example/invoices",
649
- "headers": { "Authorization": "Bearer ${BILLING_KEY}" }
650
- }
651
- }
652
- ]
653
- }
654
- ```
655
-
656
- ### Adding a third-party tool
657
-
658
- When asked to add/integrate a product as a tool (e.g. "add Notion", "wire up Linear"), **never invent an endpoint or write a placeholder URL** — a `.tool` pointing at a made-up host is useless:
659
-
660
- 1. **Find the real endpoint from the vendor's own docs.** Prefer the vendor's official **remote MCP server** if one exists; otherwise fall back to their **REST API** base. No endpoint is named here on purpose — a URL copied into this file would be asserted long after it stopped being true, which is the failure this step exists to prevent. Use web search/extract to confirm the exact URL, transport, and auth scheme — don't answer from memory. If you have no web access or genuinely can't find it, **ask the user** for the endpoint URL and auth instead of guessing.
661
- 2. **Pick the home from what you found.** An MCP server → an entry in the plugin's `mcp.json` (`type: "streamable-http"` with the official `url` — use the `https://…` URL, **never** `ws://`/`wss://`). A plain REST/HTTP endpoint → a `.tool` with `type: http`. Use `type: inline` only when hand-authoring the individual HTTP calls.
662
- 3. **An OAuth-protected MCP server usually needs NOTHING beyond its `mcp.json` entry.** Write just those two and let the app probe the server: it discovers the sign-in provider (MCP authorization spec), registers itself, and surfaces a per-user sign-in on the Connect page. That is the `oauth-auto` case, and for it you must NOT declare `variables` or `headers`.
663
-
664
- Some providers do not support automatic registration (`oauth-manual` — HubSpot, Google; see the walkthrough below). Those DO need a sign-in variable holding the client id of an app the owner registers, and an admin pastes the client secret on the tool's page. You do not have to guess which kind you are facing: write the two lines, then run `list_tool_setup` and read `setup.kind` — and `setup.reason`, which spells out the next step (including the redirect URI to register).
665
- 4. **For key-based auth, wire it as `variables`, never a hard-coded secret.** Reference credentials as `${VAR}` in `headers` (e.g. `Authorization: Bearer ${NOTION_TOKEN}`) and declare each in the `variables` block with a scope (`admin` = one shared value; `user` = per-user). Users fill the values in the Secrets Vault.
666
- 5. **Say so when a tool is reachable ONLY from the user's own machine.** For an MCP server (e.g. one on `localhost`), declare `local: true` on its entry in the plugin.json extensions block — `remote: false` is a `.tool` frontmatter field and means nothing in `mcp.json`. For an `http`/`inline` `.tool`, set `remote: false`. Otherwise leave the tool remote-capable.
667
-
668
- ### Checking what an admin still needs to configure
669
-
670
- Call the **`list_tool_setup`** tool to see, for every accessible tool — `.tool` manuals and `mcp.json` servers alike — what is configured and what is still missing. Use it whenever a tool isn't working, after adding a tool, or when asked "what do I need to set up?" — then EXPLAIN the remaining steps to the user rather than guessing. Per tool it reports:
671
-
672
- - **`setup.kind`** (for MCP servers): `open` = no credentials needed; `oauth-auto` = the platform registered itself with the server automatically and users just authorize on the **Connect page**; `oauth-manual` = the sign-in uses an OAuth app the owner registers (the provider offers no automatic registration, or the declaration already names a client id). `setup.reason` is present only while something still blocks that sign-in — no declaration yet, or endpoints that could not be discovered — and says what to do.
673
- - **Per variable**: `adminConfigured` (the shared value — or, for a sign-in, the owner-side provider setup — is done), `userConfigured` / `authorized` (the CURRENT user's own value / sign-in), and `canWrite` (whether the current user may set the tool's shared config).
674
-
675
- **Tools are served from the default branch only.** An `mcp.json` entry or `.tool` you write on a draft is committed to that draft and nowhere else: it is not listed, not callable and has no sign-in on the Connect page until the draft is merged. After declaring a tool on a draft, call `list_tool_setup` with `branch` set to that draft — `onBranchOnly` names what is still waiting there — and tell the user it goes live once the change request is merged. A tool that stays in `tools` is released, and a restart does not remove it or its sign-ins; if one disappears, check the caller's read access to the file that declares it.
676
-
677
- **A released tool or skill is live within ten seconds — no reconnect.** A commit on the default branch that adds, changes or removes a `.tool`, an `mcp.json`, a `plugin.json` or a `SKILL.md` drops the catalogs at once, whichever way the commit arrived: the app, the file tools, a git push, or an approved change request being applied. The hosted endpoint is stateless — it reads the live catalog on every request, so the very next call sees the change. The local `hexis-mcp` server checks the workspace's catalog whenever its connection is USED — when a tool call finishes, and when a client lists the tools — and re-registers what changed, local-only servers included; on a connection in use the change is there within ten seconds of the commit — unless a tool call is still running on that connection, which holds the refresh for as long as that call runs, to a limit of fifteen seconds (see the caveat below) — and `list_tools`, `list_tool_setup` and `list_local_tools` then answer with the new state on the connection you already have. A listing runs a check and waits for it, so what you are handed is never a list a refresh is halfway through replacing; checks are collapsed to at most one every two seconds, so a listing arriving inside that window is answered from what the last check confirmed rather than from a fresh read — up to two of those ten seconds are that window alone, before the workspace has been asked anything. After a call, the change lands once that call finishes, so a new tool is callable from the call after that one. An IDLE connection is deliberately outside that window: it asks the workspace nothing about its catalog, holds nothing open beyond the one MCP session it serves tools through, and is told nothing — no catalog timer, no extra socket parked per laptop, so an unused connection costs the workspace nothing more than being connected (a browser-signed-in server still renews its own sign-in shortly before it expires, a single request every few hours) — and it catches up at its next use. Skills need no check at all: `list_skills` and `get_skill` read the workspace on every request, so a committed `SKILL.md` is in the very next answer, and the two resolve a skill the same way, so a skill you can load by name is a skill the listing shows.
678
-
679
- The platform also sends the MCP tool-list-changed and prompt-list-changed notifications when it can. **A client that CACHES the list it got at connect time — rather than honouring those notifications — will not see the change: it must re-list, or reconnect.** That is a property of the client, not of the workspace; if a tool you just wrote is missing, call `list_tools` again before assuming anything is wrong. One caveat on the local `hexis-mcp` server: a refresh there waits for a tool call that is still running, but only for fifteen seconds, so a commit made mid-call lands once that call finishes — or, if the call is still running after those fifteen seconds, while it is still running — and a LOCAL-only server (`local: true`, or a `type: "stdio"` command) that changed is restarted by that refresh, so a call to it made in the same moment may see it come back.
680
-
681
- The listing is scoped by the same access controls as everything else: a tool the caller can't READ doesn't appear at all, and `canWrite` means write access **on the file that declares it** — the `.tool` file itself (via its frontmatter `write:`/`owner:` verbs or the `access.md` chain), or the plugin's `mcp.json` for an MCP server (via the plugin's `access.md` chain — `mcp.json` carries no verb list of its own) — NOT any platform role. The people who manage that file are exactly the people who configure its shared secrets. To delegate a `.tool` to someone, add them to that file's `write:`/`owner:` list; to delegate an MCP server, grant them `write` on the plugin in its `access.md` (both are edits you can make via change request). That alone lets them configure it.
682
-
683
- **Agents never handle secret VALUES.** Never ask for an API key, token, or client secret in the conversation, and there is no tool to set one. Point the right person at the right surface instead:
684
-
685
- - **Shared (admin) values and OAuth client secrets** → a tool writer pastes them into the fields on the tool's page in the app (the "Your connection" section; for a `.tool` file, the setup panel is also in its editor sidebar).
686
- - **Per-user values and sign-ins** → each user enters/authorizes on the **Connect page**.
687
-
688
- For **`oauth-manual`** (e.g. HubSpot, Google, GitHub, Slack — no dynamic client registration), walk the admin through the one-time setup:
689
-
690
- 1. Register an OAuth app in the provider's console, with redirect URI `<backend>/api/secrets/oauth/callback` (the exact URI is in `setup.reason`).
691
- 2. Ask for the app's **client id** (public — fine to receive in chat) and write the sign-in declaration yourself: for an `mcp.json` server, the `variables` entry with `oauth: { clientId }` plus the `Authorization: Bearer ${VAR}` header in the plugin.json extensions entry (see "Declaring an OAuth sign-in" above — no URLs needed); for a `.tool`, the same entry with `authorizationUrl` and `tokenUrl` as well. You can do this edit for them via a change request. A human can do the same under "Edit server" on the tool's page — the form's fields are exactly this block.
692
- 3. Run `list_tool_setup` again: `setup.reason` must be gone. If it says the endpoints could not be discovered, add `authorizationUrl`/`tokenUrl` from the provider's docs.
693
- 4. The admin pastes the app's **client secret** into the "Client secret" field on the tool's page — never into the file, never into the chat.
694
- 5. Every user then authorizes on the Connect page.
695
-
696
- ## Conventions
697
-
698
- These are conventions, not validations — nothing rejects a file for breaking
699
- them. They exist because a knowledge base people can navigate beats one that is
700
- merely correct.
701
-
702
- 1. **Descriptive file names.** `Weekly-Sync-2026-03-14.md` beats `notes3.md`.
703
- Avoid spaces; they survive git fine but make links noisier to read.
704
-
705
- 2. **Markdown links between documents.** Use
706
- `[Page Name](relative/path/to/Page.md)`, relative to the LINKING file's
707
- directory rather than the repo root, so links resolve both in the app and on
708
- the git host.
709
-
710
- 3. **Absolute dates.** `YYYY-MM-DD`, never "last Tuesday" — a saved file
711
- outlives the moment it was written.
712
-
713
- 4. **Search before creating.** If a document on the subject exists, extend it
714
- rather than starting a rival.
715
-
716
- 5. **Preserve what is there.** Append or edit sections; do not overwrite a file
717
- wholesale unless asked to.
718
-
719
- 6. **Say where it came from.** When a claim rests on a specific source — a
720
- person, a ticket, a document, a URL — name it inline near the claim, with
721
- the date it was true. The next reader's first question is "says who, and is
722
- it still true?".
723
-
724
- ## Finding things
725
-
726
- - `grep` for keywords across `{{knowledgeBaseDir}}/`.
727
- - Follow markdown links: when you read `[Some Page](relative/path/Some Page.md)`,
728
- that path is relative to the file you are reading.
729
- - `list_files` to see the shape of a folder before assuming where something
730
- lives.