@halofy/agent-connect 0.12.1 → 0.13.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 (251) hide show
  1. package/README.md +211 -8
  2. package/bin/halofy-agent.mjs +83 -8
  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 +15 -0
  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/install.mjs +8 -0
  247. package/src/installer-cli.mjs +25 -3
  248. package/src/instructions.mjs +5 -1
  249. package/src/skill-guard.mjs +171 -0
  250. package/src/skills-sync.mjs +32 -16
  251. package/src/version.mjs +3 -3
@@ -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
  }
@@ -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.0";
3
+ export const RUNTIME_VERSION = "0.13.0";
4
+ export const DISCLOSURE_VERSION = "halofy-agent-lifecycle-2026-09-17.1";