@danielblomma/cortex-mcp 2.4.0 → 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 +19 -3
- package/bin/cortex.mjs +308 -75
- 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 +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
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
// ryugraph's CSV reader treats an empty field as NULL by default
|
|
3
|
+
// (DEFAULT_CSV_NULL_STRINGS = {""}), so a quoted empty string round-trips to
|
|
4
|
+
// null rather than "". Every column the graph loader emits is a real
|
|
5
|
+
// string/number/bool produced by asString/asNumber/asBoolean — never null —
|
|
6
|
+
// so we override NULL_STRINGS with a sentinel that cannot occur in any cell.
|
|
7
|
+
// A NUL byte qualifies: ingest's binary-file filter rejects any source file
|
|
8
|
+
// containing one, and filesystem paths and computed ids cannot contain NUL
|
|
9
|
+
// either. This keeps empty strings as "" and matches the prepared-statement
|
|
10
|
+
// loader byte-for-byte.
|
|
11
|
+
export const CSV_NULL_SENTINEL = String.fromCharCode(0);
|
|
12
|
+
// PARALLEL=false is required: ryugraph's parallel CSV reader rejects quoted
|
|
13
|
+
// newlines, and chunk/rule/ADR bodies routinely contain them. ESCAPE='"'
|
|
14
|
+
// plus QUOTE='"' is RFC 4180 style (a literal quote is written as "").
|
|
15
|
+
export const CSV_COPY_OPTIONS = `(HEADER=true, DELIM=",", QUOTE='"', ESCAPE='"', PARALLEL=false, NULL_STRINGS=['${CSV_NULL_SENTINEL}'])`;
|
|
16
|
+
// Every cell is quoted unconditionally and internal quotes are doubled. The
|
|
17
|
+
// reader coerces quoted "true"/"123" back to BOOL/INT64 columns, so a single
|
|
18
|
+
// uniform escaping path covers strings, numbers, and booleans.
|
|
19
|
+
export function toCsvCell(value) {
|
|
20
|
+
const text = value === null || value === undefined
|
|
21
|
+
? ""
|
|
22
|
+
: typeof value === "boolean"
|
|
23
|
+
? value
|
|
24
|
+
? "true"
|
|
25
|
+
: "false"
|
|
26
|
+
: String(value);
|
|
27
|
+
return `"${text.replace(/"/g, '""')}"`;
|
|
28
|
+
}
|
|
29
|
+
export function toCsvRow(values) {
|
|
30
|
+
return values.map(toCsvCell).join(",");
|
|
31
|
+
}
|
|
32
|
+
export function writeCsv(filePath, header, rows) {
|
|
33
|
+
const fd = fs.openSync(filePath, "w");
|
|
34
|
+
let rowCount = 0;
|
|
35
|
+
try {
|
|
36
|
+
fs.writeSync(fd, `${toCsvRow(header)}\n`, undefined, "utf8");
|
|
37
|
+
for (const row of rows) {
|
|
38
|
+
fs.writeSync(fd, `${toCsvRow(row)}\n`, undefined, "utf8");
|
|
39
|
+
rowCount += 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
fs.closeSync(fd);
|
|
44
|
+
}
|
|
45
|
+
return rowCount;
|
|
46
|
+
}
|
|
47
|
+
// Escape a filesystem path for use inside a double-quoted ryugraph COPY path
|
|
48
|
+
// literal. Separators are normalized to "/" (accepted on every platform,
|
|
49
|
+
// Windows included) and any embedded double quote is backslash-escaped, so a
|
|
50
|
+
// repo or cache path containing a quote does not break the COPY statement and
|
|
51
|
+
// silently lose the bulk-load path. Order matters: normalize separators first,
|
|
52
|
+
// then escape quotes, so the escaping backslash is not itself rewritten.
|
|
53
|
+
export function toCopyPathLiteral(filePath) {
|
|
54
|
+
return filePath.replace(/\\/g, "/").replace(/"/g, '\\"');
|
|
55
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function relationDegree(relations) {
|
|
2
|
+
const degrees = new Map();
|
|
3
|
+
for (const relation of relations) {
|
|
4
|
+
degrees.set(relation.from, (degrees.get(relation.from) ?? 0) + 1);
|
|
5
|
+
degrees.set(relation.to, (degrees.get(relation.to) ?? 0) + 1);
|
|
6
|
+
}
|
|
7
|
+
return degrees;
|
|
8
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { call } from "../daemon/client.js";
|
|
2
|
+
import { evaluateToolCall } from "../core/workflow/enforcement.js";
|
|
3
|
+
import { ensureDaemon, isEnterpriseProject, normalizeToolCall, parseInput, readStdin, resolveDaemonEntry, sendHeartbeat, serializeForAudit, getStringField, } from "./shared.js";
|
|
4
|
+
async function main() {
|
|
5
|
+
const raw = await readStdin();
|
|
6
|
+
const input = parseInput(raw);
|
|
7
|
+
const normalized = normalizeToolCall(input);
|
|
8
|
+
const enterprise = isEnterpriseProject(normalized.cwd);
|
|
9
|
+
ensureDaemon(resolveDaemonEntry(import.meta.url));
|
|
10
|
+
if (normalized.sessionId) {
|
|
11
|
+
void sendHeartbeat({
|
|
12
|
+
cli: "codex",
|
|
13
|
+
hook: "PermissionRequest",
|
|
14
|
+
session_id: normalized.sessionId,
|
|
15
|
+
cwd: normalized.cwd,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const activeTaskId = process.env.CORTEX_ACTIVE_TASK_ID?.trim();
|
|
19
|
+
if (activeTaskId) {
|
|
20
|
+
try {
|
|
21
|
+
const verdict = evaluateToolCall({
|
|
22
|
+
cwd: normalized.cwd,
|
|
23
|
+
taskId: activeTaskId,
|
|
24
|
+
call: { toolName: normalized.toolName, toolInput: normalized.toolInput },
|
|
25
|
+
});
|
|
26
|
+
if (!verdict.allowed) {
|
|
27
|
+
process.stderr.write(`[cortex] Permission denied by workflow: ${verdict.reason}\n`);
|
|
28
|
+
process.exit(2);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
process.stderr.write(`[cortex] permission capability evaluation failed (${err instanceof Error ? err.message : String(err)}); deferring to policy.check\n`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const policyPayload = {
|
|
36
|
+
tool: normalized.toolName,
|
|
37
|
+
cwd: normalized.cwd,
|
|
38
|
+
input: normalized.toolInput,
|
|
39
|
+
};
|
|
40
|
+
const policyRes = await call("policy.check", policyPayload, {
|
|
41
|
+
timeoutMs: 5000,
|
|
42
|
+
});
|
|
43
|
+
const approvalReason = getStringField(input, ["reason", "permission_reason", "permissionReason"]);
|
|
44
|
+
const auditPayload = {
|
|
45
|
+
cwd: normalized.cwd,
|
|
46
|
+
entry: {
|
|
47
|
+
timestamp: new Date().toISOString(),
|
|
48
|
+
tool: "permission.request",
|
|
49
|
+
input: {
|
|
50
|
+
tool_name: normalized.toolName,
|
|
51
|
+
command: normalized.toolInput.command ?? null,
|
|
52
|
+
prefix_rule: normalized.toolInput.prefix_rule ?? null,
|
|
53
|
+
sandbox_permissions: normalized.toolInput.sandbox_permissions ?? null,
|
|
54
|
+
},
|
|
55
|
+
event_type: "session",
|
|
56
|
+
evidence_level: "diagnostic",
|
|
57
|
+
resource_type: "approval_request",
|
|
58
|
+
session_id: normalized.sessionId,
|
|
59
|
+
metadata: {
|
|
60
|
+
tool_name: normalized.toolName,
|
|
61
|
+
reason: approvalReason ?? null,
|
|
62
|
+
command_preview: serializeForAudit(normalized.toolInput.command),
|
|
63
|
+
},
|
|
64
|
+
...(policyRes.ok && !policyRes.result.allow
|
|
65
|
+
? {
|
|
66
|
+
status: "error",
|
|
67
|
+
}
|
|
68
|
+
: {}),
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
void call("audit.log", auditPayload, { timeoutMs: 3000 });
|
|
72
|
+
if (policyRes.ok) {
|
|
73
|
+
if (policyRes.result.allow) {
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
process.stderr.write(`[cortex] Permission denied by policy: ${policyRes.result.reason ?? "unspecified"}\n`);
|
|
77
|
+
process.exit(2);
|
|
78
|
+
}
|
|
79
|
+
if (enterprise) {
|
|
80
|
+
process.stderr.write(`[cortex] Enterprise daemon unreachable (${policyRes.error}). Denying permission per fail-closed policy.\n`);
|
|
81
|
+
process.exit(2);
|
|
82
|
+
}
|
|
83
|
+
process.stderr.write(`[cortex] Daemon unreachable (${policyRes.error}). Allowing permission request (community mode).\n`);
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
main().catch((err) => {
|
|
87
|
+
process.stderr.write(`[cortex permission-request] error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
88
|
+
process.exit(0);
|
|
89
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { call } from "../daemon/client.js";
|
|
2
|
+
import { evaluateToolCall } from "../core/workflow/enforcement.js";
|
|
3
|
+
import { ensureDaemon, getBooleanField, getNumberField, getRecordField, getStringField, isEnterpriseProject, normalizeToolCall, parseInput, readStdin, resolveDaemonEntry, sendHeartbeat, serializeForAudit, } from "./shared.js";
|
|
4
|
+
function extractToolOutput(input) {
|
|
5
|
+
const record = getRecordField(input, ["tool_output", "toolOutput", "tool_result", "toolResult", "result"]) ??
|
|
6
|
+
{};
|
|
7
|
+
const outputText = getStringField(input, ["output", "stdout", "stderr"]);
|
|
8
|
+
if (outputText && record.output === undefined) {
|
|
9
|
+
record.output = outputText;
|
|
10
|
+
}
|
|
11
|
+
const success = getBooleanField(input, ["success"]);
|
|
12
|
+
if (success !== undefined && record.success === undefined) {
|
|
13
|
+
record.success = success;
|
|
14
|
+
}
|
|
15
|
+
const exitCode = getNumberField(input, ["exit_code", "exitCode"]);
|
|
16
|
+
if (exitCode !== undefined && record.exit_code === undefined) {
|
|
17
|
+
record.exit_code = exitCode;
|
|
18
|
+
}
|
|
19
|
+
return record;
|
|
20
|
+
}
|
|
21
|
+
async function main() {
|
|
22
|
+
const raw = await readStdin();
|
|
23
|
+
const input = parseInput(raw);
|
|
24
|
+
const normalized = normalizeToolCall(input);
|
|
25
|
+
const enterprise = isEnterpriseProject(normalized.cwd);
|
|
26
|
+
const toolOutput = extractToolOutput(input);
|
|
27
|
+
ensureDaemon(resolveDaemonEntry(import.meta.url));
|
|
28
|
+
if (normalized.sessionId) {
|
|
29
|
+
void sendHeartbeat({
|
|
30
|
+
cli: "codex",
|
|
31
|
+
hook: "PostToolUse",
|
|
32
|
+
session_id: normalized.sessionId,
|
|
33
|
+
cwd: normalized.cwd,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const success = getBooleanField(input, ["success"]) ??
|
|
37
|
+
(typeof toolOutput.exit_code === "number" ? toolOutput.exit_code === 0 : undefined);
|
|
38
|
+
const durationMs = getNumberField(input, ["duration_ms", "durationMs"]);
|
|
39
|
+
const auditPayload = {
|
|
40
|
+
cwd: normalized.cwd,
|
|
41
|
+
entry: {
|
|
42
|
+
timestamp: new Date().toISOString(),
|
|
43
|
+
tool: normalized.toolName,
|
|
44
|
+
input: normalized.toolInput,
|
|
45
|
+
event_type: "tool",
|
|
46
|
+
evidence_level: "diagnostic",
|
|
47
|
+
resource_type: "tool_result",
|
|
48
|
+
session_id: normalized.sessionId,
|
|
49
|
+
...(durationMs !== undefined ? { duration_ms: durationMs } : {}),
|
|
50
|
+
...(success !== undefined
|
|
51
|
+
? { status: success ? "success" : "error" }
|
|
52
|
+
: {}),
|
|
53
|
+
metadata: {
|
|
54
|
+
hook: "PostToolUse",
|
|
55
|
+
tool_output_preview: serializeForAudit(toolOutput),
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
void call("audit.log", auditPayload, { timeoutMs: 3000 });
|
|
60
|
+
const activeTaskId = process.env.CORTEX_ACTIVE_TASK_ID?.trim();
|
|
61
|
+
if (activeTaskId) {
|
|
62
|
+
try {
|
|
63
|
+
const verdict = evaluateToolCall({
|
|
64
|
+
cwd: normalized.cwd,
|
|
65
|
+
taskId: activeTaskId,
|
|
66
|
+
call: { toolName: normalized.toolName, toolInput: normalized.toolInput },
|
|
67
|
+
});
|
|
68
|
+
if (!verdict.allowed) {
|
|
69
|
+
process.stderr.write(`[cortex] Blocked after tool execution: ${verdict.reason}\n`);
|
|
70
|
+
process.exit(2);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
process.stderr.write(`[cortex] post-tool capability evaluation failed (${err instanceof Error ? err.message : String(err)}); deferring to policy.check\n`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (Object.keys(toolOutput).length === 0) {
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
const policyPayload = {
|
|
81
|
+
tool: `${normalized.toolName}.result`,
|
|
82
|
+
cwd: normalized.cwd,
|
|
83
|
+
input: toolOutput,
|
|
84
|
+
};
|
|
85
|
+
const policyRes = await call("policy.check", policyPayload, {
|
|
86
|
+
timeoutMs: 5000,
|
|
87
|
+
});
|
|
88
|
+
if (policyRes.ok) {
|
|
89
|
+
if (policyRes.result.allow) {
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
process.stderr.write(`[cortex] Blocked after tool execution by policy: ${policyRes.result.reason ?? "unspecified"}\n`);
|
|
93
|
+
process.exit(2);
|
|
94
|
+
}
|
|
95
|
+
if (enterprise) {
|
|
96
|
+
process.stderr.write(`[cortex] Enterprise daemon unreachable (${policyRes.error}). Blocking continuation per fail-closed policy.\n`);
|
|
97
|
+
process.exit(2);
|
|
98
|
+
}
|
|
99
|
+
process.stderr.write(`[cortex] Daemon unreachable (${policyRes.error}). Allowing continuation (community mode).\n`);
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
main().catch((err) => {
|
|
103
|
+
process.stderr.write(`[cortex post-tool-use] error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
104
|
+
process.exit(0);
|
|
105
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { call } from "../daemon/client.js";
|
|
2
|
+
import { ensureDaemon, parseInput, readStdin, resolveDaemonEntry, } from "./shared.js";
|
|
3
|
+
async function main() {
|
|
4
|
+
const raw = await readStdin();
|
|
5
|
+
const input = parseInput(raw);
|
|
6
|
+
const cwd = input.cwd ?? process.cwd();
|
|
7
|
+
ensureDaemon(resolveDaemonEntry(import.meta.url));
|
|
8
|
+
const payload = {
|
|
9
|
+
cwd,
|
|
10
|
+
entry: {
|
|
11
|
+
timestamp: new Date().toISOString(),
|
|
12
|
+
tool: "session.pre_compact",
|
|
13
|
+
input: {
|
|
14
|
+
session_id: input.session_id ?? null,
|
|
15
|
+
context_size_tokens: input.context_size_tokens ?? null,
|
|
16
|
+
},
|
|
17
|
+
event_type: "session",
|
|
18
|
+
evidence_level: "diagnostic",
|
|
19
|
+
resource_type: "session",
|
|
20
|
+
session_id: input.session_id,
|
|
21
|
+
metadata: {
|
|
22
|
+
context_size_tokens: input.context_size_tokens ?? null,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
await call("audit.log", payload, { timeoutMs: 3000 });
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
main().catch(() => process.exit(0));
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { call } from "../daemon/client.js";
|
|
2
|
+
import { ensureDaemon, isEnterpriseProject, parseInput, readStdin, resolveDaemonEntry, sendHeartbeat, } from "./shared.js";
|
|
3
|
+
import { evaluateToolCall } from "../core/workflow/enforcement.js";
|
|
4
|
+
async function main() {
|
|
5
|
+
const raw = await readStdin();
|
|
6
|
+
const input = parseInput(raw);
|
|
7
|
+
const cwd = input.cwd || process.cwd();
|
|
8
|
+
const tool = input.tool_name || "unknown";
|
|
9
|
+
const enterprise = isEnterpriseProject(cwd);
|
|
10
|
+
// Try to bring the daemon up if it's not already.
|
|
11
|
+
ensureDaemon(resolveDaemonEntry(import.meta.url));
|
|
12
|
+
if (input.session_id) {
|
|
13
|
+
void sendHeartbeat({
|
|
14
|
+
cli: "claude",
|
|
15
|
+
hook: "PreToolUse",
|
|
16
|
+
session_id: input.session_id,
|
|
17
|
+
cwd,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
// Workflow capability gate. Runs before policy.check so harness-level
|
|
21
|
+
// restrictions block before the daemon's general policy machinery sees
|
|
22
|
+
// the call. No-op when CORTEX_ACTIVE_TASK_ID is unset.
|
|
23
|
+
const activeTaskId = process.env.CORTEX_ACTIVE_TASK_ID?.trim();
|
|
24
|
+
if (activeTaskId) {
|
|
25
|
+
try {
|
|
26
|
+
const verdict = evaluateToolCall({
|
|
27
|
+
cwd,
|
|
28
|
+
taskId: activeTaskId,
|
|
29
|
+
call: { toolName: tool, toolInput: input.tool_input ?? {} },
|
|
30
|
+
});
|
|
31
|
+
if (!verdict.allowed) {
|
|
32
|
+
process.stderr.write(`[cortex] Blocked by harness capability: ${verdict.reason}\n`);
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
// Capability evaluation should never crash the hook — if it does,
|
|
38
|
+
// log and fall through to the existing policy.check rather than
|
|
39
|
+
// accidentally blocking a legitimate tool.
|
|
40
|
+
process.stderr.write(`[cortex] capability evaluation failed (${err instanceof Error ? err.message : String(err)}); deferring to policy.check\n`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const payload = {
|
|
44
|
+
tool,
|
|
45
|
+
cwd,
|
|
46
|
+
input: input.tool_input ?? {},
|
|
47
|
+
};
|
|
48
|
+
const res = await call("policy.check", payload, {
|
|
49
|
+
timeoutMs: 5000,
|
|
50
|
+
});
|
|
51
|
+
if (res.ok) {
|
|
52
|
+
if (res.result.allow) {
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// Hook spec: exit 2 + stderr message → Claude Code blocks the tool.
|
|
57
|
+
process.stderr.write(`[cortex] Blocked by policy: ${res.result.reason ?? "unspecified"}\n`);
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Daemon unreachable — apply split fail-mode.
|
|
62
|
+
if (enterprise) {
|
|
63
|
+
process.stderr.write(`[cortex] Enterprise daemon unreachable (${res.error}). Blocking tool per fail-closed policy.\n`);
|
|
64
|
+
process.stderr.write("[cortex] Start the daemon with: cortex daemon start\n");
|
|
65
|
+
process.exit(2);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
process.stderr.write(`[cortex] Daemon unreachable (${res.error}). Allowing tool (community mode).\n`);
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
main().catch((err) => {
|
|
73
|
+
process.stderr.write(`[cortex pre-tool-use] error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
74
|
+
// Hook errors should not block the user — fail-open on internal exceptions
|
|
75
|
+
// regardless of mode. The split fail-mode applies only to the explicit
|
|
76
|
+
// "daemon unreachable" path above.
|
|
77
|
+
process.exit(0);
|
|
78
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { call } from "../daemon/client.js";
|
|
2
|
+
import { ensureDaemon, parseInput, readStdin, resolveDaemonEntry, sendHeartbeat, } from "./shared.js";
|
|
3
|
+
async function main() {
|
|
4
|
+
const raw = await readStdin();
|
|
5
|
+
const input = parseInput(raw);
|
|
6
|
+
const cwd = input.cwd ?? process.cwd();
|
|
7
|
+
ensureDaemon(resolveDaemonEntry(import.meta.url));
|
|
8
|
+
// Audit first — even if telemetry push fails, we want the session-end
|
|
9
|
+
// record locally so cortex-web can later derive missing data.
|
|
10
|
+
const auditPayload = {
|
|
11
|
+
cwd,
|
|
12
|
+
entry: {
|
|
13
|
+
timestamp: new Date().toISOString(),
|
|
14
|
+
tool: "session.end",
|
|
15
|
+
input: { session_id: input.session_id ?? null },
|
|
16
|
+
event_type: "session",
|
|
17
|
+
evidence_level: "diagnostic",
|
|
18
|
+
resource_type: "session",
|
|
19
|
+
session_id: input.session_id,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
await call("audit.log", auditPayload, { timeoutMs: 3000 });
|
|
23
|
+
// Final flush — best-effort, may have nothing on disk if MCP already
|
|
24
|
+
// flushed during shutdown.
|
|
25
|
+
const flushPayload = {
|
|
26
|
+
reason: "session_end",
|
|
27
|
+
session_id: input.session_id,
|
|
28
|
+
cwd,
|
|
29
|
+
};
|
|
30
|
+
await call("telemetry.flush", flushPayload, {
|
|
31
|
+
timeoutMs: 5000,
|
|
32
|
+
});
|
|
33
|
+
if (input.session_id) {
|
|
34
|
+
await sendHeartbeat({
|
|
35
|
+
cli: "claude",
|
|
36
|
+
hook: "SessionEnd",
|
|
37
|
+
session_id: input.session_id,
|
|
38
|
+
cwd,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
main().catch(() => process.exit(0));
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { call } from "../daemon/client.js";
|
|
2
|
+
import { ensureDaemon, isEnforcedMode, parseInput, readStdin, readTamperLockJson, resolveDaemonEntry, sendHeartbeat, } from "./shared.js";
|
|
3
|
+
async function main() {
|
|
4
|
+
const raw = await readStdin();
|
|
5
|
+
const input = parseInput(raw);
|
|
6
|
+
const cwd = input.cwd ?? process.cwd();
|
|
7
|
+
const sessionId = input.session_id ?? "";
|
|
8
|
+
// Tamper-lock check happens BEFORE we spawn the daemon. We don't want
|
|
9
|
+
// a tampered project to keep getting fresh daemons started.
|
|
10
|
+
const lock = readTamperLockJson(cwd);
|
|
11
|
+
if (lock && isEnforcedMode(cwd)) {
|
|
12
|
+
process.stderr.write("[cortex] Govern enforced: session blocked because hook tampering was detected.\n" +
|
|
13
|
+
" Run 'sudo cortex enterprise repair' to verify managed-settings\n" +
|
|
14
|
+
" integrity and clear .context/.cortex-tamper.lock, then retry.\n");
|
|
15
|
+
process.exit(2);
|
|
16
|
+
}
|
|
17
|
+
ensureDaemon(resolveDaemonEntry(import.meta.url));
|
|
18
|
+
const payload = {
|
|
19
|
+
cwd,
|
|
20
|
+
entry: {
|
|
21
|
+
timestamp: new Date().toISOString(),
|
|
22
|
+
tool: "session.start",
|
|
23
|
+
input: { session_id: sessionId || null },
|
|
24
|
+
event_type: "session",
|
|
25
|
+
evidence_level: "diagnostic",
|
|
26
|
+
resource_type: "session",
|
|
27
|
+
session_id: sessionId,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
await call("audit.log", payload, { timeoutMs: 3000 });
|
|
31
|
+
if (sessionId) {
|
|
32
|
+
await sendHeartbeat({
|
|
33
|
+
cli: "claude",
|
|
34
|
+
hook: "SessionStart",
|
|
35
|
+
session_id: sessionId,
|
|
36
|
+
cwd,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
main().catch(() => process.exit(0));
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { call, isDaemonRunning, spawnDaemon } from "../daemon/client.js";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
export async function readStdin() {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
let data = "";
|
|
8
|
+
process.stdin.setEncoding("utf8");
|
|
9
|
+
process.stdin.on("data", (chunk) => {
|
|
10
|
+
data += chunk;
|
|
11
|
+
});
|
|
12
|
+
process.stdin.on("end", () => resolve(data));
|
|
13
|
+
// If stdin is a TTY (running interactively for testing), don't hang.
|
|
14
|
+
if (process.stdin.isTTY)
|
|
15
|
+
resolve("");
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export function parseInput(raw) {
|
|
19
|
+
if (!raw.trim())
|
|
20
|
+
return {};
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(raw);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function isRecord(value) {
|
|
29
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
export function getStringField(input, keys) {
|
|
32
|
+
for (const key of keys) {
|
|
33
|
+
const value = input[key];
|
|
34
|
+
if (typeof value === "string" && value.trim())
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
export function getNumberField(input, keys) {
|
|
40
|
+
for (const key of keys) {
|
|
41
|
+
const value = input[key];
|
|
42
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
export function getBooleanField(input, keys) {
|
|
48
|
+
for (const key of keys) {
|
|
49
|
+
const value = input[key];
|
|
50
|
+
if (typeof value === "boolean")
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
export function getRecordField(input, keys) {
|
|
56
|
+
for (const key of keys) {
|
|
57
|
+
const value = input[key];
|
|
58
|
+
if (isRecord(value))
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
export function normalizeToolInput(value) {
|
|
64
|
+
if (isRecord(value))
|
|
65
|
+
return value;
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
export function normalizeToolCall(input) {
|
|
69
|
+
const cwd = getStringField(input, ["cwd", "working_directory", "workingDirectory"]) ?? process.cwd();
|
|
70
|
+
const sessionId = getStringField(input, ["session_id", "sessionId"]);
|
|
71
|
+
const toolName = getStringField(input, ["tool_name", "toolName", "tool"]) ??
|
|
72
|
+
(typeof input.command === "string" ? "Bash" : "unknown");
|
|
73
|
+
const toolInput = getRecordField(input, ["tool_input", "toolInput", "tool_args", "toolArgs", "input", "args"]) ??
|
|
74
|
+
{};
|
|
75
|
+
if (typeof input.command === "string" && toolInput.command === undefined) {
|
|
76
|
+
toolInput.command = input.command;
|
|
77
|
+
}
|
|
78
|
+
if (Array.isArray(input.prefix_rule) && toolInput.prefix_rule === undefined) {
|
|
79
|
+
toolInput.prefix_rule = input.prefix_rule;
|
|
80
|
+
}
|
|
81
|
+
if (typeof input.sandbox_permissions === "string" &&
|
|
82
|
+
toolInput.sandbox_permissions === undefined) {
|
|
83
|
+
toolInput.sandbox_permissions = input.sandbox_permissions;
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
cwd,
|
|
87
|
+
...(sessionId ? { sessionId } : {}),
|
|
88
|
+
toolName,
|
|
89
|
+
toolInput,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function serializeForAudit(value, maxLen = 1200) {
|
|
93
|
+
if (value === undefined)
|
|
94
|
+
return undefined;
|
|
95
|
+
const raw = typeof value === "string"
|
|
96
|
+
? value
|
|
97
|
+
: (() => {
|
|
98
|
+
try {
|
|
99
|
+
return JSON.stringify(value);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return String(value);
|
|
103
|
+
}
|
|
104
|
+
})();
|
|
105
|
+
if (!raw)
|
|
106
|
+
return undefined;
|
|
107
|
+
return raw.length <= maxLen ? raw : `${raw.slice(0, maxLen)}...`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Detect whether the current project is running enterprise mode.
|
|
111
|
+
* Lightweight YAML peek — we don't want to load the full config parser
|
|
112
|
+
* on every hook invocation.
|
|
113
|
+
*/
|
|
114
|
+
export function isEnterpriseProject(cwd) {
|
|
115
|
+
const candidates = [
|
|
116
|
+
join(cwd, ".context", "enterprise.yml"),
|
|
117
|
+
join(cwd, ".context", "enterprise.yaml"),
|
|
118
|
+
];
|
|
119
|
+
for (const path of candidates) {
|
|
120
|
+
if (!existsSync(path))
|
|
121
|
+
continue;
|
|
122
|
+
try {
|
|
123
|
+
const raw = readFileSync(path, "utf8");
|
|
124
|
+
// Look for an api_key under enterprise: section.
|
|
125
|
+
// Conservative: any non-empty api_key value implies "intent to be enterprise".
|
|
126
|
+
const match = raw.match(/^\s*api_key:\s*(\S.*?)\s*$/m);
|
|
127
|
+
if (match && match[1] && match[1] !== "")
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// ignore
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
export function ensureDaemon(daemonEntry) {
|
|
137
|
+
if (isDaemonRunning())
|
|
138
|
+
return;
|
|
139
|
+
spawnDaemon(daemonEntry);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Resolve daemon entry script path relative to this hook script's location.
|
|
143
|
+
* Hooks live at dist/hooks/<name>.js, daemon at dist/daemon/main.js.
|
|
144
|
+
*/
|
|
145
|
+
export function resolveDaemonEntry(hookFileUrl) {
|
|
146
|
+
const hookPath = fileURLToPath(hookFileUrl);
|
|
147
|
+
// dist/hooks/<x>.js → dist/daemon/main.js
|
|
148
|
+
return join(hookPath, "..", "..", "daemon", "main.js");
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Send a heartbeat to the daemon for tamper-detection bookkeeping.
|
|
152
|
+
* Failure to reach the daemon is non-fatal — daemon-down is treated as
|
|
153
|
+
* "we don't know" rather than tamper. Returns whether the daemon
|
|
154
|
+
* reported an active tamper-lock for this cwd.
|
|
155
|
+
*/
|
|
156
|
+
export async function sendHeartbeat(payload) {
|
|
157
|
+
const result = await call("heartbeat", { ...payload, ts: new Date().toISOString() }, { timeoutMs: 1500 });
|
|
158
|
+
if (!result.ok)
|
|
159
|
+
return { tamperLockActive: false };
|
|
160
|
+
return { tamperLockActive: result.result.tamper_lock_active === true };
|
|
161
|
+
}
|
|
162
|
+
const TAMPER_LOCK_FILENAME = ".cortex-tamper.lock";
|
|
163
|
+
export function readTamperLockJson(cwd) {
|
|
164
|
+
const path = join(cwd, ".context", TAMPER_LOCK_FILENAME);
|
|
165
|
+
if (!existsSync(path))
|
|
166
|
+
return null;
|
|
167
|
+
try {
|
|
168
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* True iff this cwd is in enforced govern mode. Reads govern.local.json,
|
|
176
|
+
* which is written by the install/install-flow. We don't fall back to
|
|
177
|
+
* enterprise.yml because the user's intent is captured at install time.
|
|
178
|
+
*/
|
|
179
|
+
export function isEnforcedMode(cwd) {
|
|
180
|
+
const path = join(cwd, ".context", "govern.local.json");
|
|
181
|
+
if (!existsSync(path))
|
|
182
|
+
return false;
|
|
183
|
+
try {
|
|
184
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
185
|
+
for (const inst of Object.values(parsed.installs ?? {})) {
|
|
186
|
+
if (inst.mode === "enforced")
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { call } from "../daemon/client.js";
|
|
2
|
+
import { ensureDaemon, parseInput, readStdin, resolveDaemonEntry, } from "./shared.js";
|
|
3
|
+
async function main() {
|
|
4
|
+
const raw = await readStdin();
|
|
5
|
+
const input = parseInput(raw);
|
|
6
|
+
ensureDaemon(resolveDaemonEntry(import.meta.url));
|
|
7
|
+
const payload = {
|
|
8
|
+
reason: "stop",
|
|
9
|
+
session_id: input.session_id,
|
|
10
|
+
cwd: input.cwd ?? process.cwd(),
|
|
11
|
+
};
|
|
12
|
+
const res = await call("telemetry.flush", payload, {
|
|
13
|
+
timeoutMs: 5000,
|
|
14
|
+
});
|
|
15
|
+
if (res.ok && res.result.flushed) {
|
|
16
|
+
process.stderr.write(`[cortex] Telemetry flushed: ${res.result.events_pushed} events\n`);
|
|
17
|
+
}
|
|
18
|
+
else if (!res.ok) {
|
|
19
|
+
// Best-effort: silent in normal output. Daemon-reachability is logged
|
|
20
|
+
// through other channels (PreToolUse warnings, daemon log).
|
|
21
|
+
process.stderr.write(`[cortex] Telemetry flush skipped: ${res.error}\n`);
|
|
22
|
+
}
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
main().catch(() => {
|
|
26
|
+
// Telemetry must never block. Swallow all errors.
|
|
27
|
+
process.exit(0);
|
|
28
|
+
});
|