@halofy/agent-connect 0.12.1 → 0.13.1

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 (253) hide show
  1. package/README.md +249 -10
  2. package/bin/halofy-agent.mjs +96 -11
  3. package/node_modules/yaml/LICENSE +13 -0
  4. package/node_modules/yaml/README.md +172 -0
  5. package/node_modules/yaml/bin.mjs +11 -0
  6. package/node_modules/yaml/browser/dist/compose/compose-collection.js +88 -0
  7. package/node_modules/yaml/browser/dist/compose/compose-doc.js +43 -0
  8. package/node_modules/yaml/browser/dist/compose/compose-node.js +109 -0
  9. package/node_modules/yaml/browser/dist/compose/compose-scalar.js +86 -0
  10. package/node_modules/yaml/browser/dist/compose/composer.js +219 -0
  11. package/node_modules/yaml/browser/dist/compose/resolve-block-map.js +115 -0
  12. package/node_modules/yaml/browser/dist/compose/resolve-block-scalar.js +198 -0
  13. package/node_modules/yaml/browser/dist/compose/resolve-block-seq.js +49 -0
  14. package/node_modules/yaml/browser/dist/compose/resolve-end.js +37 -0
  15. package/node_modules/yaml/browser/dist/compose/resolve-flow-collection.js +207 -0
  16. package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +226 -0
  17. package/node_modules/yaml/browser/dist/compose/resolve-props.js +146 -0
  18. package/node_modules/yaml/browser/dist/compose/util-contains-newline.js +34 -0
  19. package/node_modules/yaml/browser/dist/compose/util-empty-scalar-position.js +26 -0
  20. package/node_modules/yaml/browser/dist/compose/util-flow-indent-check.js +15 -0
  21. package/node_modules/yaml/browser/dist/compose/util-map-includes.js +13 -0
  22. package/node_modules/yaml/browser/dist/doc/Document.js +335 -0
  23. package/node_modules/yaml/browser/dist/doc/anchors.js +71 -0
  24. package/node_modules/yaml/browser/dist/doc/applyReviver.js +55 -0
  25. package/node_modules/yaml/browser/dist/doc/createNode.js +88 -0
  26. package/node_modules/yaml/browser/dist/doc/directives.js +176 -0
  27. package/node_modules/yaml/browser/dist/errors.js +57 -0
  28. package/node_modules/yaml/browser/dist/index.js +17 -0
  29. package/node_modules/yaml/browser/dist/log.js +11 -0
  30. package/node_modules/yaml/browser/dist/nodes/Alias.js +118 -0
  31. package/node_modules/yaml/browser/dist/nodes/Collection.js +147 -0
  32. package/node_modules/yaml/browser/dist/nodes/Node.js +38 -0
  33. package/node_modules/yaml/browser/dist/nodes/Pair.js +36 -0
  34. package/node_modules/yaml/browser/dist/nodes/Scalar.js +24 -0
  35. package/node_modules/yaml/browser/dist/nodes/YAMLMap.js +144 -0
  36. package/node_modules/yaml/browser/dist/nodes/YAMLSeq.js +113 -0
  37. package/node_modules/yaml/browser/dist/nodes/addPairToJSMap.js +63 -0
  38. package/node_modules/yaml/browser/dist/nodes/identity.js +36 -0
  39. package/node_modules/yaml/browser/dist/nodes/toJS.js +37 -0
  40. package/node_modules/yaml/browser/dist/parse/cst-scalar.js +214 -0
  41. package/node_modules/yaml/browser/dist/parse/cst-stringify.js +61 -0
  42. package/node_modules/yaml/browser/dist/parse/cst-visit.js +97 -0
  43. package/node_modules/yaml/browser/dist/parse/cst.js +98 -0
  44. package/node_modules/yaml/browser/dist/parse/lexer.js +721 -0
  45. package/node_modules/yaml/browser/dist/parse/line-counter.js +39 -0
  46. package/node_modules/yaml/browser/dist/parse/parser.js +975 -0
  47. package/node_modules/yaml/browser/dist/public-api.js +102 -0
  48. package/node_modules/yaml/browser/dist/schema/Schema.js +37 -0
  49. package/node_modules/yaml/browser/dist/schema/common/map.js +17 -0
  50. package/node_modules/yaml/browser/dist/schema/common/null.js +15 -0
  51. package/node_modules/yaml/browser/dist/schema/common/seq.js +17 -0
  52. package/node_modules/yaml/browser/dist/schema/common/string.js +14 -0
  53. package/node_modules/yaml/browser/dist/schema/core/bool.js +19 -0
  54. package/node_modules/yaml/browser/dist/schema/core/float.js +43 -0
  55. package/node_modules/yaml/browser/dist/schema/core/int.js +38 -0
  56. package/node_modules/yaml/browser/dist/schema/core/schema.js +23 -0
  57. package/node_modules/yaml/browser/dist/schema/json/schema.js +62 -0
  58. package/node_modules/yaml/browser/dist/schema/tags.js +96 -0
  59. package/node_modules/yaml/browser/dist/schema/yaml-1.1/binary.js +58 -0
  60. package/node_modules/yaml/browser/dist/schema/yaml-1.1/bool.js +26 -0
  61. package/node_modules/yaml/browser/dist/schema/yaml-1.1/float.js +46 -0
  62. package/node_modules/yaml/browser/dist/schema/yaml-1.1/int.js +71 -0
  63. package/node_modules/yaml/browser/dist/schema/yaml-1.1/merge.js +67 -0
  64. package/node_modules/yaml/browser/dist/schema/yaml-1.1/omap.js +74 -0
  65. package/node_modules/yaml/browser/dist/schema/yaml-1.1/pairs.js +78 -0
  66. package/node_modules/yaml/browser/dist/schema/yaml-1.1/schema.js +39 -0
  67. package/node_modules/yaml/browser/dist/schema/yaml-1.1/set.js +93 -0
  68. package/node_modules/yaml/browser/dist/schema/yaml-1.1/timestamp.js +101 -0
  69. package/node_modules/yaml/browser/dist/stringify/foldFlowLines.js +146 -0
  70. package/node_modules/yaml/browser/dist/stringify/stringify.js +129 -0
  71. package/node_modules/yaml/browser/dist/stringify/stringifyCollection.js +153 -0
  72. package/node_modules/yaml/browser/dist/stringify/stringifyComment.js +20 -0
  73. package/node_modules/yaml/browser/dist/stringify/stringifyDocument.js +85 -0
  74. package/node_modules/yaml/browser/dist/stringify/stringifyNumber.js +25 -0
  75. package/node_modules/yaml/browser/dist/stringify/stringifyPair.js +150 -0
  76. package/node_modules/yaml/browser/dist/stringify/stringifyString.js +336 -0
  77. package/node_modules/yaml/browser/dist/util.js +11 -0
  78. package/node_modules/yaml/browser/dist/visit.js +233 -0
  79. package/node_modules/yaml/browser/index.js +5 -0
  80. package/node_modules/yaml/browser/package.json +3 -0
  81. package/node_modules/yaml/dist/cli.d.ts +8 -0
  82. package/node_modules/yaml/dist/cli.mjs +201 -0
  83. package/node_modules/yaml/dist/compose/compose-collection.d.ts +11 -0
  84. package/node_modules/yaml/dist/compose/compose-collection.js +90 -0
  85. package/node_modules/yaml/dist/compose/compose-doc.d.ts +7 -0
  86. package/node_modules/yaml/dist/compose/compose-doc.js +45 -0
  87. package/node_modules/yaml/dist/compose/compose-node.d.ts +29 -0
  88. package/node_modules/yaml/dist/compose/compose-node.js +112 -0
  89. package/node_modules/yaml/dist/compose/compose-scalar.d.ts +5 -0
  90. package/node_modules/yaml/dist/compose/compose-scalar.js +88 -0
  91. package/node_modules/yaml/dist/compose/composer.d.ts +63 -0
  92. package/node_modules/yaml/dist/compose/composer.js +224 -0
  93. package/node_modules/yaml/dist/compose/resolve-block-map.d.ts +6 -0
  94. package/node_modules/yaml/dist/compose/resolve-block-map.js +117 -0
  95. package/node_modules/yaml/dist/compose/resolve-block-scalar.d.ts +11 -0
  96. package/node_modules/yaml/dist/compose/resolve-block-scalar.js +200 -0
  97. package/node_modules/yaml/dist/compose/resolve-block-seq.d.ts +6 -0
  98. package/node_modules/yaml/dist/compose/resolve-block-seq.js +51 -0
  99. package/node_modules/yaml/dist/compose/resolve-end.d.ts +6 -0
  100. package/node_modules/yaml/dist/compose/resolve-end.js +39 -0
  101. package/node_modules/yaml/dist/compose/resolve-flow-collection.d.ts +7 -0
  102. package/node_modules/yaml/dist/compose/resolve-flow-collection.js +209 -0
  103. package/node_modules/yaml/dist/compose/resolve-flow-scalar.d.ts +10 -0
  104. package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +228 -0
  105. package/node_modules/yaml/dist/compose/resolve-props.d.ts +23 -0
  106. package/node_modules/yaml/dist/compose/resolve-props.js +148 -0
  107. package/node_modules/yaml/dist/compose/util-contains-newline.d.ts +2 -0
  108. package/node_modules/yaml/dist/compose/util-contains-newline.js +36 -0
  109. package/node_modules/yaml/dist/compose/util-empty-scalar-position.d.ts +2 -0
  110. package/node_modules/yaml/dist/compose/util-empty-scalar-position.js +28 -0
  111. package/node_modules/yaml/dist/compose/util-flow-indent-check.d.ts +3 -0
  112. package/node_modules/yaml/dist/compose/util-flow-indent-check.js +17 -0
  113. package/node_modules/yaml/dist/compose/util-map-includes.d.ts +4 -0
  114. package/node_modules/yaml/dist/compose/util-map-includes.js +15 -0
  115. package/node_modules/yaml/dist/doc/Document.d.ts +141 -0
  116. package/node_modules/yaml/dist/doc/Document.js +337 -0
  117. package/node_modules/yaml/dist/doc/anchors.d.ts +24 -0
  118. package/node_modules/yaml/dist/doc/anchors.js +76 -0
  119. package/node_modules/yaml/dist/doc/applyReviver.d.ts +9 -0
  120. package/node_modules/yaml/dist/doc/applyReviver.js +57 -0
  121. package/node_modules/yaml/dist/doc/createNode.d.ts +17 -0
  122. package/node_modules/yaml/dist/doc/createNode.js +90 -0
  123. package/node_modules/yaml/dist/doc/directives.d.ts +49 -0
  124. package/node_modules/yaml/dist/doc/directives.js +178 -0
  125. package/node_modules/yaml/dist/errors.d.ts +21 -0
  126. package/node_modules/yaml/dist/errors.js +62 -0
  127. package/node_modules/yaml/dist/index.d.ts +25 -0
  128. package/node_modules/yaml/dist/index.js +50 -0
  129. package/node_modules/yaml/dist/log.d.ts +3 -0
  130. package/node_modules/yaml/dist/log.js +19 -0
  131. package/node_modules/yaml/dist/nodes/Alias.d.ts +29 -0
  132. package/node_modules/yaml/dist/nodes/Alias.js +120 -0
  133. package/node_modules/yaml/dist/nodes/Collection.d.ts +73 -0
  134. package/node_modules/yaml/dist/nodes/Collection.js +151 -0
  135. package/node_modules/yaml/dist/nodes/Node.d.ts +53 -0
  136. package/node_modules/yaml/dist/nodes/Node.js +40 -0
  137. package/node_modules/yaml/dist/nodes/Pair.d.ts +22 -0
  138. package/node_modules/yaml/dist/nodes/Pair.js +39 -0
  139. package/node_modules/yaml/dist/nodes/Scalar.d.ts +47 -0
  140. package/node_modules/yaml/dist/nodes/Scalar.js +27 -0
  141. package/node_modules/yaml/dist/nodes/YAMLMap.d.ts +53 -0
  142. package/node_modules/yaml/dist/nodes/YAMLMap.js +147 -0
  143. package/node_modules/yaml/dist/nodes/YAMLSeq.d.ts +60 -0
  144. package/node_modules/yaml/dist/nodes/YAMLSeq.js +115 -0
  145. package/node_modules/yaml/dist/nodes/addPairToJSMap.d.ts +4 -0
  146. package/node_modules/yaml/dist/nodes/addPairToJSMap.js +65 -0
  147. package/node_modules/yaml/dist/nodes/identity.d.ts +23 -0
  148. package/node_modules/yaml/dist/nodes/identity.js +53 -0
  149. package/node_modules/yaml/dist/nodes/toJS.d.ts +29 -0
  150. package/node_modules/yaml/dist/nodes/toJS.js +39 -0
  151. package/node_modules/yaml/dist/options.d.ts +350 -0
  152. package/node_modules/yaml/dist/parse/cst-scalar.d.ts +64 -0
  153. package/node_modules/yaml/dist/parse/cst-scalar.js +218 -0
  154. package/node_modules/yaml/dist/parse/cst-stringify.d.ts +8 -0
  155. package/node_modules/yaml/dist/parse/cst-stringify.js +63 -0
  156. package/node_modules/yaml/dist/parse/cst-visit.d.ts +39 -0
  157. package/node_modules/yaml/dist/parse/cst-visit.js +99 -0
  158. package/node_modules/yaml/dist/parse/cst.d.ts +109 -0
  159. package/node_modules/yaml/dist/parse/cst.js +112 -0
  160. package/node_modules/yaml/dist/parse/lexer.d.ts +87 -0
  161. package/node_modules/yaml/dist/parse/lexer.js +723 -0
  162. package/node_modules/yaml/dist/parse/line-counter.d.ts +22 -0
  163. package/node_modules/yaml/dist/parse/line-counter.js +41 -0
  164. package/node_modules/yaml/dist/parse/parser.d.ts +84 -0
  165. package/node_modules/yaml/dist/parse/parser.js +980 -0
  166. package/node_modules/yaml/dist/public-api.d.ts +44 -0
  167. package/node_modules/yaml/dist/public-api.js +107 -0
  168. package/node_modules/yaml/dist/schema/Schema.d.ts +17 -0
  169. package/node_modules/yaml/dist/schema/Schema.js +39 -0
  170. package/node_modules/yaml/dist/schema/common/map.d.ts +2 -0
  171. package/node_modules/yaml/dist/schema/common/map.js +19 -0
  172. package/node_modules/yaml/dist/schema/common/null.d.ts +4 -0
  173. package/node_modules/yaml/dist/schema/common/null.js +17 -0
  174. package/node_modules/yaml/dist/schema/common/seq.d.ts +2 -0
  175. package/node_modules/yaml/dist/schema/common/seq.js +19 -0
  176. package/node_modules/yaml/dist/schema/common/string.d.ts +2 -0
  177. package/node_modules/yaml/dist/schema/common/string.js +16 -0
  178. package/node_modules/yaml/dist/schema/core/bool.d.ts +4 -0
  179. package/node_modules/yaml/dist/schema/core/bool.js +21 -0
  180. package/node_modules/yaml/dist/schema/core/float.d.ts +4 -0
  181. package/node_modules/yaml/dist/schema/core/float.js +47 -0
  182. package/node_modules/yaml/dist/schema/core/int.d.ts +4 -0
  183. package/node_modules/yaml/dist/schema/core/int.js +42 -0
  184. package/node_modules/yaml/dist/schema/core/schema.d.ts +1 -0
  185. package/node_modules/yaml/dist/schema/core/schema.js +25 -0
  186. package/node_modules/yaml/dist/schema/json/schema.d.ts +2 -0
  187. package/node_modules/yaml/dist/schema/json/schema.js +64 -0
  188. package/node_modules/yaml/dist/schema/json-schema.d.ts +69 -0
  189. package/node_modules/yaml/dist/schema/tags.d.ts +48 -0
  190. package/node_modules/yaml/dist/schema/tags.js +99 -0
  191. package/node_modules/yaml/dist/schema/types.d.ts +92 -0
  192. package/node_modules/yaml/dist/schema/yaml-1.1/binary.d.ts +2 -0
  193. package/node_modules/yaml/dist/schema/yaml-1.1/binary.js +70 -0
  194. package/node_modules/yaml/dist/schema/yaml-1.1/bool.d.ts +7 -0
  195. package/node_modules/yaml/dist/schema/yaml-1.1/bool.js +29 -0
  196. package/node_modules/yaml/dist/schema/yaml-1.1/float.d.ts +4 -0
  197. package/node_modules/yaml/dist/schema/yaml-1.1/float.js +50 -0
  198. package/node_modules/yaml/dist/schema/yaml-1.1/int.d.ts +5 -0
  199. package/node_modules/yaml/dist/schema/yaml-1.1/int.js +76 -0
  200. package/node_modules/yaml/dist/schema/yaml-1.1/merge.d.ts +9 -0
  201. package/node_modules/yaml/dist/schema/yaml-1.1/merge.js +71 -0
  202. package/node_modules/yaml/dist/schema/yaml-1.1/omap.d.ts +22 -0
  203. package/node_modules/yaml/dist/schema/yaml-1.1/omap.js +77 -0
  204. package/node_modules/yaml/dist/schema/yaml-1.1/pairs.d.ts +10 -0
  205. package/node_modules/yaml/dist/schema/yaml-1.1/pairs.js +82 -0
  206. package/node_modules/yaml/dist/schema/yaml-1.1/schema.d.ts +1 -0
  207. package/node_modules/yaml/dist/schema/yaml-1.1/schema.js +41 -0
  208. package/node_modules/yaml/dist/schema/yaml-1.1/set.d.ts +28 -0
  209. package/node_modules/yaml/dist/schema/yaml-1.1/set.js +96 -0
  210. package/node_modules/yaml/dist/schema/yaml-1.1/timestamp.d.ts +6 -0
  211. package/node_modules/yaml/dist/schema/yaml-1.1/timestamp.js +105 -0
  212. package/node_modules/yaml/dist/stringify/foldFlowLines.d.ts +34 -0
  213. package/node_modules/yaml/dist/stringify/foldFlowLines.js +151 -0
  214. package/node_modules/yaml/dist/stringify/stringify.d.ts +21 -0
  215. package/node_modules/yaml/dist/stringify/stringify.js +132 -0
  216. package/node_modules/yaml/dist/stringify/stringifyCollection.d.ts +17 -0
  217. package/node_modules/yaml/dist/stringify/stringifyCollection.js +155 -0
  218. package/node_modules/yaml/dist/stringify/stringifyComment.d.ts +10 -0
  219. package/node_modules/yaml/dist/stringify/stringifyComment.js +24 -0
  220. package/node_modules/yaml/dist/stringify/stringifyDocument.d.ts +4 -0
  221. package/node_modules/yaml/dist/stringify/stringifyDocument.js +87 -0
  222. package/node_modules/yaml/dist/stringify/stringifyNumber.d.ts +2 -0
  223. package/node_modules/yaml/dist/stringify/stringifyNumber.js +27 -0
  224. package/node_modules/yaml/dist/stringify/stringifyPair.d.ts +3 -0
  225. package/node_modules/yaml/dist/stringify/stringifyPair.js +152 -0
  226. package/node_modules/yaml/dist/stringify/stringifyString.d.ts +9 -0
  227. package/node_modules/yaml/dist/stringify/stringifyString.js +338 -0
  228. package/node_modules/yaml/dist/test-events.d.ts +4 -0
  229. package/node_modules/yaml/dist/test-events.js +134 -0
  230. package/node_modules/yaml/dist/util.d.ts +16 -0
  231. package/node_modules/yaml/dist/util.js +28 -0
  232. package/node_modules/yaml/dist/visit.d.ts +102 -0
  233. package/node_modules/yaml/dist/visit.js +236 -0
  234. package/node_modules/yaml/package.json +97 -0
  235. package/node_modules/yaml/util.js +2 -0
  236. package/package.json +8 -2
  237. package/src/claude-config.mjs +60 -2
  238. package/src/claude-hook.mjs +8 -1
  239. package/src/client-registry.mjs +13 -0
  240. package/src/delivery-sync.mjs +14 -0
  241. package/src/health.mjs +26 -4
  242. package/src/hermes-config.mjs +283 -0
  243. package/src/hermes-health.mjs +45 -0
  244. package/src/hermes-hook.mjs +86 -0
  245. package/src/hermes-plugin.py +67 -0
  246. package/src/host-hook.mjs +6 -8
  247. package/src/install.mjs +8 -0
  248. package/src/installer-cli.mjs +25 -3
  249. package/src/instructions.mjs +5 -1
  250. package/src/runtime.mjs +33 -22
  251. package/src/skill-guard.mjs +171 -0
  252. package/src/skills-sync.mjs +32 -16
  253. package/src/version.mjs +3 -3
@@ -15,11 +15,12 @@ import {
15
15
  syncInstalledDelivery,
16
16
  } from "./install.mjs";
17
17
  import { configureClaudeProject } from "./claude-config.mjs";
18
+ import { configureHermes, detectHermes, preflightHermes } from "./hermes-config.mjs";
18
19
  import { configureCline, configureCodex, configureCursor, configureGemini, configureKimi, configureVscode } from "./host-config.mjs";
19
20
  import { CLIENT_KINDS, lifecycleClient } from "./client-registry.mjs";
20
21
  import { defaultRuntimeDirectory } from "./storage.mjs";
21
22
  import { instructionProfile } from "./instructions.mjs";
22
- import { DISCLOSURE_VERSION, INSTALLER_VERSION } from "./version.mjs";
23
+ import { DISCLOSURE_VERSION, INSTALLER_VERSION, RUNTIME_VERSION } from "./version.mjs";
23
24
  import { managedSkillsDirectory } from "./skills-sync.mjs";
24
25
 
25
26
  const CLAIM_PATTERN = /^hsc_[A-Za-z0-9_-]{43}$/;
@@ -118,6 +119,7 @@ export function detectCline(home = homedir()) {
118
119
 
119
120
  export function detectClient(clientKind) {
120
121
  const client = lifecycleClient(clientKind);
122
+ if (clientKind === "hermes-agents") return detectHermes();
121
123
  if (clientKind === "cline") return detectCline();
122
124
  if (!client.command) throw new Error(`${client.label} does not have a packaged detector`);
123
125
  const result = spawnSync(client.command, ["--version"], {
@@ -188,9 +190,12 @@ export function disclosureText({ serverUrl, projectRoot, clientKind, clientVersi
188
190
  "- encrypted local retry queue and governed retained conversations,",
189
191
  "- canonical learning when the selected badge permits writes, and",
190
192
  skillsRoot
191
- ? `- approved company and team skills written to ${skillsRoot}/<skill>/SKILL.md and kept current at each session start; withdrawn skills are moved aside, never deleted.`
193
+ ? `- approved company and team skills written to ${skillsRoot}/<skill>/SKILL.md and refreshed at supported session starts; withdrawn skills are moved aside, never deleted.`
192
194
  : "- no managed skills folder for this host (skills stay available through skill_invoke).",
193
195
  skillsRoot ? "- authorized company and team policy/knowledge references copied locally; failed refreshes remove managed context from discovery." : "",
196
+ clientKind === "codex" ? "Codex exec/headless hooks are not verified. Automatic startup sync and capture must not be assumed; use the exact syncCommand executable and arguments printed after installation before headless work. Explicit sync does not enable capture." : "",
197
+ clientKind === "claude-code" ? "Claude receives a Read allow rule only for this installation's policy/knowledge context folder. Existing ask/deny rules and project trust still apply; assigned skills and MCP tools may require separate host permission." : "",
198
+ clientKind === "hermes-agents" ? "Hermes 0.21.3 only: installs an observer plugin and signed MCP in the selected HERMES_HOME profile. Captures text prompts, final uninterrupted replies, tool calls/results and finalization. Intermediate or interrupted replies, compaction, subagents, tokens, managed skills and organization instructions are unavailable. No tool-override permission or automatic recall injection is enabled." : "",
194
199
  "",
195
200
  `Supported capture categories: ${supported.length > 0 ? supported.join(", ") : "none"}.`,
196
201
  `Unsupported capture categories: ${unsupported.length > 0 ? unsupported.join(", ") : "none"}.`,
@@ -231,6 +236,7 @@ export function allDisclosureText({ serverUrl, projectRoot, organization = null,
231
236
  lines.push(
232
237
  "",
233
238
  `--- ${client.label} (${host.clientVersion}) ---`,
239
+ host.clientKind === "hermes-agents" ? "Hermes 0.21.3 only: selected profile observer and signed MCP; final uninterrupted text only. Interrupted/intermediate replies, compaction, subagents, token usage, managed instructions and skills are unavailable. No automatic recall injection or tool-override grant." : "",
234
240
  `Supported capture categories: ${supported.length > 0 ? supported.join(", ") : "none"}.`,
235
241
  `Unsupported capture categories: ${unsupported.length > 0 ? unsupported.join(", ") : "none"}.`,
236
242
  client.coverage === "complete"
@@ -238,7 +244,7 @@ export function allDisclosureText({ serverUrl, projectRoot, organization = null,
238
244
  : `This host has partial coverage (${client.reason}); missing categories remain explicit.`,
239
245
  managedSkillsDirectory(host.clientKind)
240
246
  ? `Approved company and team skills are written to ${managedSkillsDirectory(host.clientKind)}/<skill>/SKILL.md ` +
241
- "and kept current at each session start; withdrawn skills are moved aside, never deleted. " +
247
+ "and refreshed at supported session starts; withdrawn skills are moved aside, never deleted. " +
242
248
  "Authorized company and team policy/knowledge references are copied locally; failed refreshes remove managed context from discovery."
243
249
  : "No managed skills folder for this host; skills stay available through skill_invoke.",
244
250
  );
@@ -252,6 +258,7 @@ export function allDisclosureText({ serverUrl, projectRoot, organization = null,
252
258
  }
253
259
  lines.push(
254
260
  "",
261
+ "Codex exec/headless hooks are not verified: use the exact syncCommand executable and arguments printed after installation before work; this does not enable capture. Claude Read grants cover only the installation's policy/knowledge context, preserve ask/deny, and require project trust; skills and MCP tools may need separate permission.",
255
262
  "Authorized organization managers may review retained conversations and summaries.",
256
263
  "This reads and writes its managed skill folders and instruction sections; it does not scan historical files, other applications, clipboard, keystrokes, or other agents.",
257
264
  "Organization instructions: supported adapters manage an additive global rule file or marked section across projects in the selected local agent profile. Existing personal instructions are preserved. Updates sync at installation and supported session starts; restart the host to load changes.",
@@ -275,6 +282,7 @@ export async function confirmDisclosure({ input = process.stdin, output = proces
275
282
  }
276
283
 
277
284
  async function configureHost(clientKind, common, { claudeConfigPath } = {}) {
285
+ if (clientKind === "hermes-agents") return configureHermes(common);
278
286
  if (clientKind === "claude-code") {
279
287
  return configureClaudeProject({ ...common, ...(claudeConfigPath ? { claudeConfigPath } : {}) });
280
288
  }
@@ -351,6 +359,9 @@ async function runAllInstaller(input, {
351
359
  const results = [];
352
360
  for (const host of detected) {
353
361
  try {
362
+ if (host.clientKind === "hermes-agents") await preflightHermes({ root,
363
+ runtimePath: bundle.runtimePath, serverUrl: input.serverUrl,
364
+ ...(skillsHome ? { home: skillsHome } : {}) });
354
365
  const installed = await installLocalConnection({
355
366
  serverUrl: input.serverUrl,
356
367
  claim: host.claim,
@@ -361,10 +372,12 @@ async function runAllInstaller(input, {
361
372
  instructionProfile: instructionProfile(host.clientKind, skillsHome ? { home: skillsHome } : {}),
362
373
  });
363
374
  const configured = await configureHost(host.clientKind, {
375
+ root,
364
376
  projectRoot: input.projectRoot,
365
377
  installationId: installed.installationId,
366
378
  serverUrl: input.serverUrl,
367
379
  runtimePath: bundle.runtimePath,
380
+ ...(skillsHome ? { home: skillsHome } : {}),
368
381
  }, { claudeConfigPath });
369
382
  await recordHealthTargets({ root, installationId: installed.installationId, configured, runtimePath: bundle.runtimePath });
370
383
  let heartbeat = false;
@@ -390,6 +403,8 @@ async function runAllInstaller(input, {
390
403
  : null,
391
404
  proofStorage: installed.proofStorage,
392
405
  instructions: installed.instructions,
406
+ syncCommand: { command: process.execPath, args: [bundle.runtimePath, "sync", "--connection", installed.installationId], environment: { HALOFY_AGENT_HOME: root } },
407
+ skillGuard: configured.skillGuard,
393
408
  configuredPaths: configured.configuredPaths || [configured.mcpPath, configured.settingsPath],
394
409
  replacedLegacyMcpEntries: configured.replacedLegacyMcpEntries || 0,
395
410
  nextStep: `Restart ${lifecycleClient(host.clientKind).label}, then check the connection in Halofy.`,
@@ -457,6 +472,9 @@ export async function runInstaller(argv, {
457
472
  })}\n`);
458
473
  await confirm();
459
474
 
475
+ if (input.clientKind === "hermes-agents") await preflightHermes({ root,
476
+ runtimePath: join(root, "runtime", RUNTIME_VERSION, "bin", "halofy-agent.mjs"), serverUrl: input.serverUrl,
477
+ ...(skillsHome ? { home: skillsHome } : {}) });
460
478
  const installed = await installLocalConnection({
461
479
  serverUrl: input.serverUrl,
462
480
  claim: input.claim,
@@ -468,10 +486,12 @@ export async function runInstaller(argv, {
468
486
  });
469
487
  const bundle = await installRuntimeBundle({ root, ...(sourceRoot ? { sourceRoot } : {}) });
470
488
  const common = {
489
+ root,
471
490
  projectRoot: input.projectRoot,
472
491
  installationId: installed.installationId,
473
492
  serverUrl: input.serverUrl,
474
493
  runtimePath: bundle.runtimePath,
494
+ ...(skillsHome ? { home: skillsHome } : {}),
475
495
  };
476
496
  const configured = await configureHost(input.clientKind, common, { claudeConfigPath });
477
497
  await recordHealthTargets({ root, installationId: installed.installationId, configured, runtimePath: bundle.runtimePath });
@@ -496,6 +516,8 @@ export async function runInstaller(argv, {
496
516
  publishedPackage: true,
497
517
  projectConfigured: true,
498
518
  instructions: installed.instructions,
519
+ syncCommand: { command: process.execPath, args: [bundle.runtimePath, "sync", "--connection", installed.installationId], environment: { HALOFY_AGENT_HOME: root } },
520
+ skillGuard: configured.skillGuard,
499
521
  configuredPaths: configured.configuredPaths || [configured.mcpPath, configured.settingsPath],
500
522
  replacedLegacyMcpEntries: configured.replacedLegacyMcpEntries || 0,
501
523
  mcpConfiguration: localMcpSnippet({
@@ -136,7 +136,11 @@ async function targetFor(connection, owner) {
136
136
  function render(bundle, owner, cursor) {
137
137
  const begin = `${MARKER}${owner}:BEGIN -->`;
138
138
  const end = `${MARKER}${owner}:END -->`;
139
- const content = bundle.documents.map((doc) => `## ${doc.title}\n\n${doc.content}`).join("\n\n");
139
+ const content = bundle.documents.map((doc) => {
140
+ // Preserve the authored bytes; omit only our redundant display heading.
141
+ const heading = doc.content.trimStart().split(/\r?\n/, 1)[0].match(/^#{1,6}\s+(.+?)(?:\s+#+)?\s*$/);
142
+ return heading?.[1] === doc.title ? doc.content : `## ${doc.title}\n\n${doc.content}`;
143
+ }).join("\n\n");
140
144
  const text = `${cursor ? "---\nalwaysApply: true\n---\n" : ""}${begin}\n# Organization agent instructions\n\n${content}\n${end}\n`;
141
145
  return { begin: Buffer.from(begin), end: Buffer.from(end), bytes: Buffer.from(text) };
142
146
  }
package/src/runtime.mjs CHANGED
@@ -11,6 +11,21 @@ import { readJson, withFileLock, writePrivateFile } from "./storage.mjs";
11
11
  import { capturePaused, captureStateSnapshot, captureAdmission, inspectCaptureHealth } from "./health.mjs";
12
12
  import { syncAgentInstructions } from "./instructions.mjs";
13
13
  import { RUNTIME_VERSION } from "./version.mjs";
14
+ import { isActiveInstallation } from "./active.mjs";
15
+
16
+ function acknowledgedPrefix(response, batchLength) {
17
+ const accepted = response?.acceptedCount !== undefined ? response.acceptedCount
18
+ : response?.acceptedPrefix !== undefined ? response.acceptedPrefix : response?.accepted;
19
+ const duplicates = response?.acceptedCount === undefined && response?.acceptedPrefix === undefined
20
+ ? (response?.duplicates ?? 0) : 0;
21
+ if (!Number.isSafeInteger(accepted) || accepted < 0 ||
22
+ !Number.isSafeInteger(duplicates) || duplicates < 0 || accepted + duplicates > batchLength) {
23
+ const error = new Error("append response did not acknowledge a valid event prefix");
24
+ error.code = "invalid_append_acknowledgement";
25
+ throw error;
26
+ }
27
+ return accepted + duplicates;
28
+ }
14
29
 
15
30
  export class LifecycleRuntime {
16
31
  constructor(connection, {
@@ -177,7 +192,8 @@ export class LifecycleRuntime {
177
192
  if (usageGaps > 0) await this.cursors.bumpUsageGaps(usageGaps);
178
193
  return result;
179
194
  });
180
- if (queued.queued === 0) return { queued: 0, acknowledged: 0 };
195
+ // A previous hook may have persisted this entire suffix before its append
196
+ // failed. Retry that durable queue even when there are no new host records.
181
197
  const replay = await this.replay();
182
198
  return { queued: queued.queued, acknowledged: replay.acknowledged };
183
199
  }
@@ -228,13 +244,20 @@ export class LifecycleRuntime {
228
244
  await this.cursors.rememberEventKeys(sessionHash, unseenEvents.map((event) => event.eventKey));
229
245
  }
230
246
 
231
- async replay() {
232
- return withFileLock(this.operationLockPath, () => this.#replayUnlocked());
247
+ async replay({ maxBatches = Number.MAX_SAFE_INTEGER, onlyWhileActive = false } = {}) {
248
+ if (!Number.isSafeInteger(maxBatches) || maxBatches < 1) throw new Error("invalid replay batch limit");
249
+ return withFileLock(this.operationLockPath, async () => {
250
+ if (onlyWhileActive && (await capturePaused(this.root, this.connection.installationId) ||
251
+ !await isActiveInstallation(this.connection, this.root))) {
252
+ return { acknowledged: 0, ...(await this.queue.diagnostics()) };
253
+ }
254
+ return this.#replayUnlocked(maxBatches);
255
+ });
233
256
  }
234
257
 
235
- async #replayUnlocked() {
258
+ async #replayUnlocked(maxBatches = Number.MAX_SAFE_INTEGER) {
236
259
  let acknowledged = 0;
237
- while (true) {
260
+ for (let batch = 0; batch < maxBatches; batch += 1) {
238
261
  const head = await this.queue.peek();
239
262
  if (!head) break;
240
263
  const sessionId = await this.resolveSessionByHash(head.sessionHash);
@@ -244,23 +267,11 @@ export class LifecycleRuntime {
244
267
  payloadSha256,
245
268
  ...event
246
269
  }) => ({ ...event, eventId: eventKey })));
247
- const accepted = Math.max(0, Math.min(
248
- head.events.length,
249
- Number(response?.acceptedCount ?? response?.acceptedPrefix ??
250
- (response?.accepted === undefined ? head.events.length : Number(response.accepted) + Number(response.duplicates ?? 0))),
251
- ));
252
- if (accepted === 0) {
253
- if (Number.isSafeInteger(response?.nextExpectedSequence)) {
254
- const expected = response.nextExpectedSequence;
255
- const firstExpected = head.events.findIndex((event) => event.sequence >= expected);
256
- if (firstExpected > 0) {
257
- await this.#acknowledge(head, firstExpected);
258
- acknowledged += firstExpected;
259
- continue;
260
- }
261
- }
262
- break;
263
- }
270
+ // A success HTTP status or an advanced server cursor is insufficient:
271
+ // the prefix must be explicitly accepted or confirmed as duplicates.
272
+ // In particular, event_conflict must retain the rejected local bodies.
273
+ const accepted = acknowledgedPrefix(response, head.events.length);
274
+ if (accepted === 0) break;
264
275
  await this.#acknowledge(head, accepted);
265
276
  acknowledged += accepted;
266
277
  }
@@ -0,0 +1,171 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, readdir, readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { safeDirectory, writePrivateFile } from "./storage.mjs";
5
+ import { readPrivateJson, withContextLock } from "./context-sync.mjs";
6
+ import { loadSkillsManifest, managedSkillsDirectory, readManagedSkillTuples } from "./skills-sync.mjs";
7
+ import { SignedRuntimeTransport } from "./transport.mjs";
8
+
9
+ const PREFIX = "halofy-managed-";
10
+ const ID = /^[A-Za-z0-9_-]{1,160}$/;
11
+ const KEY = /^[a-z0-9][a-z0-9._-]{0,199}$/i;
12
+ const SHA = /^[a-f0-9]{64}$/;
13
+ const hash = value => createHash("sha256").update(value).digest("hex");
14
+ const enabled = connection => connection.clientKind === "claude-code" && connection.skillGuardVersion === 1;
15
+ const stateRoot = root => join(root, "claude-skill-guard");
16
+ const validEntry = entry => entry && ID.test(entry.installationId) && ID.test(entry.skillId)
17
+ && KEY.test(entry.skillKey) && !entry.skillKey.includes("..") && SHA.test(entry.sha)
18
+ && entry.nativeSkillKey === guardedSkillName({ installationId: entry.installationId }, entry);
19
+
20
+ /** Native identity is immutable across updates, restores and reconnects. */
21
+ export function guardedSkillName(connection, skill) {
22
+ return `${PREFIX}${hash(JSON.stringify([connection.installationId, skill.skillId, skill.sha])).slice(0, 40)}`;
23
+ }
24
+ export function managedNativeSkillKey(connection, skill) {
25
+ return enabled(connection) ? guardedSkillName(connection, skill) : skill.skillKey;
26
+ }
27
+
28
+ async function readHistory(root) {
29
+ const dir = stateRoot(root);
30
+ await safeDirectory(dir);
31
+ for (const name of ["initialized.json", "history.json"]) {
32
+ const entry = await lstat(join(dir, name));
33
+ if (!entry.isFile() || entry.isSymbolicLink() || entry.size > 4 * 1024 * 1024) throw new Error("invalid skill guard evidence");
34
+ }
35
+ const anchor = await readPrivateJson(join(dir, "initialized.json"));
36
+ const history = await readPrivateJson(join(dir, "history.json"));
37
+ if (anchor?.version !== 1 || history?.version !== 1 || !Array.isArray(history.entries)
38
+ || !Array.isArray(history.legacyKeys) || history.entries.length > 10000 || history.legacyKeys.length > 10000
39
+ || history.entries.some(entry => !validEntry(entry))
40
+ || history.legacyKeys.some(key => typeof key !== "string" || !KEY.test(key) || key.includes(".."))
41
+ || new Set(history.entries.map(entry => entry.nativeSkillKey)).size !== history.entries.length) {
42
+ throw new Error("invalid skill guard evidence");
43
+ }
44
+ return history;
45
+ }
46
+
47
+ /** Explicit installer bootstrap only. Hook/sync reads never reset missing history. */
48
+ export async function initializeSkillGuard(root) {
49
+ const dir = stateRoot(root);
50
+ await safeDirectory(dir, true);
51
+ return withContextLock(dir, async () => {
52
+ const names = await readdir(dir);
53
+ if (names.includes("initialized.json") || names.includes("history.json")) return readHistory(root);
54
+ const connections = await readdir(join(root, "connections")).catch(error => {
55
+ if (error.code === "ENOENT") return []; throw error;
56
+ });
57
+ if (connections.length > 1000) throw new Error("skill guard connection limit");
58
+ const legacyKeys = new Set();
59
+ for (const name of connections) {
60
+ if (!/^[A-Za-z0-9_-]{1,160}\.json$/.test(name)) continue;
61
+ const connection = await readPrivateJson(join(root, "connections", name));
62
+ if (connection?.clientKind !== "claude-code") continue;
63
+ if (connection.skillGuardVersion === 1) throw new Error("missing initialized skill history");
64
+ const manifest = await loadSkillsManifest(root, connection.installationId);
65
+ for (const entry of manifest.installs) legacyKeys.add(entry.skillKey);
66
+ // Pre-guard runtimes removed manifest entries after withdrawal, but kept
67
+ // owned directories in quarantine. Preserve those legacy cache identities.
68
+ const quarantine = join(root, "skills-quarantine", connection.installationId);
69
+ let withdrawn = [];
70
+ try { await safeDirectory(quarantine); withdrawn = await readdir(quarantine); }
71
+ catch (error) { if (error.code !== "ENOENT" && error.code !== "directory_missing") throw error; }
72
+ if (withdrawn.length > 10000) throw new Error("legacy skill history limit");
73
+ for (const name of withdrawn) {
74
+ const match = /^(.+)-[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{3}Z$/.exec(name);
75
+ if (!match || !KEY.test(match[1]) || match[1].includes("..")) throw new Error("unrecognized legacy quarantine");
76
+ await safeDirectory(join(quarantine, name));
77
+ legacyKeys.add(match[1]);
78
+ }
79
+ }
80
+ if (legacyKeys.size > 10000) throw new Error("legacy skill history limit");
81
+ const history = { version: 1, entries: [], legacyKeys: [...legacyKeys] };
82
+ await writePrivateFile(join(dir, "history.json"), JSON.stringify(history));
83
+ await writePrivateFile(join(dir, "initialized.json"), JSON.stringify({ version: 1 }));
84
+ return history;
85
+ });
86
+ }
87
+
88
+ /** Record ownership before activation/withdrawal; records are never removed. */
89
+ export async function recordSkillOwnership(connection, root, entries) {
90
+ if (!enabled(connection)) return;
91
+ const dir = stateRoot(root);
92
+ await safeDirectory(dir);
93
+ await withContextLock(dir, async () => {
94
+ const history = await readHistory(root);
95
+ for (const entry of entries) {
96
+ if (!entry.nativeSkillKey) {
97
+ if (!history.legacyKeys.includes(entry.skillKey)) history.legacyKeys.push(entry.skillKey);
98
+ } else {
99
+ const record = { installationId: connection.installationId, skillId: entry.skillId,
100
+ skillKey: entry.skillKey, sha: entry.sha, nativeSkillKey: entry.nativeSkillKey };
101
+ if (!validEntry(record)) throw new Error("invalid owned skill");
102
+ if (!history.entries.some(item => item.nativeSkillKey === record.nativeSkillKey)) history.entries.push(record);
103
+ }
104
+ }
105
+ if (history.entries.length > 10000 || history.legacyKeys.length > 10000) throw new Error("skill guard history limit");
106
+ await writePrivateFile(join(dir, "history.json"), JSON.stringify(history));
107
+ });
108
+ }
109
+
110
+ export function skillInvocationDenial(reason = "This Halofy managed skill is withdrawn, changed, or could not be verified. Refresh Halofy and start a new conversation before using it.") {
111
+ return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny",
112
+ permissionDecisionReason: reason } };
113
+ }
114
+
115
+ /** No allow decision: host permissions retain their normal authority. */
116
+ async function checkClaudeSkillInvocation({ connection, root, input, home, env, transport, signal }) {
117
+ if (typeof input?.tool_name === "string" && input.tool_name !== "Skill") return null;
118
+ try {
119
+ if (typeof input.tool_input?.skill !== "string") throw new Error("missing skill identity");
120
+ // Claude trims native names before resolving cached plain-name skills.
121
+ const name = input.tool_input.skill.trim().replace(/^\//, "").trim().toLowerCase();
122
+ let history;
123
+ try { history = await readHistory(root); }
124
+ catch { return skillInvocationDenial("Halofy skill ownership evidence is unavailable; managed and employee skills cannot be safely distinguished. Skill calls are paused. Ask your administrator to restore verified ownership history or review the reconnect migration; do not recreate an empty history."); }
125
+ if (history.legacyKeys.some(key => key.toLowerCase() === name)) return skillInvocationDenial();
126
+ const owned = history.entries.find(entry => entry.nativeSkillKey === name);
127
+ if (!owned && !name.startsWith(PREFIX)) return null;
128
+ if (!owned || !enabled(connection) || owned.installationId !== connection.installationId) throw new Error("unknown managed skill");
129
+ const active = async () => {
130
+ await safeDirectory(root);
131
+ const marker = await readPrivateJson(join(root, "active-claude-code.json"));
132
+ return marker?.installationId === connection.installationId && marker?.protocolVersion === "1";
133
+ };
134
+ if (!(await active())) throw new Error("inactive installation");
135
+ const manifest = await loadSkillsManifest(root, connection.installationId);
136
+ const local = manifest.installs.find(entry => entry.nativeSkillKey === name && entry.skillId === owned.skillId && entry.sha === owned.sha);
137
+ if (!local) throw new Error("withdrawn managed copy");
138
+ // Verify all recorded bytes, then verify the exact local alias again after the network read.
139
+ await readManagedSkillTuples({ connection, root, home, env });
140
+ const current = await (transport ?? new SignedRuntimeTransport(connection, { timeoutMs: 3000, signal }))
141
+ .skillsCheckin([{ skillId: owned.skillId, sha: owned.sha }]);
142
+ if (!Array.isArray(current?.items) || current.items.length !== 1
143
+ || current.items[0].skillId !== owned.skillId || current.items[0].status !== "approved"
144
+ || !Array.isArray(current.skills) || !current.skills.some(entry => entry.skillId === owned.skillId
145
+ && entry.skillKey === owned.skillKey && entry.sha === owned.sha)) throw new Error("skill no longer eligible");
146
+ const path = join(managedSkillsDirectory("claude-code", { home, env }), name, "SKILL.md");
147
+ const bytes = await readFile(path);
148
+ if (hash(bytes) !== local.fileSha256 || !(await active())) throw new Error("changed managed copy");
149
+ await readManagedSkillTuples({ connection, root, home, env });
150
+ const latest = await loadSkillsManifest(root, connection.installationId);
151
+ if (!latest.installs.some(entry => entry.nativeSkillKey === name && entry.skillId === owned.skillId && entry.sha === owned.sha)) throw new Error("withdrawn during check");
152
+ await readHistory(root);
153
+ return null;
154
+ } catch { return skillInvocationDenial(); }
155
+ }
156
+
157
+ /** Bound the entire guard, including local reads, below the native hook timeout.
158
+ * The CLI flushes and exits after this decision so stuck filesystem work cannot
159
+ * turn a host timeout into a fail-open native invocation. */
160
+ export async function guardClaudeSkillInvocation(options) {
161
+ const controller = new AbortController();
162
+ let timer;
163
+ try {
164
+ return await Promise.race([
165
+ checkClaudeSkillInvocation({ ...options, signal: controller.signal }),
166
+ new Promise(resolve => { timer = setTimeout(() => {
167
+ controller.abort(); resolve(skillInvocationDenial());
168
+ }, 4500); }),
169
+ ]);
170
+ } finally { clearTimeout(timer); controller.abort(); }
171
+ }
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { mkdir, readFile, rename, lstat } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { basename, dirname, join } from "node:path";
5
+ import { managedNativeSkillKey, recordSkillOwnership } from "./skill-guard.mjs";
5
6
  import { isActiveInstallation } from "./active.mjs";
6
7
  import { ensurePrivateDirectory, readJson, safeDirectory, writePrivateFile } from "./storage.mjs";
7
8
 
@@ -11,11 +12,12 @@ import { ensurePrivateDirectory, readJson, safeDirectory, writePrivateFile } fro
11
12
  * skill on its team + org chain; this module mirrors that set into the host's
12
13
  * own skills folder and keeps it there.
13
14
  *
14
- * Every managed copy is written as `<skillsRoot>/<skillKey>/SKILL.md`, the
15
+ * Every managed copy is written as `<skillsRoot>/<nativeSkillKey>/SKILL.md`, the
15
16
  * layout Claude Code and Codex both read. A manifest under the runtime home
16
17
  * records what was written (id, sha, file digest) so the next check-in can
17
18
  * report it, drift can be repaired, and a withdrawn skill can be quarantined
18
19
  * — moved, never deleted (D7 on the client mirror).
20
+ * Guarded Claude installs use immutable aliases; other hosts use skillKey.
19
21
  *
20
22
  * A folder this runtime did not create is never touched: an employee's own
21
23
  * skill with a colliding name is reported as a conflict and left alone.
@@ -111,15 +113,16 @@ export async function loadSkillsManifest(root, installationId) {
111
113
  if (!manifestFile) return { version: 1, installationId, installs: [], lastCheckinAt: null };
112
114
  if (!manifest || manifest.version !== 1 || !Array.isArray(manifest.installs)) throw new Error("invalid skill manifest");
113
115
  if (manifest.installationId !== installationId || manifest.installs.some((entry) =>
114
- !safeKey(entry?.skillKey) || typeof entry.skillId !== "string" || !/^[a-f0-9]{64}$/.test(entry.sha))) throw new Error("invalid skill manifest");
116
+ !safeKey(entry?.skillKey) || (entry.nativeSkillKey !== undefined && !safeKey(entry.nativeSkillKey)) || typeof entry.skillId !== "string" || !/^[a-f0-9]{64}$/.test(entry.sha))) throw new Error("invalid skill manifest");
115
117
  return manifest;
116
118
  }
117
119
 
118
120
  /**
119
121
  * One sync pass. Never throws for server or filesystem trouble on a single
120
122
  * skill; the summary names what happened so callers (installer, SessionStart
121
- * hooks) can report it. A failed check-in leaves installed copies usable
122
- * until the 7-day staleness allowance runs out, then quarantines them (D37).
123
+ * hooks) can report it. A failed check-in retains installed copies until the
124
+ * 7-day staleness allowance runs out, then quarantines them (D37). Retention
125
+ * is not invocation permission: guarded Claude calls require a fresh check.
123
126
  */
124
127
  export async function syncManagedSkills({
125
128
  connection,
@@ -150,6 +153,7 @@ export async function syncManagedSkills({
150
153
  const quarantineRoot = join(root, "skills-quarantine", connection.installationId);
151
154
  const manifest = await loadSkillsManifest(root, connection.installationId);
152
155
  const current = now();
156
+ await recordSkillOwnership(connection, root, manifest.installs);
153
157
 
154
158
  const active = () => isActiveInstallation(connection, root);
155
159
  if (!(await active())) {
@@ -176,7 +180,7 @@ export async function syncManagedSkills({
176
180
  const failedWithdrawals = [];
177
181
  for (const entry of manifest.installs) {
178
182
  try {
179
- const moved = await quarantine(skillsRoot, quarantineRoot, entry.skillKey, current);
183
+ const moved = await quarantine(skillsRoot, quarantineRoot, entry.nativeSkillKey || entry.skillKey, current);
180
184
  if (moved) summary.quarantined.push({ skillKey: entry.skillKey, reason: "stale_checkin", to: moved });
181
185
  } catch (fsError) {
182
186
  failedWithdrawals.push(entry);
@@ -202,10 +206,12 @@ export async function syncManagedSkills({
202
206
  for (const entry of manifest.installs) {
203
207
  const verdict = verdicts.get(entry.skillId);
204
208
  const status = verdict?.status;
205
- const withdrawn = status === "revoked" || status === "unknown" || (!wanted.has(entry.skillId) && status !== "stale");
209
+ const desired = wanted.get(entry.skillId) ?? wanted.get(verdict?.currentSkillId);
210
+ const renamed = desired && (entry.nativeSkillKey || entry.skillKey) !== managedNativeSkillKey(connection, desired);
211
+ const withdrawn = renamed || status === "revoked" || status === "unknown" || (!wanted.has(entry.skillId) && status !== "stale");
206
212
  if (!withdrawn) { survivors.push(entry); continue; }
207
213
  try {
208
- const moved = await quarantine(skillsRoot, quarantineRoot, entry.skillKey, current);
214
+ const moved = await quarantine(skillsRoot, quarantineRoot, entry.nativeSkillKey || entry.skillKey, current);
209
215
  summary.quarantined.push({ skillKey: entry.skillKey, reason: status || "not_resolved", to: moved });
210
216
  } catch (error) {
211
217
  summary.errors.push({ stage: "quarantine", skillKey: entry.skillKey, code: error?.code || "fs_error" });
@@ -220,14 +226,15 @@ export async function syncManagedSkills({
220
226
  // to the row that replaced it.
221
227
  for (const entry of manifest.installs) {
222
228
  const verdict = verdicts.get(entry.skillId);
223
- if (verdict?.status === "stale" && verdict.currentSkillId && !wanted.has(entry.skillId)) {
229
+ if (connection.skillGuardVersion !== 1 && verdict?.status === "stale" && verdict.currentSkillId && !wanted.has(entry.skillId)) {
224
230
  entry.skillId = verdict.currentSkillId;
225
231
  }
226
232
  }
227
233
  const next = [...failedWithdrawals];
228
234
  for (const skill of wanted.values()) {
229
- const entry = manifest.installs.find((candidate) => candidate.skillId === skill.skillId);
230
- const dir = join(skillsRoot, skill.skillKey);
235
+ const entry = manifest.installs.find((candidate) => candidate.skillId === skill.skillId && (candidate.nativeSkillKey || candidate.skillKey) === managedNativeSkillKey(connection, skill));
236
+ const nativeSkillKey = managedNativeSkillKey(connection, skill);
237
+ const dir = join(skillsRoot, nativeSkillKey);
231
238
  const file = join(dir, "SKILL.md");
232
239
  if (!(await active())) {
233
240
  summary.errors.push({ stage: "download", code: "inactive_installation" });
@@ -246,17 +253,24 @@ export async function syncManagedSkills({
246
253
  next.push({ ...entry, lastCheckinAt: current.toISOString() });
247
254
  continue;
248
255
  }
249
- if (!(await isManaged(skillsRoot, skill.skillKey, entry)) && (await pathState(dir))) {
256
+ if (!(await isManaged(skillsRoot, nativeSkillKey, entry)) && (await pathState(dir))) {
250
257
  summary.conflicts.push({ skillKey: skill.skillKey, reason: "unmanaged_folder" });
251
258
  continue;
252
259
  }
253
260
  try {
254
261
  const exported = await transport.skillDownload(skill.skillId);
255
- const content = typeof exported?.content === "string" ? exported.content : null;
262
+ let content = typeof exported?.content === "string" ? exported.content : null;
256
263
  if (!content) throw Object.assign(new Error("skill export was empty"), { code: "empty_export" });
257
264
  if (!content.startsWith("---\n") || !content.split("\n---\n")[0].split("\n").includes(`content_sha256: ${skill.sha}`)) {
258
265
  throw Object.assign(new Error("skill digest mismatch"), { code: "skill_digest_mismatch" });
259
266
  }
267
+ if (connection.clientKind === "claude-code" && connection.skillGuardVersion === 1) {
268
+ const boundary = content.indexOf("\n---\n");
269
+ const frontmatter = content.slice(0, boundary);
270
+ if ((frontmatter.match(/^name:.*$/gm) || []).length !== 1) throw new Error("missing native skill name");
271
+ content = frontmatter.replace(/^name:.*$/m, `name: ${nativeSkillKey}`) + content.slice(boundary);
272
+ await recordSkillOwnership(connection, root, [{ ...skill, nativeSkillKey }]);
273
+ }
260
274
  if (!(await active())) throw Object.assign(new Error("inactive installation"), { code: "inactive_installation" });
261
275
  await safeDirectory(dir, true);
262
276
  if ((await pathState(file))?.isSymbolicLink()) throw new Error("unsafe skill file");
@@ -264,6 +278,7 @@ export async function syncManagedSkills({
264
278
  const record = {
265
279
  skillId: skill.skillId,
266
280
  skillKey: skill.skillKey,
281
+ ...(nativeSkillKey !== skill.skillKey ? { nativeSkillKey } : {}),
267
282
  sha: skill.sha,
268
283
  fileSha256: digest(content),
269
284
  namespace: skill.namespace,
@@ -325,8 +340,8 @@ export async function readManagedSkillTuples({ connection, root, home = homedir(
325
340
  if (manifest.installationId !== connection.installationId || !manifest.lastCheckinAt) throw new Error("incomplete skill evidence");
326
341
  for (const entry of manifest.installs) {
327
342
  if (!safeKey(entry.skillKey) || typeof entry.skillId !== "string" || !/^[a-f0-9]{64}$/.test(entry.sha)) throw new Error("invalid skill evidence");
328
- const dir = join(skillsRoot, entry.skillKey), file = join(dir, "SKILL.md");
329
- if ((await pathState(dir))?.isSymbolicLink() || (await pathState(file))?.isSymbolicLink()) throw new Error("unsafe skill evidence");
343
+ const dir = join(skillsRoot, entry.nativeSkillKey || entry.skillKey), file = join(dir, "SKILL.md");
344
+ if (!(await pathState(dir))?.isDirectory() || !(await pathState(file))?.isFile() || (await pathState(dir))?.isSymbolicLink() || (await pathState(file))?.isSymbolicLink()) throw new Error("unsafe skill evidence");
330
345
  const content = await readText(file);
331
346
  const separator = content?.indexOf("\n---\n\n");
332
347
  // The export wrapper adds exactly one blank line and one terminal newline.
@@ -343,14 +358,15 @@ export async function retireManagedSkills({ connection, root, home = homedir(),
343
358
  const skillsRoot = managedSkillsDirectory(connection.clientKind, { home, env });
344
359
  if (!skillsRoot) return;
345
360
  const manifest = await loadSkillsManifest(root, connection.installationId);
361
+ await recordSkillOwnership(connection, root, manifest.installs);
346
362
  if (!manifest.installs.length) return;
347
363
  await safeDirectory(skillsRoot);
348
364
  for (const entry of manifest.installs) {
349
- const dir = join(skillsRoot, entry.skillKey);
365
+ const dir = join(skillsRoot, entry.nativeSkillKey || entry.skillKey);
350
366
  const disk = await pathState(dir);
351
367
  if (!disk) continue;
352
368
  if (disk.isSymbolicLink() || !disk.isDirectory()) throw new Error("unsafe predecessor skill");
353
- await quarantine(skillsRoot, join(root, "skills-quarantine", connection.installationId), entry.skillKey, now());
369
+ await quarantine(skillsRoot, join(root, "skills-quarantine", connection.installationId), entry.nativeSkillKey || entry.skillKey, now());
354
370
  }
355
371
  manifest.installs = [];
356
372
  await writePrivateFile(skillsManifestPath(root, connection.installationId), `${JSON.stringify(manifest, null, 2)}\n`);
package/src/version.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  export const PACKAGE_NAME = "@halofy/agent-connect";
2
- export const INSTALLER_VERSION = "0.12.1";
3
- export const RUNTIME_VERSION = "0.12.1";
4
- export const DISCLOSURE_VERSION = "halofy-agent-lifecycle-2026-09-10.1";
2
+ export const INSTALLER_VERSION = "0.13.1";
3
+ export const RUNTIME_VERSION = "0.13.1";
4
+ export const DISCLOSURE_VERSION = "halofy-agent-lifecycle-2026-09-17.1";