@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,449 @@
|
|
|
1
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { CortexDaemon } from "./server.js";
|
|
5
|
+
import { loadEnterpriseConfig, resolveEnterpriseActivation } from "../core/config.js";
|
|
6
|
+
import { configuredEnterpriseCredentialId } from "../core/enterprise-identity.js";
|
|
7
|
+
import { matchesEnterpriseHostIdentity } from "../core/enterprise-host-identity.js";
|
|
8
|
+
import { startGlobalUngovernedEventsPusher } from "./global-host-events.js";
|
|
9
|
+
import { pushMetrics } from "../enterprise/telemetry/sync.js";
|
|
10
|
+
import { TelemetryCollector } from "../core/telemetry/collector.js";
|
|
11
|
+
import { resolveTelemetryStateDir, telemetryStatePath } from "../core/telemetry/state-dir.js";
|
|
12
|
+
import { AuditWriter } from "../core/audit/writer.js";
|
|
13
|
+
import { PolicyStore } from "../core/policy/store.js";
|
|
14
|
+
import { enforceInjectionPolicy, isInjectionDefenseActive, } from "../core/policy/enforce.js";
|
|
15
|
+
import { startUngovernedScanner } from "./ungoverned-scanner.js";
|
|
16
|
+
import { HeartbeatTracker, writeTamperLock, emitTamperAudit, } from "./heartbeat-tracker.js";
|
|
17
|
+
import { startSyncTimer } from "./sync-checker.js";
|
|
18
|
+
import { startSkillSyncTimer } from "./skill-sync-checker.js";
|
|
19
|
+
import { startWorkflowSyncTimer } from "./workflow-sync-checker.js";
|
|
20
|
+
import { startCapabilitySyncTimer } from "./capability-sync-checker.js";
|
|
21
|
+
import { startHostEventsPusher } from "./host-events-pusher.js";
|
|
22
|
+
import { startEgressProxy } from "./egress-proxy.js";
|
|
23
|
+
import { startHeartbeatPusher } from "./heartbeat-pusher.js";
|
|
24
|
+
import { normalizeProjectCwd, ProjectServiceRegistry, } from "./project-service-registry.js";
|
|
25
|
+
/**
|
|
26
|
+
* Daemon entry point. Run by `cortex daemon start` (or auto-spawned by
|
|
27
|
+
* the first hook that needs it).
|
|
28
|
+
*
|
|
29
|
+
* v2.0.0: policy.check is currently a stub allowing all calls (real policy
|
|
30
|
+
* evaluation in subsequent commit). telemetry.flush is fully wired — the
|
|
31
|
+
* Stop hook now reliably pushes metrics.json even if MCP died abruptly.
|
|
32
|
+
*/
|
|
33
|
+
function extractStringFields(value, out = []) {
|
|
34
|
+
if (typeof value === "string") {
|
|
35
|
+
out.push(value);
|
|
36
|
+
}
|
|
37
|
+
else if (Array.isArray(value)) {
|
|
38
|
+
for (const v of value)
|
|
39
|
+
extractStringFields(v, out);
|
|
40
|
+
}
|
|
41
|
+
else if (value && typeof value === "object") {
|
|
42
|
+
for (const v of Object.values(value)) {
|
|
43
|
+
extractStringFields(v, out);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
async function policyCheck(payload) {
|
|
49
|
+
if (!payload.cwd)
|
|
50
|
+
return { allow: true };
|
|
51
|
+
const contextDir = join(payload.cwd, ".context");
|
|
52
|
+
if (!existsSync(contextDir))
|
|
53
|
+
return { allow: true };
|
|
54
|
+
const store = new PolicyStore(contextDir);
|
|
55
|
+
const policies = store.getMergedPolicies();
|
|
56
|
+
if (!isInjectionDefenseActive(policies)) {
|
|
57
|
+
return { allow: true };
|
|
58
|
+
}
|
|
59
|
+
const haystack = extractStringFields(payload.input).join("\n");
|
|
60
|
+
if (!haystack)
|
|
61
|
+
return { allow: true };
|
|
62
|
+
const result = enforceInjectionPolicy(haystack, policies);
|
|
63
|
+
if (result.allowed)
|
|
64
|
+
return { allow: true };
|
|
65
|
+
const topMatch = result.scan.matches[0];
|
|
66
|
+
const reason = topMatch
|
|
67
|
+
? `prompt-injection-defense: ${topMatch.category} (${topMatch.matched.slice(0, 80)})`
|
|
68
|
+
: "prompt-injection-defense: flagged";
|
|
69
|
+
return { allow: false, reason };
|
|
70
|
+
}
|
|
71
|
+
function readMetrics(contextDir) {
|
|
72
|
+
const path = telemetryStatePath(contextDir, "metrics.json");
|
|
73
|
+
if (!existsSync(path))
|
|
74
|
+
return null;
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function pendingPushPath(contextDir) {
|
|
83
|
+
return telemetryStatePath(contextDir, "pending-push.json");
|
|
84
|
+
}
|
|
85
|
+
function readPendingPush(contextDir) {
|
|
86
|
+
const path = pendingPushPath(contextDir);
|
|
87
|
+
if (!existsSync(path))
|
|
88
|
+
return null;
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function writePendingPush(contextDir, pending) {
|
|
97
|
+
const path = pendingPushPath(contextDir);
|
|
98
|
+
mkdirSync(resolveTelemetryStateDir(contextDir), { recursive: true });
|
|
99
|
+
writeFileSync(path, JSON.stringify(pending, null, 2), "utf8");
|
|
100
|
+
}
|
|
101
|
+
function deletePendingPush(contextDir) {
|
|
102
|
+
const path = pendingPushPath(contextDir);
|
|
103
|
+
try {
|
|
104
|
+
rmSync(path, { force: true });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// best effort
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function ackOnDisk(contextDir, pushed) {
|
|
111
|
+
const collector = new TelemetryCollector(contextDir, pushed.client_version || "unknown");
|
|
112
|
+
collector.acknowledgePush(pushed);
|
|
113
|
+
collector.flush();
|
|
114
|
+
}
|
|
115
|
+
const telemetryBackoff = new Map();
|
|
116
|
+
const TELEMETRY_BACKOFF_BASE_MS = 60_000;
|
|
117
|
+
const TELEMETRY_BACKOFF_CAP_MS = 30 * 60_000;
|
|
118
|
+
function shouldSkipTelemetryPush(cwd, now = Date.now()) {
|
|
119
|
+
const state = telemetryBackoff.get(cwd);
|
|
120
|
+
return state ? now < state.nextPushAt : false;
|
|
121
|
+
}
|
|
122
|
+
function recordTelemetryPushOutcome(cwd, success, now = Date.now()) {
|
|
123
|
+
if (success) {
|
|
124
|
+
telemetryBackoff.delete(cwd);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const prev = telemetryBackoff.get(cwd) ?? { nextPushAt: 0, consecutiveFailures: 0 };
|
|
128
|
+
const failures = prev.consecutiveFailures + 1;
|
|
129
|
+
const delay = Math.min(TELEMETRY_BACKOFF_BASE_MS * 2 ** (failures - 1), TELEMETRY_BACKOFF_CAP_MS);
|
|
130
|
+
telemetryBackoff.set(cwd, {
|
|
131
|
+
consecutiveFailures: failures,
|
|
132
|
+
nextPushAt: now + delay,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async function telemetryFlush(payload) {
|
|
136
|
+
const cwd = payload.cwd ?? process.cwd();
|
|
137
|
+
const contextDir = join(cwd, ".context");
|
|
138
|
+
if (!existsSync(contextDir)) {
|
|
139
|
+
return { flushed: false, events_pushed: 0 };
|
|
140
|
+
}
|
|
141
|
+
const config = loadEnterpriseConfig(contextDir);
|
|
142
|
+
const activation = resolveEnterpriseActivation(config);
|
|
143
|
+
if (!activation.active || !config.telemetry.enabled) {
|
|
144
|
+
// Community mode or telemetry disabled → nothing to push.
|
|
145
|
+
return { flushed: false, events_pushed: 0 };
|
|
146
|
+
}
|
|
147
|
+
if (!config.telemetry.endpoint || !config.telemetry.api_key) {
|
|
148
|
+
return { flushed: false, events_pushed: 0 };
|
|
149
|
+
}
|
|
150
|
+
if (shouldSkipTelemetryPush(cwd)) {
|
|
151
|
+
return { flushed: false, events_pushed: 0 };
|
|
152
|
+
}
|
|
153
|
+
const repo = basename(cwd);
|
|
154
|
+
const endpoint = config.telemetry.endpoint;
|
|
155
|
+
const apiKey = config.telemetry.api_key;
|
|
156
|
+
// Recovery: if a pending push exists, retry it first with the same
|
|
157
|
+
// push_id so the server can deduplicate against an earlier in-flight
|
|
158
|
+
// attempt that may have crashed before delete.
|
|
159
|
+
const pending = readPendingPush(contextDir);
|
|
160
|
+
if (pending) {
|
|
161
|
+
const result = await pushMetrics(pending.snapshot, endpoint, apiKey, {
|
|
162
|
+
repo,
|
|
163
|
+
session_id: payload.session_id,
|
|
164
|
+
push_id: pending.push_id,
|
|
165
|
+
});
|
|
166
|
+
recordTelemetryPushOutcome(cwd, result.success);
|
|
167
|
+
if (!result.success) {
|
|
168
|
+
process.stderr.write(`[cortex-daemon] pending telemetry push retry failed: ${result.error ?? "unknown"}\n`);
|
|
169
|
+
return { flushed: false, events_pushed: 0 };
|
|
170
|
+
}
|
|
171
|
+
ackOnDisk(contextDir, pending.snapshot);
|
|
172
|
+
deletePendingPush(contextDir);
|
|
173
|
+
return { flushed: true, events_pushed: pending.snapshot.total_tool_calls };
|
|
174
|
+
}
|
|
175
|
+
const metrics = readMetrics(contextDir);
|
|
176
|
+
if (!metrics) {
|
|
177
|
+
// No metrics on disk yet — MCP hasn't flushed. Nothing to push.
|
|
178
|
+
return { flushed: false, events_pushed: 0 };
|
|
179
|
+
}
|
|
180
|
+
const push_id = randomUUID();
|
|
181
|
+
writePendingPush(contextDir, {
|
|
182
|
+
snapshot: metrics,
|
|
183
|
+
push_id,
|
|
184
|
+
written_at: new Date().toISOString(),
|
|
185
|
+
});
|
|
186
|
+
const result = await pushMetrics(metrics, endpoint, apiKey, {
|
|
187
|
+
repo,
|
|
188
|
+
session_id: payload.session_id,
|
|
189
|
+
push_id,
|
|
190
|
+
});
|
|
191
|
+
recordTelemetryPushOutcome(cwd, result.success);
|
|
192
|
+
if (!result.success) {
|
|
193
|
+
process.stderr.write(`[cortex-daemon] telemetry push failed: ${result.error ?? "unknown"}\n`);
|
|
194
|
+
// Pending stays on disk; next tick (after backoff) will retry.
|
|
195
|
+
return { flushed: false, events_pushed: 0 };
|
|
196
|
+
}
|
|
197
|
+
ackOnDisk(contextDir, metrics);
|
|
198
|
+
deletePendingPush(contextDir);
|
|
199
|
+
return {
|
|
200
|
+
flushed: true,
|
|
201
|
+
events_pushed: metrics.total_tool_calls,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
// Per-cwd AuditWriter cache. Daemon serves multiple projects so we don't
|
|
205
|
+
// want to instantiate (and lose buffered state) on every audit.log call.
|
|
206
|
+
const auditWriters = new Map();
|
|
207
|
+
function getAuditWriter(cwd) {
|
|
208
|
+
const contextDir = join(cwd, ".context");
|
|
209
|
+
let writer = auditWriters.get(contextDir);
|
|
210
|
+
if (!writer) {
|
|
211
|
+
writer = new AuditWriter(contextDir);
|
|
212
|
+
auditWriters.set(contextDir, writer);
|
|
213
|
+
}
|
|
214
|
+
return writer;
|
|
215
|
+
}
|
|
216
|
+
async function auditLog(payload) {
|
|
217
|
+
if (!payload.cwd || !payload.entry) {
|
|
218
|
+
return { written: false };
|
|
219
|
+
}
|
|
220
|
+
const contextDir = join(payload.cwd, ".context");
|
|
221
|
+
if (!existsSync(contextDir)) {
|
|
222
|
+
return { written: false };
|
|
223
|
+
}
|
|
224
|
+
const writer = getAuditWriter(payload.cwd);
|
|
225
|
+
const entry = {
|
|
226
|
+
timestamp: payload.entry.timestamp,
|
|
227
|
+
tool: payload.entry.tool,
|
|
228
|
+
input: payload.entry.input,
|
|
229
|
+
result_count: payload.entry.result_count ?? 0,
|
|
230
|
+
entities_returned: [],
|
|
231
|
+
rules_applied: [],
|
|
232
|
+
duration_ms: payload.entry.duration_ms ?? 0,
|
|
233
|
+
status: payload.entry.status,
|
|
234
|
+
event_type: payload.entry.event_type,
|
|
235
|
+
evidence_level: payload.entry.evidence_level,
|
|
236
|
+
resource_type: payload.entry.resource_type,
|
|
237
|
+
session_id: payload.entry.session_id,
|
|
238
|
+
metadata: payload.entry.metadata,
|
|
239
|
+
};
|
|
240
|
+
writer.log(entry);
|
|
241
|
+
return { written: true };
|
|
242
|
+
}
|
|
243
|
+
async function main() {
|
|
244
|
+
// Phase 6: hook heartbeat tracker (per-session activity record + tamper detect).
|
|
245
|
+
const tracker = new HeartbeatTracker();
|
|
246
|
+
let projectServices = null;
|
|
247
|
+
function registerEnterpriseProject(cwd) {
|
|
248
|
+
const config = loadEnterpriseConfig(join(cwd, ".context"));
|
|
249
|
+
if (!resolveEnterpriseActivation(config).active)
|
|
250
|
+
return true;
|
|
251
|
+
if (projectServices?.has(cwd))
|
|
252
|
+
return true;
|
|
253
|
+
const credentialId = configuredEnterpriseCredentialId(cwd);
|
|
254
|
+
if (!credentialId || !matchesEnterpriseHostIdentity(credentialId)) {
|
|
255
|
+
process.stderr.write(`[cortex-daemon] rejected Enterprise project ${cwd}: ` +
|
|
256
|
+
"this user profile is enrolled to another endpoint or API key\n");
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
if (!projectServices?.register(cwd)) {
|
|
260
|
+
process.stderr.write(`[cortex-daemon] rejected Enterprise project ${cwd}: ` +
|
|
261
|
+
"this user daemon is already bound to another endpoint or API key\n");
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
async function heartbeat(payload) {
|
|
267
|
+
const cwd = normalizeProjectCwd(payload.cwd);
|
|
268
|
+
if (!cwd) {
|
|
269
|
+
return { recorded: false, tamper_lock_active: false };
|
|
270
|
+
}
|
|
271
|
+
if (!registerEnterpriseProject(cwd)) {
|
|
272
|
+
return { recorded: false, tamper_lock_active: false };
|
|
273
|
+
}
|
|
274
|
+
return tracker.recordHeartbeat({ ...payload, cwd });
|
|
275
|
+
}
|
|
276
|
+
const daemon = new CortexDaemon({
|
|
277
|
+
onPolicyCheck: policyCheck,
|
|
278
|
+
onTelemetryFlush: telemetryFlush,
|
|
279
|
+
onAuditLog: auditLog,
|
|
280
|
+
onHeartbeat: heartbeat,
|
|
281
|
+
});
|
|
282
|
+
// Phase 5: Tier 3 ungoverned-session detection. Periodic process scan, audit
|
|
283
|
+
// emit per finding, optional SIGTERM in enforced mode (same-user only).
|
|
284
|
+
const scanInterval = parseInt(process.env.CORTEX_UNGOVERNED_SCAN_MS ?? "", 10);
|
|
285
|
+
const intervalMs = Number.isFinite(scanInterval) && scanInterval > 0 ? scanInterval : 60_000;
|
|
286
|
+
// Phase 6: periodic tamper-checker. For each active session that had at
|
|
287
|
+
// least one tool-fired hook then went silent past missing_threshold_seconds,
|
|
288
|
+
// write .cortex-tamper.lock + audit event. The next SessionStart in
|
|
289
|
+
// enforced mode will refuse to register tools until 'cortex enterprise
|
|
290
|
+
// repair' clears the lock.
|
|
291
|
+
const tamperThreshold = parseInt(process.env.CORTEX_TAMPER_MISSING_THRESHOLD_S ?? "", 10);
|
|
292
|
+
const missingThresholdSeconds = Number.isFinite(tamperThreshold) && tamperThreshold > 0 ? tamperThreshold : 300;
|
|
293
|
+
const tamperCheckInterval = parseInt(process.env.CORTEX_TAMPER_CHECK_MS ?? "", 10);
|
|
294
|
+
const tamperCheckMs = Number.isFinite(tamperCheckInterval) && tamperCheckInterval > 0 ? tamperCheckInterval : 60_000;
|
|
295
|
+
// Phase 7: periodic sync-version-check + host-events push to cortex-web.
|
|
296
|
+
// Daemon runs as the user post-Fas-3 privilege drop, so sync only checks
|
|
297
|
+
// version availability (writes a notification + audit). Re-applying
|
|
298
|
+
// managed-settings still requires 'sudo cortex enterprise sync'.
|
|
299
|
+
const syncIntervalRaw = parseInt(process.env.CORTEX_SYNC_CHECK_MS ?? "", 10);
|
|
300
|
+
const syncIntervalMs = Number.isFinite(syncIntervalRaw) && syncIntervalRaw > 0 ? syncIntervalRaw : 60 * 60 * 1000;
|
|
301
|
+
const pushIntervalRaw = parseInt(process.env.CORTEX_HOST_EVENTS_PUSH_MS ?? "", 10);
|
|
302
|
+
const pushIntervalMs = Number.isFinite(pushIntervalRaw) && pushIntervalRaw > 0 ? pushIntervalRaw : 5 * 60 * 1000;
|
|
303
|
+
// Skills v3: poll cortex-web for org-authored skills, write SKILL.md
|
|
304
|
+
// files into per-CLI user-scope directories. Runs at the same cadence
|
|
305
|
+
// as the govern-config sync check by default but is independently
|
|
306
|
+
// configurable.
|
|
307
|
+
const skillSyncRaw = parseInt(process.env.CORTEX_SKILL_SYNC_MS ?? "", 10);
|
|
308
|
+
const skillSyncMs = Number.isFinite(skillSyncRaw) && skillSyncRaw > 0 ? skillSyncRaw : syncIntervalMs;
|
|
309
|
+
// Harness Phase 2: poll cortex-web for org-authored workflows, cache
|
|
310
|
+
// their definitions locally so cortex.workflow.start can resolve
|
|
311
|
+
// org-specific workflow_ids ahead of bundled defaults. Same cadence
|
|
312
|
+
// as the skill sync by default; independently configurable via
|
|
313
|
+
// CORTEX_WORKFLOW_SYNC_MS / CORTEX_DISABLE_WORKFLOW_SYNC.
|
|
314
|
+
const workflowSyncRaw = parseInt(process.env.CORTEX_WORKFLOW_SYNC_MS ?? "", 10);
|
|
315
|
+
const workflowSyncMs = Number.isFinite(workflowSyncRaw) && workflowSyncRaw > 0
|
|
316
|
+
? workflowSyncRaw
|
|
317
|
+
: skillSyncMs;
|
|
318
|
+
// Harness Phase 2: poll cortex-web for org-authored capabilities and
|
|
319
|
+
// cache definitions locally so evaluateToolCall can merge them over
|
|
320
|
+
// bundled DEFAULT_CAPABILITIES on the pre-tool-use path. Same cadence
|
|
321
|
+
// as the workflow sync by default; independently configurable via
|
|
322
|
+
// CORTEX_CAPABILITY_SYNC_MS / CORTEX_DISABLE_CAPABILITY_SYNC.
|
|
323
|
+
const capabilitySyncRaw = parseInt(process.env.CORTEX_CAPABILITY_SYNC_MS ?? "", 10);
|
|
324
|
+
const capabilitySyncMs = Number.isFinite(capabilitySyncRaw) && capabilitySyncRaw > 0
|
|
325
|
+
? capabilitySyncRaw
|
|
326
|
+
: workflowSyncMs;
|
|
327
|
+
// Govern host heartbeat — fills host_enrollment on cortex-web so the
|
|
328
|
+
// dashboard at /dashboard/govern actually shows this host.
|
|
329
|
+
const heartbeatRaw = parseInt(process.env.CORTEX_HEARTBEAT_PUSH_MS ?? "", 10);
|
|
330
|
+
const heartbeatMs = Number.isFinite(heartbeatRaw) && heartbeatRaw > 0 ? heartbeatRaw : 5 * 60 * 1000;
|
|
331
|
+
projectServices = new ProjectServiceRegistry((cwd) => {
|
|
332
|
+
const handles = [];
|
|
333
|
+
if (process.env.CORTEX_DISABLE_SYNC_CHECK !== "1") {
|
|
334
|
+
handles.push(startSyncTimer(cwd, syncIntervalMs));
|
|
335
|
+
}
|
|
336
|
+
if (process.env.CORTEX_DISABLE_HOST_EVENTS_PUSH !== "1") {
|
|
337
|
+
handles.push(startHostEventsPusher(cwd, pushIntervalMs));
|
|
338
|
+
}
|
|
339
|
+
return handles;
|
|
340
|
+
}, {
|
|
341
|
+
credentialIdForCwd: configuredEnterpriseCredentialId,
|
|
342
|
+
hostFactory: (cwd, credentialId) => {
|
|
343
|
+
const handles = [];
|
|
344
|
+
// These services operate on user-global process or artifact state.
|
|
345
|
+
// One explicit host governance project owns them; projects carrying
|
|
346
|
+
// another Enterprise identity are rejected by the registry.
|
|
347
|
+
if (process.env.CORTEX_DISABLE_UNGOVERNED_SCAN !== "1") {
|
|
348
|
+
// Process snapshots do not carry a trustworthy project cwd. Keep the
|
|
349
|
+
// single host-wide scanner detection-only so an enforced project can
|
|
350
|
+
// never terminate a process belonging to an advisory project.
|
|
351
|
+
handles.push(startUngovernedScanner({
|
|
352
|
+
cwd,
|
|
353
|
+
credentialId,
|
|
354
|
+
intervalMs,
|
|
355
|
+
mode: "advisory",
|
|
356
|
+
}));
|
|
357
|
+
}
|
|
358
|
+
if (process.env.CORTEX_DISABLE_HOST_EVENTS_PUSH !== "1") {
|
|
359
|
+
handles.push(startGlobalUngovernedEventsPusher(cwd, credentialId, pushIntervalMs));
|
|
360
|
+
}
|
|
361
|
+
if (process.env.CORTEX_DISABLE_SKILL_SYNC !== "1") {
|
|
362
|
+
handles.push(startSkillSyncTimer(cwd, skillSyncMs));
|
|
363
|
+
}
|
|
364
|
+
if (process.env.CORTEX_DISABLE_WORKFLOW_SYNC !== "1") {
|
|
365
|
+
handles.push(startWorkflowSyncTimer(cwd, workflowSyncMs));
|
|
366
|
+
}
|
|
367
|
+
if (process.env.CORTEX_DISABLE_CAPABILITY_SYNC !== "1") {
|
|
368
|
+
handles.push(startCapabilitySyncTimer(cwd, capabilitySyncMs));
|
|
369
|
+
}
|
|
370
|
+
if (process.env.CORTEX_DISABLE_HEARTBEAT_PUSH !== "1") {
|
|
371
|
+
handles.push(startHeartbeatPusher(cwd, heartbeatMs));
|
|
372
|
+
}
|
|
373
|
+
return handles;
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
await daemon.start();
|
|
377
|
+
const startupCwd = normalizeProjectCwd(process.cwd());
|
|
378
|
+
if (startupCwd)
|
|
379
|
+
registerEnterpriseProject(startupCwd);
|
|
380
|
+
// Phase 4 task 19: cortex egress proxy. Logs SNI + destination per
|
|
381
|
+
// outbound connection (no TLS termination). cortex run sets
|
|
382
|
+
// HTTPS_PROXY/HTTP_PROXY for the Copilot wrap; other AI CLIs respect
|
|
383
|
+
// these env vars too if a developer wires them in.
|
|
384
|
+
const proxyPortRaw = parseInt(process.env.CORTEX_EGRESS_PROXY_PORT ?? "", 10);
|
|
385
|
+
const proxyPort = Number.isFinite(proxyPortRaw) && proxyPortRaw > 0 ? proxyPortRaw : 18888;
|
|
386
|
+
if (process.env.CORTEX_DISABLE_EGRESS_PROXY !== "1") {
|
|
387
|
+
// A shared proxy connection has no trustworthy project identity. Keep
|
|
388
|
+
// transport support, but suppress project audit emission until callers
|
|
389
|
+
// use an attributable per-project channel.
|
|
390
|
+
startEgressProxy({ port: proxyPort })
|
|
391
|
+
.then((handle) => {
|
|
392
|
+
process.stderr.write(`[cortex-daemon] egress proxy listening on 127.0.0.1:${handle.port}\n`);
|
|
393
|
+
})
|
|
394
|
+
.catch((err) => {
|
|
395
|
+
process.stderr.write(`[cortex-daemon] egress proxy failed to start: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
// Periodic telemetry push. Daemon owns the network call so MCP doesn't
|
|
399
|
+
// race with itself or with this loop. Walks active sessions, dedupes
|
|
400
|
+
// cwds, and runs the existing per-cwd flush handler.
|
|
401
|
+
const telemetryPushRaw = parseInt(process.env.CORTEX_TELEMETRY_PUSH_MS ?? "", 10);
|
|
402
|
+
const telemetryPushMs = Number.isFinite(telemetryPushRaw) && telemetryPushRaw > 0
|
|
403
|
+
? telemetryPushRaw
|
|
404
|
+
: 5 * 60 * 1000;
|
|
405
|
+
if (process.env.CORTEX_DISABLE_TELEMETRY_PUSH !== "1") {
|
|
406
|
+
const telemetryTimer = setInterval(async () => {
|
|
407
|
+
const cwds = new Set();
|
|
408
|
+
for (const [, state] of tracker.getActiveSessions()) {
|
|
409
|
+
if (state.cwd)
|
|
410
|
+
cwds.add(state.cwd);
|
|
411
|
+
}
|
|
412
|
+
for (const cwd of cwds) {
|
|
413
|
+
try {
|
|
414
|
+
await telemetryFlush({ reason: "interval", cwd });
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
process.stderr.write(`[cortex-daemon] telemetry push failed for ${cwd}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}, telemetryPushMs);
|
|
421
|
+
if (typeof telemetryTimer.unref === "function")
|
|
422
|
+
telemetryTimer.unref();
|
|
423
|
+
}
|
|
424
|
+
if (process.env.CORTEX_DISABLE_TAMPER_CHECK !== "1") {
|
|
425
|
+
const checkTimer = setInterval(() => {
|
|
426
|
+
const detected = tracker.detectTamper({
|
|
427
|
+
cwds: projectServices?.projectCwds() ?? [],
|
|
428
|
+
missingThresholdSeconds,
|
|
429
|
+
});
|
|
430
|
+
for (const entry of detected) {
|
|
431
|
+
try {
|
|
432
|
+
writeTamperLock(entry.cwd, entry);
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
process.stderr.write(`[cortex-daemon] failed to write tamper lock: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
436
|
+
}
|
|
437
|
+
void emitTamperAudit(entry.cwd, entry).catch((err) => {
|
|
438
|
+
process.stderr.write(`[cortex-daemon] failed to emit tamper audit: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}, tamperCheckMs);
|
|
442
|
+
if (typeof checkTimer.unref === "function")
|
|
443
|
+
checkTimer.unref();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
main().catch((err) => {
|
|
447
|
+
process.stderr.write(`[cortex-daemon] fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
448
|
+
process.exit(1);
|
|
449
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { homedir, tmpdir, userInfo } from "node:os";
|
|
3
|
+
import { mkdirSync } from "node:fs";
|
|
4
|
+
/**
|
|
5
|
+
* Resolves filesystem locations the daemon and hooks share.
|
|
6
|
+
* Per-user, not per-project — one daemon serves all projects so warm graph
|
|
7
|
+
* + embeddings stay loaded across switches.
|
|
8
|
+
*/
|
|
9
|
+
function safeUid() {
|
|
10
|
+
try {
|
|
11
|
+
const info = userInfo();
|
|
12
|
+
if (typeof info.uid === "number" && info.uid >= 0) {
|
|
13
|
+
return String(info.uid);
|
|
14
|
+
}
|
|
15
|
+
return info.username || "anon";
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return "anon";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function daemonDir() {
|
|
22
|
+
const dir = join(homedir(), ".cortex");
|
|
23
|
+
mkdirSync(dir, { recursive: true });
|
|
24
|
+
return dir;
|
|
25
|
+
}
|
|
26
|
+
export function pidFilePath() {
|
|
27
|
+
return join(daemonDir(), "daemon.pid");
|
|
28
|
+
}
|
|
29
|
+
export function logFilePath() {
|
|
30
|
+
return join(daemonDir(), "daemon.log");
|
|
31
|
+
}
|
|
32
|
+
export function socketPath() {
|
|
33
|
+
// Keep socket in tmpdir per-user — Linux has 108-char path limit on
|
|
34
|
+
// sockaddr_un.sun_path so we avoid putting it under $HOME.
|
|
35
|
+
return join(tmpdir(), `cortex-${safeUid()}.sock`);
|
|
36
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
export function normalizeProjectCwd(cwd) {
|
|
4
|
+
if (!cwd.trim())
|
|
5
|
+
return null;
|
|
6
|
+
const candidate = resolve(cwd);
|
|
7
|
+
try {
|
|
8
|
+
if (!existsSync(candidate) || !statSync(candidate).isDirectory()) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
if (!existsSync(join(candidate, ".context"))) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return realpathSync(candidate);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Owns background services for every project observed by the per-user daemon.
|
|
22
|
+
* Request-time handlers already carry cwd in their payloads; this registry
|
|
23
|
+
* gives the periodic services the same project isolation.
|
|
24
|
+
*/
|
|
25
|
+
export class ProjectServiceRegistry {
|
|
26
|
+
factory;
|
|
27
|
+
options;
|
|
28
|
+
projects = new Map();
|
|
29
|
+
hostCredentialId = null;
|
|
30
|
+
hostHandles = [];
|
|
31
|
+
constructor(factory, options = {}) {
|
|
32
|
+
this.factory = factory;
|
|
33
|
+
this.options = options;
|
|
34
|
+
}
|
|
35
|
+
register(cwd) {
|
|
36
|
+
const normalized = normalizeProjectCwd(cwd);
|
|
37
|
+
if (!normalized || this.projects.has(normalized))
|
|
38
|
+
return false;
|
|
39
|
+
const credentialId = this.options.credentialIdForCwd?.(normalized) ?? "project-local";
|
|
40
|
+
if (!credentialId)
|
|
41
|
+
return false;
|
|
42
|
+
if (this.hostCredentialId &&
|
|
43
|
+
this.hostCredentialId !== credentialId) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (!this.hostCredentialId) {
|
|
47
|
+
this.hostCredentialId = credentialId;
|
|
48
|
+
this.hostHandles =
|
|
49
|
+
this.options.hostFactory?.(normalized, credentialId) ?? [];
|
|
50
|
+
}
|
|
51
|
+
const handles = this.factory(normalized);
|
|
52
|
+
this.projects.set(normalized, handles);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
has(cwd) {
|
|
56
|
+
const normalized = normalizeProjectCwd(cwd);
|
|
57
|
+
return normalized ? this.projects.has(normalized) : false;
|
|
58
|
+
}
|
|
59
|
+
projectCwds() {
|
|
60
|
+
return Array.from(this.projects.keys());
|
|
61
|
+
}
|
|
62
|
+
async stopAll() {
|
|
63
|
+
const handles = [
|
|
64
|
+
...Array.from(this.projects.values()).flat(),
|
|
65
|
+
...this.hostHandles,
|
|
66
|
+
];
|
|
67
|
+
this.projects.clear();
|
|
68
|
+
this.hostHandles = [];
|
|
69
|
+
this.hostCredentialId = null;
|
|
70
|
+
await Promise.allSettled(handles.map((handle) => Promise.resolve(handle.stop())));
|
|
71
|
+
}
|
|
72
|
+
size() {
|
|
73
|
+
return this.projects.size;
|
|
74
|
+
}
|
|
75
|
+
activeCredentialId() {
|
|
76
|
+
return this.hostCredentialId;
|
|
77
|
+
}
|
|
78
|
+
}
|