@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.
- package/README.md +249 -10
- package/bin/halofy-agent.mjs +96 -11
- 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 +26 -4
- 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/host-hook.mjs +6 -8
- package/src/install.mjs +8 -0
- package/src/installer-cli.mjs +25 -3
- package/src/instructions.mjs +5 -1
- package/src/runtime.mjs +33 -22
- package/src/skill-guard.mjs +171 -0
- package/src/skills-sync.mjs +32 -16
- package/src/version.mjs +3 -3
package/src/installer-cli.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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({
|
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
|
}
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
+
}
|
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.1";
|
|
3
|
+
export const RUNTIME_VERSION = "0.13.1";
|
|
4
|
+
export const DISCLOSURE_VERSION = "halofy-agent-lifecycle-2026-09-17.1";
|