@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.
- package/README.md +211 -8
- package/bin/halofy-agent.mjs +83 -8
- package/node_modules/yaml/LICENSE +13 -0
- package/node_modules/yaml/README.md +172 -0
- package/node_modules/yaml/bin.mjs +11 -0
- package/node_modules/yaml/browser/dist/compose/compose-collection.js +88 -0
- package/node_modules/yaml/browser/dist/compose/compose-doc.js +43 -0
- package/node_modules/yaml/browser/dist/compose/compose-node.js +109 -0
- package/node_modules/yaml/browser/dist/compose/compose-scalar.js +86 -0
- package/node_modules/yaml/browser/dist/compose/composer.js +219 -0
- package/node_modules/yaml/browser/dist/compose/resolve-block-map.js +115 -0
- package/node_modules/yaml/browser/dist/compose/resolve-block-scalar.js +198 -0
- package/node_modules/yaml/browser/dist/compose/resolve-block-seq.js +49 -0
- package/node_modules/yaml/browser/dist/compose/resolve-end.js +37 -0
- package/node_modules/yaml/browser/dist/compose/resolve-flow-collection.js +207 -0
- package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +226 -0
- package/node_modules/yaml/browser/dist/compose/resolve-props.js +146 -0
- package/node_modules/yaml/browser/dist/compose/util-contains-newline.js +34 -0
- package/node_modules/yaml/browser/dist/compose/util-empty-scalar-position.js +26 -0
- package/node_modules/yaml/browser/dist/compose/util-flow-indent-check.js +15 -0
- package/node_modules/yaml/browser/dist/compose/util-map-includes.js +13 -0
- package/node_modules/yaml/browser/dist/doc/Document.js +335 -0
- package/node_modules/yaml/browser/dist/doc/anchors.js +71 -0
- package/node_modules/yaml/browser/dist/doc/applyReviver.js +55 -0
- package/node_modules/yaml/browser/dist/doc/createNode.js +88 -0
- package/node_modules/yaml/browser/dist/doc/directives.js +176 -0
- package/node_modules/yaml/browser/dist/errors.js +57 -0
- package/node_modules/yaml/browser/dist/index.js +17 -0
- package/node_modules/yaml/browser/dist/log.js +11 -0
- package/node_modules/yaml/browser/dist/nodes/Alias.js +118 -0
- package/node_modules/yaml/browser/dist/nodes/Collection.js +147 -0
- package/node_modules/yaml/browser/dist/nodes/Node.js +38 -0
- package/node_modules/yaml/browser/dist/nodes/Pair.js +36 -0
- package/node_modules/yaml/browser/dist/nodes/Scalar.js +24 -0
- package/node_modules/yaml/browser/dist/nodes/YAMLMap.js +144 -0
- package/node_modules/yaml/browser/dist/nodes/YAMLSeq.js +113 -0
- package/node_modules/yaml/browser/dist/nodes/addPairToJSMap.js +63 -0
- package/node_modules/yaml/browser/dist/nodes/identity.js +36 -0
- package/node_modules/yaml/browser/dist/nodes/toJS.js +37 -0
- package/node_modules/yaml/browser/dist/parse/cst-scalar.js +214 -0
- package/node_modules/yaml/browser/dist/parse/cst-stringify.js +61 -0
- package/node_modules/yaml/browser/dist/parse/cst-visit.js +97 -0
- package/node_modules/yaml/browser/dist/parse/cst.js +98 -0
- package/node_modules/yaml/browser/dist/parse/lexer.js +721 -0
- package/node_modules/yaml/browser/dist/parse/line-counter.js +39 -0
- package/node_modules/yaml/browser/dist/parse/parser.js +975 -0
- package/node_modules/yaml/browser/dist/public-api.js +102 -0
- package/node_modules/yaml/browser/dist/schema/Schema.js +37 -0
- package/node_modules/yaml/browser/dist/schema/common/map.js +17 -0
- package/node_modules/yaml/browser/dist/schema/common/null.js +15 -0
- package/node_modules/yaml/browser/dist/schema/common/seq.js +17 -0
- package/node_modules/yaml/browser/dist/schema/common/string.js +14 -0
- package/node_modules/yaml/browser/dist/schema/core/bool.js +19 -0
- package/node_modules/yaml/browser/dist/schema/core/float.js +43 -0
- package/node_modules/yaml/browser/dist/schema/core/int.js +38 -0
- package/node_modules/yaml/browser/dist/schema/core/schema.js +23 -0
- package/node_modules/yaml/browser/dist/schema/json/schema.js +62 -0
- package/node_modules/yaml/browser/dist/schema/tags.js +96 -0
- package/node_modules/yaml/browser/dist/schema/yaml-1.1/binary.js +58 -0
- package/node_modules/yaml/browser/dist/schema/yaml-1.1/bool.js +26 -0
- package/node_modules/yaml/browser/dist/schema/yaml-1.1/float.js +46 -0
- package/node_modules/yaml/browser/dist/schema/yaml-1.1/int.js +71 -0
- package/node_modules/yaml/browser/dist/schema/yaml-1.1/merge.js +67 -0
- package/node_modules/yaml/browser/dist/schema/yaml-1.1/omap.js +74 -0
- package/node_modules/yaml/browser/dist/schema/yaml-1.1/pairs.js +78 -0
- package/node_modules/yaml/browser/dist/schema/yaml-1.1/schema.js +39 -0
- package/node_modules/yaml/browser/dist/schema/yaml-1.1/set.js +93 -0
- package/node_modules/yaml/browser/dist/schema/yaml-1.1/timestamp.js +101 -0
- package/node_modules/yaml/browser/dist/stringify/foldFlowLines.js +146 -0
- package/node_modules/yaml/browser/dist/stringify/stringify.js +129 -0
- package/node_modules/yaml/browser/dist/stringify/stringifyCollection.js +153 -0
- package/node_modules/yaml/browser/dist/stringify/stringifyComment.js +20 -0
- package/node_modules/yaml/browser/dist/stringify/stringifyDocument.js +85 -0
- package/node_modules/yaml/browser/dist/stringify/stringifyNumber.js +25 -0
- package/node_modules/yaml/browser/dist/stringify/stringifyPair.js +150 -0
- package/node_modules/yaml/browser/dist/stringify/stringifyString.js +336 -0
- package/node_modules/yaml/browser/dist/util.js +11 -0
- package/node_modules/yaml/browser/dist/visit.js +233 -0
- package/node_modules/yaml/browser/index.js +5 -0
- package/node_modules/yaml/browser/package.json +3 -0
- package/node_modules/yaml/dist/cli.d.ts +8 -0
- package/node_modules/yaml/dist/cli.mjs +201 -0
- package/node_modules/yaml/dist/compose/compose-collection.d.ts +11 -0
- package/node_modules/yaml/dist/compose/compose-collection.js +90 -0
- package/node_modules/yaml/dist/compose/compose-doc.d.ts +7 -0
- package/node_modules/yaml/dist/compose/compose-doc.js +45 -0
- package/node_modules/yaml/dist/compose/compose-node.d.ts +29 -0
- package/node_modules/yaml/dist/compose/compose-node.js +112 -0
- package/node_modules/yaml/dist/compose/compose-scalar.d.ts +5 -0
- package/node_modules/yaml/dist/compose/compose-scalar.js +88 -0
- package/node_modules/yaml/dist/compose/composer.d.ts +63 -0
- package/node_modules/yaml/dist/compose/composer.js +224 -0
- package/node_modules/yaml/dist/compose/resolve-block-map.d.ts +6 -0
- package/node_modules/yaml/dist/compose/resolve-block-map.js +117 -0
- package/node_modules/yaml/dist/compose/resolve-block-scalar.d.ts +11 -0
- package/node_modules/yaml/dist/compose/resolve-block-scalar.js +200 -0
- package/node_modules/yaml/dist/compose/resolve-block-seq.d.ts +6 -0
- package/node_modules/yaml/dist/compose/resolve-block-seq.js +51 -0
- package/node_modules/yaml/dist/compose/resolve-end.d.ts +6 -0
- package/node_modules/yaml/dist/compose/resolve-end.js +39 -0
- package/node_modules/yaml/dist/compose/resolve-flow-collection.d.ts +7 -0
- package/node_modules/yaml/dist/compose/resolve-flow-collection.js +209 -0
- package/node_modules/yaml/dist/compose/resolve-flow-scalar.d.ts +10 -0
- package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +228 -0
- package/node_modules/yaml/dist/compose/resolve-props.d.ts +23 -0
- package/node_modules/yaml/dist/compose/resolve-props.js +148 -0
- package/node_modules/yaml/dist/compose/util-contains-newline.d.ts +2 -0
- package/node_modules/yaml/dist/compose/util-contains-newline.js +36 -0
- package/node_modules/yaml/dist/compose/util-empty-scalar-position.d.ts +2 -0
- package/node_modules/yaml/dist/compose/util-empty-scalar-position.js +28 -0
- package/node_modules/yaml/dist/compose/util-flow-indent-check.d.ts +3 -0
- package/node_modules/yaml/dist/compose/util-flow-indent-check.js +17 -0
- package/node_modules/yaml/dist/compose/util-map-includes.d.ts +4 -0
- package/node_modules/yaml/dist/compose/util-map-includes.js +15 -0
- package/node_modules/yaml/dist/doc/Document.d.ts +141 -0
- package/node_modules/yaml/dist/doc/Document.js +337 -0
- package/node_modules/yaml/dist/doc/anchors.d.ts +24 -0
- package/node_modules/yaml/dist/doc/anchors.js +76 -0
- package/node_modules/yaml/dist/doc/applyReviver.d.ts +9 -0
- package/node_modules/yaml/dist/doc/applyReviver.js +57 -0
- package/node_modules/yaml/dist/doc/createNode.d.ts +17 -0
- package/node_modules/yaml/dist/doc/createNode.js +90 -0
- package/node_modules/yaml/dist/doc/directives.d.ts +49 -0
- package/node_modules/yaml/dist/doc/directives.js +178 -0
- package/node_modules/yaml/dist/errors.d.ts +21 -0
- package/node_modules/yaml/dist/errors.js +62 -0
- package/node_modules/yaml/dist/index.d.ts +25 -0
- package/node_modules/yaml/dist/index.js +50 -0
- package/node_modules/yaml/dist/log.d.ts +3 -0
- package/node_modules/yaml/dist/log.js +19 -0
- package/node_modules/yaml/dist/nodes/Alias.d.ts +29 -0
- package/node_modules/yaml/dist/nodes/Alias.js +120 -0
- package/node_modules/yaml/dist/nodes/Collection.d.ts +73 -0
- package/node_modules/yaml/dist/nodes/Collection.js +151 -0
- package/node_modules/yaml/dist/nodes/Node.d.ts +53 -0
- package/node_modules/yaml/dist/nodes/Node.js +40 -0
- package/node_modules/yaml/dist/nodes/Pair.d.ts +22 -0
- package/node_modules/yaml/dist/nodes/Pair.js +39 -0
- package/node_modules/yaml/dist/nodes/Scalar.d.ts +47 -0
- package/node_modules/yaml/dist/nodes/Scalar.js +27 -0
- package/node_modules/yaml/dist/nodes/YAMLMap.d.ts +53 -0
- package/node_modules/yaml/dist/nodes/YAMLMap.js +147 -0
- package/node_modules/yaml/dist/nodes/YAMLSeq.d.ts +60 -0
- package/node_modules/yaml/dist/nodes/YAMLSeq.js +115 -0
- package/node_modules/yaml/dist/nodes/addPairToJSMap.d.ts +4 -0
- package/node_modules/yaml/dist/nodes/addPairToJSMap.js +65 -0
- package/node_modules/yaml/dist/nodes/identity.d.ts +23 -0
- package/node_modules/yaml/dist/nodes/identity.js +53 -0
- package/node_modules/yaml/dist/nodes/toJS.d.ts +29 -0
- package/node_modules/yaml/dist/nodes/toJS.js +39 -0
- package/node_modules/yaml/dist/options.d.ts +350 -0
- package/node_modules/yaml/dist/parse/cst-scalar.d.ts +64 -0
- package/node_modules/yaml/dist/parse/cst-scalar.js +218 -0
- package/node_modules/yaml/dist/parse/cst-stringify.d.ts +8 -0
- package/node_modules/yaml/dist/parse/cst-stringify.js +63 -0
- package/node_modules/yaml/dist/parse/cst-visit.d.ts +39 -0
- package/node_modules/yaml/dist/parse/cst-visit.js +99 -0
- package/node_modules/yaml/dist/parse/cst.d.ts +109 -0
- package/node_modules/yaml/dist/parse/cst.js +112 -0
- package/node_modules/yaml/dist/parse/lexer.d.ts +87 -0
- package/node_modules/yaml/dist/parse/lexer.js +723 -0
- package/node_modules/yaml/dist/parse/line-counter.d.ts +22 -0
- package/node_modules/yaml/dist/parse/line-counter.js +41 -0
- package/node_modules/yaml/dist/parse/parser.d.ts +84 -0
- package/node_modules/yaml/dist/parse/parser.js +980 -0
- package/node_modules/yaml/dist/public-api.d.ts +44 -0
- package/node_modules/yaml/dist/public-api.js +107 -0
- package/node_modules/yaml/dist/schema/Schema.d.ts +17 -0
- package/node_modules/yaml/dist/schema/Schema.js +39 -0
- package/node_modules/yaml/dist/schema/common/map.d.ts +2 -0
- package/node_modules/yaml/dist/schema/common/map.js +19 -0
- package/node_modules/yaml/dist/schema/common/null.d.ts +4 -0
- package/node_modules/yaml/dist/schema/common/null.js +17 -0
- package/node_modules/yaml/dist/schema/common/seq.d.ts +2 -0
- package/node_modules/yaml/dist/schema/common/seq.js +19 -0
- package/node_modules/yaml/dist/schema/common/string.d.ts +2 -0
- package/node_modules/yaml/dist/schema/common/string.js +16 -0
- package/node_modules/yaml/dist/schema/core/bool.d.ts +4 -0
- package/node_modules/yaml/dist/schema/core/bool.js +21 -0
- package/node_modules/yaml/dist/schema/core/float.d.ts +4 -0
- package/node_modules/yaml/dist/schema/core/float.js +47 -0
- package/node_modules/yaml/dist/schema/core/int.d.ts +4 -0
- package/node_modules/yaml/dist/schema/core/int.js +42 -0
- package/node_modules/yaml/dist/schema/core/schema.d.ts +1 -0
- package/node_modules/yaml/dist/schema/core/schema.js +25 -0
- package/node_modules/yaml/dist/schema/json/schema.d.ts +2 -0
- package/node_modules/yaml/dist/schema/json/schema.js +64 -0
- package/node_modules/yaml/dist/schema/json-schema.d.ts +69 -0
- package/node_modules/yaml/dist/schema/tags.d.ts +48 -0
- package/node_modules/yaml/dist/schema/tags.js +99 -0
- package/node_modules/yaml/dist/schema/types.d.ts +92 -0
- package/node_modules/yaml/dist/schema/yaml-1.1/binary.d.ts +2 -0
- package/node_modules/yaml/dist/schema/yaml-1.1/binary.js +70 -0
- package/node_modules/yaml/dist/schema/yaml-1.1/bool.d.ts +7 -0
- package/node_modules/yaml/dist/schema/yaml-1.1/bool.js +29 -0
- package/node_modules/yaml/dist/schema/yaml-1.1/float.d.ts +4 -0
- package/node_modules/yaml/dist/schema/yaml-1.1/float.js +50 -0
- package/node_modules/yaml/dist/schema/yaml-1.1/int.d.ts +5 -0
- package/node_modules/yaml/dist/schema/yaml-1.1/int.js +76 -0
- package/node_modules/yaml/dist/schema/yaml-1.1/merge.d.ts +9 -0
- package/node_modules/yaml/dist/schema/yaml-1.1/merge.js +71 -0
- package/node_modules/yaml/dist/schema/yaml-1.1/omap.d.ts +22 -0
- package/node_modules/yaml/dist/schema/yaml-1.1/omap.js +77 -0
- package/node_modules/yaml/dist/schema/yaml-1.1/pairs.d.ts +10 -0
- package/node_modules/yaml/dist/schema/yaml-1.1/pairs.js +82 -0
- package/node_modules/yaml/dist/schema/yaml-1.1/schema.d.ts +1 -0
- package/node_modules/yaml/dist/schema/yaml-1.1/schema.js +41 -0
- package/node_modules/yaml/dist/schema/yaml-1.1/set.d.ts +28 -0
- package/node_modules/yaml/dist/schema/yaml-1.1/set.js +96 -0
- package/node_modules/yaml/dist/schema/yaml-1.1/timestamp.d.ts +6 -0
- package/node_modules/yaml/dist/schema/yaml-1.1/timestamp.js +105 -0
- package/node_modules/yaml/dist/stringify/foldFlowLines.d.ts +34 -0
- package/node_modules/yaml/dist/stringify/foldFlowLines.js +151 -0
- package/node_modules/yaml/dist/stringify/stringify.d.ts +21 -0
- package/node_modules/yaml/dist/stringify/stringify.js +132 -0
- package/node_modules/yaml/dist/stringify/stringifyCollection.d.ts +17 -0
- package/node_modules/yaml/dist/stringify/stringifyCollection.js +155 -0
- package/node_modules/yaml/dist/stringify/stringifyComment.d.ts +10 -0
- package/node_modules/yaml/dist/stringify/stringifyComment.js +24 -0
- package/node_modules/yaml/dist/stringify/stringifyDocument.d.ts +4 -0
- package/node_modules/yaml/dist/stringify/stringifyDocument.js +87 -0
- package/node_modules/yaml/dist/stringify/stringifyNumber.d.ts +2 -0
- package/node_modules/yaml/dist/stringify/stringifyNumber.js +27 -0
- package/node_modules/yaml/dist/stringify/stringifyPair.d.ts +3 -0
- package/node_modules/yaml/dist/stringify/stringifyPair.js +152 -0
- package/node_modules/yaml/dist/stringify/stringifyString.d.ts +9 -0
- package/node_modules/yaml/dist/stringify/stringifyString.js +338 -0
- package/node_modules/yaml/dist/test-events.d.ts +4 -0
- package/node_modules/yaml/dist/test-events.js +134 -0
- package/node_modules/yaml/dist/util.d.ts +16 -0
- package/node_modules/yaml/dist/util.js +28 -0
- package/node_modules/yaml/dist/visit.d.ts +102 -0
- package/node_modules/yaml/dist/visit.js +236 -0
- package/node_modules/yaml/package.json +97 -0
- package/node_modules/yaml/util.js +2 -0
- package/package.json +8 -2
- package/src/claude-config.mjs +60 -2
- package/src/claude-hook.mjs +8 -1
- package/src/client-registry.mjs +13 -0
- package/src/delivery-sync.mjs +14 -0
- package/src/health.mjs +15 -0
- package/src/hermes-config.mjs +283 -0
- package/src/hermes-health.mjs +45 -0
- package/src/hermes-hook.mjs +86 -0
- package/src/hermes-plugin.py +67 -0
- package/src/install.mjs +8 -0
- package/src/installer-cli.mjs +25 -3
- package/src/instructions.mjs +5 -1
- package/src/skill-guard.mjs +171 -0
- package/src/skills-sync.mjs +32 -16
- package/src/version.mjs +3 -3
package/src/instructions.mjs
CHANGED
|
@@ -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) =>
|
|
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
|
+
}
|
package/src/skills-sync.mjs
CHANGED
|
@@ -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>/<
|
|
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
|
|
122
|
-
*
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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.
|
|
3
|
-
export const RUNTIME_VERSION = "0.
|
|
4
|
-
export const DISCLOSURE_VERSION = "halofy-agent-lifecycle-2026-09-
|
|
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";
|