@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
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { loadEnterpriseConfig } from "../core/config.js";
|
|
5
|
+
import { isAllowedEnterpriseEndpoint } from "../core/secure-endpoint.js";
|
|
6
|
+
import { writeHostAuditEvent } from "./ungoverned-scanner.js";
|
|
7
|
+
/**
|
|
8
|
+
* Phase 7 sync flow — daemon side.
|
|
9
|
+
*
|
|
10
|
+
* The daemon periodically pings cortex-web /api/v1/govern/config to learn
|
|
11
|
+
* whether a new config version is available. It does NOT re-apply (that
|
|
12
|
+
* would require root, which the daemon explicitly doesn't have post-Fas-3
|
|
13
|
+
* privilege drop). Instead it emits an audit event and writes a
|
|
14
|
+
* notification file that 'cortex enterprise status' surfaces. The
|
|
15
|
+
* operator must then run 'sudo cortex enterprise sync' to actually
|
|
16
|
+
* re-fetch + write managed-settings.
|
|
17
|
+
*
|
|
18
|
+
* Three audit outcomes per tick:
|
|
19
|
+
* - govern_config_unchanged (304 / same version) — heartbeat that
|
|
20
|
+
* cortex-web is reachable and we're current.
|
|
21
|
+
* - govern_config_available (200 with new version) — operator action needed.
|
|
22
|
+
* - govern_config_sync_failed (network / auth error) — also written so
|
|
23
|
+
* admin sees blackouts in audit timeline.
|
|
24
|
+
*/
|
|
25
|
+
const NOTIFICATION_FILENAME = ".govern-update-available.json";
|
|
26
|
+
function readLocalGovernState(cwd) {
|
|
27
|
+
const path = join(cwd, ".context", "govern.local.json");
|
|
28
|
+
if (!existsSync(path))
|
|
29
|
+
return {};
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function activeFrameworks(cwd) {
|
|
38
|
+
const state = readLocalGovernState(cwd);
|
|
39
|
+
for (const inst of Object.values(state.installs ?? {})) {
|
|
40
|
+
if (inst.frameworks?.length) {
|
|
41
|
+
return inst.frameworks.map((f) => f.id);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Fall back to enterprise.yml's compliance.frameworks
|
|
45
|
+
const config = loadEnterpriseConfig(join(cwd, ".context"));
|
|
46
|
+
return config.compliance.frameworks;
|
|
47
|
+
}
|
|
48
|
+
function currentVersion(cwd, cli) {
|
|
49
|
+
const state = readLocalGovernState(cwd);
|
|
50
|
+
return state.installs?.[cli]?.version ?? null;
|
|
51
|
+
}
|
|
52
|
+
export async function checkSyncForCli(options) {
|
|
53
|
+
const cwd = options.cwd;
|
|
54
|
+
const config = loadEnterpriseConfig(join(cwd, ".context"));
|
|
55
|
+
const apiKey = config.enterprise.api_key.trim();
|
|
56
|
+
const baseUrl = (config.enterprise.base_url || config.enterprise.endpoint).trim();
|
|
57
|
+
if (!apiKey || !baseUrl) {
|
|
58
|
+
return { kind: "failed", error: "enterprise not configured" };
|
|
59
|
+
}
|
|
60
|
+
if (!isAllowedEnterpriseEndpoint(baseUrl)) {
|
|
61
|
+
return { kind: "failed", error: "insecure or invalid enterprise endpoint" };
|
|
62
|
+
}
|
|
63
|
+
const frameworks = activeFrameworks(cwd);
|
|
64
|
+
if (frameworks.length === 0) {
|
|
65
|
+
return { kind: "failed", error: "no active frameworks" };
|
|
66
|
+
}
|
|
67
|
+
const url = new URL(baseUrl.replace(/\/$/, "") + "/api/v1/govern/config");
|
|
68
|
+
url.searchParams.set("cli", options.cli);
|
|
69
|
+
url.searchParams.set("frameworks", frameworks.join(","));
|
|
70
|
+
const installedVersion = currentVersion(cwd, options.cli);
|
|
71
|
+
const headers = {
|
|
72
|
+
Authorization: `Bearer ${apiKey}`,
|
|
73
|
+
};
|
|
74
|
+
if (installedVersion)
|
|
75
|
+
headers["If-None-Match"] = `"${installedVersion}"`;
|
|
76
|
+
let res;
|
|
77
|
+
try {
|
|
78
|
+
res = await fetch(url, { headers });
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
return { kind: "failed", error: err instanceof Error ? err.message : String(err) };
|
|
82
|
+
}
|
|
83
|
+
if (res.status === 304) {
|
|
84
|
+
return { kind: "unchanged", version: installedVersion ?? "unknown" };
|
|
85
|
+
}
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
return { kind: "failed", error: `HTTP ${res.status} ${res.statusText}` };
|
|
88
|
+
}
|
|
89
|
+
const etag = (res.headers.get("etag") ?? "").replace(/"/g, "");
|
|
90
|
+
if (etag && etag === installedVersion) {
|
|
91
|
+
return { kind: "unchanged", version: etag };
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
kind: "available",
|
|
95
|
+
latest_version: etag || "unknown",
|
|
96
|
+
current_version: installedVersion,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function writeUpdateNotification(cwd, data) {
|
|
100
|
+
const dir = join(cwd, ".context");
|
|
101
|
+
mkdirSync(dir, { recursive: true });
|
|
102
|
+
writeFileSync(join(dir, NOTIFICATION_FILENAME), JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
103
|
+
}
|
|
104
|
+
export async function runSyncCheckOnce(cwd, clis) {
|
|
105
|
+
const outcomes = [];
|
|
106
|
+
const now = new Date().toISOString();
|
|
107
|
+
for (const cli of clis) {
|
|
108
|
+
const outcome = await checkSyncForCli({ cwd, cli });
|
|
109
|
+
outcomes.push(outcome);
|
|
110
|
+
const eventBase = {
|
|
111
|
+
timestamp: now,
|
|
112
|
+
host_id: hostname(),
|
|
113
|
+
cli,
|
|
114
|
+
};
|
|
115
|
+
if (outcome.kind === "unchanged") {
|
|
116
|
+
await writeHostAuditEvent(cwd, {
|
|
117
|
+
...eventBase,
|
|
118
|
+
event_type: "govern_config_unchanged",
|
|
119
|
+
version: outcome.version,
|
|
120
|
+
}).catch(() => undefined);
|
|
121
|
+
}
|
|
122
|
+
else if (outcome.kind === "available") {
|
|
123
|
+
await writeHostAuditEvent(cwd, {
|
|
124
|
+
...eventBase,
|
|
125
|
+
event_type: "govern_config_available",
|
|
126
|
+
latest_version: outcome.latest_version,
|
|
127
|
+
current_version: outcome.current_version,
|
|
128
|
+
}).catch(() => undefined);
|
|
129
|
+
writeUpdateNotification(cwd, {
|
|
130
|
+
latest_version: outcome.latest_version,
|
|
131
|
+
current_version: outcome.current_version,
|
|
132
|
+
cli,
|
|
133
|
+
detected_at: now,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
await writeHostAuditEvent(cwd, {
|
|
138
|
+
...eventBase,
|
|
139
|
+
event_type: "govern_config_sync_failed",
|
|
140
|
+
error: outcome.error,
|
|
141
|
+
}).catch(() => undefined);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return outcomes;
|
|
145
|
+
}
|
|
146
|
+
export function startSyncTimer(cwd, intervalMs) {
|
|
147
|
+
const tick = () => {
|
|
148
|
+
const state = readLocalGovernState(cwd);
|
|
149
|
+
const clis = Object.keys(state.installs ?? {});
|
|
150
|
+
if (clis.length === 0)
|
|
151
|
+
return;
|
|
152
|
+
void runSyncCheckOnce(cwd, clis).catch((err) => {
|
|
153
|
+
process.stderr.write(`[cortex-daemon] sync check failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
void Promise.resolve().then(tick);
|
|
157
|
+
const handle = setInterval(tick, intervalMs);
|
|
158
|
+
if (typeof handle.unref === "function")
|
|
159
|
+
handle.unref();
|
|
160
|
+
return {
|
|
161
|
+
stop() {
|
|
162
|
+
clearInterval(handle);
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { platform, userInfo } from "node:os";
|
|
5
|
+
import { detectUngoverned, enforceFinding, } from "../cli/ungoverned-detector.js";
|
|
6
|
+
import { writeGlobalUngovernedEvent } from "./global-host-events.js";
|
|
7
|
+
import { getGovernManagedPath } from "../core/govern-paths.js";
|
|
8
|
+
const DEFAULT_INTERVAL_MS = 60_000;
|
|
9
|
+
const TIER1_CLIS = new Set(["claude", "codex"]);
|
|
10
|
+
function readMode(cwd) {
|
|
11
|
+
const stateFile = join(cwd, ".context", "govern.local.json");
|
|
12
|
+
if (!existsSync(stateFile))
|
|
13
|
+
return "advisory";
|
|
14
|
+
try {
|
|
15
|
+
const raw = readFileSync(stateFile, "utf8");
|
|
16
|
+
const parsed = JSON.parse(raw);
|
|
17
|
+
for (const inst of Object.values(parsed.installs ?? {})) {
|
|
18
|
+
if (inst.mode === "enforced")
|
|
19
|
+
return "enforced";
|
|
20
|
+
}
|
|
21
|
+
return "advisory";
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return "advisory";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function isExpectedManagedArtifact(cli, path) {
|
|
28
|
+
try {
|
|
29
|
+
const stat = lstatSync(path);
|
|
30
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
31
|
+
return false;
|
|
32
|
+
const raw = readFileSync(path, "utf8");
|
|
33
|
+
if (cli === "codex") {
|
|
34
|
+
return raw.startsWith("# Cortex govern — codex requirements");
|
|
35
|
+
}
|
|
36
|
+
const parsed = JSON.parse(raw);
|
|
37
|
+
return parsed.allowManagedHooksOnly === true;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function readManagedTier1Clis(cwd, overrides) {
|
|
44
|
+
const stateFile = join(cwd, ".context", "govern.local.json");
|
|
45
|
+
const managed = new Set();
|
|
46
|
+
if (!existsSync(stateFile))
|
|
47
|
+
return managed;
|
|
48
|
+
try {
|
|
49
|
+
const raw = readFileSync(stateFile, "utf8");
|
|
50
|
+
const parsed = JSON.parse(raw);
|
|
51
|
+
for (const [cli, inst] of Object.entries(parsed.installs ?? {})) {
|
|
52
|
+
if (!TIER1_CLIS.has(cli))
|
|
53
|
+
continue;
|
|
54
|
+
const tier1Cli = cli;
|
|
55
|
+
const expectedPath = overrides?.[tier1Cli] ??
|
|
56
|
+
getGovernManagedPath(tier1Cli, platform());
|
|
57
|
+
if (!isExpectedManagedArtifact(tier1Cli, expectedPath)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
managed.add(cli);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return managed;
|
|
65
|
+
}
|
|
66
|
+
return managed;
|
|
67
|
+
}
|
|
68
|
+
function filterManagedTier1Findings(findings, managedTier1Clis) {
|
|
69
|
+
if (managedTier1Clis.size === 0)
|
|
70
|
+
return findings;
|
|
71
|
+
return findings.filter((finding) => !managedTier1Clis.has(finding.cli));
|
|
72
|
+
}
|
|
73
|
+
export async function writeHostAuditEvent(cwd, event) {
|
|
74
|
+
const auditDir = join(cwd, ".context", "audit");
|
|
75
|
+
await mkdir(auditDir, { recursive: true });
|
|
76
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
77
|
+
const file = join(auditDir, `host-events-${date}.jsonl`);
|
|
78
|
+
await appendFile(file, JSON.stringify(event) + "\n");
|
|
79
|
+
}
|
|
80
|
+
export async function runScanOnce(options) {
|
|
81
|
+
const mode = options.mode ?? readMode(options.cwd);
|
|
82
|
+
const managedTier1Clis = readManagedTier1Clis(options.cwd, options.managedPathOverrides);
|
|
83
|
+
const findings = filterManagedTier1Findings(detectUngoverned(options.detectorOptions), managedTier1Clis);
|
|
84
|
+
const me = userInfo().username;
|
|
85
|
+
for (const finding of findings) {
|
|
86
|
+
const action = enforceFinding(finding, { mode, currentUser: me });
|
|
87
|
+
// A host process snapshot has no trustworthy project attribution. Store
|
|
88
|
+
// it only in the credential-bound user-global queue; never in whichever
|
|
89
|
+
// project happened to register first.
|
|
90
|
+
if (options.credentialId) {
|
|
91
|
+
writeGlobalUngovernedEvent(options.credentialId, {
|
|
92
|
+
event_type: "ungoverned_ai_session_detected",
|
|
93
|
+
timestamp: finding.detected_at,
|
|
94
|
+
host_id: finding.host_id,
|
|
95
|
+
cli: finding.cli,
|
|
96
|
+
binary: finding.binary,
|
|
97
|
+
pid: finding.pid,
|
|
98
|
+
ppid: finding.ppid,
|
|
99
|
+
user: finding.user,
|
|
100
|
+
args: finding.args,
|
|
101
|
+
parent_chain: finding.parent_chain,
|
|
102
|
+
mode,
|
|
103
|
+
action,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
options.onFinding?.({ ...finding, action });
|
|
107
|
+
}
|
|
108
|
+
return findings;
|
|
109
|
+
}
|
|
110
|
+
export function startUngovernedScanner(options) {
|
|
111
|
+
const interval = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
112
|
+
let running = true;
|
|
113
|
+
const tick = async () => {
|
|
114
|
+
if (!running)
|
|
115
|
+
return;
|
|
116
|
+
try {
|
|
117
|
+
await runScanOnce(options);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
process.stderr.write(`[cortex-daemon] ungoverned scan failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
void tick();
|
|
124
|
+
const handle = setInterval(() => void tick(), interval);
|
|
125
|
+
if (typeof handle.unref === "function")
|
|
126
|
+
handle.unref();
|
|
127
|
+
return {
|
|
128
|
+
stop() {
|
|
129
|
+
running = false;
|
|
130
|
+
clearInterval(handle);
|
|
131
|
+
},
|
|
132
|
+
isRunning() {
|
|
133
|
+
return running;
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { loadEnterpriseConfig } from "../core/config.js";
|
|
5
|
+
import { matchesEnterpriseHostIdentity } from "../core/enterprise-host-identity.js";
|
|
6
|
+
import { enterpriseCredentialId, isAllowedLicenseEndpoint, } from "../core/license.js";
|
|
7
|
+
import { workflowDefinitionSchema } from "../core/workflow/schemas.js";
|
|
8
|
+
import { writeHostAuditEvent } from "./ungoverned-scanner.js";
|
|
9
|
+
import { daemonDir } from "./paths.js";
|
|
10
|
+
/**
|
|
11
|
+
* Org-workflow sync flow — daemon side.
|
|
12
|
+
*
|
|
13
|
+
* The daemon polls cortex-web /api/v1/govern/workflows/manifest each tick
|
|
14
|
+
* to learn what workflows the org has authored. It diffs against a local
|
|
15
|
+
* state file, fetches changed full definitions, and caches them locally.
|
|
16
|
+
* cortex.workflow.start (and the cortex stage CLI) read the cache via
|
|
17
|
+
* loadSyncedWorkflows() and merge with bundled DEFAULT_WORKFLOWS, with
|
|
18
|
+
* org definitions taking precedence on workflow_id collisions.
|
|
19
|
+
*
|
|
20
|
+
* Three audit outcomes per tick:
|
|
21
|
+
* - workflows_unchanged — manifest matches local state
|
|
22
|
+
* - workflows_synced — at least one workflow was added / changed /
|
|
23
|
+
* removed (metadata: counts)
|
|
24
|
+
* - workflows_sync_failed — network / auth / parse error
|
|
25
|
+
*
|
|
26
|
+
* Unlike skills, there is no on-disk artifact to write per workflow —
|
|
27
|
+
* the cached JSON is the only product. No "restart Claude Code"
|
|
28
|
+
* notification is needed because workflow lookup happens at run-start.
|
|
29
|
+
*/
|
|
30
|
+
const STATE_FILENAME = "workflows.local.json";
|
|
31
|
+
function stateFilePath() {
|
|
32
|
+
return join(daemonDir(), STATE_FILENAME);
|
|
33
|
+
}
|
|
34
|
+
export function readSyncedWorkflowsState() {
|
|
35
|
+
const path = stateFilePath();
|
|
36
|
+
if (!existsSync(path))
|
|
37
|
+
return { workflows: {} };
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
40
|
+
return {
|
|
41
|
+
credential_id: typeof parsed.credential_id === "string"
|
|
42
|
+
? parsed.credential_id
|
|
43
|
+
: undefined,
|
|
44
|
+
workflows: parsed.workflows ?? {},
|
|
45
|
+
last_synced_at: parsed.last_synced_at,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return { workflows: {} };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function writeSyncedWorkflowsState(state) {
|
|
53
|
+
writeFileSync(stateFilePath(), JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
54
|
+
}
|
|
55
|
+
async function fetchManifest(baseUrl, apiKey) {
|
|
56
|
+
const url = new URL(baseUrl.replace(/\/$/, "") + "/api/v1/govern/workflows/manifest");
|
|
57
|
+
const res = await fetch(url, {
|
|
58
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
62
|
+
}
|
|
63
|
+
const body = (await res.json());
|
|
64
|
+
return body.workflows ?? [];
|
|
65
|
+
}
|
|
66
|
+
async function fetchWorkflow(baseUrl, apiKey, workflowId) {
|
|
67
|
+
const url = new URL(baseUrl.replace(/\/$/, "") +
|
|
68
|
+
"/api/v1/govern/workflows/" +
|
|
69
|
+
encodeURIComponent(workflowId));
|
|
70
|
+
const res = await fetch(url, {
|
|
71
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
72
|
+
});
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
75
|
+
}
|
|
76
|
+
const body = (await res.json());
|
|
77
|
+
if (!body.workflow) {
|
|
78
|
+
throw new Error(`Response for ${workflowId} missing 'workflow' field`);
|
|
79
|
+
}
|
|
80
|
+
return body.workflow;
|
|
81
|
+
}
|
|
82
|
+
export async function runWorkflowSyncOnce(cwd) {
|
|
83
|
+
const config = loadEnterpriseConfig(join(cwd, ".context"));
|
|
84
|
+
const apiKey = config.enterprise.api_key.trim();
|
|
85
|
+
const baseUrl = (config.enterprise.base_url || config.enterprise.endpoint).trim();
|
|
86
|
+
if (!apiKey || !baseUrl) {
|
|
87
|
+
const outcome = {
|
|
88
|
+
kind: "failed",
|
|
89
|
+
error: "enterprise not configured",
|
|
90
|
+
};
|
|
91
|
+
await writeAudit(cwd, outcome);
|
|
92
|
+
return outcome;
|
|
93
|
+
}
|
|
94
|
+
if (!isAllowedLicenseEndpoint(baseUrl)) {
|
|
95
|
+
const outcome = {
|
|
96
|
+
kind: "failed",
|
|
97
|
+
error: "insecure or invalid enterprise endpoint",
|
|
98
|
+
};
|
|
99
|
+
await writeAudit(cwd, outcome);
|
|
100
|
+
return outcome;
|
|
101
|
+
}
|
|
102
|
+
const credentialId = enterpriseCredentialId(baseUrl, apiKey);
|
|
103
|
+
if (!matchesEnterpriseHostIdentity(credentialId)) {
|
|
104
|
+
const outcome = {
|
|
105
|
+
kind: "failed",
|
|
106
|
+
error: "enterprise identity conflict: this user profile is already enrolled to another endpoint or API key",
|
|
107
|
+
};
|
|
108
|
+
await writeAudit(cwd, outcome);
|
|
109
|
+
return outcome;
|
|
110
|
+
}
|
|
111
|
+
const state = readSyncedWorkflowsState();
|
|
112
|
+
const identityChanged = state.credential_id !== credentialId;
|
|
113
|
+
state.credential_id = credentialId;
|
|
114
|
+
let manifest;
|
|
115
|
+
try {
|
|
116
|
+
manifest = await fetchManifest(baseUrl, apiKey);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
const outcome = {
|
|
120
|
+
kind: "failed",
|
|
121
|
+
error: err instanceof Error ? err.message : String(err),
|
|
122
|
+
};
|
|
123
|
+
await writeAudit(cwd, outcome);
|
|
124
|
+
return outcome;
|
|
125
|
+
}
|
|
126
|
+
const remoteByName = new Map(manifest.map((e) => [e.workflow_id, e]));
|
|
127
|
+
const added = [];
|
|
128
|
+
const changed = [];
|
|
129
|
+
const removed = [];
|
|
130
|
+
for (const entry of manifest) {
|
|
131
|
+
const local = state.workflows[entry.workflow_id];
|
|
132
|
+
const isNew = !local;
|
|
133
|
+
const isChanged = Boolean(local) &&
|
|
134
|
+
(identityChanged ||
|
|
135
|
+
local.updated_at !== entry.updated_at ||
|
|
136
|
+
local.version !== entry.version);
|
|
137
|
+
if (!isNew && !isChanged)
|
|
138
|
+
continue;
|
|
139
|
+
let fetched;
|
|
140
|
+
try {
|
|
141
|
+
fetched = await fetchWorkflow(baseUrl, apiKey, entry.workflow_id);
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
const outcome = {
|
|
145
|
+
kind: "failed",
|
|
146
|
+
error: err instanceof Error
|
|
147
|
+
? `fetch ${entry.workflow_id}: ${err.message}`
|
|
148
|
+
: `fetch ${entry.workflow_id}: ${String(err)}`,
|
|
149
|
+
};
|
|
150
|
+
await writeAudit(cwd, outcome);
|
|
151
|
+
return outcome;
|
|
152
|
+
}
|
|
153
|
+
let validated;
|
|
154
|
+
try {
|
|
155
|
+
validated = workflowDefinitionSchema.parse(fetched.definition);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
const outcome = {
|
|
159
|
+
kind: "failed",
|
|
160
|
+
error: err instanceof Error
|
|
161
|
+
? `validate ${entry.workflow_id}: ${err.message}`
|
|
162
|
+
: `validate ${entry.workflow_id}: ${String(err)}`,
|
|
163
|
+
};
|
|
164
|
+
await writeAudit(cwd, outcome);
|
|
165
|
+
return outcome;
|
|
166
|
+
}
|
|
167
|
+
state.workflows[entry.workflow_id] = {
|
|
168
|
+
workflow_id: entry.workflow_id,
|
|
169
|
+
version: fetched.version,
|
|
170
|
+
updated_at: fetched.updated_at,
|
|
171
|
+
definition: validated,
|
|
172
|
+
};
|
|
173
|
+
(isNew ? added : changed).push(entry.workflow_id);
|
|
174
|
+
}
|
|
175
|
+
for (const name of Object.keys(state.workflows)) {
|
|
176
|
+
if (remoteByName.has(name))
|
|
177
|
+
continue;
|
|
178
|
+
delete state.workflows[name];
|
|
179
|
+
removed.push(name);
|
|
180
|
+
}
|
|
181
|
+
const totalChanged = added.length + changed.length + removed.length;
|
|
182
|
+
if (totalChanged === 0) {
|
|
183
|
+
if (identityChanged) {
|
|
184
|
+
state.last_synced_at = new Date().toISOString();
|
|
185
|
+
writeSyncedWorkflowsState(state);
|
|
186
|
+
}
|
|
187
|
+
const outcome = {
|
|
188
|
+
kind: "unchanged",
|
|
189
|
+
count: manifest.length,
|
|
190
|
+
};
|
|
191
|
+
await writeAudit(cwd, outcome);
|
|
192
|
+
return outcome;
|
|
193
|
+
}
|
|
194
|
+
state.last_synced_at = new Date().toISOString();
|
|
195
|
+
writeSyncedWorkflowsState(state);
|
|
196
|
+
const outcome = {
|
|
197
|
+
kind: "synced",
|
|
198
|
+
added,
|
|
199
|
+
changed,
|
|
200
|
+
removed,
|
|
201
|
+
};
|
|
202
|
+
await writeAudit(cwd, outcome);
|
|
203
|
+
return outcome;
|
|
204
|
+
}
|
|
205
|
+
async function writeAudit(cwd, outcome) {
|
|
206
|
+
const eventBase = {
|
|
207
|
+
timestamp: new Date().toISOString(),
|
|
208
|
+
host_id: hostname(),
|
|
209
|
+
};
|
|
210
|
+
if (outcome.kind === "unchanged") {
|
|
211
|
+
await writeHostAuditEvent(cwd, {
|
|
212
|
+
...eventBase,
|
|
213
|
+
event_type: "workflows_unchanged",
|
|
214
|
+
count: outcome.count,
|
|
215
|
+
}).catch(() => undefined);
|
|
216
|
+
}
|
|
217
|
+
else if (outcome.kind === "synced") {
|
|
218
|
+
await writeHostAuditEvent(cwd, {
|
|
219
|
+
...eventBase,
|
|
220
|
+
event_type: "workflows_synced",
|
|
221
|
+
added: outcome.added,
|
|
222
|
+
changed: outcome.changed,
|
|
223
|
+
removed: outcome.removed,
|
|
224
|
+
}).catch(() => undefined);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
await writeHostAuditEvent(cwd, {
|
|
228
|
+
...eventBase,
|
|
229
|
+
event_type: "workflows_sync_failed",
|
|
230
|
+
error: outcome.error,
|
|
231
|
+
}).catch(() => undefined);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
export function startWorkflowSyncTimer(cwd, intervalMs) {
|
|
235
|
+
const tick = () => {
|
|
236
|
+
void runWorkflowSyncOnce(cwd).catch((err) => {
|
|
237
|
+
process.stderr.write(`[cortex-daemon] workflow sync failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
void Promise.resolve().then(tick);
|
|
241
|
+
const handle = setInterval(tick, intervalMs);
|
|
242
|
+
if (typeof handle.unref === "function")
|
|
243
|
+
handle.unref();
|
|
244
|
+
return {
|
|
245
|
+
stop() {
|
|
246
|
+
clearInterval(handle);
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|