@danielblomma/cortex-mcp 1.7.1 → 2.0.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/bin/cortex.mjs +679 -32
- package/bin/style.mjs +349 -0
- package/package.json +4 -3
- package/scaffold/mcp/package-lock.json +834 -671
- package/scaffold/mcp/package.json +1 -1
- package/scaffold/mcp/src/cli/enterprise-setup.ts +124 -0
- package/scaffold/mcp/src/cli/govern.ts +987 -0
- package/scaffold/mcp/src/cli/run.ts +306 -0
- package/scaffold/mcp/src/cli/telemetry-test.ts +158 -0
- package/scaffold/mcp/src/cli/ungoverned-detector.ts +168 -0
- package/scaffold/mcp/src/core/audit/query.ts +81 -0
- package/scaffold/mcp/src/core/audit/writer.ts +68 -0
- package/scaffold/mcp/src/core/config.ts +329 -0
- package/scaffold/mcp/src/core/index.ts +34 -0
- package/scaffold/mcp/src/core/license.ts +202 -0
- package/scaffold/mcp/src/core/policy/enforce.ts +98 -0
- package/scaffold/mcp/src/core/policy/injection.ts +229 -0
- package/scaffold/mcp/src/core/policy/store.ts +197 -0
- package/scaffold/mcp/src/core/rbac/check.ts +40 -0
- package/scaffold/mcp/src/core/telemetry/collector.ts +234 -0
- package/scaffold/mcp/src/core/validators/builtins.ts +711 -0
- package/scaffold/mcp/src/core/validators/config.ts +47 -0
- package/scaffold/mcp/src/core/validators/engine.ts +199 -0
- package/scaffold/mcp/src/core/validators/evaluators/code_comments.ts +294 -0
- package/scaffold/mcp/src/core/validators/evaluators/regex.ts +144 -0
- package/scaffold/mcp/src/daemon/client.ts +155 -0
- package/scaffold/mcp/src/daemon/egress-proxy.ts +331 -0
- package/scaffold/mcp/src/daemon/heartbeat-pusher.ts +147 -0
- package/scaffold/mcp/src/daemon/heartbeat-tracker.ts +223 -0
- package/scaffold/mcp/src/daemon/host-events-pusher.ts +285 -0
- package/scaffold/mcp/src/daemon/main.ts +300 -0
- package/scaffold/mcp/src/daemon/paths.ts +41 -0
- package/scaffold/mcp/src/daemon/protocol.ts +101 -0
- package/scaffold/mcp/src/daemon/server.ts +227 -0
- package/scaffold/mcp/src/daemon/sync-checker.ts +213 -0
- package/scaffold/mcp/src/daemon/ungoverned-scanner.ts +149 -0
- package/scaffold/mcp/src/embed.ts +1 -1
- package/scaffold/mcp/src/embeddings.ts +1 -1
- package/scaffold/mcp/src/enterprise/audit/push.ts +84 -0
- package/scaffold/mcp/src/enterprise/index.ts +415 -0
- package/scaffold/mcp/src/enterprise/model/deploy.ts +33 -0
- package/scaffold/mcp/src/enterprise/policy/sync.ts +146 -0
- package/scaffold/mcp/src/enterprise/privacy/boundary.ts +212 -0
- package/scaffold/mcp/src/enterprise/reviews/push.ts +79 -0
- package/scaffold/mcp/src/enterprise/telemetry/sync.ts +72 -0
- package/scaffold/mcp/src/enterprise/tools/enterprise.ts +1031 -0
- package/scaffold/mcp/src/enterprise/tools/walk.ts +79 -0
- package/scaffold/mcp/src/enterprise/violations/push.ts +102 -0
- package/scaffold/mcp/src/enterprise/workflow/push.ts +60 -0
- package/scaffold/mcp/src/enterprise/workflow/state.ts +535 -0
- package/scaffold/mcp/src/hooks/pre-compact.ts +54 -0
- package/scaffold/mcp/src/hooks/pre-tool-use.ts +96 -0
- package/scaffold/mcp/src/hooks/session-end.ts +73 -0
- package/scaffold/mcp/src/hooks/session-start.ts +78 -0
- package/scaffold/mcp/src/hooks/shared.ts +134 -0
- package/scaffold/mcp/src/hooks/stop.ts +60 -0
- package/scaffold/mcp/src/hooks/user-prompt-submit.ts +64 -0
- package/scaffold/mcp/src/plugin.ts +150 -0
- package/scaffold/mcp/src/server.ts +218 -7
- package/scaffold/mcp/tests/copilot-shim.test.mjs +146 -0
- package/scaffold/mcp/tests/daemon-client.test.mjs +32 -0
- package/scaffold/mcp/tests/egress-proxy.test.mjs +239 -0
- package/scaffold/mcp/tests/enterprise-config.test.mjs +154 -0
- package/scaffold/mcp/tests/govern-install.test.mjs +320 -0
- package/scaffold/mcp/tests/govern-repair.test.mjs +157 -0
- package/scaffold/mcp/tests/govern-status.test.mjs +538 -0
- package/scaffold/mcp/tests/govern.test.mjs +74 -0
- package/scaffold/mcp/tests/heartbeat-pusher.test.mjs +154 -0
- package/scaffold/mcp/tests/heartbeat-tracker.test.mjs +237 -0
- package/scaffold/mcp/tests/host-events-pusher.test.mjs +347 -0
- package/scaffold/mcp/tests/policy-check.test.mjs +220 -0
- package/scaffold/mcp/tests/repo-name.test.mjs +134 -0
- package/scaffold/mcp/tests/run.test.mjs +109 -0
- package/scaffold/mcp/tests/sync-checker.test.mjs +188 -0
- package/scaffold/mcp/tests/ungoverned-detector.test.mjs +191 -0
- package/scaffold/mcp/tests/ungoverned-scanner.test.mjs +198 -0
- package/scaffold/scripts/bootstrap.sh +0 -11
- package/scaffold/scripts/doctor.sh +24 -4
- package/types.js +5 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
import { loadEnterpriseConfig } from "../dist/core/config.js";
|
|
8
|
+
|
|
9
|
+
function makeContextDir(content) {
|
|
10
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-enterprise-config-"));
|
|
11
|
+
const contextDir = path.join(tempRoot, ".context");
|
|
12
|
+
fs.mkdirSync(contextDir, { recursive: true });
|
|
13
|
+
if (content !== undefined) {
|
|
14
|
+
fs.writeFileSync(path.join(contextDir, "enterprise.yml"), content);
|
|
15
|
+
}
|
|
16
|
+
return { tempRoot, contextDir };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test("loader: missing file returns defaults with empty api_key", () => {
|
|
20
|
+
const { tempRoot, contextDir } = makeContextDir(undefined);
|
|
21
|
+
try {
|
|
22
|
+
const cfg = loadEnterpriseConfig(contextDir);
|
|
23
|
+
assert.equal(cfg.enterprise.api_key, "");
|
|
24
|
+
assert.equal(cfg.govern.mode, "off");
|
|
25
|
+
assert.equal(cfg.compliance.frameworks.length, 0);
|
|
26
|
+
} finally {
|
|
27
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("loader: new schema — single api_key flows to telemetry+policy, base_url derives endpoints", () => {
|
|
32
|
+
const yaml = [
|
|
33
|
+
"enterprise:",
|
|
34
|
+
" api_key: ent_test_key_1234",
|
|
35
|
+
" base_url: https://example.com",
|
|
36
|
+
"",
|
|
37
|
+
"compliance:",
|
|
38
|
+
" frameworks: [iso27001, soc2]",
|
|
39
|
+
" eu_addons: false",
|
|
40
|
+
"",
|
|
41
|
+
"govern:",
|
|
42
|
+
" mode: advisory",
|
|
43
|
+
" tier_claude: prevent",
|
|
44
|
+
" tier_codex: prevent",
|
|
45
|
+
" tier_copilot: wrap",
|
|
46
|
+
"",
|
|
47
|
+
].join("\n");
|
|
48
|
+
const { tempRoot, contextDir } = makeContextDir(yaml);
|
|
49
|
+
try {
|
|
50
|
+
const cfg = loadEnterpriseConfig(contextDir);
|
|
51
|
+
assert.equal(cfg.enterprise.api_key, "ent_test_key_1234");
|
|
52
|
+
assert.equal(cfg.enterprise.base_url, "https://example.com");
|
|
53
|
+
assert.equal(cfg.telemetry.api_key, "ent_test_key_1234");
|
|
54
|
+
assert.equal(cfg.telemetry.endpoint, "https://example.com/api/v1/telemetry/push");
|
|
55
|
+
assert.equal(cfg.policy.api_key, "ent_test_key_1234");
|
|
56
|
+
assert.equal(cfg.policy.endpoint, "https://example.com/api/v1/policies/sync");
|
|
57
|
+
assert.equal(cfg.govern.govern_endpoint, "https://example.com/api/v1/govern");
|
|
58
|
+
assert.deepEqual(cfg.compliance.frameworks, ["iso27001", "soc2"]);
|
|
59
|
+
assert.equal(cfg.compliance.eu_addons, false);
|
|
60
|
+
assert.equal(cfg.govern.mode, "advisory");
|
|
61
|
+
assert.equal(cfg.govern.tier_copilot, "wrap");
|
|
62
|
+
assert.equal(cfg.govern.detect_ungoverned, true);
|
|
63
|
+
} finally {
|
|
64
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("loader: eu_addons=true merges gdpr/ai_act/nis2 with default frameworks", () => {
|
|
69
|
+
const yaml = [
|
|
70
|
+
"enterprise:",
|
|
71
|
+
" api_key: ent_test_key_1234",
|
|
72
|
+
" base_url: https://example.com",
|
|
73
|
+
"compliance:",
|
|
74
|
+
" eu_addons: true",
|
|
75
|
+
"",
|
|
76
|
+
].join("\n");
|
|
77
|
+
const { tempRoot, contextDir } = makeContextDir(yaml);
|
|
78
|
+
try {
|
|
79
|
+
const cfg = loadEnterpriseConfig(contextDir);
|
|
80
|
+
assert.deepEqual(
|
|
81
|
+
cfg.compliance.frameworks.sort(),
|
|
82
|
+
["ai_act", "gdpr", "iso27001", "iso42001", "nis2", "soc2"],
|
|
83
|
+
);
|
|
84
|
+
assert.equal(cfg.compliance.eu_addons, true);
|
|
85
|
+
} finally {
|
|
86
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("loader: legacy schema — telemetry/policy own keys still work", () => {
|
|
91
|
+
const yaml = [
|
|
92
|
+
"enterprise:",
|
|
93
|
+
" endpoint: https://legacy.example.com",
|
|
94
|
+
" api_key: ent_legacy_key_1234",
|
|
95
|
+
"",
|
|
96
|
+
"telemetry:",
|
|
97
|
+
" enabled: true",
|
|
98
|
+
" endpoint: https://legacy.example.com/api/v1/telemetry/push",
|
|
99
|
+
" api_key: ent_legacy_key_1234",
|
|
100
|
+
"",
|
|
101
|
+
"policy:",
|
|
102
|
+
" endpoint: https://legacy.example.com/api/v1/policies/sync",
|
|
103
|
+
" api_key: ent_legacy_key_1234",
|
|
104
|
+
"",
|
|
105
|
+
].join("\n");
|
|
106
|
+
const { tempRoot, contextDir } = makeContextDir(yaml);
|
|
107
|
+
try {
|
|
108
|
+
const cfg = loadEnterpriseConfig(contextDir);
|
|
109
|
+
assert.equal(cfg.enterprise.api_key, "ent_legacy_key_1234");
|
|
110
|
+
assert.equal(cfg.telemetry.endpoint, "https://legacy.example.com/api/v1/telemetry/push");
|
|
111
|
+
assert.equal(cfg.policy.endpoint, "https://legacy.example.com/api/v1/policies/sync");
|
|
112
|
+
assert.equal(cfg.govern.mode, "off");
|
|
113
|
+
} finally {
|
|
114
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("loader: invalid frameworks are dropped silently", () => {
|
|
119
|
+
const yaml = [
|
|
120
|
+
"enterprise:",
|
|
121
|
+
" api_key: ent_test_key_1234",
|
|
122
|
+
" base_url: https://example.com",
|
|
123
|
+
"compliance:",
|
|
124
|
+
" frameworks: [iso27001, made_up_framework, soc2]",
|
|
125
|
+
"",
|
|
126
|
+
].join("\n");
|
|
127
|
+
const { tempRoot, contextDir } = makeContextDir(yaml);
|
|
128
|
+
try {
|
|
129
|
+
const cfg = loadEnterpriseConfig(contextDir);
|
|
130
|
+
assert.deepEqual(cfg.compliance.frameworks, ["iso27001", "soc2"]);
|
|
131
|
+
} finally {
|
|
132
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("loader: invalid govern mode falls back to default", () => {
|
|
137
|
+
const yaml = [
|
|
138
|
+
"enterprise:",
|
|
139
|
+
" api_key: ent_test_key_1234",
|
|
140
|
+
" base_url: https://example.com",
|
|
141
|
+
"govern:",
|
|
142
|
+
" mode: bogus_mode",
|
|
143
|
+
" tier_claude: not_a_tier",
|
|
144
|
+
"",
|
|
145
|
+
].join("\n");
|
|
146
|
+
const { tempRoot, contextDir } = makeContextDir(yaml);
|
|
147
|
+
try {
|
|
148
|
+
const cfg = loadEnterpriseConfig(contextDir);
|
|
149
|
+
assert.equal(cfg.govern.mode, "off");
|
|
150
|
+
assert.equal(cfg.govern.tier_claude, "prevent");
|
|
151
|
+
} finally {
|
|
152
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import http from "node:http";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
runGovernInstall,
|
|
10
|
+
runGovernUninstall,
|
|
11
|
+
runGovernStatus,
|
|
12
|
+
} from "../dist/cli/govern.js";
|
|
13
|
+
|
|
14
|
+
function startMockServer(handlers) {
|
|
15
|
+
const server = http.createServer((req, res) => {
|
|
16
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
17
|
+
const handler = handlers[`${req.method} ${url.pathname}`];
|
|
18
|
+
if (!handler) {
|
|
19
|
+
res.statusCode = 404;
|
|
20
|
+
res.end("not found");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
let body = "";
|
|
24
|
+
req.on("data", (c) => (body += c));
|
|
25
|
+
req.on("end", () => handler(req, res, url, body));
|
|
26
|
+
});
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
server.listen(0, "127.0.0.1", () => {
|
|
29
|
+
const addr = server.address();
|
|
30
|
+
resolve({ server, baseUrl: `http://127.0.0.1:${addr.port}` });
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeProject({ apiKey, baseUrl, frameworks = ["iso27001"] }) {
|
|
36
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-govern-"));
|
|
37
|
+
const ctx = path.join(root, ".context");
|
|
38
|
+
fs.mkdirSync(ctx, { recursive: true });
|
|
39
|
+
const yaml = [
|
|
40
|
+
"enterprise:",
|
|
41
|
+
` api_key: ${apiKey}`,
|
|
42
|
+
` base_url: ${baseUrl}`,
|
|
43
|
+
"compliance:",
|
|
44
|
+
` frameworks: [${frameworks.join(", ")}]`,
|
|
45
|
+
"",
|
|
46
|
+
].join("\n");
|
|
47
|
+
fs.writeFileSync(path.join(ctx, "enterprise.yml"), yaml);
|
|
48
|
+
return { root, ctx };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test("install --cli claude writes managed-settings.json and records state", async () => {
|
|
52
|
+
let appliedCalls = 0;
|
|
53
|
+
const { server, baseUrl } = await startMockServer({
|
|
54
|
+
"GET /api/v1/govern/config": (req, res) => {
|
|
55
|
+
const config = {
|
|
56
|
+
cli: "claude",
|
|
57
|
+
managed_settings: {
|
|
58
|
+
allowManagedHooksOnly: true,
|
|
59
|
+
permissions: { deny: ["Edit(~/.claude/settings.json)"] },
|
|
60
|
+
},
|
|
61
|
+
deny_rules: [{ pattern: "Edit(~/.claude/settings.json)", source_frameworks: ["iso27001"] }],
|
|
62
|
+
tamper_config: { heartbeat_interval_seconds: 60, missing_threshold_seconds: 300 },
|
|
63
|
+
frameworks: [{ id: "iso27001", version: "0.1.0-seed" }],
|
|
64
|
+
};
|
|
65
|
+
res.setHeader("ETag", '"abc123version"');
|
|
66
|
+
res.setHeader("Content-Type", "application/json");
|
|
67
|
+
res.end(JSON.stringify(config));
|
|
68
|
+
},
|
|
69
|
+
"POST /api/v1/govern/applied": (req, res, url, body) => {
|
|
70
|
+
appliedCalls += 1;
|
|
71
|
+
const payload = JSON.parse(body);
|
|
72
|
+
assert.equal(payload.cli, "claude");
|
|
73
|
+
assert.equal(payload.success, true);
|
|
74
|
+
res.setHeader("Content-Type", "application/json");
|
|
75
|
+
res.end(JSON.stringify({ ok: true }));
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const { root, ctx } = makeProject({ apiKey: "ent_test_key_12345678", baseUrl });
|
|
80
|
+
const claudeManagedPath = path.join(root, "fake-managed-settings.json");
|
|
81
|
+
try {
|
|
82
|
+
const result = await runGovernInstall({
|
|
83
|
+
cli: "claude",
|
|
84
|
+
cwd: root,
|
|
85
|
+
mode: "advisory",
|
|
86
|
+
pathOverride: { claude: claudeManagedPath },
|
|
87
|
+
skipRoot: true,
|
|
88
|
+
});
|
|
89
|
+
assert.equal(result.ok, true, result.message);
|
|
90
|
+
assert.deepEqual(result.installed, ["claude"]);
|
|
91
|
+
|
|
92
|
+
const written = JSON.parse(fs.readFileSync(claudeManagedPath, "utf8"));
|
|
93
|
+
assert.equal(written.allowManagedHooksOnly, true);
|
|
94
|
+
assert.deepEqual(written.permissions.deny, ["Edit(~/.claude/settings.json)"]);
|
|
95
|
+
|
|
96
|
+
const state = JSON.parse(fs.readFileSync(path.join(ctx, "govern.local.json"), "utf8"));
|
|
97
|
+
assert.equal(state.installs.claude.path, claudeManagedPath);
|
|
98
|
+
assert.equal(state.installs.claude.version, "abc123version");
|
|
99
|
+
assert.equal(state.installs.claude.mode, "advisory");
|
|
100
|
+
assert.equal(appliedCalls, 1);
|
|
101
|
+
} finally {
|
|
102
|
+
server.close();
|
|
103
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("install --cli codex writes requirements.toml with sandbox bounds", async () => {
|
|
108
|
+
const { server, baseUrl } = await startMockServer({
|
|
109
|
+
"GET /api/v1/govern/config": (req, res) => {
|
|
110
|
+
const config = {
|
|
111
|
+
cli: "codex",
|
|
112
|
+
managed_settings: {},
|
|
113
|
+
deny_rules: [
|
|
114
|
+
{ pattern: "Edit(~/.codex/config.toml)", source_frameworks: ["iso27001"] },
|
|
115
|
+
],
|
|
116
|
+
tamper_config: { heartbeat_interval_seconds: 60, missing_threshold_seconds: 300 },
|
|
117
|
+
frameworks: [{ id: "iso27001", version: "0.1.0-seed" }],
|
|
118
|
+
};
|
|
119
|
+
res.setHeader("ETag", '"codex_v1"');
|
|
120
|
+
res.setHeader("Content-Type", "application/json");
|
|
121
|
+
res.end(JSON.stringify(config));
|
|
122
|
+
},
|
|
123
|
+
"POST /api/v1/govern/applied": (req, res) => res.end(JSON.stringify({ ok: true })),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const { root } = makeProject({ apiKey: "ent_test_key_12345678", baseUrl });
|
|
127
|
+
const codexPath = path.join(root, "fake-codex-requirements.toml");
|
|
128
|
+
try {
|
|
129
|
+
const result = await runGovernInstall({
|
|
130
|
+
cli: "codex",
|
|
131
|
+
cwd: root,
|
|
132
|
+
pathOverride: { codex: codexPath },
|
|
133
|
+
skipRoot: true,
|
|
134
|
+
});
|
|
135
|
+
assert.equal(result.ok, true, result.message);
|
|
136
|
+
|
|
137
|
+
const toml = fs.readFileSync(codexPath, "utf8");
|
|
138
|
+
assert.match(toml, /allowed_sandbox_modes = \["read-only", "workspace-write"\]/);
|
|
139
|
+
assert.match(toml, /\[permissions\.filesystem\]/);
|
|
140
|
+
assert.match(toml, /deny_read = \["~\/.codex\/config\.toml"\]/);
|
|
141
|
+
} finally {
|
|
142
|
+
server.close();
|
|
143
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("install --all installs claude+codex managed files plus copilot Tier-2 shim", async () => {
|
|
148
|
+
const { server, baseUrl } = await startMockServer({
|
|
149
|
+
"GET /api/v1/govern/config": (req, res) => {
|
|
150
|
+
const config = {
|
|
151
|
+
cli: req.url.includes("cli=claude") ? "claude" : "codex",
|
|
152
|
+
managed_settings: {},
|
|
153
|
+
deny_rules: [],
|
|
154
|
+
tamper_config: { heartbeat_interval_seconds: 60, missing_threshold_seconds: 300 },
|
|
155
|
+
frameworks: [{ id: "iso27001", version: "0.1.0-seed" }],
|
|
156
|
+
};
|
|
157
|
+
res.setHeader("ETag", '"v1"');
|
|
158
|
+
res.setHeader("Content-Type", "application/json");
|
|
159
|
+
res.end(JSON.stringify(config));
|
|
160
|
+
},
|
|
161
|
+
"POST /api/v1/govern/applied": (req, res) => res.end(JSON.stringify({ ok: true })),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const { root } = makeProject({ apiKey: "ent_test_key_12345678", baseUrl });
|
|
165
|
+
// Stand up a fake real-copilot binary on a temp PATH so the shim install
|
|
166
|
+
// can find it; otherwise copilot is skipped for missing-binary reasons.
|
|
167
|
+
const realDir = path.join(root, "real-bin");
|
|
168
|
+
fs.mkdirSync(realDir, { recursive: true });
|
|
169
|
+
const realCopilot = path.join(realDir, "copilot");
|
|
170
|
+
fs.writeFileSync(realCopilot, "#!/bin/sh\necho real copilot\n", { mode: 0o755 });
|
|
171
|
+
const origPath = process.env.PATH;
|
|
172
|
+
process.env.PATH = `${realDir}:${origPath ?? ""}`;
|
|
173
|
+
try {
|
|
174
|
+
const result = await runGovernInstall({
|
|
175
|
+
cli: "all",
|
|
176
|
+
cwd: root,
|
|
177
|
+
pathOverride: {
|
|
178
|
+
claude: path.join(root, "claude-managed.json"),
|
|
179
|
+
codex: path.join(root, "codex-requirements.toml"),
|
|
180
|
+
copilot: path.join(root, "fake-copilot-shim"),
|
|
181
|
+
},
|
|
182
|
+
skipRoot: true,
|
|
183
|
+
});
|
|
184
|
+
assert.equal(result.ok, true, result.message);
|
|
185
|
+
assert.deepEqual(result.installed.sort(), ["claude", "codex", "copilot"]);
|
|
186
|
+
const shimContents = fs.readFileSync(path.join(root, "fake-copilot-shim"), "utf8");
|
|
187
|
+
assert.match(shimContents, /cortex-shim-v1/);
|
|
188
|
+
assert.match(shimContents, new RegExp(realCopilot));
|
|
189
|
+
} finally {
|
|
190
|
+
process.env.PATH = origPath;
|
|
191
|
+
server.close();
|
|
192
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("uninstall removes managed file and updates state", async () => {
|
|
197
|
+
const { server, baseUrl } = await startMockServer({
|
|
198
|
+
"GET /api/v1/govern/config": (req, res) => {
|
|
199
|
+
res.setHeader("ETag", '"v1"');
|
|
200
|
+
res.end(
|
|
201
|
+
JSON.stringify({
|
|
202
|
+
cli: "claude",
|
|
203
|
+
managed_settings: { allowManagedHooksOnly: true },
|
|
204
|
+
deny_rules: [],
|
|
205
|
+
tamper_config: { heartbeat_interval_seconds: 60, missing_threshold_seconds: 300 },
|
|
206
|
+
frameworks: [{ id: "iso27001", version: "0.1.0-seed" }],
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
209
|
+
},
|
|
210
|
+
"POST /api/v1/govern/applied": (req, res) => res.end(JSON.stringify({ ok: true })),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const { root, ctx } = makeProject({ apiKey: "ent_test_key_12345678", baseUrl });
|
|
214
|
+
const target = path.join(root, "claude-managed.json");
|
|
215
|
+
try {
|
|
216
|
+
await runGovernInstall({
|
|
217
|
+
cli: "claude",
|
|
218
|
+
cwd: root,
|
|
219
|
+
mode: "advisory",
|
|
220
|
+
pathOverride: { claude: target },
|
|
221
|
+
skipRoot: true,
|
|
222
|
+
});
|
|
223
|
+
assert.equal(fs.existsSync(target), true);
|
|
224
|
+
|
|
225
|
+
const result = await runGovernUninstall({ cli: "claude", cwd: root, skipRoot: true });
|
|
226
|
+
assert.equal(result.ok, true, result.message);
|
|
227
|
+
assert.equal(fs.existsSync(target), false);
|
|
228
|
+
|
|
229
|
+
const state = JSON.parse(fs.readFileSync(path.join(ctx, "govern.local.json"), "utf8"));
|
|
230
|
+
assert.equal(state.installs.claude, undefined);
|
|
231
|
+
} finally {
|
|
232
|
+
server.close();
|
|
233
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("uninstall in enforced mode requires --break-glass + --reason", async () => {
|
|
238
|
+
const { server, baseUrl } = await startMockServer({
|
|
239
|
+
"GET /api/v1/govern/config": (req, res) => {
|
|
240
|
+
res.setHeader("ETag", '"v1"');
|
|
241
|
+
res.end(
|
|
242
|
+
JSON.stringify({
|
|
243
|
+
cli: "claude",
|
|
244
|
+
managed_settings: { allowManagedHooksOnly: true },
|
|
245
|
+
deny_rules: [],
|
|
246
|
+
tamper_config: { heartbeat_interval_seconds: 60, missing_threshold_seconds: 300 },
|
|
247
|
+
frameworks: [{ id: "iso27001", version: "0.1.0-seed" }],
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
},
|
|
251
|
+
"POST /api/v1/govern/applied": (req, res) => res.end(JSON.stringify({ ok: true })),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const { root } = makeProject({ apiKey: "ent_test_key_12345678", baseUrl });
|
|
255
|
+
const target = path.join(root, "claude-managed.json");
|
|
256
|
+
try {
|
|
257
|
+
await runGovernInstall({
|
|
258
|
+
cli: "claude",
|
|
259
|
+
cwd: root,
|
|
260
|
+
mode: "enforced",
|
|
261
|
+
pathOverride: { claude: target },
|
|
262
|
+
skipRoot: true,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const blocked = await runGovernUninstall({ cli: "claude", cwd: root, skipRoot: true });
|
|
266
|
+
assert.equal(blocked.ok, false);
|
|
267
|
+
assert.match(blocked.message, /enforced mode/);
|
|
268
|
+
assert.equal(fs.existsSync(target), true);
|
|
269
|
+
|
|
270
|
+
const noReason = await runGovernUninstall({
|
|
271
|
+
cli: "claude",
|
|
272
|
+
cwd: root,
|
|
273
|
+
breakGlass: true,
|
|
274
|
+
skipRoot: true,
|
|
275
|
+
});
|
|
276
|
+
assert.equal(noReason.ok, false);
|
|
277
|
+
assert.match(noReason.message, /requires --reason/);
|
|
278
|
+
|
|
279
|
+
const allowed = await runGovernUninstall({
|
|
280
|
+
cli: "claude",
|
|
281
|
+
cwd: root,
|
|
282
|
+
breakGlass: true,
|
|
283
|
+
reason: "Incident response",
|
|
284
|
+
skipRoot: true,
|
|
285
|
+
});
|
|
286
|
+
assert.equal(allowed.ok, true, allowed.message);
|
|
287
|
+
assert.equal(fs.existsSync(target), false);
|
|
288
|
+
} finally {
|
|
289
|
+
server.close();
|
|
290
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("install with no api_key returns helpful error", async () => {
|
|
295
|
+
const { root } = makeProject({ apiKey: "", baseUrl: "http://example.com" });
|
|
296
|
+
try {
|
|
297
|
+
const result = await runGovernInstall({ cli: "claude", cwd: root, skipRoot: true });
|
|
298
|
+
assert.equal(result.ok, false);
|
|
299
|
+
assert.match(result.message, /No enterprise.api_key/);
|
|
300
|
+
} finally {
|
|
301
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("status with no installs prints the empty-state hint", () => {
|
|
306
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-govern-status-"));
|
|
307
|
+
fs.mkdirSync(path.join(root, ".context"));
|
|
308
|
+
const lines = [];
|
|
309
|
+
const origLog = console.log;
|
|
310
|
+
console.log = (...args) => lines.push(args.join(" "));
|
|
311
|
+
try {
|
|
312
|
+
runGovernStatus({ cwd: root });
|
|
313
|
+
} finally {
|
|
314
|
+
console.log = origLog;
|
|
315
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
316
|
+
}
|
|
317
|
+
const output = lines.join("\n");
|
|
318
|
+
assert.match(output, /No CLIs governed/);
|
|
319
|
+
assert.match(output, /sudo cortex enterprise/);
|
|
320
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
import { runGovernRepair } from "../dist/cli/govern.js";
|
|
8
|
+
import { writeTamperLock } from "../dist/daemon/heartbeat-tracker.js";
|
|
9
|
+
|
|
10
|
+
function makeWorkspace({ installs }) {
|
|
11
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-repair-"));
|
|
12
|
+
const ctx = path.join(root, ".context");
|
|
13
|
+
fs.mkdirSync(ctx, { recursive: true });
|
|
14
|
+
fs.writeFileSync(
|
|
15
|
+
path.join(ctx, "govern.local.json"),
|
|
16
|
+
JSON.stringify({ installs }, null, 2),
|
|
17
|
+
);
|
|
18
|
+
return { root, ctx };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test("repair: errors when nothing is governed yet", async () => {
|
|
22
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-repair-empty-"));
|
|
23
|
+
fs.mkdirSync(path.join(root, ".context"), { recursive: true });
|
|
24
|
+
try {
|
|
25
|
+
const result = await runGovernRepair({ cwd: root, skipRoot: true });
|
|
26
|
+
assert.equal(result.ok, false);
|
|
27
|
+
assert.match(result.message, /nothing to repair/);
|
|
28
|
+
} finally {
|
|
29
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("repair: errors when managed file is missing", async () => {
|
|
34
|
+
const { root } = makeWorkspace({
|
|
35
|
+
installs: {
|
|
36
|
+
claude: {
|
|
37
|
+
path: path.join(os.tmpdir(), "definitely-not-here-claude-managed.json"),
|
|
38
|
+
version: "v1",
|
|
39
|
+
frameworks: [{ id: "iso27001", version: "0.1" }],
|
|
40
|
+
installed_at: new Date().toISOString(),
|
|
41
|
+
mode: "advisory",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
try {
|
|
46
|
+
const result = await runGovernRepair({ cwd: root, skipRoot: true });
|
|
47
|
+
assert.equal(result.ok, false);
|
|
48
|
+
assert.match(result.message, /missing/);
|
|
49
|
+
} finally {
|
|
50
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("repair: errors when copilot shim has been replaced", async () => {
|
|
55
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-repair-shim-"));
|
|
56
|
+
const ctx = path.join(root, ".context");
|
|
57
|
+
fs.mkdirSync(ctx, { recursive: true });
|
|
58
|
+
const fakeShim = path.join(root, "fake-copilot");
|
|
59
|
+
fs.writeFileSync(fakeShim, "#!/bin/sh\necho NOT a cortex shim\n", { mode: 0o755 });
|
|
60
|
+
fs.writeFileSync(
|
|
61
|
+
path.join(ctx, "govern.local.json"),
|
|
62
|
+
JSON.stringify({
|
|
63
|
+
installs: {
|
|
64
|
+
copilot: {
|
|
65
|
+
path: fakeShim,
|
|
66
|
+
version: "shim-v1",
|
|
67
|
+
frameworks: [],
|
|
68
|
+
installed_at: new Date().toISOString(),
|
|
69
|
+
mode: "advisory",
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
try {
|
|
75
|
+
const result = await runGovernRepair({ cwd: root, skipRoot: true });
|
|
76
|
+
assert.equal(result.ok, false);
|
|
77
|
+
assert.match(result.message, /no longer a cortex shim/);
|
|
78
|
+
} finally {
|
|
79
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("repair: clears tamper lock when managed paths verify clean", async () => {
|
|
84
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-repair-ok-"));
|
|
85
|
+
const ctx = path.join(root, ".context");
|
|
86
|
+
fs.mkdirSync(ctx, { recursive: true });
|
|
87
|
+
const claudeManaged = path.join(root, "managed.json");
|
|
88
|
+
fs.writeFileSync(claudeManaged, '{"allowManagedHooksOnly":true}');
|
|
89
|
+
fs.writeFileSync(
|
|
90
|
+
path.join(ctx, "govern.local.json"),
|
|
91
|
+
JSON.stringify({
|
|
92
|
+
installs: {
|
|
93
|
+
claude: {
|
|
94
|
+
path: claudeManaged,
|
|
95
|
+
version: "v1",
|
|
96
|
+
frameworks: [],
|
|
97
|
+
installed_at: new Date().toISOString(),
|
|
98
|
+
mode: "enforced",
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
writeTamperLock(root, {
|
|
104
|
+
version: 1,
|
|
105
|
+
detected_at: new Date().toISOString(),
|
|
106
|
+
cli: "claude",
|
|
107
|
+
session_id: "s",
|
|
108
|
+
hook_name: "any",
|
|
109
|
+
last_seen: new Date(Date.now() - 60_000).toISOString(),
|
|
110
|
+
missing_seconds: 60,
|
|
111
|
+
host_id: "h",
|
|
112
|
+
cwd: root,
|
|
113
|
+
});
|
|
114
|
+
try {
|
|
115
|
+
const result = await runGovernRepair({
|
|
116
|
+
cwd: root,
|
|
117
|
+
skipRoot: true,
|
|
118
|
+
reason: "Operator reviewed and cleared",
|
|
119
|
+
});
|
|
120
|
+
assert.equal(result.ok, true, result.message);
|
|
121
|
+
assert.equal(result.removed_lock, true);
|
|
122
|
+
assert.deepEqual(result.reverified, ["claude"]);
|
|
123
|
+
assert.equal(fs.existsSync(path.join(ctx, ".cortex-tamper.lock")), false);
|
|
124
|
+
} finally {
|
|
125
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("repair: success even when there is no lock — paths still verified", async () => {
|
|
130
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-repair-clean-"));
|
|
131
|
+
const ctx = path.join(root, ".context");
|
|
132
|
+
fs.mkdirSync(ctx, { recursive: true });
|
|
133
|
+
const claudeManaged = path.join(root, "managed.json");
|
|
134
|
+
fs.writeFileSync(claudeManaged, "{}");
|
|
135
|
+
fs.writeFileSync(
|
|
136
|
+
path.join(ctx, "govern.local.json"),
|
|
137
|
+
JSON.stringify({
|
|
138
|
+
installs: {
|
|
139
|
+
claude: {
|
|
140
|
+
path: claudeManaged,
|
|
141
|
+
version: "v1",
|
|
142
|
+
frameworks: [],
|
|
143
|
+
installed_at: new Date().toISOString(),
|
|
144
|
+
mode: "advisory",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
try {
|
|
150
|
+
const result = await runGovernRepair({ cwd: root, skipRoot: true });
|
|
151
|
+
assert.equal(result.ok, true, result.message);
|
|
152
|
+
assert.equal(result.removed_lock, false);
|
|
153
|
+
assert.match(result.message, /No tamper lock present/);
|
|
154
|
+
} finally {
|
|
155
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
});
|