@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
|
+
import http from "node:http";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
buildHeartbeatPayload,
|
|
10
|
+
pushHeartbeat,
|
|
11
|
+
} from "../dist/daemon/heartbeat-pusher.js";
|
|
12
|
+
|
|
13
|
+
function startMockServer(handler) {
|
|
14
|
+
const server = http.createServer((req, res) => {
|
|
15
|
+
let body = "";
|
|
16
|
+
req.on("data", (c) => (body += c));
|
|
17
|
+
req.on("end", () => handler(req, res, body));
|
|
18
|
+
});
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
server.listen(0, "127.0.0.1", () => {
|
|
21
|
+
const addr = server.address();
|
|
22
|
+
resolve({ server, baseUrl: `http://127.0.0.1:${addr.port}` });
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeProject({ baseUrl = "https://example.com", apiKey = "ent_test_key_12345678", installs = {}, frameworks = ["iso27001"] } = {}) {
|
|
28
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-heartbeat-"));
|
|
29
|
+
const ctx = path.join(root, ".context");
|
|
30
|
+
fs.mkdirSync(ctx, { recursive: true });
|
|
31
|
+
fs.writeFileSync(
|
|
32
|
+
path.join(ctx, "enterprise.yml"),
|
|
33
|
+
[
|
|
34
|
+
"enterprise:",
|
|
35
|
+
` api_key: ${apiKey}`,
|
|
36
|
+
` base_url: ${baseUrl}`,
|
|
37
|
+
"compliance:",
|
|
38
|
+
` frameworks: [${frameworks.join(", ")}]`,
|
|
39
|
+
"",
|
|
40
|
+
].join("\n"),
|
|
41
|
+
);
|
|
42
|
+
if (Object.keys(installs).length > 0) {
|
|
43
|
+
fs.writeFileSync(path.join(ctx, "govern.local.json"), JSON.stringify({ installs }));
|
|
44
|
+
}
|
|
45
|
+
return { root, ctx };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
test("buildHeartbeatPayload: empty installs → mode off, empty ai_clis", () => {
|
|
49
|
+
const { root } = makeProject();
|
|
50
|
+
try {
|
|
51
|
+
const payload = buildHeartbeatPayload(root, "test-host");
|
|
52
|
+
assert.equal(payload.host_id, "test-host");
|
|
53
|
+
assert.equal(payload.govern_mode, "off");
|
|
54
|
+
assert.deepEqual(payload.ai_clis_detected, []);
|
|
55
|
+
assert.deepEqual(payload.active_frameworks, ["iso27001"]);
|
|
56
|
+
assert.equal(payload.config_version, null);
|
|
57
|
+
} finally {
|
|
58
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("buildHeartbeatPayload: enforced wins over advisory across multiple installs", () => {
|
|
63
|
+
const { root } = makeProject({
|
|
64
|
+
installs: {
|
|
65
|
+
claude: { mode: "advisory", version: "v1", frameworks: [] },
|
|
66
|
+
codex: { mode: "enforced", version: "v2", frameworks: [] },
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
try {
|
|
70
|
+
const payload = buildHeartbeatPayload(root);
|
|
71
|
+
assert.equal(payload.govern_mode, "enforced");
|
|
72
|
+
assert.equal(payload.ai_clis_detected.length, 2);
|
|
73
|
+
assert.equal(payload.ai_clis_detected.find((c) => c.name === "claude").tier, "prevent");
|
|
74
|
+
assert.equal(payload.ai_clis_detected.find((c) => c.name === "codex").tier, "prevent");
|
|
75
|
+
} finally {
|
|
76
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("buildHeartbeatPayload: copilot maps to wrap tier", () => {
|
|
81
|
+
const { root } = makeProject({
|
|
82
|
+
installs: {
|
|
83
|
+
copilot: { mode: "advisory", version: "shim-v1", frameworks: [] },
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
try {
|
|
87
|
+
const payload = buildHeartbeatPayload(root);
|
|
88
|
+
assert.equal(payload.ai_clis_detected[0].name, "copilot");
|
|
89
|
+
assert.equal(payload.ai_clis_detected[0].tier, "wrap");
|
|
90
|
+
} finally {
|
|
91
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("pushHeartbeat: posts canonical payload to /api/v1/govern/heartbeat", async () => {
|
|
96
|
+
let received = null;
|
|
97
|
+
let receivedAuth = null;
|
|
98
|
+
const { server, baseUrl } = await startMockServer((req, res, body) => {
|
|
99
|
+
if (req.url === "/api/v1/govern/heartbeat" && req.method === "POST") {
|
|
100
|
+
received = JSON.parse(body);
|
|
101
|
+
receivedAuth = req.headers["authorization"];
|
|
102
|
+
res.statusCode = 200;
|
|
103
|
+
res.setHeader("content-type", "application/json");
|
|
104
|
+
res.end(JSON.stringify({ ok: true, server_time: new Date().toISOString() }));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
res.statusCode = 404;
|
|
108
|
+
res.end();
|
|
109
|
+
});
|
|
110
|
+
const { root } = makeProject({
|
|
111
|
+
baseUrl,
|
|
112
|
+
installs: { claude: { mode: "enforced", version: "v1", frameworks: [] } },
|
|
113
|
+
});
|
|
114
|
+
try {
|
|
115
|
+
const result = await pushHeartbeat(root);
|
|
116
|
+
assert.equal(result.ok, true);
|
|
117
|
+
assert.equal(receivedAuth, "Bearer ent_test_key_12345678");
|
|
118
|
+
assert.equal(received.govern_mode, "enforced");
|
|
119
|
+
assert.equal(received.config_version, "v1");
|
|
120
|
+
assert.ok(received.host_id.length > 0);
|
|
121
|
+
assert.ok(["darwin", "linux", "windows"].includes(received.os));
|
|
122
|
+
} finally {
|
|
123
|
+
server.close();
|
|
124
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("pushHeartbeat: returns error when enterprise not configured", async () => {
|
|
129
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-heartbeat-bare-"));
|
|
130
|
+
fs.mkdirSync(path.join(root, ".context"));
|
|
131
|
+
try {
|
|
132
|
+
const result = await pushHeartbeat(root);
|
|
133
|
+
assert.equal(result.ok, false);
|
|
134
|
+
assert.match(result.error, /enterprise not configured/);
|
|
135
|
+
} finally {
|
|
136
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("pushHeartbeat: server 500 surfaces error string", async () => {
|
|
141
|
+
const { server, baseUrl } = await startMockServer((req, res) => {
|
|
142
|
+
res.statusCode = 500;
|
|
143
|
+
res.end("boom");
|
|
144
|
+
});
|
|
145
|
+
const { root } = makeProject({ baseUrl });
|
|
146
|
+
try {
|
|
147
|
+
const result = await pushHeartbeat(root);
|
|
148
|
+
assert.equal(result.ok, false);
|
|
149
|
+
assert.match(result.error, /HTTP 500/);
|
|
150
|
+
} finally {
|
|
151
|
+
server.close();
|
|
152
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
@@ -0,0 +1,237 @@
|
|
|
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 {
|
|
8
|
+
HeartbeatTracker,
|
|
9
|
+
TAMPER_LOCK_FILENAME,
|
|
10
|
+
writeTamperLock,
|
|
11
|
+
readTamperLock,
|
|
12
|
+
removeTamperLock,
|
|
13
|
+
} from "../dist/daemon/heartbeat-tracker.js";
|
|
14
|
+
|
|
15
|
+
function makeWorkspace() {
|
|
16
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-heartbeat-"));
|
|
17
|
+
fs.mkdirSync(path.join(root, ".context"), { recursive: true });
|
|
18
|
+
return root;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ts(offsetMs = 0) {
|
|
22
|
+
return new Date(Date.now() + offsetMs).toISOString();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test("recordHeartbeat: SessionStart registers an active session", () => {
|
|
26
|
+
const tracker = new HeartbeatTracker({ hostId: "test-host" });
|
|
27
|
+
tracker.recordHeartbeat({
|
|
28
|
+
cli: "claude",
|
|
29
|
+
hook: "SessionStart",
|
|
30
|
+
session_id: "sess-1",
|
|
31
|
+
cwd: "/p",
|
|
32
|
+
ts: ts(),
|
|
33
|
+
});
|
|
34
|
+
assert.equal(tracker.getActiveSessions().length, 1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("recordHeartbeat: SessionEnd marks session ended", () => {
|
|
38
|
+
const tracker = new HeartbeatTracker();
|
|
39
|
+
tracker.recordHeartbeat({
|
|
40
|
+
cli: "claude",
|
|
41
|
+
hook: "SessionStart",
|
|
42
|
+
session_id: "sess-1",
|
|
43
|
+
cwd: "/p",
|
|
44
|
+
ts: ts(),
|
|
45
|
+
});
|
|
46
|
+
tracker.recordHeartbeat({
|
|
47
|
+
cli: "claude",
|
|
48
|
+
hook: "SessionEnd",
|
|
49
|
+
session_id: "sess-1",
|
|
50
|
+
cwd: "/p",
|
|
51
|
+
ts: ts(),
|
|
52
|
+
});
|
|
53
|
+
assert.equal(tracker.getActiveSessions().length, 0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("detectTamper: pure idle (only SessionStart) is NOT flagged", () => {
|
|
57
|
+
const tracker = new HeartbeatTracker();
|
|
58
|
+
tracker.recordHeartbeat({
|
|
59
|
+
cli: "claude",
|
|
60
|
+
hook: "SessionStart",
|
|
61
|
+
session_id: "sess-idle",
|
|
62
|
+
cwd: "/p",
|
|
63
|
+
ts: ts(-10 * 60 * 1000), // 10 min ago
|
|
64
|
+
});
|
|
65
|
+
const findings = tracker.detectTamper({
|
|
66
|
+
cwds: ["/p"],
|
|
67
|
+
missingThresholdSeconds: 60,
|
|
68
|
+
});
|
|
69
|
+
assert.equal(findings.length, 0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("detectTamper: had-activity-then-silence IS flagged", () => {
|
|
73
|
+
const tracker = new HeartbeatTracker({ hostId: "h" });
|
|
74
|
+
tracker.recordHeartbeat({
|
|
75
|
+
cli: "claude",
|
|
76
|
+
hook: "SessionStart",
|
|
77
|
+
session_id: "sess-tamper",
|
|
78
|
+
cwd: "/p",
|
|
79
|
+
ts: ts(-10 * 60 * 1000),
|
|
80
|
+
});
|
|
81
|
+
tracker.recordHeartbeat({
|
|
82
|
+
cli: "claude",
|
|
83
|
+
hook: "PreToolUse",
|
|
84
|
+
session_id: "sess-tamper",
|
|
85
|
+
cwd: "/p",
|
|
86
|
+
ts: ts(-9 * 60 * 1000), // last activity 9 min ago
|
|
87
|
+
});
|
|
88
|
+
const findings = tracker.detectTamper({
|
|
89
|
+
cwds: ["/p"],
|
|
90
|
+
missingThresholdSeconds: 60, // 1 min threshold → 9 min silence is tamper
|
|
91
|
+
});
|
|
92
|
+
assert.equal(findings.length, 1);
|
|
93
|
+
assert.equal(findings[0].cli, "claude");
|
|
94
|
+
assert.equal(findings[0].session_id, "sess-tamper");
|
|
95
|
+
assert.equal(findings[0].cwd, "/p");
|
|
96
|
+
assert.ok(findings[0].missing_seconds >= 60);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("detectTamper: same session is not flagged twice", () => {
|
|
100
|
+
const tracker = new HeartbeatTracker();
|
|
101
|
+
tracker.recordHeartbeat({
|
|
102
|
+
cli: "claude",
|
|
103
|
+
hook: "SessionStart",
|
|
104
|
+
session_id: "sess-once",
|
|
105
|
+
cwd: "/p",
|
|
106
|
+
ts: ts(-10 * 60 * 1000),
|
|
107
|
+
});
|
|
108
|
+
tracker.recordHeartbeat({
|
|
109
|
+
cli: "claude",
|
|
110
|
+
hook: "PreToolUse",
|
|
111
|
+
session_id: "sess-once",
|
|
112
|
+
cwd: "/p",
|
|
113
|
+
ts: ts(-9 * 60 * 1000),
|
|
114
|
+
});
|
|
115
|
+
const first = tracker.detectTamper({ cwds: ["/p"], missingThresholdSeconds: 60 });
|
|
116
|
+
assert.equal(first.length, 1);
|
|
117
|
+
const second = tracker.detectTamper({ cwds: ["/p"], missingThresholdSeconds: 60 });
|
|
118
|
+
assert.equal(second.length, 0, "session marked ended after first detection");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("detectTamper: ended session is not flagged", () => {
|
|
122
|
+
const tracker = new HeartbeatTracker();
|
|
123
|
+
tracker.recordHeartbeat({
|
|
124
|
+
cli: "claude",
|
|
125
|
+
hook: "SessionStart",
|
|
126
|
+
session_id: "sess-ended",
|
|
127
|
+
cwd: "/p",
|
|
128
|
+
ts: ts(-15 * 60 * 1000),
|
|
129
|
+
});
|
|
130
|
+
tracker.recordHeartbeat({
|
|
131
|
+
cli: "claude",
|
|
132
|
+
hook: "PreToolUse",
|
|
133
|
+
session_id: "sess-ended",
|
|
134
|
+
cwd: "/p",
|
|
135
|
+
ts: ts(-14 * 60 * 1000),
|
|
136
|
+
});
|
|
137
|
+
tracker.recordHeartbeat({
|
|
138
|
+
cli: "claude",
|
|
139
|
+
hook: "SessionEnd",
|
|
140
|
+
session_id: "sess-ended",
|
|
141
|
+
cwd: "/p",
|
|
142
|
+
ts: ts(-13 * 60 * 1000),
|
|
143
|
+
});
|
|
144
|
+
const findings = tracker.detectTamper({
|
|
145
|
+
cwds: ["/p"],
|
|
146
|
+
missingThresholdSeconds: 60,
|
|
147
|
+
});
|
|
148
|
+
assert.equal(findings.length, 0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("detectTamper: stale session beyond cleanupAfterMs is auto-removed", () => {
|
|
152
|
+
const tracker = new HeartbeatTracker({ cleanupAfterMs: 1000 });
|
|
153
|
+
tracker.recordHeartbeat({
|
|
154
|
+
cli: "claude",
|
|
155
|
+
hook: "SessionStart",
|
|
156
|
+
session_id: "sess-stale",
|
|
157
|
+
cwd: "/p",
|
|
158
|
+
ts: ts(-10 * 1000),
|
|
159
|
+
});
|
|
160
|
+
tracker.recordHeartbeat({
|
|
161
|
+
cli: "claude",
|
|
162
|
+
hook: "PreToolUse",
|
|
163
|
+
session_id: "sess-stale",
|
|
164
|
+
cwd: "/p",
|
|
165
|
+
ts: ts(-9 * 1000),
|
|
166
|
+
});
|
|
167
|
+
assert.equal(tracker._size(), 1);
|
|
168
|
+
tracker.detectTamper({ cwds: ["/p"], missingThresholdSeconds: 60 });
|
|
169
|
+
assert.equal(tracker._size(), 0, "stale session removed");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("writeTamperLock + readTamperLock + removeTamperLock round-trip", () => {
|
|
173
|
+
const root = makeWorkspace();
|
|
174
|
+
try {
|
|
175
|
+
const entry = {
|
|
176
|
+
version: 1,
|
|
177
|
+
detected_at: ts(),
|
|
178
|
+
cli: "claude",
|
|
179
|
+
session_id: "sess-1",
|
|
180
|
+
hook_name: "any",
|
|
181
|
+
last_seen: ts(-60_000),
|
|
182
|
+
missing_seconds: 60,
|
|
183
|
+
host_id: "h",
|
|
184
|
+
cwd: root,
|
|
185
|
+
};
|
|
186
|
+
const written = writeTamperLock(root, entry);
|
|
187
|
+
assert.match(written, new RegExp(TAMPER_LOCK_FILENAME));
|
|
188
|
+
|
|
189
|
+
const read = readTamperLock(root);
|
|
190
|
+
assert.deepEqual(read, entry);
|
|
191
|
+
|
|
192
|
+
const removed = removeTamperLock(root);
|
|
193
|
+
assert.equal(removed, true);
|
|
194
|
+
assert.equal(readTamperLock(root), null);
|
|
195
|
+
assert.equal(removeTamperLock(root), false, "no-op on second remove");
|
|
196
|
+
} finally {
|
|
197
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("recordHeartbeat: response includes tamper_lock_active flag", () => {
|
|
202
|
+
const root = makeWorkspace();
|
|
203
|
+
try {
|
|
204
|
+
const tracker = new HeartbeatTracker();
|
|
205
|
+
const before = tracker.recordHeartbeat({
|
|
206
|
+
cli: "claude",
|
|
207
|
+
hook: "SessionStart",
|
|
208
|
+
session_id: "s",
|
|
209
|
+
cwd: root,
|
|
210
|
+
ts: ts(),
|
|
211
|
+
});
|
|
212
|
+
assert.equal(before.tamper_lock_active, false);
|
|
213
|
+
|
|
214
|
+
writeTamperLock(root, {
|
|
215
|
+
version: 1,
|
|
216
|
+
detected_at: ts(),
|
|
217
|
+
cli: "claude",
|
|
218
|
+
session_id: "s",
|
|
219
|
+
hook_name: "any",
|
|
220
|
+
last_seen: ts(),
|
|
221
|
+
missing_seconds: 0,
|
|
222
|
+
host_id: "h",
|
|
223
|
+
cwd: root,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const after = tracker.recordHeartbeat({
|
|
227
|
+
cli: "claude",
|
|
228
|
+
hook: "PreToolUse",
|
|
229
|
+
session_id: "s",
|
|
230
|
+
cwd: root,
|
|
231
|
+
ts: ts(),
|
|
232
|
+
});
|
|
233
|
+
assert.equal(after.tamper_lock_active, true);
|
|
234
|
+
} finally {
|
|
235
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
236
|
+
}
|
|
237
|
+
});
|