@danielblomma/cortex-mcp 2.4.0 → 2.4.2
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 +226 -0
- package/README.md +24 -3
- package/bin/cli/arguments.mjs +60 -0
- package/bin/cli/context-passthrough.mjs +129 -0
- package/bin/cli/daemon.mjs +157 -0
- package/bin/cli/enterprise.mjs +516 -0
- package/bin/cli/help.mjs +88 -0
- package/bin/cli/hooks.mjs +199 -0
- package/bin/cli/mcp-command.mjs +87 -0
- package/bin/cli/paths.mjs +14 -0
- package/bin/cli/process.mjs +49 -0
- package/bin/cli/project-commands.mjs +237 -0
- package/bin/cli/project-runtime.mjs +34 -0
- package/bin/cli/query-command.mjs +18 -0
- package/bin/cli/router.mjs +66 -0
- package/bin/cli/run-command.mjs +44 -0
- package/bin/cli/scaffold-ownership.mjs +936 -0
- package/bin/cli/scaffold.mjs +687 -0
- package/bin/cli/stage-command.mjs +9 -0
- package/bin/cli/telemetry-command.mjs +18 -0
- package/bin/cli/trusted-runtime.mjs +18 -0
- package/bin/cortex.mjs +12 -1607
- package/bin/daemon-control.mjs +162 -0
- package/mcp-registry-submission.json +1 -1
- package/package.json +8 -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 +321 -200
- 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/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/enterprise/audit/push.ts +8 -0
- package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -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/violations/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
- package/scaffold/mcp/src/plugin.ts +20 -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/enterprise-identity-sync.test.mjs +401 -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/project-service-registry.test.mjs +172 -0
- package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -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/ownership/baseline-v2.4.1.json +22 -0
- package/scaffold/ownership/current.json +4 -0
- package/scaffold/ownership/v1.json +456 -0
- package/scaffold/scripts/ingest-parsers.mjs +1 -387
- package/scaffold/scripts/ingest-worker.mjs +4 -1
- package/scaffold/scripts/ingest.mjs +5 -3899
- package/scaffold/scripts/lib/ingest/arguments.mjs +78 -0
- package/scaffold/scripts/lib/ingest/chunks.mjs +212 -0
- package/scaffold/scripts/lib/ingest/config.mjs +120 -0
- package/scaffold/scripts/lib/ingest/constants.mjs +222 -0
- package/scaffold/scripts/lib/ingest/files.mjs +387 -0
- package/scaffold/scripts/lib/ingest/incremental-state.mjs +131 -0
- package/scaffold/scripts/lib/ingest/io.mjs +78 -0
- package/scaffold/scripts/lib/ingest/main.mjs +76 -0
- package/scaffold/scripts/lib/ingest/parser-composition.mjs +140 -0
- package/scaffold/scripts/lib/ingest/parser-registry.mjs +387 -0
- package/scaffold/scripts/lib/ingest/pipeline-stages.mjs +1625 -0
- package/scaffold/scripts/lib/ingest/projects.mjs +272 -0
- package/scaffold/scripts/lib/ingest/relations.mjs +860 -0
- package/scaffold/scripts/lib/ingest/runtime-paths.mjs +11 -0
- package/scaffold/scripts/lib/ingest/workers.mjs +264 -0
|
@@ -6,9 +6,34 @@ import path from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
-
runSkillSyncForCli,
|
|
10
|
-
runSkillSyncOnce,
|
|
9
|
+
runSkillSyncForCli as runSkillSyncForCliImpl,
|
|
10
|
+
runSkillSyncOnce as runSkillSyncOnceImpl,
|
|
11
11
|
} from "../dist/daemon/skill-sync-checker.js";
|
|
12
|
+
import { enterpriseCredentialId } from "../dist/core/license.js";
|
|
13
|
+
import { claimEnterpriseHostIdentity } from "../dist/core/enterprise-host-identity.js";
|
|
14
|
+
|
|
15
|
+
const TEST_ENDPOINT = "https://example.com";
|
|
16
|
+
const TEST_API_KEY = "ent_test_12345678";
|
|
17
|
+
|
|
18
|
+
function enrollTestIdentity() {
|
|
19
|
+
assert.equal(
|
|
20
|
+
claimEnterpriseHostIdentity(
|
|
21
|
+
enterpriseCredentialId(TEST_ENDPOINT, TEST_API_KEY),
|
|
22
|
+
TEST_ENDPOINT,
|
|
23
|
+
),
|
|
24
|
+
true,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function runSkillSyncForCli(cwd, cli) {
|
|
29
|
+
enrollTestIdentity();
|
|
30
|
+
return runSkillSyncForCliImpl(cwd, cli);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function runSkillSyncOnce(cwd, clis) {
|
|
34
|
+
enrollTestIdentity();
|
|
35
|
+
return runSkillSyncOnceImpl(cwd, clis);
|
|
36
|
+
}
|
|
12
37
|
|
|
13
38
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
39
|
const skillFixture = JSON.parse(
|
|
@@ -76,6 +101,7 @@ function createFetchStub(state) {
|
|
|
76
101
|
}
|
|
77
102
|
|
|
78
103
|
if (url.pathname.startsWith("/api/v1/govern/skills/")) {
|
|
104
|
+
if (typeof state.bodyFetches === "number") state.bodyFetches += 1;
|
|
79
105
|
const name = decodeURIComponent(url.pathname.split("/").pop() ?? "");
|
|
80
106
|
if (!(name in state.bodies)) {
|
|
81
107
|
return {
|
|
@@ -275,3 +301,421 @@ test("runSkillSyncOnce: consumes the shared org-skillz contract fixture end-to-e
|
|
|
275
301
|
);
|
|
276
302
|
assert.ok(!fs.existsSync(skillPath(homeDir, "claude", "alpha-codex")));
|
|
277
303
|
});
|
|
304
|
+
|
|
305
|
+
test("runSkillSyncForCli: rejects unsafe remote names before fetching or writing skill bodies", async () => {
|
|
306
|
+
const { cwd } = makeWorkspace();
|
|
307
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
|
|
308
|
+
process.env.HOME = homeDir;
|
|
309
|
+
const victimDir = path.join(homeDir, "Documents");
|
|
310
|
+
fs.mkdirSync(victimDir);
|
|
311
|
+
fs.writeFileSync(path.join(victimDir, "sentinel.txt"), "keep", "utf8");
|
|
312
|
+
let bodyFetches = 0;
|
|
313
|
+
|
|
314
|
+
globalThis.fetch = async (input) => {
|
|
315
|
+
const url = input instanceof URL ? input : new URL(input);
|
|
316
|
+
if (url.pathname.endsWith("/manifest")) {
|
|
317
|
+
return {
|
|
318
|
+
ok: true,
|
|
319
|
+
json: async () => ({
|
|
320
|
+
skills: [{
|
|
321
|
+
name: "../../Documents",
|
|
322
|
+
scope: "global",
|
|
323
|
+
updated_at: "2026-07-28T00:00:00.000Z",
|
|
324
|
+
}],
|
|
325
|
+
}),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
bodyFetches += 1;
|
|
329
|
+
throw new Error("body fetch must not occur");
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const outcome = await runSkillSyncForCli(cwd, "codex");
|
|
333
|
+
assert.equal(outcome.kind, "failed");
|
|
334
|
+
assert.match(outcome.error, /invalid skill name/);
|
|
335
|
+
assert.equal(bodyFetches, 0);
|
|
336
|
+
assert.equal(
|
|
337
|
+
fs.readFileSync(path.join(victimDir, "sentinel.txt"), "utf8"),
|
|
338
|
+
"keep",
|
|
339
|
+
);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("runSkillSyncForCli: ignores persisted absolute paths when removing legacy state", async () => {
|
|
343
|
+
const { cwd } = makeWorkspace();
|
|
344
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
|
|
345
|
+
process.env.HOME = homeDir;
|
|
346
|
+
const victimDir = path.join(homeDir, "Documents");
|
|
347
|
+
fs.mkdirSync(victimDir);
|
|
348
|
+
fs.writeFileSync(path.join(victimDir, "SKILL.md"), "not managed", "utf8");
|
|
349
|
+
fs.writeFileSync(path.join(victimDir, "sentinel.txt"), "keep", "utf8");
|
|
350
|
+
fs.mkdirSync(path.dirname(daemonStatePath(homeDir)), { recursive: true });
|
|
351
|
+
fs.writeFileSync(
|
|
352
|
+
daemonStatePath(homeDir),
|
|
353
|
+
JSON.stringify({
|
|
354
|
+
skills: {
|
|
355
|
+
"codex:safe-skill": {
|
|
356
|
+
cli: "codex",
|
|
357
|
+
scope: "global",
|
|
358
|
+
updated_at: "2026-07-27T00:00:00.000Z",
|
|
359
|
+
path: path.join(victimDir, "SKILL.md"),
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
}),
|
|
363
|
+
"utf8",
|
|
364
|
+
);
|
|
365
|
+
globalThis.fetch = createFetchStub({
|
|
366
|
+
manifests: { codex: [] },
|
|
367
|
+
bodies: {},
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const outcome = await runSkillSyncForCli(cwd, "codex");
|
|
371
|
+
assert.equal(outcome.kind, "synced");
|
|
372
|
+
assert.deepEqual(outcome.removed, ["safe-skill"]);
|
|
373
|
+
assert.equal(
|
|
374
|
+
fs.readFileSync(path.join(victimDir, "sentinel.txt"), "utf8"),
|
|
375
|
+
"keep",
|
|
376
|
+
);
|
|
377
|
+
assert.equal(fs.existsSync(path.join(victimDir, "SKILL.md")), true);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("runSkillSyncForCli: refuses to write through a managed skill directory symlink", {
|
|
381
|
+
skip: process.platform === "win32",
|
|
382
|
+
}, async () => {
|
|
383
|
+
const { cwd } = makeWorkspace();
|
|
384
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
|
|
385
|
+
process.env.HOME = homeDir;
|
|
386
|
+
const victimDir = path.join(homeDir, "victim");
|
|
387
|
+
const skillsRoot = path.join(homeDir, ".codex", "skills");
|
|
388
|
+
fs.mkdirSync(victimDir);
|
|
389
|
+
fs.mkdirSync(skillsRoot, { recursive: true });
|
|
390
|
+
fs.writeFileSync(path.join(victimDir, "SKILL.md"), "keep", "utf8");
|
|
391
|
+
fs.symlinkSync(victimDir, path.join(skillsRoot, "global-skill"), "dir");
|
|
392
|
+
const remote = {
|
|
393
|
+
bodyFetches: 0,
|
|
394
|
+
manifests: {
|
|
395
|
+
codex: [{
|
|
396
|
+
name: "global-skill",
|
|
397
|
+
scope: "global",
|
|
398
|
+
updated_at: "2026-07-28T00:00:00.000Z",
|
|
399
|
+
}],
|
|
400
|
+
},
|
|
401
|
+
bodies: {
|
|
402
|
+
"global-skill": "remote replacement",
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
globalThis.fetch = createFetchStub(remote);
|
|
406
|
+
|
|
407
|
+
const outcome = await runSkillSyncForCli(cwd, "codex");
|
|
408
|
+
assert.equal(outcome.kind, "failed");
|
|
409
|
+
assert.match(outcome.error, /symbolic link/);
|
|
410
|
+
assert.equal(remote.bodyFetches, 0);
|
|
411
|
+
assert.equal(
|
|
412
|
+
fs.readFileSync(path.join(victimDir, "SKILL.md"), "utf8"),
|
|
413
|
+
"keep",
|
|
414
|
+
);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test("runSkillSyncForCli: refuses to overwrite an unmanaged personal skill collision", async () => {
|
|
418
|
+
const { cwd } = makeWorkspace();
|
|
419
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
|
|
420
|
+
process.env.HOME = homeDir;
|
|
421
|
+
const personalDir = path.dirname(
|
|
422
|
+
skillPath(homeDir, "codex", "global-skill"),
|
|
423
|
+
);
|
|
424
|
+
fs.mkdirSync(personalDir, { recursive: true });
|
|
425
|
+
fs.writeFileSync(
|
|
426
|
+
path.join(personalDir, "SKILL.md"),
|
|
427
|
+
"personal content",
|
|
428
|
+
"utf8",
|
|
429
|
+
);
|
|
430
|
+
fs.writeFileSync(path.join(personalDir, "notes.txt"), "keep", "utf8");
|
|
431
|
+
const remote = {
|
|
432
|
+
bodyFetches: 0,
|
|
433
|
+
manifests: {
|
|
434
|
+
codex: [{
|
|
435
|
+
name: "global-skill",
|
|
436
|
+
scope: "global",
|
|
437
|
+
updated_at: "2026-07-28T00:00:00.000Z",
|
|
438
|
+
}],
|
|
439
|
+
},
|
|
440
|
+
bodies: { "global-skill": "remote replacement" },
|
|
441
|
+
};
|
|
442
|
+
globalThis.fetch = createFetchStub(remote);
|
|
443
|
+
|
|
444
|
+
const outcome = await runSkillSyncForCli(cwd, "codex");
|
|
445
|
+
assert.equal(outcome.kind, "failed");
|
|
446
|
+
assert.match(outcome.error, /not owned by Cortex/);
|
|
447
|
+
assert.equal(remote.bodyFetches, 0);
|
|
448
|
+
assert.equal(
|
|
449
|
+
fs.readFileSync(path.join(personalDir, "SKILL.md"), "utf8"),
|
|
450
|
+
"personal content",
|
|
451
|
+
);
|
|
452
|
+
assert.equal(
|
|
453
|
+
fs.readFileSync(path.join(personalDir, "notes.txt"), "utf8"),
|
|
454
|
+
"keep",
|
|
455
|
+
);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("runSkillSyncForCli: refuses a symlinked CLI skills root", {
|
|
459
|
+
skip: process.platform === "win32",
|
|
460
|
+
}, async () => {
|
|
461
|
+
const { cwd } = makeWorkspace();
|
|
462
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
|
|
463
|
+
process.env.HOME = homeDir;
|
|
464
|
+
const victimDir = path.join(homeDir, "outside");
|
|
465
|
+
const cliDir = path.join(homeDir, ".codex");
|
|
466
|
+
fs.mkdirSync(victimDir);
|
|
467
|
+
fs.mkdirSync(cliDir);
|
|
468
|
+
fs.symlinkSync(victimDir, path.join(cliDir, "skills"), "dir");
|
|
469
|
+
const remote = {
|
|
470
|
+
bodyFetches: 0,
|
|
471
|
+
manifests: {
|
|
472
|
+
codex: [{
|
|
473
|
+
name: "global-skill",
|
|
474
|
+
scope: "global",
|
|
475
|
+
updated_at: "2026-07-28T00:00:00.000Z",
|
|
476
|
+
}],
|
|
477
|
+
},
|
|
478
|
+
bodies: { "global-skill": "remote replacement" },
|
|
479
|
+
};
|
|
480
|
+
globalThis.fetch = createFetchStub(remote);
|
|
481
|
+
|
|
482
|
+
const outcome = await runSkillSyncForCli(cwd, "codex");
|
|
483
|
+
assert.equal(outcome.kind, "failed");
|
|
484
|
+
assert.match(outcome.error, /skills root must not be a symbolic link/);
|
|
485
|
+
assert.equal(remote.bodyFetches, 0);
|
|
486
|
+
assert.deepEqual(fs.readdirSync(victimDir), []);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
test("runSkillSyncForCli: a second credential cannot mutate the first credential's managed skills", async () => {
|
|
490
|
+
const { cwd, contextDir } = makeWorkspace();
|
|
491
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
|
|
492
|
+
process.env.HOME = homeDir;
|
|
493
|
+
const remote = {
|
|
494
|
+
manifests: {
|
|
495
|
+
codex: [{
|
|
496
|
+
name: "global-skill",
|
|
497
|
+
scope: "global",
|
|
498
|
+
updated_at: "2026-07-28T00:00:00.000Z",
|
|
499
|
+
}],
|
|
500
|
+
},
|
|
501
|
+
bodies: { "global-skill": "identity A" },
|
|
502
|
+
};
|
|
503
|
+
globalThis.fetch = createFetchStub(remote);
|
|
504
|
+
|
|
505
|
+
const first = await runSkillSyncForCli(cwd, "codex");
|
|
506
|
+
assert.equal(first.kind, "synced");
|
|
507
|
+
const installed = skillPath(homeDir, "codex", "global-skill");
|
|
508
|
+
assert.equal(fs.readFileSync(installed, "utf8"), "identity A");
|
|
509
|
+
assert.ok(
|
|
510
|
+
fs.existsSync(path.join(path.dirname(installed), ".cortex-managed.json")),
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
fs.writeFileSync(
|
|
514
|
+
path.join(contextDir, "enterprise.yml"),
|
|
515
|
+
[
|
|
516
|
+
"enterprise:",
|
|
517
|
+
" api_key: ent_other_12345678",
|
|
518
|
+
" endpoint: https://other.example.com",
|
|
519
|
+
"",
|
|
520
|
+
].join("\n"),
|
|
521
|
+
"utf8",
|
|
522
|
+
);
|
|
523
|
+
remote.manifests.codex = [];
|
|
524
|
+
|
|
525
|
+
const second = await runSkillSyncForCli(cwd, "codex");
|
|
526
|
+
assert.equal(second.kind, "failed");
|
|
527
|
+
assert.match(second.error, /identity conflict/);
|
|
528
|
+
assert.equal(fs.readFileSync(installed, "utf8"), "identity A");
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("runSkillSyncForCli: removal refuses unowned files added to a managed directory", async () => {
|
|
532
|
+
const { cwd } = makeWorkspace();
|
|
533
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
|
|
534
|
+
process.env.HOME = homeDir;
|
|
535
|
+
const remote = {
|
|
536
|
+
manifests: {
|
|
537
|
+
codex: [{
|
|
538
|
+
name: "global-skill",
|
|
539
|
+
scope: "global",
|
|
540
|
+
updated_at: "2026-07-28T00:00:00.000Z",
|
|
541
|
+
}],
|
|
542
|
+
},
|
|
543
|
+
bodies: { "global-skill": "managed" },
|
|
544
|
+
};
|
|
545
|
+
globalThis.fetch = createFetchStub(remote);
|
|
546
|
+
assert.equal((await runSkillSyncForCli(cwd, "codex")).kind, "synced");
|
|
547
|
+
|
|
548
|
+
const installedDir = path.dirname(
|
|
549
|
+
skillPath(homeDir, "codex", "global-skill"),
|
|
550
|
+
);
|
|
551
|
+
fs.writeFileSync(path.join(installedDir, "personal-note.txt"), "keep", "utf8");
|
|
552
|
+
remote.manifests.codex = [];
|
|
553
|
+
|
|
554
|
+
const outcome = await runSkillSyncForCli(cwd, "codex");
|
|
555
|
+
assert.equal(outcome.kind, "failed");
|
|
556
|
+
assert.match(outcome.error, /contains unowned files/);
|
|
557
|
+
assert.equal(
|
|
558
|
+
fs.readFileSync(path.join(installedDir, "personal-note.txt"), "utf8"),
|
|
559
|
+
"keep",
|
|
560
|
+
);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test("runSkillSyncForCli: refuses a managed SKILL.md symlink before fetching replacement content", {
|
|
564
|
+
skip: process.platform === "win32",
|
|
565
|
+
}, async () => {
|
|
566
|
+
const { cwd } = makeWorkspace();
|
|
567
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
|
|
568
|
+
process.env.HOME = homeDir;
|
|
569
|
+
const remote = {
|
|
570
|
+
bodyFetches: 0,
|
|
571
|
+
manifests: {
|
|
572
|
+
codex: [{
|
|
573
|
+
name: "global-skill",
|
|
574
|
+
scope: "global",
|
|
575
|
+
updated_at: "2026-07-28T00:00:00.000Z",
|
|
576
|
+
}],
|
|
577
|
+
},
|
|
578
|
+
bodies: { "global-skill": "managed v1" },
|
|
579
|
+
};
|
|
580
|
+
globalThis.fetch = createFetchStub(remote);
|
|
581
|
+
assert.equal((await runSkillSyncForCli(cwd, "codex")).kind, "synced");
|
|
582
|
+
assert.equal(remote.bodyFetches, 1);
|
|
583
|
+
|
|
584
|
+
const installed = skillPath(homeDir, "codex", "global-skill");
|
|
585
|
+
const victim = path.join(homeDir, "victim.txt");
|
|
586
|
+
fs.writeFileSync(victim, "keep", "utf8");
|
|
587
|
+
fs.unlinkSync(installed);
|
|
588
|
+
fs.symlinkSync(victim, installed);
|
|
589
|
+
remote.manifests.codex[0].updated_at = "2026-07-28T01:00:00.000Z";
|
|
590
|
+
remote.bodies["global-skill"] = "managed v2";
|
|
591
|
+
|
|
592
|
+
const outcome = await runSkillSyncForCli(cwd, "codex");
|
|
593
|
+
assert.equal(outcome.kind, "failed");
|
|
594
|
+
assert.match(outcome.error, /skill file must not be a symbolic link/);
|
|
595
|
+
assert.equal(remote.bodyFetches, 1);
|
|
596
|
+
assert.equal(fs.readFileSync(victim, "utf8"), "keep");
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test("runSkillSyncForCli: rejects malformed manifest variants before any body fetch", async () => {
|
|
600
|
+
const variants = [
|
|
601
|
+
{
|
|
602
|
+
label: "absolute name",
|
|
603
|
+
manifest: [{
|
|
604
|
+
name: "/tmp/owned",
|
|
605
|
+
scope: "global",
|
|
606
|
+
updated_at: "2026-07-28T00:00:00.000Z",
|
|
607
|
+
}],
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
label: "separator-only name",
|
|
611
|
+
manifest: [{
|
|
612
|
+
name: "../",
|
|
613
|
+
scope: "global",
|
|
614
|
+
updated_at: "2026-07-28T00:00:00.000Z",
|
|
615
|
+
}],
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
label: "dot name",
|
|
619
|
+
manifest: [{
|
|
620
|
+
name: ".",
|
|
621
|
+
scope: "global",
|
|
622
|
+
updated_at: "2026-07-28T00:00:00.000Z",
|
|
623
|
+
}],
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
label: "malformed entry",
|
|
627
|
+
manifest: [null],
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
label: "invalid scope",
|
|
631
|
+
manifest: [{
|
|
632
|
+
name: "safe-name",
|
|
633
|
+
scope: "project",
|
|
634
|
+
updated_at: "2026-07-28T00:00:00.000Z",
|
|
635
|
+
}],
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
label: "missing timestamp",
|
|
639
|
+
manifest: [{
|
|
640
|
+
name: "safe-name",
|
|
641
|
+
scope: "global",
|
|
642
|
+
updated_at: "",
|
|
643
|
+
}],
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
label: "duplicate name",
|
|
647
|
+
manifest: [
|
|
648
|
+
{
|
|
649
|
+
name: "safe-name",
|
|
650
|
+
scope: "global",
|
|
651
|
+
updated_at: "2026-07-28T00:00:00.000Z",
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
name: "safe-name",
|
|
655
|
+
scope: "global",
|
|
656
|
+
updated_at: "2026-07-28T00:00:00.000Z",
|
|
657
|
+
},
|
|
658
|
+
],
|
|
659
|
+
},
|
|
660
|
+
];
|
|
661
|
+
|
|
662
|
+
for (const variant of variants) {
|
|
663
|
+
const { cwd } = makeWorkspace();
|
|
664
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
|
|
665
|
+
process.env.HOME = homeDir;
|
|
666
|
+
let bodyFetches = 0;
|
|
667
|
+
globalThis.fetch = async (input) => {
|
|
668
|
+
const url = input instanceof URL ? input : new URL(input);
|
|
669
|
+
if (url.pathname.endsWith("/manifest")) {
|
|
670
|
+
return {
|
|
671
|
+
ok: true,
|
|
672
|
+
status: 200,
|
|
673
|
+
statusText: "OK",
|
|
674
|
+
json: async () => ({ skills: variant.manifest }),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
bodyFetches += 1;
|
|
678
|
+
throw new Error("body fetch must not occur");
|
|
679
|
+
};
|
|
680
|
+
const outcome = await runSkillSyncForCli(cwd, "codex");
|
|
681
|
+
assert.equal(outcome.kind, "failed", variant.label);
|
|
682
|
+
assert.equal(bodyFetches, 0, variant.label);
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
test("runSkillSyncForCli: sanitizes an unsafe legacy state key without filesystem mutation", async () => {
|
|
687
|
+
const { cwd } = makeWorkspace();
|
|
688
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-skill-home-"));
|
|
689
|
+
process.env.HOME = homeDir;
|
|
690
|
+
const victimDir = path.join(homeDir, "Documents");
|
|
691
|
+
fs.mkdirSync(victimDir);
|
|
692
|
+
fs.writeFileSync(path.join(victimDir, "sentinel.txt"), "keep", "utf8");
|
|
693
|
+
fs.mkdirSync(path.dirname(daemonStatePath(homeDir)), { recursive: true });
|
|
694
|
+
fs.writeFileSync(
|
|
695
|
+
daemonStatePath(homeDir),
|
|
696
|
+
JSON.stringify({
|
|
697
|
+
skills: {
|
|
698
|
+
"codex:../../Documents": {
|
|
699
|
+
cli: "codex",
|
|
700
|
+
scope: "global",
|
|
701
|
+
updated_at: "2026-07-27T00:00:00.000Z",
|
|
702
|
+
path: victimDir,
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
}),
|
|
706
|
+
"utf8",
|
|
707
|
+
);
|
|
708
|
+
globalThis.fetch = createFetchStub({
|
|
709
|
+
manifests: { codex: [] },
|
|
710
|
+
bodies: {},
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
const outcome = await runSkillSyncForCli(cwd, "codex");
|
|
714
|
+
assert.equal(outcome.kind, "unchanged");
|
|
715
|
+
assert.equal(
|
|
716
|
+
fs.readFileSync(path.join(victimDir, "sentinel.txt"), "utf8"),
|
|
717
|
+
"keep",
|
|
718
|
+
);
|
|
719
|
+
const state = JSON.parse(fs.readFileSync(daemonStatePath(homeDir), "utf8"));
|
|
720
|
+
assert.deepEqual(state.skills, {});
|
|
721
|
+
});
|
|
@@ -5,6 +5,8 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
|
|
7
7
|
import { runScanOnce, writeHostAuditEvent, startUngovernedScanner } from "../dist/daemon/ungoverned-scanner.js";
|
|
8
|
+
import { claimEnterpriseHostIdentity } from "../dist/core/enterprise-host-identity.js";
|
|
9
|
+
import { enterpriseCredentialId } from "../dist/core/license.js";
|
|
8
10
|
|
|
9
11
|
function makeWorkspace(governMode, installs = null) {
|
|
10
12
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-ungoverned-"));
|
|
@@ -41,7 +43,7 @@ test("writeHostAuditEvent appends one JSONL line per call", async () => {
|
|
|
41
43
|
}
|
|
42
44
|
});
|
|
43
45
|
|
|
44
|
-
test("runScanOnce:
|
|
46
|
+
test("runScanOnce: does not write unattributed host process metadata into project audit", async () => {
|
|
45
47
|
const { root } = makeWorkspace("advisory");
|
|
46
48
|
try {
|
|
47
49
|
const fakeProcs = [
|
|
@@ -54,20 +56,64 @@ test("runScanOnce: writes audit event with action=logged in advisory mode", asyn
|
|
|
54
56
|
});
|
|
55
57
|
assert.equal(findings.length, 1);
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
assert.equal(events[0].event_type, "ungoverned_ai_session_detected");
|
|
62
|
-
assert.equal(events[0].cli, "claude");
|
|
63
|
-
assert.equal(events[0].mode, "advisory");
|
|
64
|
-
assert.equal(events[0].action, "logged");
|
|
65
|
-
assert.equal(events[0].host_id, "test-host");
|
|
59
|
+
assert.equal(
|
|
60
|
+
fs.existsSync(path.join(root, ".context", "audit")),
|
|
61
|
+
false,
|
|
62
|
+
);
|
|
66
63
|
} finally {
|
|
67
64
|
fs.rmSync(root, { recursive: true, force: true });
|
|
68
65
|
}
|
|
69
66
|
});
|
|
70
67
|
|
|
68
|
+
test("runScanOnce: writes detections only to the credential-bound user-global queue", async () => {
|
|
69
|
+
const { root } = makeWorkspace("advisory");
|
|
70
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-global-events-"));
|
|
71
|
+
const endpoint = "https://events.example.com";
|
|
72
|
+
const apiKey = "ent_global_events_12345678";
|
|
73
|
+
const credentialId = enterpriseCredentialId(endpoint, apiKey);
|
|
74
|
+
process.env.HOME = homeDir;
|
|
75
|
+
try {
|
|
76
|
+
assert.equal(
|
|
77
|
+
claimEnterpriseHostIdentity(credentialId, endpoint),
|
|
78
|
+
true,
|
|
79
|
+
);
|
|
80
|
+
await runScanOnce({
|
|
81
|
+
cwd: root,
|
|
82
|
+
credentialId,
|
|
83
|
+
detectorOptions: {
|
|
84
|
+
hostId: "host-global",
|
|
85
|
+
processes: [
|
|
86
|
+
{
|
|
87
|
+
pid: 4321,
|
|
88
|
+
ppid: 1,
|
|
89
|
+
user: os.userInfo().username,
|
|
90
|
+
comm: "claude",
|
|
91
|
+
args: "claude --prompt private",
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
assert.equal(fs.existsSync(path.join(root, ".context", "audit")), false);
|
|
98
|
+
const queueDir = path.join(homeDir, ".cortex", "host-events");
|
|
99
|
+
const queueFile = fs.readdirSync(queueDir).find((name) =>
|
|
100
|
+
name.startsWith("ungoverned-")
|
|
101
|
+
);
|
|
102
|
+
assert.ok(queueFile);
|
|
103
|
+
assert.equal(fs.statSync(path.join(queueDir, queueFile)).mode & 0o777, 0o600);
|
|
104
|
+
const event = JSON.parse(
|
|
105
|
+
fs.readFileSync(path.join(queueDir, queueFile), "utf8").trim(),
|
|
106
|
+
);
|
|
107
|
+
assert.equal(event.credential_id, credentialId);
|
|
108
|
+
assert.equal(event.pid, 4321);
|
|
109
|
+
assert.match(event.args, /private/);
|
|
110
|
+
} finally {
|
|
111
|
+
delete process.env.HOME;
|
|
112
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
113
|
+
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
71
117
|
test("runScanOnce: enforced mode marks action=sigterm but our mock doesn't actually signal real procs", async () => {
|
|
72
118
|
const { root } = makeWorkspace("enforced");
|
|
73
119
|
try {
|
|
@@ -91,12 +137,11 @@ test("runScanOnce: enforced mode marks action=sigterm but our mock doesn't actua
|
|
|
91
137
|
process.kill = origKill;
|
|
92
138
|
}
|
|
93
139
|
|
|
94
|
-
const date = new Date().toISOString().slice(0, 10);
|
|
95
|
-
const file = path.join(root, ".context", "audit", `host-events-${date}.jsonl`);
|
|
96
|
-
const events = fs.readFileSync(file, "utf8").trim().split("\n").map(JSON.parse);
|
|
97
|
-
assert.equal(events[0].mode, "enforced");
|
|
98
|
-
assert.equal(events[0].action, "sigterm");
|
|
99
140
|
assert.deepEqual(killed, [99999, "SIGTERM"]);
|
|
141
|
+
assert.equal(
|
|
142
|
+
fs.existsSync(path.join(root, ".context", "audit")),
|
|
143
|
+
false,
|
|
144
|
+
);
|
|
100
145
|
} finally {
|
|
101
146
|
fs.rmSync(root, { recursive: true, force: true });
|
|
102
147
|
}
|
|
@@ -127,9 +172,15 @@ test("runScanOnce: emits onFinding callback per detection", async () => {
|
|
|
127
172
|
test("runScanOnce: skips Tier 1 CLIs that already have managed installs", async () => {
|
|
128
173
|
const { root } = makeWorkspace("enforced");
|
|
129
174
|
const managedClaudePath = path.join(root, "managed-settings.json");
|
|
130
|
-
fs.writeFileSync(
|
|
175
|
+
fs.writeFileSync(
|
|
176
|
+
managedClaudePath,
|
|
177
|
+
JSON.stringify({ allowManagedHooksOnly: true }) + "\n",
|
|
178
|
+
);
|
|
131
179
|
const managedCodexPath = path.join(root, "requirements.toml");
|
|
132
|
-
fs.writeFileSync(
|
|
180
|
+
fs.writeFileSync(
|
|
181
|
+
managedCodexPath,
|
|
182
|
+
"# Cortex govern — codex requirements (test).\n",
|
|
183
|
+
);
|
|
133
184
|
fs.writeFileSync(
|
|
134
185
|
path.join(root, ".context", "govern.local.json"),
|
|
135
186
|
JSON.stringify({
|
|
@@ -160,16 +211,47 @@ test("runScanOnce: skips Tier 1 CLIs that already have managed installs", async
|
|
|
160
211
|
const findings = await runScanOnce({
|
|
161
212
|
cwd: root,
|
|
162
213
|
mode: "enforced",
|
|
214
|
+
managedPathOverrides: {
|
|
215
|
+
claude: managedClaudePath,
|
|
216
|
+
codex: managedCodexPath,
|
|
217
|
+
},
|
|
163
218
|
detectorOptions: { processes: fakeProcs, hostId: "test-host" },
|
|
164
219
|
});
|
|
165
220
|
assert.equal(findings.length, 1);
|
|
166
221
|
assert.equal(findings[0].cli, "copilot");
|
|
167
222
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
223
|
+
assert.equal(fs.existsSync(path.join(root, ".context", "audit")), false);
|
|
224
|
+
} finally {
|
|
225
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("runScanOnce: forged persisted paths cannot suppress Tier 1 findings", async () => {
|
|
230
|
+
const { root } = makeWorkspace("advisory", {
|
|
231
|
+
claude: {
|
|
232
|
+
mode: "advisory",
|
|
233
|
+
path: "/bin/sh",
|
|
234
|
+
version: "forged",
|
|
235
|
+
frameworks: [],
|
|
236
|
+
installed_at: "now",
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
try {
|
|
240
|
+
const findings = await runScanOnce({
|
|
241
|
+
cwd: root,
|
|
242
|
+
mode: "advisory",
|
|
243
|
+
detectorOptions: {
|
|
244
|
+
processes: [{
|
|
245
|
+
pid: 7654,
|
|
246
|
+
ppid: 1,
|
|
247
|
+
user: os.userInfo().username,
|
|
248
|
+
comm: "claude",
|
|
249
|
+
args: "claude --prompt hi",
|
|
250
|
+
}],
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
assert.equal(findings.length, 1);
|
|
254
|
+
assert.equal(findings[0].cli, "claude");
|
|
173
255
|
} finally {
|
|
174
256
|
fs.rmSync(root, { recursive: true, force: true });
|
|
175
257
|
}
|
|
@@ -5,6 +5,10 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
|
|
7
7
|
import { runStageCommand } from "../dist/cli/stage.js";
|
|
8
|
+
import { enterpriseCredentialId } from "../dist/core/license.js";
|
|
9
|
+
|
|
10
|
+
const ENTERPRISE_ENDPOINT = "https://licenses.example.com";
|
|
11
|
+
const ENTERPRISE_KEY = "ent_workflow_cli_12345678";
|
|
8
12
|
|
|
9
13
|
const TINY_WORKFLOW = {
|
|
10
14
|
id: "tiny",
|
|
@@ -41,7 +45,12 @@ function makeWorkspace() {
|
|
|
41
45
|
fs.mkdirSync(path.join(dir, ".context"), { recursive: true });
|
|
42
46
|
fs.writeFileSync(
|
|
43
47
|
path.join(dir, ".context", "enterprise.yml"),
|
|
44
|
-
|
|
48
|
+
[
|
|
49
|
+
"enterprise:",
|
|
50
|
+
` api_key: ${ENTERPRISE_KEY}`,
|
|
51
|
+
` endpoint: ${ENTERPRISE_ENDPOINT}`,
|
|
52
|
+
"",
|
|
53
|
+
].join("\n"),
|
|
45
54
|
"utf8",
|
|
46
55
|
);
|
|
47
56
|
return dir;
|
|
@@ -53,6 +62,10 @@ function writeSyncedWorkflow(homeDir, workflowId, definition) {
|
|
|
53
62
|
fs.writeFileSync(
|
|
54
63
|
path.join(cortexDir, "workflows.local.json"),
|
|
55
64
|
JSON.stringify({
|
|
65
|
+
credential_id: enterpriseCredentialId(
|
|
66
|
+
ENTERPRISE_ENDPOINT,
|
|
67
|
+
ENTERPRISE_KEY,
|
|
68
|
+
),
|
|
56
69
|
workflows: {
|
|
57
70
|
[workflowId]: {
|
|
58
71
|
workflow_id: workflowId,
|