@danielblomma/cortex-mcp 2.2.5 → 2.4.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/CHANGELOG.md +201 -0
- package/README.md +73 -3
- package/bin/cortex.mjs +339 -242
- package/bin/daemon-control.mjs +162 -0
- package/package.json +7 -3
- package/scaffold/mcp/dist/.cortex-build-hash +1 -0
- package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
- package/scaffold/mcp/dist/cli/govern.js +937 -0
- package/scaffold/mcp/dist/cli/query.js +409 -0
- package/scaffold/mcp/dist/cli/run.js +295 -0
- package/scaffold/mcp/dist/cli/stage.js +308 -0
- package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
- package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
- package/scaffold/mcp/dist/contextEntities.js +282 -0
- package/scaffold/mcp/dist/core/audit/query.js +72 -0
- package/scaffold/mcp/dist/core/audit/writer.js +28 -0
- package/scaffold/mcp/dist/core/config.js +235 -0
- package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
- package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
- package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
- package/scaffold/mcp/dist/core/govern-paths.js +21 -0
- package/scaffold/mcp/dist/core/index.js +14 -0
- package/scaffold/mcp/dist/core/license.js +278 -0
- package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
- package/scaffold/mcp/dist/core/policy/injection.js +172 -0
- package/scaffold/mcp/dist/core/policy/store.js +179 -0
- package/scaffold/mcp/dist/core/rbac/check.js +30 -0
- package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
- package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
- package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
- package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
- package/scaffold/mcp/dist/core/validators/config.js +44 -0
- package/scaffold/mcp/dist/core/validators/engine.js +120 -0
- package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
- package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
- package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
- package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
- package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
- package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
- package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
- package/scaffold/mcp/dist/core/workflow/index.js +11 -0
- package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
- package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
- package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
- package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
- package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
- package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
- package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
- package/scaffold/mcp/dist/daemon/client.js +130 -0
- package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
- package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
- package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
- package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
- package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
- package/scaffold/mcp/dist/daemon/main.js +449 -0
- package/scaffold/mcp/dist/daemon/paths.js +36 -0
- package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
- package/scaffold/mcp/dist/daemon/protocol.js +8 -0
- package/scaffold/mcp/dist/daemon/server.js +180 -0
- package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
- package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
- package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
- package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
- package/scaffold/mcp/dist/defaults.js +6 -0
- package/scaffold/mcp/dist/embed.js +627 -0
- package/scaffold/mcp/dist/embedScheduler.js +479 -0
- package/scaffold/mcp/dist/embeddings.js +167 -0
- package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
- package/scaffold/mcp/dist/enterprise/index.js +327 -0
- package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
- package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
- package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
- package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
- package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
- package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
- package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
- package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
- package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
- package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
- package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
- package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
- package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
- package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
- package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
- package/scaffold/mcp/dist/frontmatter.js +33 -0
- package/scaffold/mcp/dist/graph.js +769 -0
- package/scaffold/mcp/dist/graphCsv.js +55 -0
- package/scaffold/mcp/dist/graphMetrics.js +8 -0
- package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
- package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
- package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
- package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
- package/scaffold/mcp/dist/hooks/session-end.js +43 -0
- package/scaffold/mcp/dist/hooks/session-start.js +41 -0
- package/scaffold/mcp/dist/hooks/shared.js +194 -0
- package/scaffold/mcp/dist/hooks/stop.js +28 -0
- package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
- package/scaffold/mcp/dist/impactPresentation.js +137 -0
- package/scaffold/mcp/dist/impactRanking.js +191 -0
- package/scaffold/mcp/dist/impactResponse.js +30 -0
- package/scaffold/mcp/dist/impactResults.js +105 -0
- package/scaffold/mcp/dist/impactSeed.js +20 -0
- package/scaffold/mcp/dist/impactTraversal.js +64 -0
- package/scaffold/mcp/dist/jsonl.js +77 -0
- package/scaffold/mcp/dist/loadGraph.js +759 -0
- package/scaffold/mcp/dist/lruCache.js +38 -0
- package/scaffold/mcp/dist/paths.js +97 -0
- package/scaffold/mcp/dist/patternEvidence.js +272 -0
- package/scaffold/mcp/dist/plugin.js +81 -0
- package/scaffold/mcp/dist/presets.js +78 -0
- package/scaffold/mcp/dist/relatedResponse.js +18 -0
- package/scaffold/mcp/dist/relatedTraversal.js +78 -0
- package/scaffold/mcp/dist/rules.js +23 -0
- package/scaffold/mcp/dist/search.js +212 -0
- package/scaffold/mcp/dist/searchCore.js +457 -0
- package/scaffold/mcp/dist/searchResults.js +230 -0
- package/scaffold/mcp/dist/server.js +317 -0
- package/scaffold/mcp/dist/types.js +1 -0
- package/scaffold/mcp/package-lock.json +336 -212
- package/scaffold/mcp/package.json +15 -6
- package/scaffold/mcp/src/cli/enterprise-setup.ts +82 -8
- package/scaffold/mcp/src/cli/govern.ts +137 -52
- package/scaffold/mcp/src/cli/query.ts +36 -2
- package/scaffold/mcp/src/cli/run.ts +53 -19
- package/scaffold/mcp/src/cli/stage.ts +8 -3
- package/scaffold/mcp/src/core/config.ts +9 -2
- package/scaffold/mcp/src/core/enterprise-host-identity.ts +259 -0
- package/scaffold/mcp/src/core/enterprise-identity.ts +20 -0
- package/scaffold/mcp/src/core/enterprise-rotation.ts +185 -0
- package/scaffold/mcp/src/core/govern-paths.ts +30 -0
- package/scaffold/mcp/src/core/license.ts +186 -17
- package/scaffold/mcp/src/core/secure-endpoint.ts +23 -0
- package/scaffold/mcp/src/core/workflow/enforcement.ts +8 -1
- package/scaffold/mcp/src/core/workflow/mcp-tools.ts +3 -0
- package/scaffold/mcp/src/core/workflow/resolution.ts +9 -1
- package/scaffold/mcp/src/core/workflow/synced-capability-registry.ts +15 -0
- package/scaffold/mcp/src/core/workflow/synced-registry.ts +15 -0
- package/scaffold/mcp/src/daemon/capability-sync-checker.ts +38 -2
- package/scaffold/mcp/src/daemon/egress-proxy.ts +14 -8
- package/scaffold/mcp/src/daemon/global-host-events.ts +288 -0
- package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +4 -0
- package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +2 -0
- package/scaffold/mcp/src/daemon/host-events-pusher.ts +5 -0
- package/scaffold/mcp/src/daemon/main.ts +99 -25
- package/scaffold/mcp/src/daemon/project-service-registry.ts +107 -0
- package/scaffold/mcp/src/daemon/skill-sync-checker.ts +368 -31
- package/scaffold/mcp/src/daemon/sync-checker.ts +4 -0
- package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +57 -25
- package/scaffold/mcp/src/daemon/workflow-sync-checker.ts +41 -2
- package/scaffold/mcp/src/embed.ts +211 -10
- package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
- package/scaffold/mcp/src/enterprise/index.ts +1 -1
- package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
- package/scaffold/mcp/src/enterprise/reviews/changed-files.ts +34 -0
- package/scaffold/mcp/src/enterprise/reviews/pattern-context.ts +231 -0
- package/scaffold/mcp/src/enterprise/reviews/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/reviews/trust-state.ts +3 -1
- package/scaffold/mcp/src/enterprise/telemetry/sync.ts +9 -0
- package/scaffold/mcp/src/enterprise/tools/enterprise.ts +49 -36
- package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
- package/scaffold/mcp/src/paths.ts +3 -5
- package/scaffold/mcp/src/patternEvidence.ts +347 -0
- package/scaffold/mcp/src/plugin.ts +20 -0
- package/scaffold/mcp/src/search.ts +35 -9
- package/scaffold/mcp/src/searchCore.ts +254 -12
- package/scaffold/mcp/src/searchResults.ts +96 -9
- package/scaffold/mcp/src/types.ts +7 -0
- package/scaffold/mcp/tests/changed-files.test.mjs +41 -0
- package/scaffold/mcp/tests/copilot-shim.test.mjs +19 -1
- package/scaffold/mcp/tests/egress-proxy.test.mjs +37 -0
- package/scaffold/mcp/tests/embed-entities.test.mjs +109 -1
- package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
- package/scaffold/mcp/tests/enterprise-pattern-context.test.mjs +322 -0
- package/scaffold/mcp/tests/enterprise-setup.test.mjs +189 -0
- package/scaffold/mcp/tests/global-host-events.test.mjs +81 -0
- package/scaffold/mcp/tests/govern-install.test.mjs +95 -2
- package/scaffold/mcp/tests/govern-repair.test.mjs +20 -3
- package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +26 -0
- package/scaffold/mcp/tests/license.test.mjs +367 -0
- package/scaffold/mcp/tests/paths.test.mjs +11 -3
- package/scaffold/mcp/tests/pattern-evidence.test.mjs +321 -0
- package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
- package/scaffold/mcp/tests/query-cli.test.mjs +73 -1
- package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
- package/scaffold/mcp/tests/search-graph-score.test.mjs +167 -0
- package/scaffold/mcp/tests/secure-enterprise-endpoint.test.mjs +46 -0
- package/scaffold/mcp/tests/skill-sync-checker.test.mjs +446 -2
- package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +104 -22
- package/scaffold/mcp/tests/workflow-cli.test.mjs +14 -1
- package/scaffold/mcp/tests/workflow-synced-capabilities.test.mjs +37 -0
- package/scaffold/mcp/tests/workflow-synced-registry.test.mjs +40 -1
- package/scaffold/scripts/dashboard.mjs +29 -18
- package/scaffold/scripts/doctor.sh +26 -8
- package/scaffold/scripts/ingest.mjs +37 -4
- package/scaffold/scripts/status.sh +13 -6
package/bin/cortex.mjs
CHANGED
|
@@ -5,6 +5,11 @@ import path from "node:path";
|
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
import { normalizeProjectRoot } from "./wsl.mjs";
|
|
8
|
+
import {
|
|
9
|
+
callDaemon,
|
|
10
|
+
probeVerifiedDaemon,
|
|
11
|
+
stopVerifiedDaemon,
|
|
12
|
+
} from "./daemon-control.mjs";
|
|
8
13
|
import {
|
|
9
14
|
bullet,
|
|
10
15
|
printBullet,
|
|
@@ -81,20 +86,21 @@ function printHelp() {
|
|
|
81
86
|
console.log(helpRow("impact <query|entity-id> [--json]", "Trace likely impact paths"));
|
|
82
87
|
console.log(helpRow("rules [--json]", "List active context rules"));
|
|
83
88
|
console.log(helpRow("explain <query|entity-id> [--json]", "Show search score evidence"));
|
|
89
|
+
console.log(helpRow("pattern-evidence <file|entity-id> [--query <text>] [--top-k <n>] [--json]", "Collect cited repo-local pattern evidence"));
|
|
84
90
|
console.log(helpRow("dashboard [--interval <sec>]", "Live local dashboard"));
|
|
85
91
|
console.log(helpRow("memory-compile [--dry-run] [--verbose]", "Compile memory artifacts"));
|
|
86
92
|
console.log(helpRow("memory-lint [--verbose] [--json]", "Lint compiled memory"));
|
|
87
93
|
console.log(helpRow("watch [start|stop|status|run|once]", "Background sync (--interval, --debounce, --mode)"));
|
|
88
94
|
|
|
89
95
|
console.log(helpSection("GOVERNANCE"));
|
|
90
|
-
console.log(helpRow("enterprise
|
|
96
|
+
console.log(helpRow("enterprise install --api-key-stdin", "Install enforcement + hooks + daemon (sudo)"));
|
|
91
97
|
console.log(helpRow(" ", "[--endpoint <url>] [--frameworks <csv>] [--no-hooks] [--no-daemon]"));
|
|
92
98
|
console.log(helpRow("enterprise status", "Show local enforcement state"));
|
|
93
99
|
console.log(helpRow("enterprise sync", "Force re-fetch + re-apply (sudo)"));
|
|
94
100
|
console.log(helpRow("enterprise uninstall", "Remove enforcement (sudo, --break-glass --reason)"));
|
|
95
101
|
console.log(helpRow("enterprise repair", "Verify managed paths, clear tamper-lock (sudo)"));
|
|
96
102
|
console.log(helpRow("run <claude|codex|copilot> [args...]", "Wrap an AI CLI in cortex enforcement"));
|
|
97
|
-
console.log(helpRow("daemon [start|stop|status]", "Local supervisor daemon"));
|
|
103
|
+
console.log(helpRow("daemon [start|stop|restart|status]", "Local supervisor daemon"));
|
|
98
104
|
console.log(helpRow("hooks [install|uninstall|status] [--project]", "Claude Code hooks"));
|
|
99
105
|
console.log(helpRow("telemetry test", "Smoke-test the push pipeline"));
|
|
100
106
|
|
|
@@ -198,81 +204,7 @@ function ensureScaffoldExists() {
|
|
|
198
204
|
// Files that should never be overwritten if they already exist in the target.
|
|
199
205
|
// These contain user-specific configuration that would be lost on re-init.
|
|
200
206
|
const PRESERVE_FILES = new Set(["config.yaml", "rules.yaml", "enterprise.yml", "enterprise.yaml", "CLAUDE.md", "AGENTS.md"]);
|
|
201
|
-
const DEFAULT_SOURCE_PATHS = [
|
|
202
|
-
"src",
|
|
203
|
-
"docs",
|
|
204
|
-
"design",
|
|
205
|
-
".context/notes",
|
|
206
|
-
".context/decisions",
|
|
207
|
-
"README.md"
|
|
208
|
-
];
|
|
209
|
-
const INIT_SKIP_DIRECTORIES = new Set([
|
|
210
|
-
".git",
|
|
211
|
-
".idea",
|
|
212
|
-
".vscode",
|
|
213
|
-
"node_modules",
|
|
214
|
-
"dist",
|
|
215
|
-
"build",
|
|
216
|
-
"coverage",
|
|
217
|
-
".next",
|
|
218
|
-
".cache",
|
|
219
|
-
".context",
|
|
220
|
-
"scripts",
|
|
221
|
-
".githooks",
|
|
222
|
-
"bin",
|
|
223
|
-
"obj"
|
|
224
|
-
]);
|
|
225
|
-
const INIT_SOURCE_EXTENSIONS = new Set([
|
|
226
|
-
".md",
|
|
227
|
-
".mdx",
|
|
228
|
-
".txt",
|
|
229
|
-
".adoc",
|
|
230
|
-
".rst",
|
|
231
|
-
".yaml",
|
|
232
|
-
".yml",
|
|
233
|
-
".json",
|
|
234
|
-
".toml",
|
|
235
|
-
".csv",
|
|
236
|
-
".ts",
|
|
237
|
-
".tsx",
|
|
238
|
-
".mts",
|
|
239
|
-
".cts",
|
|
240
|
-
".js",
|
|
241
|
-
".jsx",
|
|
242
|
-
".mjs",
|
|
243
|
-
".cjs",
|
|
244
|
-
".py",
|
|
245
|
-
".go",
|
|
246
|
-
".java",
|
|
247
|
-
".cs",
|
|
248
|
-
".vb",
|
|
249
|
-
".sln",
|
|
250
|
-
".vbproj",
|
|
251
|
-
".csproj",
|
|
252
|
-
".fsproj",
|
|
253
|
-
".props",
|
|
254
|
-
".targets",
|
|
255
|
-
".config",
|
|
256
|
-
".resx",
|
|
257
|
-
".settings",
|
|
258
|
-
".rb",
|
|
259
|
-
".rs",
|
|
260
|
-
".php",
|
|
261
|
-
".swift",
|
|
262
|
-
".kt",
|
|
263
|
-
".sql",
|
|
264
|
-
".sh",
|
|
265
|
-
".bash",
|
|
266
|
-
".zsh",
|
|
267
|
-
".ps1",
|
|
268
|
-
".c",
|
|
269
|
-
".h",
|
|
270
|
-
".cpp",
|
|
271
|
-
".hpp",
|
|
272
|
-
".cc",
|
|
273
|
-
".hh"
|
|
274
|
-
]);
|
|
275
|
-
const ROOT_DOC_PATHS = new Set(["docs", "design"]);
|
|
207
|
+
const DEFAULT_SOURCE_PATHS = ["."];
|
|
276
208
|
|
|
277
209
|
function copyDirectory(sourceDir, targetDir) {
|
|
278
210
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
@@ -318,92 +250,8 @@ function slugifyRepoId(value) {
|
|
|
318
250
|
return dashed || "cortex";
|
|
319
251
|
}
|
|
320
252
|
|
|
321
|
-
function isInterestingSourceFile(fileName) {
|
|
322
|
-
const base = fileName.toLowerCase();
|
|
323
|
-
const ext = path.extname(fileName).toLowerCase();
|
|
324
|
-
return INIT_SOURCE_EXTENSIONS.has(ext) || base === "readme" || base.startsWith("readme.");
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function directoryContainsInterestingFiles(directoryPath) {
|
|
328
|
-
const stack = [directoryPath];
|
|
329
|
-
while (stack.length > 0) {
|
|
330
|
-
const current = stack.pop();
|
|
331
|
-
let entries = [];
|
|
332
|
-
try {
|
|
333
|
-
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
334
|
-
} catch {
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
for (const entry of entries) {
|
|
339
|
-
const absolutePath = path.join(current, entry.name);
|
|
340
|
-
if (entry.isDirectory()) {
|
|
341
|
-
if (INIT_SKIP_DIRECTORIES.has(entry.name)) {
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
stack.push(absolutePath);
|
|
345
|
-
continue;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (entry.isFile() && isInterestingSourceFile(entry.name)) {
|
|
349
|
-
return true;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
return false;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
253
|
function detectInitialSourcePaths(targetDir) {
|
|
358
|
-
|
|
359
|
-
return [...DEFAULT_SOURCE_PATHS];
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
let entries = [];
|
|
363
|
-
try {
|
|
364
|
-
entries = fs.readdirSync(targetDir, { withFileTypes: true });
|
|
365
|
-
} catch {
|
|
366
|
-
return [...DEFAULT_SOURCE_PATHS];
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const codeDirs = [];
|
|
370
|
-
const docDirs = [];
|
|
371
|
-
const rootFiles = [];
|
|
372
|
-
|
|
373
|
-
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
374
|
-
const absolutePath = path.join(targetDir, entry.name);
|
|
375
|
-
|
|
376
|
-
if (entry.isDirectory()) {
|
|
377
|
-
if (INIT_SKIP_DIRECTORIES.has(entry.name)) {
|
|
378
|
-
continue;
|
|
379
|
-
}
|
|
380
|
-
if (!directoryContainsInterestingFiles(absolutePath)) {
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const bucket = ROOT_DOC_PATHS.has(entry.name) ? docDirs : codeDirs;
|
|
385
|
-
bucket.push(toPosixPath(entry.name));
|
|
386
|
-
continue;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
if (entry.isFile() && isInterestingSourceFile(entry.name)) {
|
|
390
|
-
rootFiles.push(toPosixPath(entry.name));
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const readmeFiles = rootFiles.filter((filePath) => /^readme(\.|$)/i.test(path.basename(filePath)));
|
|
395
|
-
const nonReadmeRootFiles = rootFiles.filter((filePath) => !readmeFiles.includes(filePath));
|
|
396
|
-
const detected = [
|
|
397
|
-
...codeDirs,
|
|
398
|
-
...nonReadmeRootFiles,
|
|
399
|
-
...docDirs,
|
|
400
|
-
".context/notes",
|
|
401
|
-
".context/decisions",
|
|
402
|
-
...readmeFiles
|
|
403
|
-
];
|
|
404
|
-
const uniqueDetected = [...new Set(detected)];
|
|
405
|
-
const hasConcreteRepoContent = uniqueDetected.some((value) => !value.startsWith(".context/"));
|
|
406
|
-
return hasConcreteRepoContent ? uniqueDetected : [...DEFAULT_SOURCE_PATHS];
|
|
254
|
+
return [...DEFAULT_SOURCE_PATHS];
|
|
407
255
|
}
|
|
408
256
|
|
|
409
257
|
function buildInitialConfig(targetDir) {
|
|
@@ -418,12 +266,10 @@ function buildInitialConfig(targetDir) {
|
|
|
418
266
|
" - RULE",
|
|
419
267
|
" - CODE",
|
|
420
268
|
" - WIKI",
|
|
421
|
-
"#
|
|
422
|
-
"# constrain an entity (docs are hubs, leaf code is not). Tuned as a pair with",
|
|
423
|
-
"# the midrank-percentile graph_score.",
|
|
269
|
+
"# Tuned together with the midrank-percentile graph_score in searchResults.ts.",
|
|
424
270
|
"ranking:",
|
|
425
|
-
" semantic: 0.
|
|
426
|
-
" graph: 0.
|
|
271
|
+
" semantic: 0.40",
|
|
272
|
+
" graph: 0.25",
|
|
427
273
|
" trust: 0.20",
|
|
428
274
|
" recency: 0.15",
|
|
429
275
|
"runtime:",
|
|
@@ -443,6 +289,31 @@ function initializeScaffold(targetDir, force) {
|
|
|
443
289
|
}
|
|
444
290
|
}
|
|
445
291
|
|
|
292
|
+
function hardenEnterpriseConfigPermissions(targetDir) {
|
|
293
|
+
const contextDir = path.join(targetDir, ".context");
|
|
294
|
+
for (const filename of ["enterprise.yml", "enterprise.yaml"]) {
|
|
295
|
+
const configPath = path.join(contextDir, filename);
|
|
296
|
+
let stat;
|
|
297
|
+
try {
|
|
298
|
+
stat = fs.lstatSync(configPath);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
if (err && typeof err === "object" && err.code === "ENOENT") continue;
|
|
301
|
+
throw err;
|
|
302
|
+
}
|
|
303
|
+
if (stat.isSymbolicLink()) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`Refusing symlinked Enterprise configuration: ${configPath}`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
if (!stat.isFile()) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
`Enterprise configuration is not a regular file: ${configPath}`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
fs.chmodSync(configPath, 0o600);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
446
317
|
function ensurePathWritable(targetPath, force) {
|
|
447
318
|
if (!fs.existsSync(targetPath)) {
|
|
448
319
|
return;
|
|
@@ -645,6 +516,10 @@ function installCodexAgentsSection(targetDir) {
|
|
|
645
516
|
const startMarker = "<!-- cortex:auto:start -->";
|
|
646
517
|
const endMarker = "<!-- cortex:auto:end -->";
|
|
647
518
|
const section = `## Cortex Auto Workflow
|
|
519
|
+
- Use the \`using-cortex\` skill if available; otherwise follow the commands below.
|
|
520
|
+
- Search before answering code questions: \`cortex search "<query>" --json\`; never answer from memory.
|
|
521
|
+
- Check \`cortex rules --json\` before suggesting changes and \`cortex impact "<query>" --json\` before refactors.
|
|
522
|
+
- Review changed files with \`cortex pattern-evidence <file> --json\` before finalizing.
|
|
648
523
|
- Run \`cortex update\` before completing substantial code changes.
|
|
649
524
|
- If background sync is enabled, check with \`cortex watch status\`.`;
|
|
650
525
|
const changed = upsertSectionByMarkers(agentsPath, startMarker, endMarker, section);
|
|
@@ -956,12 +831,31 @@ async function ensureProjectInitializedForMcp(targetDir) {
|
|
|
956
831
|
}
|
|
957
832
|
}
|
|
958
833
|
|
|
834
|
+
// The session bootstrap hook caches index status for 10 minutes; drop the
|
|
835
|
+
// cache whenever a command may have changed the index so new sessions never
|
|
836
|
+
// see a stale "no index"/"index is old" verdict.
|
|
837
|
+
const INDEX_MUTATING_COMMANDS = new Set(["bootstrap", "update", "refresh", "ingest", "embed", "graph-load"]);
|
|
838
|
+
|
|
839
|
+
function invalidateSessionStatusCache(cwd) {
|
|
840
|
+
try {
|
|
841
|
+
fs.rmSync(path.join(cwd, ".context", "cache", "session-status.json"), { force: true });
|
|
842
|
+
} catch {
|
|
843
|
+
// best effort: a stale cache only delays the status refresh
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
959
847
|
async function runContextCommand(cwd, contextArgs) {
|
|
960
848
|
const contextScript = path.join(cwd, CONTEXT_SCRIPTS_REL, "context.sh");
|
|
961
849
|
if (!fs.existsSync(contextScript)) {
|
|
962
850
|
throw new Error(`Missing ${contextScript}. Run 'cortex init' first.`);
|
|
963
851
|
}
|
|
964
|
-
|
|
852
|
+
try {
|
|
853
|
+
await runCommand("bash", [contextScript, ...contextArgs], cwd);
|
|
854
|
+
} finally {
|
|
855
|
+
if (INDEX_MUTATING_COMMANDS.has(contextArgs[0])) {
|
|
856
|
+
invalidateSessionStatusCache(cwd);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
965
859
|
}
|
|
966
860
|
|
|
967
861
|
async function run() {
|
|
@@ -987,6 +881,7 @@ async function run() {
|
|
|
987
881
|
printBanner("Cortex initializes repo-scoped context for AI coding agents.");
|
|
988
882
|
fs.mkdirSync(target, { recursive: true });
|
|
989
883
|
initializeScaffold(target, force);
|
|
884
|
+
hardenEnterpriseConfigPermissions(target);
|
|
990
885
|
const helpers = installAssistantHelpers(target);
|
|
991
886
|
await maybeInstallGitHooks(target);
|
|
992
887
|
|
|
@@ -1027,6 +922,7 @@ async function run() {
|
|
|
1027
922
|
|
|
1028
923
|
if (bootstrap) {
|
|
1029
924
|
await runContextCommand(target, ["bootstrap"]);
|
|
925
|
+
await restartDaemonAfterRuntimeUpgrade(target);
|
|
1030
926
|
}
|
|
1031
927
|
|
|
1032
928
|
if (connect) {
|
|
@@ -1117,23 +1013,34 @@ async function run() {
|
|
|
1117
1013
|
}
|
|
1118
1014
|
|
|
1119
1015
|
await maybeMigrateScaffold(process.cwd(), command);
|
|
1016
|
+
if (command === "bootstrap") {
|
|
1017
|
+
hardenEnterpriseConfigPermissions(process.cwd());
|
|
1018
|
+
}
|
|
1120
1019
|
await runContextCommand(process.cwd(), [command, ...rest]);
|
|
1020
|
+
if (command === "bootstrap") {
|
|
1021
|
+
await restartDaemonAfterRuntimeUpgrade(process.cwd());
|
|
1022
|
+
}
|
|
1121
1023
|
}
|
|
1122
1024
|
|
|
1123
1025
|
// ---------------------------------------------------------------------------
|
|
1124
1026
|
// v2.0.0: daemon + hooks commands
|
|
1125
1027
|
// ---------------------------------------------------------------------------
|
|
1126
1028
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1029
|
+
function daemonDirPath() {
|
|
1030
|
+
return path.join(process.env.HOME || os.homedir(), ".cortex");
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function daemonPidFilePath() {
|
|
1034
|
+
return path.join(daemonDirPath(), "daemon.pid");
|
|
1035
|
+
}
|
|
1129
1036
|
|
|
1130
1037
|
function pidFileExists() {
|
|
1131
|
-
return fs.existsSync(
|
|
1038
|
+
return fs.existsSync(daemonPidFilePath());
|
|
1132
1039
|
}
|
|
1133
1040
|
|
|
1134
1041
|
function readPid() {
|
|
1135
1042
|
try {
|
|
1136
|
-
const raw = fs.readFileSync(
|
|
1043
|
+
const raw = fs.readFileSync(daemonPidFilePath(), "utf8").trim();
|
|
1137
1044
|
const pid = Number.parseInt(raw, 10);
|
|
1138
1045
|
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
1139
1046
|
} catch {
|
|
@@ -1154,20 +1061,23 @@ function isPidAlive(pid) {
|
|
|
1154
1061
|
}
|
|
1155
1062
|
}
|
|
1156
1063
|
|
|
1157
|
-
function resolveProjectRuntimeDist() {
|
|
1064
|
+
function resolveProjectRuntimeDist(projectRoot) {
|
|
1158
1065
|
// v2.0.5: project layout was moved from <cwd>/mcp/ to <cwd>/.context/mcp/.
|
|
1159
1066
|
// The runtime still lives there for compatibility, but CLI commands now
|
|
1160
1067
|
// treat it as the local context runtime rather than an MCP-only surface.
|
|
1161
|
-
const target =
|
|
1068
|
+
const target =
|
|
1069
|
+
projectRoot ||
|
|
1070
|
+
process.env.CORTEX_PROJECT_ROOT?.trim() ||
|
|
1071
|
+
process.cwd();
|
|
1162
1072
|
return path.join(target, CONTEXT_RUNTIME_REL, "dist");
|
|
1163
1073
|
}
|
|
1164
1074
|
|
|
1165
|
-
function resolveProjectMcpDist() {
|
|
1166
|
-
return resolveProjectRuntimeDist();
|
|
1075
|
+
function resolveProjectMcpDist(projectRoot) {
|
|
1076
|
+
return resolveProjectRuntimeDist(projectRoot);
|
|
1167
1077
|
}
|
|
1168
1078
|
|
|
1169
|
-
function resolveDaemonEntry() {
|
|
1170
|
-
return path.join(resolveProjectRuntimeDist(), "daemon", "main.js");
|
|
1079
|
+
function resolveDaemonEntry(projectRoot) {
|
|
1080
|
+
return path.join(resolveProjectRuntimeDist(projectRoot), "daemon", "main.js");
|
|
1171
1081
|
}
|
|
1172
1082
|
|
|
1173
1083
|
function resolveHookEntry(name) {
|
|
@@ -1178,54 +1088,116 @@ function resolveCliEntry(name) {
|
|
|
1178
1088
|
return path.join(resolveProjectRuntimeDist(), "cli", `${name}.js`);
|
|
1179
1089
|
}
|
|
1180
1090
|
|
|
1181
|
-
|
|
1091
|
+
function resolveTrustedCliEntry(name) {
|
|
1092
|
+
return path.join(SCAFFOLD_ROOT, "mcp", "dist", "cli", `${name}.js`);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
async function daemonControlDeps() {
|
|
1096
|
+
return {
|
|
1097
|
+
readPid,
|
|
1098
|
+
isPidAlive,
|
|
1099
|
+
call: callDaemon,
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
async function waitForVerifiedDaemon(deps, timeoutMs = 5_000) {
|
|
1104
|
+
const deadline = Date.now() + timeoutMs;
|
|
1105
|
+
while (Date.now() < deadline) {
|
|
1106
|
+
const probe = await probeVerifiedDaemon(deps, 250);
|
|
1107
|
+
if (probe.verified) return probe;
|
|
1108
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1109
|
+
}
|
|
1110
|
+
return probeVerifiedDaemon(deps, 250);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
async function runDaemonCommand(args, options = {}) {
|
|
1182
1114
|
const sub = args[0] || "status";
|
|
1115
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
1116
|
+
const deps = await daemonControlDeps();
|
|
1183
1117
|
if (sub === "start") {
|
|
1184
|
-
|
|
1118
|
+
const existing = await probeVerifiedDaemon(deps);
|
|
1119
|
+
if (existing.verified) {
|
|
1185
1120
|
console.log("Daemon already running.");
|
|
1186
1121
|
return;
|
|
1187
1122
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1123
|
+
if (existing.running) {
|
|
1124
|
+
throw new Error(
|
|
1125
|
+
`Refusing to replace unverified live pid ${existing.pid} (${existing.reason}).`,
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
const daemonDir = daemonDirPath();
|
|
1129
|
+
fs.mkdirSync(daemonDir, { recursive: true });
|
|
1130
|
+
const entry = resolveDaemonEntry(projectRoot);
|
|
1190
1131
|
if (!fs.existsSync(entry)) {
|
|
1191
1132
|
throw new Error(`Daemon entry not found: ${entry}. Build cortex first.`);
|
|
1192
1133
|
}
|
|
1193
|
-
const logFd = fs.openSync(path.join(
|
|
1134
|
+
const logFd = fs.openSync(path.join(daemonDir, "daemon.log"), "a");
|
|
1194
1135
|
const child = spawn(process.execPath, [entry], {
|
|
1195
1136
|
detached: true,
|
|
1196
1137
|
stdio: ["ignore", logFd, logFd],
|
|
1138
|
+
cwd: projectRoot,
|
|
1139
|
+
env: { ...process.env, CORTEX_PROJECT_ROOT: projectRoot },
|
|
1197
1140
|
});
|
|
1198
1141
|
child.unref();
|
|
1199
|
-
|
|
1142
|
+
fs.closeSync(logFd);
|
|
1143
|
+
const started = await waitForVerifiedDaemon(deps);
|
|
1144
|
+
if (!started.verified) {
|
|
1145
|
+
throw new Error(
|
|
1146
|
+
`Daemon did not complete its verified socket handshake (${started.reason}).`,
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
console.log(`Daemon started (pid=${started.pid}). Log: ${path.join(daemonDir, "daemon.log")}`);
|
|
1200
1150
|
return;
|
|
1201
1151
|
}
|
|
1202
1152
|
if (sub === "stop") {
|
|
1203
|
-
const
|
|
1204
|
-
if (!
|
|
1153
|
+
const result = await stopVerifiedDaemon(deps);
|
|
1154
|
+
if (!result.stopped) {
|
|
1205
1155
|
console.log("Daemon not running.");
|
|
1206
1156
|
return;
|
|
1207
1157
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
}
|
|
1158
|
+
console.log(`Daemon stopped (verified pid=${result.pid}).`);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
if (sub === "restart") {
|
|
1162
|
+
await stopVerifiedDaemon(deps);
|
|
1163
|
+
await runDaemonCommand(["start"], { projectRoot });
|
|
1214
1164
|
return;
|
|
1215
1165
|
}
|
|
1216
1166
|
if (sub === "status") {
|
|
1217
|
-
const
|
|
1218
|
-
if (
|
|
1219
|
-
console.log(`Daemon running (pid=${pid})`);
|
|
1167
|
+
const probe = await probeVerifiedDaemon(deps);
|
|
1168
|
+
if (probe.verified) {
|
|
1169
|
+
console.log(`Daemon running (verified pid=${probe.pid})`);
|
|
1170
|
+
} else if (probe.running) {
|
|
1171
|
+
console.log(
|
|
1172
|
+
`Daemon state unsafe: live pid=${probe.pid}, identity not verified (${probe.reason}).`,
|
|
1173
|
+
);
|
|
1220
1174
|
} else {
|
|
1221
1175
|
console.log("Daemon not running.");
|
|
1222
1176
|
if (pidFileExists()) {
|
|
1223
|
-
console.log(`(stale pid file at ${
|
|
1177
|
+
console.log(`(stale pid file at ${daemonPidFilePath()})`);
|
|
1224
1178
|
}
|
|
1225
1179
|
}
|
|
1226
1180
|
return;
|
|
1227
1181
|
}
|
|
1228
|
-
throw new Error(`Unknown daemon subcommand: ${sub}. Try start|stop|status`);
|
|
1182
|
+
throw new Error(`Unknown daemon subcommand: ${sub}. Try start|stop|restart|status`);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
async function restartDaemonAfterRuntimeUpgrade(projectRoot) {
|
|
1186
|
+
const daemonEntry = resolveDaemonEntry(projectRoot);
|
|
1187
|
+
if (!fs.existsSync(daemonEntry)) return;
|
|
1188
|
+
const deps = await daemonControlDeps();
|
|
1189
|
+
const probe = await probeVerifiedDaemon(deps);
|
|
1190
|
+
if (!probe.running) return;
|
|
1191
|
+
if (!probe.verified) {
|
|
1192
|
+
throw new Error(
|
|
1193
|
+
`Runtime upgraded, but live pid ${probe.pid} could not be verified. ` +
|
|
1194
|
+
"Refusing to signal it; stop it manually after confirming ownership.",
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
console.log(
|
|
1198
|
+
`[cortex] restarting verified daemon pid=${probe.pid} to activate the upgraded runtime`,
|
|
1199
|
+
);
|
|
1200
|
+
await runDaemonCommand(["restart"], { projectRoot });
|
|
1229
1201
|
}
|
|
1230
1202
|
|
|
1231
1203
|
async function runHookShim(args) {
|
|
@@ -1318,7 +1290,8 @@ function writeJson(file, data) {
|
|
|
1318
1290
|
}
|
|
1319
1291
|
|
|
1320
1292
|
// Enterprise == govern. One command, sudo-elevated, hard-fail without it.
|
|
1321
|
-
// `cortex enterprise
|
|
1293
|
+
// `cortex enterprise install --api-key-stdin` does the full install.
|
|
1294
|
+
// Subcommands status/sync/uninstall
|
|
1322
1295
|
// dispatch to scaffold/mcp/dist/cli/govern.js.
|
|
1323
1296
|
|
|
1324
1297
|
function requireSudoElevation() {
|
|
@@ -1356,11 +1329,58 @@ function dropPrivileges(sudo) {
|
|
|
1356
1329
|
return sudoInfo.homedir;
|
|
1357
1330
|
}
|
|
1358
1331
|
|
|
1332
|
+
async function runAsSudoUser(sudo, operation) {
|
|
1333
|
+
const required = [
|
|
1334
|
+
"geteuid",
|
|
1335
|
+
"getegid",
|
|
1336
|
+
"seteuid",
|
|
1337
|
+
"setegid",
|
|
1338
|
+
"getgroups",
|
|
1339
|
+
"setgroups",
|
|
1340
|
+
];
|
|
1341
|
+
for (const name of required) {
|
|
1342
|
+
if (typeof process[name] !== "function") {
|
|
1343
|
+
throw new Error(
|
|
1344
|
+
`Secure Enterprise installation requires process.${name} support.`,
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
const previous = {
|
|
1350
|
+
euid: process.geteuid(),
|
|
1351
|
+
egid: process.getegid(),
|
|
1352
|
+
groups: process.getgroups(),
|
|
1353
|
+
home: process.env.HOME,
|
|
1354
|
+
user: process.env.USER,
|
|
1355
|
+
logname: process.env.LOGNAME,
|
|
1356
|
+
};
|
|
1357
|
+
const sudoInfo = os.userInfo({ uid: sudo.uid });
|
|
1358
|
+
try {
|
|
1359
|
+
process.setgroups([sudo.gid]);
|
|
1360
|
+
process.setegid(sudo.gid);
|
|
1361
|
+
process.seteuid(sudo.uid);
|
|
1362
|
+
process.env.HOME = sudoInfo.homedir;
|
|
1363
|
+
process.env.USER = sudo.user;
|
|
1364
|
+
process.env.LOGNAME = sudo.user;
|
|
1365
|
+
return await operation();
|
|
1366
|
+
} finally {
|
|
1367
|
+
process.seteuid(previous.euid);
|
|
1368
|
+
process.setegid(previous.egid);
|
|
1369
|
+
process.setgroups(previous.groups);
|
|
1370
|
+
if (previous.home === undefined) delete process.env.HOME;
|
|
1371
|
+
else process.env.HOME = previous.home;
|
|
1372
|
+
if (previous.user === undefined) delete process.env.USER;
|
|
1373
|
+
else process.env.USER = previous.user;
|
|
1374
|
+
if (previous.logname === undefined) delete process.env.LOGNAME;
|
|
1375
|
+
else process.env.LOGNAME = previous.logname;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1359
1379
|
function loadGovernModule() {
|
|
1360
|
-
const entry =
|
|
1380
|
+
const entry = resolveTrustedCliEntry("govern");
|
|
1361
1381
|
if (!fs.existsSync(entry)) {
|
|
1362
1382
|
throw new Error(
|
|
1363
|
-
`
|
|
1383
|
+
`The installed Cortex package is missing its trusted Enterprise runtime (${entry}). Reinstall Cortex.`
|
|
1364
1384
|
);
|
|
1365
1385
|
}
|
|
1366
1386
|
return import(pathToFileURL(entry).href);
|
|
@@ -1369,10 +1389,15 @@ function loadGovernModule() {
|
|
|
1369
1389
|
const ENTERPRISE_SUBCOMMANDS = new Set(["status", "sync", "uninstall", "repair", "help", "--help", "-h"]);
|
|
1370
1390
|
|
|
1371
1391
|
async function runEnterpriseCommand(args) {
|
|
1392
|
+
if (args[0] === "install") {
|
|
1393
|
+
return runEnterpriseInstall(args.slice(1));
|
|
1394
|
+
}
|
|
1372
1395
|
if (args.length === 0 || ENTERPRISE_SUBCOMMANDS.has(args[0])) {
|
|
1373
1396
|
return runEnterpriseSubcommand(args);
|
|
1374
1397
|
}
|
|
1375
|
-
|
|
1398
|
+
throw new Error(
|
|
1399
|
+
"Positional enterprise API keys are not accepted. Use 'cortex enterprise install --api-key-stdin'.",
|
|
1400
|
+
);
|
|
1376
1401
|
}
|
|
1377
1402
|
|
|
1378
1403
|
async function runEnterpriseSubcommand(args) {
|
|
@@ -1380,13 +1405,14 @@ async function runEnterpriseSubcommand(args) {
|
|
|
1380
1405
|
|
|
1381
1406
|
if (sub === "help" || sub === "--help" || sub === "-h" || !sub) {
|
|
1382
1407
|
console.log(gradient("cortex enterprise") + muted(" · governance, armed."));
|
|
1383
|
-
console.log(helpRow("enterprise
|
|
1408
|
+
console.log(helpRow("enterprise install --api-key-stdin", "Install (sudo). Managed enforcement + hooks + daemon."));
|
|
1384
1409
|
console.log(helpRow(" ", "[--endpoint <url>] [--frameworks <csv>] [--no-hooks] [--no-daemon]"));
|
|
1385
1410
|
console.log(helpRow("enterprise status [--verbose|--json]", "Show local enforcement state"));
|
|
1386
1411
|
console.log(helpRow("enterprise sync", "Force re-fetch + re-apply (sudo)"));
|
|
1387
1412
|
console.log(helpRow("enterprise uninstall", "Remove. [--break-glass --reason \"<text>\"] in enforced mode (sudo)"));
|
|
1388
1413
|
console.log(helpRow("enterprise repair", "Verify managed paths, clear .cortex-tamper.lock (sudo)"));
|
|
1389
1414
|
console.log("");
|
|
1415
|
+
console.log(muted("Example: printf '%s\\n' \"$CORTEX_API_KEY\" | sudo cortex enterprise install --api-key-stdin"));
|
|
1390
1416
|
console.log(muted("Default endpoint: https://cortex-web-rho.vercel.app"));
|
|
1391
1417
|
return;
|
|
1392
1418
|
}
|
|
@@ -1407,9 +1433,14 @@ async function runEnterpriseSubcommand(args) {
|
|
|
1407
1433
|
}
|
|
1408
1434
|
|
|
1409
1435
|
if (sub === "sync") {
|
|
1410
|
-
requireSudoElevation();
|
|
1436
|
+
const sudo = requireSudoElevation();
|
|
1411
1437
|
const mod = await loadGovernModule();
|
|
1412
|
-
|
|
1438
|
+
const projectOperation = (operation) => runAsSudoUser(sudo, operation);
|
|
1439
|
+
await mod.runGovernSync({
|
|
1440
|
+
cwd: process.cwd(),
|
|
1441
|
+
projectOperation,
|
|
1442
|
+
});
|
|
1443
|
+
dropPrivileges(sudo);
|
|
1413
1444
|
return;
|
|
1414
1445
|
}
|
|
1415
1446
|
|
|
@@ -1425,14 +1456,17 @@ async function runEnterpriseSubcommand(args) {
|
|
|
1425
1456
|
throw new Error(`Unknown enterprise uninstall option: ${args[i]}`);
|
|
1426
1457
|
}
|
|
1427
1458
|
}
|
|
1428
|
-
requireSudoElevation();
|
|
1459
|
+
const sudo = requireSudoElevation();
|
|
1429
1460
|
const mod = await loadGovernModule();
|
|
1461
|
+
const projectOperation = (operation) => runAsSudoUser(sudo, operation);
|
|
1430
1462
|
const result = await mod.runGovernUninstall({
|
|
1431
1463
|
cli: "all",
|
|
1432
1464
|
breakGlass,
|
|
1433
1465
|
reason,
|
|
1434
1466
|
cwd: process.cwd(),
|
|
1467
|
+
projectOperation,
|
|
1435
1468
|
});
|
|
1469
|
+
dropPrivileges(sudo);
|
|
1436
1470
|
if (!result.ok) {
|
|
1437
1471
|
printBullet("fail", result.message, process.stderr);
|
|
1438
1472
|
process.exit(1);
|
|
@@ -1451,9 +1485,15 @@ async function runEnterpriseSubcommand(args) {
|
|
|
1451
1485
|
throw new Error(`Unknown enterprise repair option: ${args[i]}`);
|
|
1452
1486
|
}
|
|
1453
1487
|
}
|
|
1454
|
-
requireSudoElevation();
|
|
1488
|
+
const sudo = requireSudoElevation();
|
|
1455
1489
|
const mod = await loadGovernModule();
|
|
1456
|
-
const
|
|
1490
|
+
const projectOperation = (operation) => runAsSudoUser(sudo, operation);
|
|
1491
|
+
const result = await mod.runGovernRepair({
|
|
1492
|
+
cwd: process.cwd(),
|
|
1493
|
+
reason,
|
|
1494
|
+
projectOperation,
|
|
1495
|
+
});
|
|
1496
|
+
dropPrivileges(sudo);
|
|
1457
1497
|
if (!result.ok) {
|
|
1458
1498
|
printBullet("fail", result.message, process.stderr);
|
|
1459
1499
|
process.exit(1);
|
|
@@ -1465,14 +1505,19 @@ async function runEnterpriseSubcommand(args) {
|
|
|
1465
1505
|
throw new Error(`Unknown enterprise subcommand: ${sub}`);
|
|
1466
1506
|
}
|
|
1467
1507
|
|
|
1468
|
-
async function runEnterpriseInstall(args) {
|
|
1469
|
-
|
|
1508
|
+
async function runEnterpriseInstall(args, injected = {}) {
|
|
1509
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
1510
|
+
return runEnterpriseSubcommand(["help"]);
|
|
1511
|
+
}
|
|
1512
|
+
let readKeyFromStdin = false;
|
|
1470
1513
|
let endpoint;
|
|
1471
1514
|
let frameworks;
|
|
1472
1515
|
let installHooks = true;
|
|
1473
1516
|
let startDaemon = true;
|
|
1474
|
-
for (let i =
|
|
1475
|
-
if (args[i] === "--
|
|
1517
|
+
for (let i = 0; i < args.length; i++) {
|
|
1518
|
+
if (args[i] === "--api-key-stdin") {
|
|
1519
|
+
readKeyFromStdin = true;
|
|
1520
|
+
} else if (args[i] === "--endpoint" && args[i + 1]) {
|
|
1476
1521
|
endpoint = args[i + 1];
|
|
1477
1522
|
i++;
|
|
1478
1523
|
} else if (args[i] === "--frameworks" && args[i + 1]) {
|
|
@@ -1484,23 +1529,61 @@ async function runEnterpriseInstall(args) {
|
|
|
1484
1529
|
startDaemon = false;
|
|
1485
1530
|
} else if (args[i].startsWith("-")) {
|
|
1486
1531
|
throw new Error(`Unknown enterprise install option: ${args[i]}`);
|
|
1532
|
+
} else {
|
|
1533
|
+
throw new Error(
|
|
1534
|
+
"Positional enterprise API keys are not accepted. Use --api-key-stdin.",
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
if (!readKeyFromStdin) {
|
|
1540
|
+
throw new Error(
|
|
1541
|
+
"Enterprise installation requires --api-key-stdin so the key is not exposed in process arguments.",
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
const secretInput = injected.stdin ?? process.stdin;
|
|
1545
|
+
if (secretInput.isTTY) {
|
|
1546
|
+
throw new Error(
|
|
1547
|
+
"Read the enterprise API key from a pipe or redirected stdin; interactive echo is disabled for secrets.",
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
let apiKey = "";
|
|
1551
|
+
for await (const chunk of secretInput) {
|
|
1552
|
+
apiKey += chunk.toString();
|
|
1553
|
+
if (apiKey.length > 4096) {
|
|
1554
|
+
throw new Error("Enterprise API key input is too large.");
|
|
1487
1555
|
}
|
|
1488
1556
|
}
|
|
1557
|
+
apiKey = apiKey.trim();
|
|
1558
|
+
if (!apiKey || apiKey.includes("\n") || apiKey.includes("\r")) {
|
|
1559
|
+
throw new Error("Expected exactly one enterprise API key on stdin.");
|
|
1560
|
+
}
|
|
1489
1561
|
|
|
1490
|
-
const sudo = requireSudoElevation();
|
|
1562
|
+
const sudo = (injected.requireSudoElevation ?? requireSudoElevation)();
|
|
1563
|
+
const projectOperation = (operation) =>
|
|
1564
|
+
(injected.runAsSudoUser ?? runAsSudoUser)(sudo, operation);
|
|
1491
1565
|
|
|
1492
1566
|
process.stdout.write(headerBanner({ tagline: " Cortex enterprise — activating governance" }));
|
|
1493
1567
|
|
|
1494
|
-
|
|
1495
|
-
if (!
|
|
1496
|
-
|
|
1497
|
-
|
|
1568
|
+
let enterpriseMod = injected.enterpriseMod;
|
|
1569
|
+
if (!enterpriseMod) {
|
|
1570
|
+
const enterpriseEntry = resolveTrustedCliEntry("enterprise-setup");
|
|
1571
|
+
if (!fs.existsSync(enterpriseEntry)) {
|
|
1572
|
+
printBullet("fail", `The installed Cortex package is missing its trusted Enterprise runtime (${enterpriseEntry}). Reinstall Cortex.`);
|
|
1573
|
+
process.exit(1);
|
|
1574
|
+
}
|
|
1575
|
+
enterpriseMod = await import(pathToFileURL(enterpriseEntry).href);
|
|
1498
1576
|
}
|
|
1499
|
-
const enterpriseMod = await import(pathToFileURL(enterpriseEntry).href);
|
|
1500
1577
|
|
|
1501
1578
|
// Step 1 — Initializing Cortex core (license validation + enterprise.yml).
|
|
1502
1579
|
const step1 = spinner("Initializing Cortex core");
|
|
1503
|
-
const setupResult = await
|
|
1580
|
+
const setupResult = await projectOperation(
|
|
1581
|
+
() => enterpriseMod.runEnterpriseSetup({
|
|
1582
|
+
apiKey,
|
|
1583
|
+
endpoint,
|
|
1584
|
+
cwd: process.cwd(),
|
|
1585
|
+
}),
|
|
1586
|
+
);
|
|
1504
1587
|
if (!setupResult.ok) {
|
|
1505
1588
|
step1.stop("fail", `Initializing Cortex core — ${setupResult.message}`);
|
|
1506
1589
|
process.exit(1);
|
|
@@ -1508,24 +1591,40 @@ async function runEnterpriseInstall(args) {
|
|
|
1508
1591
|
step1.stop("ok", `Initializing Cortex core — license ${setupResult.edition}, expires ${setupResult.expiresAt}`);
|
|
1509
1592
|
printBullet("info", muted(`config: ${setupResult.configPath}`));
|
|
1510
1593
|
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1594
|
+
const sudoHome =
|
|
1595
|
+
injected.sudoHome ??
|
|
1596
|
+
os.userInfo({ uid: sudo.uid }).homedir;
|
|
1597
|
+
const bindEnterpriseIdentity =
|
|
1598
|
+
injected.bindEnterpriseIdentity ??
|
|
1599
|
+
enterpriseMod.bindEnterpriseIdentity;
|
|
1600
|
+
if (
|
|
1601
|
+
typeof bindEnterpriseIdentity !== "function" ||
|
|
1602
|
+
!await projectOperation(
|
|
1603
|
+
() => bindEnterpriseIdentity({
|
|
1604
|
+
apiKey,
|
|
1605
|
+
endpoint,
|
|
1606
|
+
homeDir: sudoHome,
|
|
1607
|
+
}),
|
|
1608
|
+
)
|
|
1609
|
+
) {
|
|
1610
|
+
throw new Error(
|
|
1611
|
+
"Could not bind this user profile to the verified Enterprise identity. " +
|
|
1612
|
+
"A different endpoint is already enrolled; use a separate OS user boundary.",
|
|
1613
|
+
);
|
|
1516
1614
|
}
|
|
1517
1615
|
|
|
1518
1616
|
// Step 2 — Loading policy engine (govern install: managed config + frameworks).
|
|
1519
1617
|
const baseUrl = (endpoint ?? "https://cortex-web-rho.vercel.app").replace(/\/$/, "");
|
|
1520
1618
|
const step2 = spinner("Loading policy engine");
|
|
1521
|
-
const governMod = await loadGovernModule();
|
|
1619
|
+
const governMod = injected.governMod ?? await loadGovernModule();
|
|
1522
1620
|
const governResult = await governMod.runGovernInstall({
|
|
1523
1621
|
cli: "all",
|
|
1524
1622
|
mode: "enforced",
|
|
1525
1623
|
cwd: process.cwd(),
|
|
1526
1624
|
apiKey,
|
|
1527
1625
|
baseUrl,
|
|
1528
|
-
frameworks,
|
|
1626
|
+
frameworks: frameworks ?? ["iso27001", "iso42001", "soc2"],
|
|
1627
|
+
projectOperation,
|
|
1529
1628
|
});
|
|
1530
1629
|
if (!governResult.ok) {
|
|
1531
1630
|
step2.stop("fail", `Loading policy engine — ${governResult.message}`);
|
|
@@ -1533,31 +1632,21 @@ async function runEnterpriseInstall(args) {
|
|
|
1533
1632
|
}
|
|
1534
1633
|
step2.stop("ok", "Loading policy engine — policies armed");
|
|
1535
1634
|
|
|
1536
|
-
// govern.local.json was written as root in cwd/.context. chown it back.
|
|
1537
|
-
const governStatePath = path.join(process.cwd(), ".context", "govern.local.json");
|
|
1538
|
-
if (fs.existsSync(governStatePath)) {
|
|
1539
|
-
try {
|
|
1540
|
-
fs.chownSync(governStatePath, sudo.uid, sudo.gid);
|
|
1541
|
-
} catch (err) {
|
|
1542
|
-
printBullet("warn", `Could not chown ${governStatePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
1635
|
// Step 3 — Connecting audit pipeline (telemetry endpoint already wired by govern install).
|
|
1547
1636
|
const step3 = spinner("Connecting audit pipeline");
|
|
1548
1637
|
step3.stop("ok", `Connecting audit pipeline — endpoint ${baseUrl}`);
|
|
1549
1638
|
|
|
1550
1639
|
// Drop privileges before user-scope writes (Claude Code hooks in $HOME) and daemon spawn.
|
|
1551
|
-
dropPrivileges(sudo);
|
|
1640
|
+
(injected.dropPrivileges ?? dropPrivileges)(sudo);
|
|
1552
1641
|
|
|
1553
1642
|
// Step 4 — Preparing MCP gateway (Claude Code hooks bind the MCP surface).
|
|
1554
1643
|
if (installHooks) {
|
|
1555
1644
|
const step4 = spinner("Preparing MCP gateway");
|
|
1556
1645
|
try {
|
|
1557
|
-
if (hasManagedClaudeHooks()) {
|
|
1646
|
+
if ((injected.hasManagedClaudeHooks ?? hasManagedClaudeHooks)()) {
|
|
1558
1647
|
step4.stop("ok", "Preparing MCP gateway — managed Claude hooks active");
|
|
1559
1648
|
} else {
|
|
1560
|
-
await runHooksCommand(["install"]);
|
|
1649
|
+
await (injected.runHooksCommand ?? runHooksCommand)(["install"]);
|
|
1561
1650
|
step4.stop("ok", "Preparing MCP gateway — hooks installed");
|
|
1562
1651
|
}
|
|
1563
1652
|
} catch (err) {
|
|
@@ -1571,10 +1660,11 @@ async function runEnterpriseInstall(args) {
|
|
|
1571
1660
|
if (startDaemon) {
|
|
1572
1661
|
const step5 = spinner("Installing guardrails");
|
|
1573
1662
|
try {
|
|
1574
|
-
await runDaemonCommand(["
|
|
1663
|
+
await (injected.runDaemonCommand ?? runDaemonCommand)(["restart"]);
|
|
1575
1664
|
step5.stop("ok", "Installing guardrails — daemon online");
|
|
1576
1665
|
} catch (err) {
|
|
1577
1666
|
step5.stop("fail", `Installing guardrails — ${err instanceof Error ? err.message : String(err)}`);
|
|
1667
|
+
throw err;
|
|
1578
1668
|
}
|
|
1579
1669
|
} else {
|
|
1580
1670
|
printBullet("warn", "Installing guardrails — skipped (--no-daemon)");
|
|
@@ -1588,7 +1678,7 @@ async function runEnterpriseInstall(args) {
|
|
|
1588
1678
|
}
|
|
1589
1679
|
|
|
1590
1680
|
const RUN_CLIS = new Set(["claude", "codex", "copilot"]);
|
|
1591
|
-
const QUERY_COMMANDS = new Set(["search", "related", "impact", "rules", "explain"]);
|
|
1681
|
+
const QUERY_COMMANDS = new Set(["search", "related", "impact", "rules", "explain", "pattern-evidence"]);
|
|
1592
1682
|
|
|
1593
1683
|
async function runRunCommand(args) {
|
|
1594
1684
|
const sub = args[0];
|
|
@@ -1598,7 +1688,7 @@ async function runRunCommand(args) {
|
|
|
1598
1688
|
console.log("");
|
|
1599
1689
|
console.log("Wraps the named AI CLI in cortex enforcement:");
|
|
1600
1690
|
console.log(" claude/codex: passthrough — their own managed-config + sandbox");
|
|
1601
|
-
console.log(" cover Tier 1 enforcement after 'cortex enterprise
|
|
1691
|
+
console.log(" cover Tier 1 enforcement after 'cortex enterprise install --api-key-stdin'.");
|
|
1602
1692
|
console.log(" copilot: Tier 2 — OS-level sandbox (sandbox-exec on macOS,");
|
|
1603
1693
|
console.log(" bwrap on Linux). Denies writes to ~/.copilot/,");
|
|
1604
1694
|
console.log(" ~/.copilot.local/, /etc/copilot* so AI cannot");
|
|
@@ -1762,4 +1852,11 @@ if (invokedAsScript) {
|
|
|
1762
1852
|
});
|
|
1763
1853
|
}
|
|
1764
1854
|
|
|
1765
|
-
export {
|
|
1855
|
+
export {
|
|
1856
|
+
buildInitialConfig,
|
|
1857
|
+
detectInitialSourcePaths,
|
|
1858
|
+
hardenEnterpriseConfigPermissions,
|
|
1859
|
+
isScaffoldOutOfDate,
|
|
1860
|
+
runEnterpriseInstall,
|
|
1861
|
+
slugifyRepoId,
|
|
1862
|
+
};
|