@holmes-lab/holmes-kit 0.19.2 → 0.19.4
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/CHANGELOG.md +177 -0
- package/README.md +22 -4
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/agents.d.ts +8 -0
- package/dist/holmes/cli/agents.js +26 -2
- package/dist/holmes/cli/codex-toml.d.ts +10 -0
- package/dist/holmes/cli/codex-toml.js +76 -12
- package/dist/holmes/cli/doctor.d.ts +34 -0
- package/dist/holmes/cli/doctor.js +139 -46
- package/dist/holmes/cli/index.js +13 -0
- package/dist/holmes/cli/init.js +10 -3
- package/dist/holmes/cli/native-deps.js +4 -1
- package/dist/holmes/cli/playbook-skills.js +6 -4
- package/dist/holmes/cli/probe-process.d.ts +8 -0
- package/dist/holmes/cli/probe-process.js +73 -0
- package/dist/holmes/cli/spawn-spec.js +3 -1
- package/dist/holmes/cli/test-platform.d.ts +37 -0
- package/dist/holmes/cli/test-platform.js +126 -1
- package/dist/holmes/governance/approval-grants.js +26 -3
- package/dist/holmes/governance/autonomy.d.ts +17 -1
- package/dist/holmes/governance/autonomy.js +37 -5
- package/dist/holmes/mcp/handlers.d.ts +30 -5
- package/dist/holmes/mcp/handlers.js +111 -13
- package/dist/holmes/mcp/spec-id-guard.d.ts +1 -1
- package/dist/holmes/mcp/spec-id-guard.js +9 -13
- package/dist/holmes/mcp/tool-schemas.js +13 -0
- package/dist/holmes/project/install-scripts-policy.d.ts +16 -2
- package/dist/holmes/project/install-scripts-policy.js +16 -2
- package/dist/holmes/review/point-in-time-replay.js +43 -3
- package/dist/holmes/rtm/graph-store.d.ts +2 -0
- package/dist/holmes/rtm/graph-store.js +14 -0
- package/dist/holmes/rtm/rtm-graph.js +42 -30
- package/dist/holmes/semantic/credentials.js +86 -9
- package/dist/holmes/semantic/embedder.js +6 -39
- package/dist/holmes/semantic/local-model.d.ts +30 -0
- package/dist/holmes/semantic/local-model.js +92 -0
- package/dist/holmes/semantic/model-cache.d.ts +8 -0
- package/dist/holmes/semantic/model-cache.js +67 -0
- package/dist/holmes/semantic/tier.d.ts +7 -0
- package/dist/holmes/semantic/tier.js +9 -3
- package/dist/holmes/spec/renumber.d.ts +72 -0
- package/dist/holmes/spec/renumber.js +341 -0
- package/dist/holmes/spec/spec-id.d.ts +9 -0
- package/dist/holmes/spec/spec-id.js +23 -0
- package/docs/install-guide.md +90 -2
- package/package.json +6 -3
- package/scripts/install.ps1 +30 -27
|
@@ -12,8 +12,22 @@
|
|
|
12
12
|
* This module is the pure re-statement of that rule: no I/O, no process, no platform — the same
|
|
13
13
|
* verdict on every OS, shared by the test that pins package.json and by doctor (A-SPEC-580).
|
|
14
14
|
*/
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Dependencies whose install script must actually RUN for the module to load.
|
|
17
|
+
*
|
|
18
|
+
* A-SPEC-595, measured 2026-09-10: `sharp@0.32.6` belongs here on EVERY platform, not just the
|
|
19
|
+
* Windows machine that first hit it. `npm pack sharp@0.32.6` yields a tarball with zero entries
|
|
20
|
+
* matching `.node`, `prebuilds/` or `vendor/`; its declared hook is
|
|
21
|
+
* `(node install/libvips && node install/dll-copy && prebuild-install) || (node install/can-compile
|
|
22
|
+
* && node-gyp rebuild && node install/dll-copy)`; the lockfile records `hasInstallScript: true`;
|
|
23
|
+
* and this repository's macOS tree holds `build/Release/sharp-darwin-arm64v8.node` and
|
|
24
|
+
* `vendor/8.14.5` only because that script ran. (sharp 0.33+ moved to platform packages that DO
|
|
25
|
+
* ship prebuilds — a version bump makes this entry a re-measurement, not a given.)
|
|
26
|
+
*
|
|
27
|
+
* This list is what doctor reads, and `allowScripts` is a root-manifest policy that does not
|
|
28
|
+
* transfer to consumers — so a name missing here is a name no consumer is ever told to approve.
|
|
29
|
+
*/
|
|
30
|
+
export declare const NATIVE_INSTALL_SCRIPT_DEPS: readonly ["better-sqlite3", "sharp"];
|
|
17
31
|
/** Native dependencies that ship prebuilds and load with the script blocked (measured). */
|
|
18
32
|
export declare const PREBUILT_NATIVE_DEPS: readonly ["tree-sitter", "tree-sitter-typescript", "tree-sitter-python", "tree-sitter-c-sharp", "tree-sitter-java", "tree-sitter-go", "tree-sitter-rust", "tree-sitter-cpp"];
|
|
19
33
|
export type AllowScripts = Record<string, boolean>;
|
|
@@ -20,8 +20,22 @@ exports.policyVerdict = policyVerdict;
|
|
|
20
20
|
* This module is the pure re-statement of that rule: no I/O, no process, no platform — the same
|
|
21
21
|
* verdict on every OS, shared by the test that pins package.json and by doctor (A-SPEC-580).
|
|
22
22
|
*/
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Dependencies whose install script must actually RUN for the module to load.
|
|
25
|
+
*
|
|
26
|
+
* A-SPEC-595, measured 2026-09-10: `sharp@0.32.6` belongs here on EVERY platform, not just the
|
|
27
|
+
* Windows machine that first hit it. `npm pack sharp@0.32.6` yields a tarball with zero entries
|
|
28
|
+
* matching `.node`, `prebuilds/` or `vendor/`; its declared hook is
|
|
29
|
+
* `(node install/libvips && node install/dll-copy && prebuild-install) || (node install/can-compile
|
|
30
|
+
* && node-gyp rebuild && node install/dll-copy)`; the lockfile records `hasInstallScript: true`;
|
|
31
|
+
* and this repository's macOS tree holds `build/Release/sharp-darwin-arm64v8.node` and
|
|
32
|
+
* `vendor/8.14.5` only because that script ran. (sharp 0.33+ moved to platform packages that DO
|
|
33
|
+
* ship prebuilds — a version bump makes this entry a re-measurement, not a given.)
|
|
34
|
+
*
|
|
35
|
+
* This list is what doctor reads, and `allowScripts` is a root-manifest policy that does not
|
|
36
|
+
* transfer to consumers — so a name missing here is a name no consumer is ever told to approve.
|
|
37
|
+
*/
|
|
38
|
+
exports.NATIVE_INSTALL_SCRIPT_DEPS = ['better-sqlite3', 'sharp'];
|
|
25
39
|
/** Native dependencies that ship prebuilds and load with the script blocked (measured). */
|
|
26
40
|
exports.PREBUILT_NATIVE_DEPS = [
|
|
27
41
|
'tree-sitter', 'tree-sitter-typescript', 'tree-sitter-python', 'tree-sitter-c-sharp',
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.materializeTreeAt = materializeTreeAt;
|
|
37
37
|
exports.verifyTreeFidelity = verifyTreeFidelity;
|
|
38
|
+
// @implements A-SPEC-599
|
|
38
39
|
// @implements A-SPEC-297
|
|
39
40
|
const fs = __importStar(require("node:fs"));
|
|
40
41
|
const os = __importStar(require("node:os"));
|
|
@@ -56,12 +57,14 @@ function git(repoRoot, args) {
|
|
|
56
57
|
}
|
|
57
58
|
/** `path mode sha` triples the commit records for the pathspec, keyed by repo-relative path. */
|
|
58
59
|
function blobsAt(repoRoot, commitish, pathspec) {
|
|
59
|
-
const out = git(repoRoot, ['ls-tree', '-
|
|
60
|
+
const out = git(repoRoot, ['ls-tree', '-rz', `${commitish}^{tree}`, '--', pathspec]);
|
|
60
61
|
const blobs = new Map();
|
|
61
|
-
for (const line of out.split('\
|
|
62
|
+
for (const line of out.split('\0')) {
|
|
62
63
|
if (line.trim() === '')
|
|
63
64
|
continue;
|
|
64
|
-
const
|
|
65
|
+
const tab = line.indexOf('\t');
|
|
66
|
+
const meta = line.slice(0, tab);
|
|
67
|
+
const file = line.slice(tab + 1);
|
|
65
68
|
const parts = meta.split(/\s+/);
|
|
66
69
|
if (parts[1] !== 'blob')
|
|
67
70
|
continue; // submodules and trees are not files we can compare
|
|
@@ -119,6 +122,43 @@ function materializeTreeAt(repoRoot, commitish, pathspec, destDir, opts) {
|
|
|
119
122
|
const tar = (0, node_child_process_1.execFileSync)('git', ['-C', repoRoot, 'archive', '--format=tar', commitish, '--', pathspec], { maxBuffer: 512 * 1024 * 1024 });
|
|
120
123
|
fs.writeFileSync(tarPath, tar);
|
|
121
124
|
(0, node_child_process_1.execFileSync)('tar', ['-x', '-C', destDir, '-f', tarPath], { maxBuffer: 512 * 1024 * 1024 });
|
|
125
|
+
// Archive preserves modes/layout, but applies EOL/export substitutions. Read raw objects in
|
|
126
|
+
// one batch so fidelity is independent of user config and committed attributes alike.
|
|
127
|
+
const refsPath = path.join(path.dirname(tarPath), 'objects.txt');
|
|
128
|
+
fs.writeFileSync(refsPath, [...blobs.values()].join('\n') + '\n');
|
|
129
|
+
const refs = fs.openSync(refsPath, 'r');
|
|
130
|
+
let raw;
|
|
131
|
+
try {
|
|
132
|
+
raw = (0, node_child_process_1.execFileSync)('git', ['-C', repoRoot, 'cat-file', '--batch'], {
|
|
133
|
+
stdio: [refs, 'pipe', 'pipe'], maxBuffer: 512 * 1024 * 1024,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
fs.closeSync(refs);
|
|
138
|
+
}
|
|
139
|
+
let offset = 0;
|
|
140
|
+
for (const [file, sha] of blobs) {
|
|
141
|
+
const end = raw.indexOf(10, offset);
|
|
142
|
+
const header = raw.subarray(offset, end).toString('ascii').split(' ');
|
|
143
|
+
const size = Number(header[2]);
|
|
144
|
+
if (end < offset || header[0] !== sha || header[1] !== 'blob' || !Number.isSafeInteger(size)
|
|
145
|
+
|| size < 0 || end + 1 + size >= raw.length)
|
|
146
|
+
throw new Error('invalid cat-file response');
|
|
147
|
+
const bytes = raw.subarray(end + 1, end + 1 + size);
|
|
148
|
+
offset = end + 1 + size + 1;
|
|
149
|
+
const target = path.join(destDir, file);
|
|
150
|
+
// Keep archive's symlink representation; never follow it to overwrite a target.
|
|
151
|
+
try {
|
|
152
|
+
if (fs.lstatSync(target).isSymbolicLink())
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (error.code !== 'ENOENT')
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
160
|
+
fs.writeFileSync(target, bytes);
|
|
161
|
+
}
|
|
122
162
|
}
|
|
123
163
|
catch (err) {
|
|
124
164
|
return { ok: false, reason: `archive-or-extract-failed: could not materialize ${commitish}: ${err.message}` };
|
|
@@ -27,6 +27,8 @@ export interface GraphBasis {
|
|
|
27
27
|
*/
|
|
28
28
|
scanDigest: string;
|
|
29
29
|
}
|
|
30
|
+
/** Deterministic content fingerprint for every spec field consumed by buildRtm. */
|
|
31
|
+
export declare function specFingerprint(specs: readonly unknown[]): string;
|
|
30
32
|
/**
|
|
31
33
|
* Content address of a scan: every file's path and its symbols/edges, sorted so the digest depends
|
|
32
34
|
* on the scan's CONTENT and not on the order the scanner happened to walk the tree in.
|
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.specFingerprint = specFingerprint;
|
|
36
37
|
exports.scanDigest = scanDigest;
|
|
37
38
|
exports.openReusableGraph = openReusableGraph;
|
|
38
39
|
// @implements A-SPEC-282
|
|
@@ -43,6 +44,19 @@ const rtm_graph_1 = require("./rtm-graph");
|
|
|
43
44
|
const BASIS_FIELDS = [
|
|
44
45
|
'graphSchema', 'extractorVersion', 'sourceCommit', 'specFingerprint', 'scanDigest',
|
|
45
46
|
];
|
|
47
|
+
/** Deterministic content fingerprint for every spec field consumed by buildRtm. */
|
|
48
|
+
function specFingerprint(specs) {
|
|
49
|
+
const canonical = (value) => {
|
|
50
|
+
if (Array.isArray(value))
|
|
51
|
+
return value.map(canonical);
|
|
52
|
+
if (value !== null && typeof value === 'object') {
|
|
53
|
+
return Object.fromEntries(Object.entries(value)
|
|
54
|
+
.sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => [k, canonical(v)]));
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
};
|
|
58
|
+
return `sha256:${(0, node_crypto_1.createHash)('sha256').update(JSON.stringify(specs.map(canonical))).digest('hex')}`;
|
|
59
|
+
}
|
|
46
60
|
/**
|
|
47
61
|
* Content address of a scan: every file's path and its symbols/edges, sorted so the digest depends
|
|
48
62
|
* on the scan's CONTENT and not on the order the scanner happened to walk the tree in.
|
|
@@ -66,16 +66,17 @@ class RtmGraph {
|
|
|
66
66
|
// full rebuild.
|
|
67
67
|
constructor(dbPath = ':memory:') {
|
|
68
68
|
this.db = new better_sqlite3_1.default(dbPath);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
69
|
+
try {
|
|
70
|
+
// @implements A-SPEC-283
|
|
71
|
+
// Only a FILE-backed store can be contended. Several MCP servers can be live at once, and the
|
|
72
|
+
// hook and the CLI can touch the same project, so a reader must not be blocked by a writer and a
|
|
73
|
+
// busy file must be waited on rather than thrown at. `:memory:` is private to this process and
|
|
74
|
+
// cannot use WAL at all — setting it there would be a pragma that cannot apply.
|
|
75
|
+
if (dbPath !== ':memory:') {
|
|
76
|
+
this.db.pragma('journal_mode = WAL');
|
|
77
|
+
this.db.pragma('busy_timeout = 5000');
|
|
78
|
+
}
|
|
79
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS nodes (id TEXT PRIMARY KEY, kind TEXT NOT NULL, source_path TEXT, summary TEXT);
|
|
79
80
|
CREATE TABLE IF NOT EXISTS edges (src TEXT NOT NULL, dst TEXT NOT NULL, rel TEXT NOT NULL, source_path TEXT, PRIMARY KEY (src,dst,rel));
|
|
80
81
|
-- The PK covers src-prefixed lookups; nothing covered dst or source_path, so every reverse
|
|
81
82
|
-- traversal and every removeBySource was a full table scan. Measured on a 240k-node graph:
|
|
@@ -86,28 +87,39 @@ class RtmGraph {
|
|
|
86
87
|
CREATE INDEX IF NOT EXISTS idx_edges_dst ON edges(dst);
|
|
87
88
|
CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_path);
|
|
88
89
|
CREATE INDEX IF NOT EXISTS idx_nodes_source ON nodes(source_path);`);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
90
|
+
// @implements A-SPEC-281
|
|
91
|
+
// `CREATE TABLE IF NOT EXISTS` leaves an EXISTING database on its old schema, so a store written
|
|
92
|
+
// before provenance existed would silently reject every new insert. Add the missing columns in
|
|
93
|
+
// place; existing rows keep their data and simply report null provenance, which is the honest
|
|
94
|
+
// record for a fact nobody annotated.
|
|
95
|
+
// @implements A-SPEC-282 — a small key/value side table so a persisted graph can carry the basis
|
|
96
|
+
// it was built on. Kept out of `nodes`/`edges` because it describes the WHOLE graph, not a fact.
|
|
97
|
+
this.db.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)');
|
|
98
|
+
for (const table of ['nodes', 'edges']) {
|
|
99
|
+
const present = new Set(this.db.prepare(`PRAGMA table_info(${table})`).all()
|
|
100
|
+
.map((c) => c.name));
|
|
101
|
+
for (const column of PROVENANCE_COLUMNS) {
|
|
102
|
+
if (present.has(column))
|
|
103
|
+
continue;
|
|
104
|
+
const type = column === 'confidence' ? 'REAL' : 'TEXT';
|
|
105
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
|
|
106
|
+
}
|
|
107
|
+
// @implements A-SPEC-568.1 — same in-place migration rule as provenance: an rtm-graph/2 file
|
|
108
|
+
// must keep OPENING (openReusableGraph decides reuse; an unreadable file would look corrupt).
|
|
109
|
+
if (table === 'nodes' && !present.has('summary')) {
|
|
110
|
+
this.db.exec('ALTER TABLE nodes ADD COLUMN summary TEXT');
|
|
111
|
+
}
|
|
105
112
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
// A corrupt file can fail during pragma/schema setup after better-sqlite3 has opened it.
|
|
116
|
+
// Release that native handle before propagating the original error so the caller can replace
|
|
117
|
+
// the file on Windows instead of receiving a second EPERM/SQLITE_NOTADB failure (A-SPEC-588).
|
|
118
|
+
try {
|
|
119
|
+
this.db.close();
|
|
110
120
|
}
|
|
121
|
+
catch { /* preserve the initialization error */ }
|
|
122
|
+
throw error;
|
|
111
123
|
}
|
|
112
124
|
}
|
|
113
125
|
// @implements A-SPEC-139
|
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.resolveSemanticKey = resolveSemanticKey;
|
|
37
37
|
exports.storeSemanticKey = storeSemanticKey;
|
|
38
38
|
exports.removeSemanticKey = removeSemanticKey;
|
|
39
|
+
// @implements A-SPEC-592
|
|
39
40
|
// @implements A-SPEC-477
|
|
40
41
|
/**
|
|
41
42
|
* The cloud tier's credential: WHERE the consent lives, and in what order it is looked up.
|
|
@@ -49,9 +50,9 @@ exports.removeSemanticKey = removeSemanticKey;
|
|
|
49
50
|
* Resolution chain, the order being the contract:
|
|
50
51
|
* 1. HOLMES_SEMANTIC_API_KEY — dedicated name, CI/headless.
|
|
51
52
|
* 2. GEMINI_API_KEY — ecosystem-compatible name.
|
|
52
|
-
* 3. macOS keychain — `security` via injected exec (absent elsewhere
|
|
53
|
-
*
|
|
54
|
-
*
|
|
53
|
+
* 3. macOS keychain — `security` via injected exec (absent elsewhere).
|
|
54
|
+
* 4. ~/.holmes/credentials.json — POSIX0600 under0700, or protected current-user-only
|
|
55
|
+
* Windows DACLs, the everywhere-fallback.
|
|
55
56
|
*
|
|
56
57
|
* The VALUE never appears in argv (ps/history surfaces), logs, reports, or error messages.
|
|
57
58
|
* Keychain writes feed the secret over STDIN (`security -i`) for the same reason.
|
|
@@ -60,11 +61,59 @@ const fs = __importStar(require("node:fs"));
|
|
|
60
61
|
const path = __importStar(require("node:path"));
|
|
61
62
|
const os = __importStar(require("node:os"));
|
|
62
63
|
const node_child_process_1 = require("node:child_process");
|
|
64
|
+
const node_crypto_1 = require("node:crypto");
|
|
63
65
|
const SERVICE = 'holmes-kit';
|
|
64
66
|
const ACCOUNT = 'semantic';
|
|
65
67
|
const defaultExec = (cmd, args, stdin) => (0, node_child_process_1.execFileSync)(cmd, args, { encoding: 'utf8', input: stdin, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
66
68
|
const credFile = (home) => path.join(home, '.holmes', 'credentials.json');
|
|
67
69
|
const nonBlank = (v) => typeof v === 'string' && v.trim() !== '' ? v.trim() : null;
|
|
70
|
+
// Fixed command: its stdin contains only encoded paths, never credential content. Fresh security
|
|
71
|
+
// descriptors discard both inherited and explicit broad grants; chmod cannot do that on Windows.
|
|
72
|
+
const PRIVATE_WINDOWS_ACL = `
|
|
73
|
+
$ErrorActionPreference = 'Stop'
|
|
74
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
75
|
+
$targets = ConvertFrom-Json ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String([Console]::In.ReadToEnd())))
|
|
76
|
+
$sid = [Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
77
|
+
foreach ($target in $targets) {
|
|
78
|
+
$item = Get-Item -LiteralPath $target.path -Force
|
|
79
|
+
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'Credential path is a reparse point' }
|
|
80
|
+
if ($target.directory) {
|
|
81
|
+
$acl = [Security.AccessControl.DirectorySecurity]::new()
|
|
82
|
+
$rule = [Security.AccessControl.FileSystemAccessRule]::new($sid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
|
|
83
|
+
} else {
|
|
84
|
+
$acl = [Security.AccessControl.FileSecurity]::new()
|
|
85
|
+
$rule = [Security.AccessControl.FileSystemAccessRule]::new($sid, 'FullControl', 'Allow')
|
|
86
|
+
}
|
|
87
|
+
$acl.SetAccessRuleProtection($true, $false)
|
|
88
|
+
$acl.AddAccessRule($rule)
|
|
89
|
+
# Persist only the DACL. Set-Acl can request SACL privileges during a repeated protected update.
|
|
90
|
+
if ($target.directory) { [IO.Directory]::SetAccessControl($target.path, $acl) }
|
|
91
|
+
else { [IO.File]::SetAccessControl($target.path, $acl) }
|
|
92
|
+
$actual = Get-Acl -LiteralPath $target.path
|
|
93
|
+
$rules = @($actual.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier]))
|
|
94
|
+
if (-not $actual.AreAccessRulesProtected -or $rules.Count -ne 1 -or
|
|
95
|
+
$rules[0].IdentityReference.Value -ne $sid.Value -or $rules[0].IsInherited -or
|
|
96
|
+
$rules[0].AccessControlType -ne 'Allow' -or
|
|
97
|
+
$rules[0].FileSystemRights -ne [Security.AccessControl.FileSystemRights]::FullControl) {
|
|
98
|
+
throw 'Credential ACL verification failed'
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
`;
|
|
102
|
+
function protectWindowsPaths(targets) {
|
|
103
|
+
try {
|
|
104
|
+
(0, node_child_process_1.execFileSync)('powershell.exe', [
|
|
105
|
+
'-NoLogo', '-NoProfile', '-NonInteractive', '-EncodedCommand',
|
|
106
|
+
Buffer.from(PRIVATE_WINDOWS_ACL, 'utf16le').toString('base64'),
|
|
107
|
+
], {
|
|
108
|
+
input: Buffer.from(JSON.stringify(targets), 'utf8').toString('base64'),
|
|
109
|
+
encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true, timeout: 15000,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Native errors may carry command output. Keep the public error stable and free of values.
|
|
114
|
+
throw new Error('Unable to protect semantic credentials with a private Windows ACL; the new key was not stored.');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
68
117
|
function resolveSemanticKey(opts = {}) {
|
|
69
118
|
const env = opts.env ?? process.env;
|
|
70
119
|
const home = opts.home ?? os.homedir();
|
|
@@ -109,12 +158,40 @@ function storeSemanticKey(key, opts = {}) {
|
|
|
109
158
|
catch { /* fall through to the file */ }
|
|
110
159
|
}
|
|
111
160
|
const file = credFile(home);
|
|
112
|
-
|
|
113
|
-
fs.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
161
|
+
const directory = path.dirname(file);
|
|
162
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
163
|
+
// opts.platform selects a credential source in tests. Permissions must follow the actual host.
|
|
164
|
+
if (process.platform === 'win32') {
|
|
165
|
+
protectWindowsPaths([
|
|
166
|
+
{ path: directory, directory: true },
|
|
167
|
+
...(fs.existsSync(file) ? [{ path: file, directory: false }] : []),
|
|
168
|
+
]);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
fs.chmodSync(directory, 0o700);
|
|
172
|
+
}
|
|
173
|
+
const tmp = `${file}.${(0, node_crypto_1.randomUUID)()}.tmp`;
|
|
174
|
+
let fd;
|
|
175
|
+
let created = false;
|
|
176
|
+
try {
|
|
177
|
+
fd = fs.openSync(tmp, 'wx', 0o600);
|
|
178
|
+
created = true;
|
|
179
|
+
if (process.platform === 'win32')
|
|
180
|
+
protectWindowsPaths([{ path: tmp, directory: false }]);
|
|
181
|
+
else
|
|
182
|
+
fs.fchmodSync(fd, 0o600);
|
|
183
|
+
// The empty file is private before the first secret byte, and the descriptor stays owned.
|
|
184
|
+
fs.writeFileSync(fd, JSON.stringify({ semantic: { provider: 'gemini', key } }));
|
|
185
|
+
fs.closeSync(fd);
|
|
186
|
+
fd = undefined;
|
|
187
|
+
fs.renameSync(tmp, file);
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
if (fd !== undefined)
|
|
191
|
+
fs.closeSync(fd);
|
|
192
|
+
if (created)
|
|
193
|
+
fs.rmSync(tmp, { force: true });
|
|
194
|
+
}
|
|
118
195
|
return 'file';
|
|
119
196
|
}
|
|
120
197
|
function removeSemanticKey(opts = {}) {
|
|
@@ -1,39 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
3
|
exports.makeTierEmbedder = makeTierEmbedder;
|
|
4
|
+
// @implements A-SPEC-591
|
|
37
5
|
// @implements A-SPEC-478
|
|
38
6
|
/**
|
|
39
7
|
* The tier adapters — the productized form of exactly what the measurement scripts proved:
|
|
@@ -54,6 +22,7 @@ exports.makeTierEmbedder = makeTierEmbedder;
|
|
|
54
22
|
*/
|
|
55
23
|
const tier_1 = require("./tier");
|
|
56
24
|
const credentials_1 = require("./credentials");
|
|
25
|
+
const local_model_1 = require("./local-model");
|
|
57
26
|
const CLOUD_BATCH = 25;
|
|
58
27
|
const CLOUD_BACKOFF_MS = 30_000;
|
|
59
28
|
function makeTierEmbedder(tier, cache, opts = {}) {
|
|
@@ -61,15 +30,11 @@ function makeTierEmbedder(tier, cache, opts = {}) {
|
|
|
61
30
|
return null;
|
|
62
31
|
const model = tier.model;
|
|
63
32
|
if (tier.tier === 'local') {
|
|
64
|
-
const tag = `${model}
|
|
33
|
+
const tag = `${model}@${local_model_1.LOCAL_REVISION}-q8-${(0, tier_1.POOLING_OF)(model)}`;
|
|
65
34
|
let pipe = null;
|
|
66
35
|
const load = () => {
|
|
67
36
|
if (pipe === null) {
|
|
68
|
-
pipe = (opts.pipelineLoader ?? (
|
|
69
|
-
const specifier = '@xenova/transformers';
|
|
70
|
-
const mod = await Promise.resolve(`${specifier}`).then(s => __importStar(require(s)));
|
|
71
|
-
return mod.pipeline('feature-extraction', m, { quantized: true });
|
|
72
|
-
}))(model);
|
|
37
|
+
pipe = (opts.pipelineLoader ?? local_model_1.loadLocalPipeline)(model);
|
|
73
38
|
}
|
|
74
39
|
return pipe;
|
|
75
40
|
};
|
|
@@ -79,6 +44,8 @@ function makeTierEmbedder(tier, cache, opts = {}) {
|
|
|
79
44
|
return Array.from((await p(text, { pooling: (0, tier_1.POOLING_OF)(model), normalize: true })).data);
|
|
80
45
|
}
|
|
81
46
|
catch {
|
|
47
|
+
pipe = null;
|
|
48
|
+
(opts.log ?? ((line) => process.stderr.write(`${line}\n`)))('Local embedding unavailable. Run holmes-kit semantic-setup, then holmes-kit semantic-check.');
|
|
82
49
|
return null;
|
|
83
50
|
}
|
|
84
51
|
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { modelCacheDir, modelCacheReady } from './model-cache';
|
|
3
|
+
export { modelCacheDir, modelCacheReady };
|
|
4
|
+
export declare const LOCAL_REVISION = "4de13258303883538bd53b696b452bf8099f0858";
|
|
5
|
+
type Pipeline = (text: string, options: {
|
|
6
|
+
pooling: string;
|
|
7
|
+
normalize: boolean;
|
|
8
|
+
}) => Promise<{
|
|
9
|
+
data: Float32Array;
|
|
10
|
+
}>;
|
|
11
|
+
interface Runtime {
|
|
12
|
+
pipeline(task: string, model: string, options: Record<string, unknown>): Promise<Pipeline>;
|
|
13
|
+
}
|
|
14
|
+
export declare function loadLocalPipeline(model?: string, options?: {
|
|
15
|
+
download?: boolean;
|
|
16
|
+
importer?: () => Promise<Runtime>;
|
|
17
|
+
cacheDir?: string;
|
|
18
|
+
}): Promise<Pipeline>;
|
|
19
|
+
export declare function verifyLocalModel(options?: Parameters<typeof loadLocalPipeline>[1]): Promise<{
|
|
20
|
+
ready: boolean;
|
|
21
|
+
dimension?: number;
|
|
22
|
+
message: string;
|
|
23
|
+
}>;
|
|
24
|
+
/** Isolate setup from the MCP and bound slow downloads/inference; all platforms share this path. */
|
|
25
|
+
export declare function runModelSetup(options?: {
|
|
26
|
+
automatic?: boolean;
|
|
27
|
+
offline?: boolean;
|
|
28
|
+
env?: NodeJS.ProcessEnv;
|
|
29
|
+
spawn?: typeof spawnSync;
|
|
30
|
+
}): number;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LOCAL_REVISION = exports.modelCacheReady = exports.modelCacheDir = void 0;
|
|
4
|
+
exports.loadLocalPipeline = loadLocalPipeline;
|
|
5
|
+
exports.verifyLocalModel = verifyLocalModel;
|
|
6
|
+
exports.runModelSetup = runModelSetup;
|
|
7
|
+
// @implements A-SPEC-591
|
|
8
|
+
const node_child_process_1 = require("node:child_process");
|
|
9
|
+
const tier_1 = require("./tier");
|
|
10
|
+
const model_cache_1 = require("./model-cache");
|
|
11
|
+
Object.defineProperty(exports, "modelCacheDir", { enumerable: true, get: function () { return model_cache_1.modelCacheDir; } });
|
|
12
|
+
Object.defineProperty(exports, "modelCacheReady", { enumerable: true, get: function () { return model_cache_1.modelCacheReady; } });
|
|
13
|
+
exports.LOCAL_REVISION = '4de13258303883538bd53b696b452bf8099f0858';
|
|
14
|
+
// TypeScript's CommonJS transform rewrites import() into require(), which cannot load this ESM
|
|
15
|
+
// dependency on supported Node 20 versions. Preserve native dynamic import without shell code.
|
|
16
|
+
const importRuntime = new Function('return import("@xenova/transformers")');
|
|
17
|
+
const pipelines = new WeakMap();
|
|
18
|
+
async function loadLocalPipeline(model = tier_1.LOCAL_MODEL, options = {}) {
|
|
19
|
+
const importer = options.importer ?? importRuntime;
|
|
20
|
+
const cacheDir = options.cacheDir ?? (0, model_cache_1.modelCacheDir)();
|
|
21
|
+
const key = JSON.stringify([model, cacheDir, !!options.download]);
|
|
22
|
+
let cache = pipelines.get(importer);
|
|
23
|
+
if (!cache) {
|
|
24
|
+
cache = new Map();
|
|
25
|
+
pipelines.set(importer, cache);
|
|
26
|
+
}
|
|
27
|
+
const existing = cache.get(key);
|
|
28
|
+
if (existing)
|
|
29
|
+
return existing;
|
|
30
|
+
const pending = importer().then(runtime => runtime.pipeline('feature-extraction', model, {
|
|
31
|
+
quantized: true,
|
|
32
|
+
revision: model === tier_1.LOCAL_MODEL ? exports.LOCAL_REVISION : 'main',
|
|
33
|
+
cache_dir: cacheDir,
|
|
34
|
+
local_files_only: !options.download,
|
|
35
|
+
})).catch(error => { cache.delete(key); throw error; });
|
|
36
|
+
cache.set(key, pending);
|
|
37
|
+
return pending;
|
|
38
|
+
}
|
|
39
|
+
async function verifyLocalModel(options = {}) {
|
|
40
|
+
try {
|
|
41
|
+
const pipe = await loadLocalPipeline(tier_1.LOCAL_MODEL, options);
|
|
42
|
+
// Fixed public probe: no repository data is involved in setup.
|
|
43
|
+
const { data } = await pipe('Holmes-Kit local model verification', { pooling: (0, tier_1.POOLING_OF)(tier_1.LOCAL_MODEL), normalize: true });
|
|
44
|
+
const norm = Math.hypot(...data);
|
|
45
|
+
if (data.length !== 1024 || !Array.from(data).every(Number.isFinite) || Math.abs(norm - 1) > 0.001) {
|
|
46
|
+
return { ready: false, dimension: data.length, message: 'BGE-M3 produced an invalid embedding. Select a clean HOLMES_MODEL_CACHE and run holmes-kit semantic-setup.' };
|
|
47
|
+
}
|
|
48
|
+
return { ready: true, dimension: data.length, message: 'BGE-M3 ready: verified 1024-dimensional normalized local embedding.' };
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const sharp = String(error instanceof Error ? error.message : '').includes('sharp');
|
|
52
|
+
return { ready: false, message: sharp
|
|
53
|
+
? 'BGE-M3 runtime unavailable: sharp native installation failed. Review npm install-scripts ls and approve the sharp version, rebuild it, then run holmes-kit semantic-setup.'
|
|
54
|
+
: 'BGE-M3 unavailable: runtime/model load failed. Check network access during setup and cache permissions, then run holmes-kit semantic-setup; use semantic-check to verify offline.' };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Isolate setup from the MCP and bound slow downloads/inference; all platforms share this path. */
|
|
58
|
+
function runModelSetup(options = {}) {
|
|
59
|
+
const env = options.env ?? process.env;
|
|
60
|
+
// @implements A-SPEC-594 — the automatic path does not start a large download the user did not
|
|
61
|
+
// ask for. An explicit refusal outranks an opt-in: someone who set SKIP meant it, and reading
|
|
62
|
+
// AUTO as permission to override that would silently reverse their decision. The two skips say
|
|
63
|
+
// DIFFERENT things, because telling a user "you opted out" when they never did is a lie.
|
|
64
|
+
if (options.automatic) {
|
|
65
|
+
if (env.HOLMES_SKIP_MODEL_INSTALL === '1') {
|
|
66
|
+
process.stderr.write('BGE-M3 automatic setup skipped (HOLMES_SKIP_MODEL_INSTALL=1). Run holmes-kit semantic-setup when ready.\n');
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
if (env.HOLMES_AUTO_MODEL_INSTALL !== '1') {
|
|
70
|
+
process.stderr.write('BGE-M3 is not prepared automatically. Run `holmes-kit semantic-setup` to download the public model once (shared across projects), or set HOLMES_AUTO_MODEL_INSTALL=1 to prepare it during installation. Graph features work without it.\n');
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
process.stderr.write(options.offline ? 'Checking BGE-M3 offline...\n' : 'Preparing BGE-M3 (public model download on first installation)...\n');
|
|
75
|
+
const result = (options.spawn ?? node_child_process_1.spawnSync)(process.execPath, [__filename, '--worker', ...(options.offline ? ['--offline'] : [])], { env, stdio: 'inherit', timeout: 900_000, windowsHide: true });
|
|
76
|
+
if (result.error || result.status !== 0) {
|
|
77
|
+
process.stderr.write('BGE-M3 setup/check failed or timed out. Graph features remain available; run holmes-kit semantic-setup to retry.\n');
|
|
78
|
+
return options.automatic ? 0 : 1;
|
|
79
|
+
}
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
if (require.main === module) {
|
|
83
|
+
if (process.argv.includes('--worker')) {
|
|
84
|
+
verifyLocalModel({ download: !process.argv.includes('--offline') }).then(result => {
|
|
85
|
+
process[result.ready ? 'stdout' : 'stderr'].write(`${result.message}\n`);
|
|
86
|
+
process.exitCode = result.ready ? 0 : 1;
|
|
87
|
+
}).catch(() => { process.stderr.write('BGE-M3 verification failed. Run holmes-kit semantic-setup.\n'); process.exitCode = 1; });
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
process.exitCode = runModelSetup({ automatic: process.argv.includes('--automatic'), offline: process.argv.includes('--offline') });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function modelCacheDir(env?: NodeJS.ProcessEnv, home?: string): string;
|
|
2
|
+
/**
|
|
3
|
+
* Ready means the cache holds SOMETHING. An interrupted download leaves the directory created
|
|
4
|
+
* and empty, and reading that as ready is the whole failure mode this predicate exists to stop.
|
|
5
|
+
* Proof of a working model is `semantic-check`'s actual inference — deliberately not here, because
|
|
6
|
+
* doctor calls this on every run and must stay cheap and offline.
|
|
7
|
+
*/
|
|
8
|
+
export declare function modelCacheReady(cacheDir?: string): boolean;
|