@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,130 @@
|
|
|
1
|
+
import { connect } from "node:net";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { socketPath, pidFilePath, logFilePath } from "./paths.js";
|
|
6
|
+
import { DEFAULT_REQUEST_TIMEOUT_MS, } from "./protocol.js";
|
|
7
|
+
export function isProcessAlive(pid) {
|
|
8
|
+
try {
|
|
9
|
+
process.kill(pid, 0);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
if (err && typeof err === "object" && "code" in err && err.code === "EPERM") {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function readDaemonPid() {
|
|
20
|
+
if (!existsSync(pidFilePath()))
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
const raw = readFileSync(pidFilePath(), "utf8").trim();
|
|
24
|
+
const pid = Number.parseInt(raw, 10);
|
|
25
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
26
|
+
return null;
|
|
27
|
+
return pid;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function isDaemonRunning() {
|
|
34
|
+
const pid = readDaemonPid();
|
|
35
|
+
if (pid === null)
|
|
36
|
+
return false;
|
|
37
|
+
return isProcessAlive(pid);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Spawn the daemon as a detached process. Returns immediately — caller is
|
|
41
|
+
* responsible for waiting until socket is ready (typically via retry loop).
|
|
42
|
+
*/
|
|
43
|
+
export function spawnDaemon(daemonEntryAbsPath) {
|
|
44
|
+
const out = (() => {
|
|
45
|
+
try {
|
|
46
|
+
// openSync returns an fd we can pass to spawn for stdio redirection.
|
|
47
|
+
// Append-only — the daemon log accumulates across runs.
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
49
|
+
const fs = require("node:fs");
|
|
50
|
+
return fs.openSync(logFilePath(), "a");
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return "ignore";
|
|
54
|
+
}
|
|
55
|
+
})();
|
|
56
|
+
const child = spawn(process.execPath, [daemonEntryAbsPath], {
|
|
57
|
+
detached: true,
|
|
58
|
+
stdio: ["ignore", out, out],
|
|
59
|
+
env: { ...process.env, CORTEX_DAEMON_AUTOSTART: "1" },
|
|
60
|
+
});
|
|
61
|
+
child.unref();
|
|
62
|
+
}
|
|
63
|
+
async function connectWithRetry(timeoutMs) {
|
|
64
|
+
const start = Date.now();
|
|
65
|
+
while (Date.now() - start < timeoutMs) {
|
|
66
|
+
const sock = await new Promise((resolve) => {
|
|
67
|
+
const s = connect(socketPath());
|
|
68
|
+
s.once("connect", () => resolve(s));
|
|
69
|
+
s.once("error", () => {
|
|
70
|
+
s.destroy();
|
|
71
|
+
resolve(null);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
if (sock)
|
|
75
|
+
return sock;
|
|
76
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
export async function call(type, payload, options = {}) {
|
|
81
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
82
|
+
const id = randomUUID();
|
|
83
|
+
const sock = await connectWithRetry(timeoutMs);
|
|
84
|
+
if (!sock) {
|
|
85
|
+
return { ok: false, error: "daemon_unreachable" };
|
|
86
|
+
}
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
let buffer = "";
|
|
89
|
+
let settled = false;
|
|
90
|
+
const finish = (r) => {
|
|
91
|
+
if (settled)
|
|
92
|
+
return;
|
|
93
|
+
settled = true;
|
|
94
|
+
sock.end();
|
|
95
|
+
resolve(r);
|
|
96
|
+
};
|
|
97
|
+
const timer = setTimeout(() => {
|
|
98
|
+
finish({ ok: false, error: "timeout" });
|
|
99
|
+
}, timeoutMs);
|
|
100
|
+
timer.unref();
|
|
101
|
+
sock.on("data", (chunk) => {
|
|
102
|
+
buffer += chunk.toString("utf8");
|
|
103
|
+
const nl = buffer.indexOf("\n");
|
|
104
|
+
if (nl === -1)
|
|
105
|
+
return;
|
|
106
|
+
const line = buffer.slice(0, nl);
|
|
107
|
+
try {
|
|
108
|
+
const resp = JSON.parse(line);
|
|
109
|
+
if (resp.id !== id)
|
|
110
|
+
return; // not ours; ignore
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
if (resp.ok) {
|
|
113
|
+
finish({ ok: true, result: resp.result });
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
finish({ ok: false, error: resp.error ?? "unknown_error" });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
finish({ ok: false, error: "invalid_response" });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
sock.on("error", (err) => {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
finish({ ok: false, error: `socket_error: ${err.message}` });
|
|
126
|
+
});
|
|
127
|
+
const request = { id, type, payload };
|
|
128
|
+
sock.write(`${JSON.stringify(request)}\n`);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { createServer, connect } from "node:net";
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import { writeHostAuditEvent } from "./ungoverned-scanner.js";
|
|
4
|
+
/**
|
|
5
|
+
* Phase 4 task 19 — cortex egress proxy.
|
|
6
|
+
*
|
|
7
|
+
* Listens on a local TCP port for HTTP CONNECT (https-proxy) requests.
|
|
8
|
+
* Pipes bytes through transparently; does NOT terminate TLS. Inspects
|
|
9
|
+
* only:
|
|
10
|
+
* - the CONNECT line for the destination host:port
|
|
11
|
+
* - the first client chunk for the TLS ClientHello SNI extension
|
|
12
|
+
*
|
|
13
|
+
* Per privacy boundary v3 we never send payload bytes to cortex-web —
|
|
14
|
+
* only SNI + destination + bytes-transferred counters. The audit lands
|
|
15
|
+
* in .context/audit/host-events-YYYY-MM-DD.jsonl as event_type =
|
|
16
|
+
* "egress_connection" only when a caller supplies an attributable project
|
|
17
|
+
* cwd. The shared daemon proxy intentionally omits cwd and therefore emits
|
|
18
|
+
* no project audit record rather than misattributing cross-project traffic.
|
|
19
|
+
*
|
|
20
|
+
* Plain HTTP (non-CONNECT) is also supported but logged with the Host
|
|
21
|
+
* header in place of SNI.
|
|
22
|
+
*/
|
|
23
|
+
const CONNECT_RE = /^CONNECT\s+([^\s:]+):(\d+)\s+HTTP\/1\.[01]/i;
|
|
24
|
+
const HTTP_RE = /^([A-Z]+)\s+(http:\/\/[^\s]+)\s+HTTP\/1\.[01]/i;
|
|
25
|
+
/**
|
|
26
|
+
* Parse SNI from a TLS ClientHello buffer. Returns null if not found
|
|
27
|
+
* or buffer is malformed. Does not mutate the buffer.
|
|
28
|
+
*/
|
|
29
|
+
export function parseSni(buf) {
|
|
30
|
+
if (buf.length < 11)
|
|
31
|
+
return null;
|
|
32
|
+
if (buf[0] !== 0x16)
|
|
33
|
+
return null;
|
|
34
|
+
if (buf[1] !== 0x03)
|
|
35
|
+
return null;
|
|
36
|
+
const recordLen = buf.readUInt16BE(3);
|
|
37
|
+
const recordEnd = 5 + recordLen;
|
|
38
|
+
if (buf.length < recordEnd)
|
|
39
|
+
return null;
|
|
40
|
+
if (buf[5] !== 0x01)
|
|
41
|
+
return null;
|
|
42
|
+
const handshakeLen = (buf[6] << 16) | (buf[7] << 8) | buf[8];
|
|
43
|
+
const handshakeEnd = 9 + handshakeLen;
|
|
44
|
+
if (buf.length < Math.min(handshakeEnd, recordEnd))
|
|
45
|
+
return null;
|
|
46
|
+
let p = 9 + 2 + 32;
|
|
47
|
+
if (p + 1 > recordEnd)
|
|
48
|
+
return null;
|
|
49
|
+
const sessionIdLen = buf[p];
|
|
50
|
+
p += 1 + sessionIdLen;
|
|
51
|
+
if (p + 2 > recordEnd)
|
|
52
|
+
return null;
|
|
53
|
+
const cipherSuitesLen = buf.readUInt16BE(p);
|
|
54
|
+
p += 2 + cipherSuitesLen;
|
|
55
|
+
if (p + 1 > recordEnd)
|
|
56
|
+
return null;
|
|
57
|
+
const compMethodsLen = buf[p];
|
|
58
|
+
p += 1 + compMethodsLen;
|
|
59
|
+
if (p + 2 > recordEnd)
|
|
60
|
+
return null;
|
|
61
|
+
const extensionsLen = buf.readUInt16BE(p);
|
|
62
|
+
p += 2;
|
|
63
|
+
const extensionsEnd = p + extensionsLen;
|
|
64
|
+
if (extensionsEnd > recordEnd)
|
|
65
|
+
return null;
|
|
66
|
+
while (p + 4 <= extensionsEnd) {
|
|
67
|
+
const extType = buf.readUInt16BE(p);
|
|
68
|
+
const extLen = buf.readUInt16BE(p + 2);
|
|
69
|
+
const extEnd = p + 4 + extLen;
|
|
70
|
+
if (extEnd > extensionsEnd)
|
|
71
|
+
return null;
|
|
72
|
+
if (extType === 0x0000) {
|
|
73
|
+
let q = p + 4;
|
|
74
|
+
if (q + 2 > extEnd)
|
|
75
|
+
return null;
|
|
76
|
+
const listLen = buf.readUInt16BE(q);
|
|
77
|
+
q += 2;
|
|
78
|
+
const listEnd = q + listLen;
|
|
79
|
+
if (listEnd > extEnd)
|
|
80
|
+
return null;
|
|
81
|
+
while (q + 3 <= listEnd) {
|
|
82
|
+
const nameType = buf[q];
|
|
83
|
+
const nameLen = buf.readUInt16BE(q + 1);
|
|
84
|
+
q += 3;
|
|
85
|
+
if (q + nameLen > listEnd)
|
|
86
|
+
return null;
|
|
87
|
+
if (nameType === 0x00) {
|
|
88
|
+
return buf.subarray(q, q + nameLen).toString("ascii");
|
|
89
|
+
}
|
|
90
|
+
q += nameLen;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
p = extEnd;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const HTTP_OK = "HTTP/1.1 200 Connection Established\r\nProxy-Agent: cortex-egress\r\n\r\n";
|
|
99
|
+
const HTTP_BAD = "HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n";
|
|
100
|
+
const HTTP_BAD_GATEWAY = "HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n";
|
|
101
|
+
function emit(cwd, evt) {
|
|
102
|
+
if (!cwd)
|
|
103
|
+
return;
|
|
104
|
+
void writeHostAuditEvent(cwd, evt).catch((err) => {
|
|
105
|
+
process.stderr.write(`[cortex-egress] audit emit failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function newEvent(host, port, protocol, hostId, sourcePort) {
|
|
109
|
+
return {
|
|
110
|
+
event_type: "egress_connection",
|
|
111
|
+
timestamp: new Date().toISOString(),
|
|
112
|
+
host_id: hostId,
|
|
113
|
+
source_port: sourcePort,
|
|
114
|
+
destination: { host, port },
|
|
115
|
+
protocol,
|
|
116
|
+
sni: null,
|
|
117
|
+
bytes_client_to_server: 0,
|
|
118
|
+
bytes_server_to_client: 0,
|
|
119
|
+
duration_ms: 0,
|
|
120
|
+
closed_by: "client",
|
|
121
|
+
error: null,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function pipeWithCounting(client, upstream, evt, cwd, start) {
|
|
125
|
+
let firstClientChunk = true;
|
|
126
|
+
client.on("data", (chunk) => {
|
|
127
|
+
evt.bytes_client_to_server += chunk.length;
|
|
128
|
+
if (firstClientChunk && evt.protocol === "https") {
|
|
129
|
+
firstClientChunk = false;
|
|
130
|
+
const sni = parseSni(chunk);
|
|
131
|
+
if (sni)
|
|
132
|
+
evt.sni = sni;
|
|
133
|
+
}
|
|
134
|
+
upstream.write(chunk);
|
|
135
|
+
});
|
|
136
|
+
upstream.on("data", (chunk) => {
|
|
137
|
+
evt.bytes_server_to_client += chunk.length;
|
|
138
|
+
client.write(chunk);
|
|
139
|
+
});
|
|
140
|
+
const finalize = (closer, error = null) => {
|
|
141
|
+
if (evt.duration_ms > 0)
|
|
142
|
+
return;
|
|
143
|
+
evt.duration_ms = Date.now() - start;
|
|
144
|
+
evt.closed_by = closer;
|
|
145
|
+
evt.error = error;
|
|
146
|
+
emit(cwd, evt);
|
|
147
|
+
try {
|
|
148
|
+
client.destroy();
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// ignore
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
upstream.destroy();
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// ignore
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
client.on("end", () => finalize("client"));
|
|
161
|
+
upstream.on("end", () => finalize("server"));
|
|
162
|
+
client.on("error", (err) => finalize("error", err.message));
|
|
163
|
+
upstream.on("error", (err) => finalize("error", err.message));
|
|
164
|
+
}
|
|
165
|
+
function handleConnect(client, host, port, cwd, hostId) {
|
|
166
|
+
const evt = newEvent(host, port, "https", hostId, client.remotePort ?? null);
|
|
167
|
+
const start = Date.now();
|
|
168
|
+
const upstream = connect({ host, port }, () => {
|
|
169
|
+
client.write(HTTP_OK);
|
|
170
|
+
pipeWithCounting(client, upstream, evt, cwd, start);
|
|
171
|
+
});
|
|
172
|
+
upstream.on("error", (err) => {
|
|
173
|
+
evt.duration_ms = Date.now() - start;
|
|
174
|
+
evt.closed_by = "error";
|
|
175
|
+
evt.error = `upstream connect: ${err.message}`;
|
|
176
|
+
emit(cwd, evt);
|
|
177
|
+
try {
|
|
178
|
+
client.write(HTTP_BAD_GATEWAY);
|
|
179
|
+
client.destroy();
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// ignore
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function handleHttp(client, url, initialChunk, cwd, hostId) {
|
|
187
|
+
const port = url.port ? parseInt(url.port, 10) : 80;
|
|
188
|
+
const evt = newEvent(url.hostname, port, "http", hostId, client.remotePort ?? null);
|
|
189
|
+
evt.sni = url.hostname;
|
|
190
|
+
const start = Date.now();
|
|
191
|
+
const upstream = connect({ host: url.hostname, port }, () => {
|
|
192
|
+
upstream.write(initialChunk);
|
|
193
|
+
pipeWithCounting(client, upstream, evt, cwd, start);
|
|
194
|
+
});
|
|
195
|
+
upstream.on("error", (err) => {
|
|
196
|
+
evt.duration_ms = Date.now() - start;
|
|
197
|
+
evt.closed_by = "error";
|
|
198
|
+
evt.error = `upstream connect: ${err.message}`;
|
|
199
|
+
emit(cwd, evt);
|
|
200
|
+
try {
|
|
201
|
+
client.write(HTTP_BAD_GATEWAY);
|
|
202
|
+
client.destroy();
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// ignore
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
function handleConnection(client, cwd, hostId) {
|
|
210
|
+
let buffer = Buffer.alloc(0);
|
|
211
|
+
const onFirstChunk = (chunk) => {
|
|
212
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
213
|
+
const headerEnd = buffer.indexOf("\r\n\r\n");
|
|
214
|
+
if (headerEnd === -1) {
|
|
215
|
+
if (buffer.length > 8192) {
|
|
216
|
+
client.write(HTTP_BAD);
|
|
217
|
+
client.destroy();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
client.once("data", onFirstChunk);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const headers = buffer.subarray(0, headerEnd).toString("ascii");
|
|
224
|
+
const remainder = buffer.subarray(headerEnd + 4);
|
|
225
|
+
const firstLine = headers.split(/\r?\n/, 1)[0] ?? "";
|
|
226
|
+
const connectMatch = firstLine.match(CONNECT_RE);
|
|
227
|
+
if (connectMatch) {
|
|
228
|
+
const host = connectMatch[1];
|
|
229
|
+
const port = parseInt(connectMatch[2], 10);
|
|
230
|
+
handleConnect(client, host, port, cwd, hostId);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const httpMatch = firstLine.match(HTTP_RE);
|
|
234
|
+
if (httpMatch) {
|
|
235
|
+
let url;
|
|
236
|
+
try {
|
|
237
|
+
url = new URL(httpMatch[2]);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
client.write(HTTP_BAD);
|
|
241
|
+
client.destroy();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const path = url.pathname + url.search;
|
|
245
|
+
const rebuilt = `${httpMatch[1]} ${path} HTTP/1.1\r\n${headers.split(/\r?\n/).slice(1).join("\r\n")}\r\n\r\n`;
|
|
246
|
+
const initial = Buffer.concat([Buffer.from(rebuilt, "ascii"), remainder]);
|
|
247
|
+
handleHttp(client, url, initial, cwd, hostId);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
client.write(HTTP_BAD);
|
|
251
|
+
client.destroy();
|
|
252
|
+
};
|
|
253
|
+
client.once("data", onFirstChunk);
|
|
254
|
+
client.on("error", () => {
|
|
255
|
+
try {
|
|
256
|
+
client.destroy();
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// ignore
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
export function startEgressProxy(options) {
|
|
264
|
+
const port = options.port ?? 18888;
|
|
265
|
+
const hostId = options.hostId ?? hostname();
|
|
266
|
+
const cwd = options.cwd;
|
|
267
|
+
return new Promise((resolve, reject) => {
|
|
268
|
+
const server = createServer((client) => handleConnection(client, cwd, hostId));
|
|
269
|
+
server.once("error", (err) => {
|
|
270
|
+
reject(err);
|
|
271
|
+
});
|
|
272
|
+
server.listen(port, "127.0.0.1", () => {
|
|
273
|
+
const addr = server.address();
|
|
274
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
275
|
+
let running = true;
|
|
276
|
+
resolve({
|
|
277
|
+
port: actualPort,
|
|
278
|
+
async stop() {
|
|
279
|
+
if (!running)
|
|
280
|
+
return;
|
|
281
|
+
running = false;
|
|
282
|
+
await new Promise((res) => server.close(() => res()));
|
|
283
|
+
},
|
|
284
|
+
isRunning: () => running,
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { chmodSync, closeSync, constants, existsSync, fchmodSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, unlinkSync, writeFileSync, writeSync, } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { loadEnterpriseConfig } from "../core/config.js";
|
|
6
|
+
import { enterpriseCredentialId, isAllowedLicenseEndpoint, } from "../core/license.js";
|
|
7
|
+
import { matchesEnterpriseHostIdentity } from "../core/enterprise-host-identity.js";
|
|
8
|
+
const EVENT_FILE_RE = /^ungoverned-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
9
|
+
const CURSOR_FILENAME = "global-host-events-cursor.json";
|
|
10
|
+
function configuredHome() {
|
|
11
|
+
return process.env.HOME?.trim() || homedir();
|
|
12
|
+
}
|
|
13
|
+
function globalEventDir(create) {
|
|
14
|
+
try {
|
|
15
|
+
const home = realpathSync(configuredHome());
|
|
16
|
+
const cortexDir = join(home, ".cortex");
|
|
17
|
+
if (!existsSync(cortexDir))
|
|
18
|
+
return null;
|
|
19
|
+
const cortexStat = lstatSync(cortexDir);
|
|
20
|
+
if (!cortexStat.isDirectory() || cortexStat.isSymbolicLink())
|
|
21
|
+
return null;
|
|
22
|
+
if (realpathSync(cortexDir) !== cortexDir)
|
|
23
|
+
return null;
|
|
24
|
+
const dir = join(cortexDir, "host-events");
|
|
25
|
+
if (existsSync(dir)) {
|
|
26
|
+
const stat = lstatSync(dir);
|
|
27
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
28
|
+
return null;
|
|
29
|
+
if (realpathSync(dir) !== dir)
|
|
30
|
+
return null;
|
|
31
|
+
chmodSync(dir, 0o700);
|
|
32
|
+
return dir;
|
|
33
|
+
}
|
|
34
|
+
if (!create)
|
|
35
|
+
return null;
|
|
36
|
+
mkdirSync(dir, { mode: 0o700 });
|
|
37
|
+
chmodSync(dir, 0o700);
|
|
38
|
+
return dir;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function eventKey(event) {
|
|
45
|
+
const pid = typeof event.pid === "number" || typeof event.pid === "string"
|
|
46
|
+
? String(event.pid)
|
|
47
|
+
: "0";
|
|
48
|
+
return `${event.timestamp}#${pid}`;
|
|
49
|
+
}
|
|
50
|
+
export function writeGlobalUngovernedEvent(credentialId, event) {
|
|
51
|
+
if (!matchesEnterpriseHostIdentity(credentialId))
|
|
52
|
+
return false;
|
|
53
|
+
const dir = globalEventDir(true);
|
|
54
|
+
if (!dir)
|
|
55
|
+
return false;
|
|
56
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
57
|
+
const path = join(dir, `ungoverned-${date}.jsonl`);
|
|
58
|
+
let fd = null;
|
|
59
|
+
try {
|
|
60
|
+
fd = openSync(path, constants.O_WRONLY |
|
|
61
|
+
constants.O_APPEND |
|
|
62
|
+
constants.O_CREAT |
|
|
63
|
+
constants.O_NOFOLLOW, 0o600);
|
|
64
|
+
fchmodSync(fd, 0o600);
|
|
65
|
+
writeSync(fd, JSON.stringify({ ...event, credential_id: credentialId }) + "\n", undefined, "utf8");
|
|
66
|
+
closeSync(fd);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
if (fd !== null) {
|
|
71
|
+
try {
|
|
72
|
+
closeSync(fd);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Best effort.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function readCursor(dir, credentialId) {
|
|
82
|
+
const path = join(dir, CURSOR_FILENAME);
|
|
83
|
+
try {
|
|
84
|
+
const stat = lstatSync(path);
|
|
85
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
86
|
+
return { version: 1, credential_id: credentialId };
|
|
87
|
+
}
|
|
88
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
89
|
+
if (parsed.version !== 1 ||
|
|
90
|
+
parsed.credential_id !== credentialId ||
|
|
91
|
+
(parsed.ungoverned_last_key !== undefined &&
|
|
92
|
+
typeof parsed.ungoverned_last_key !== "string")) {
|
|
93
|
+
return { version: 1, credential_id: credentialId };
|
|
94
|
+
}
|
|
95
|
+
return parsed;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return { version: 1, credential_id: credentialId };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function writeCursor(dir, cursor) {
|
|
102
|
+
const path = join(dir, CURSOR_FILENAME);
|
|
103
|
+
const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
104
|
+
try {
|
|
105
|
+
writeFileSync(temporaryPath, JSON.stringify(cursor, null, 2) + "\n", { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
106
|
+
chmodSync(temporaryPath, 0o600);
|
|
107
|
+
renameSync(temporaryPath, path);
|
|
108
|
+
chmodSync(path, 0o600);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
try {
|
|
112
|
+
unlinkSync(temporaryPath);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Best effort.
|
|
116
|
+
}
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function readEvents(dir, credentialId) {
|
|
121
|
+
const events = [];
|
|
122
|
+
for (const name of readdirSync(dir).filter((entry) => EVENT_FILE_RE.test(entry)).sort()) {
|
|
123
|
+
const path = join(dir, name);
|
|
124
|
+
try {
|
|
125
|
+
const stat = lstatSync(path);
|
|
126
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
127
|
+
continue;
|
|
128
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
129
|
+
if (!line.trim())
|
|
130
|
+
continue;
|
|
131
|
+
const parsed = JSON.parse(line);
|
|
132
|
+
if (parsed.event_type !== "ungoverned_ai_session_detected" ||
|
|
133
|
+
parsed.credential_id !== credentialId ||
|
|
134
|
+
typeof parsed.timestamp !== "string") {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
events.push(parsed);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Skip malformed or concurrently changing files.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return events.sort((a, b) => eventKey(a).localeCompare(eventKey(b)));
|
|
145
|
+
}
|
|
146
|
+
export async function pushGlobalUngovernedEvents(cwd, credentialId) {
|
|
147
|
+
if (!matchesEnterpriseHostIdentity(credentialId)) {
|
|
148
|
+
return { pushed: 0, error: "enterprise identity conflict" };
|
|
149
|
+
}
|
|
150
|
+
const config = loadEnterpriseConfig(join(cwd, ".context"));
|
|
151
|
+
const apiKey = config.enterprise.api_key.trim();
|
|
152
|
+
const baseUrl = (config.enterprise.base_url || config.enterprise.endpoint).trim();
|
|
153
|
+
if (!apiKey ||
|
|
154
|
+
!baseUrl ||
|
|
155
|
+
!isAllowedLicenseEndpoint(baseUrl) ||
|
|
156
|
+
enterpriseCredentialId(baseUrl, apiKey) !== credentialId) {
|
|
157
|
+
return { pushed: 0, error: "enterprise identity conflict" };
|
|
158
|
+
}
|
|
159
|
+
const dir = globalEventDir(false);
|
|
160
|
+
if (!dir)
|
|
161
|
+
return { pushed: 0 };
|
|
162
|
+
const cursor = readCursor(dir, credentialId);
|
|
163
|
+
const events = readEvents(dir, credentialId).filter((event) => !cursor.ungoverned_last_key ||
|
|
164
|
+
eventKey(event) > cursor.ungoverned_last_key);
|
|
165
|
+
if (events.length === 0)
|
|
166
|
+
return { pushed: 0 };
|
|
167
|
+
try {
|
|
168
|
+
const response = await fetch(`${baseUrl.replace(/\/$/, "")}/api/v1/govern/ungoverned`, {
|
|
169
|
+
method: "POST",
|
|
170
|
+
headers: {
|
|
171
|
+
"Content-Type": "application/json",
|
|
172
|
+
Authorization: `Bearer ${apiKey}`,
|
|
173
|
+
},
|
|
174
|
+
body: JSON.stringify({
|
|
175
|
+
events: events.map((event) => ({
|
|
176
|
+
detected_at: event.timestamp,
|
|
177
|
+
host_id: event.host_id,
|
|
178
|
+
cli: event.cli,
|
|
179
|
+
binary_path: event.binary,
|
|
180
|
+
args: event.args,
|
|
181
|
+
sys_user: event.user,
|
|
182
|
+
parent_pid: event.ppid,
|
|
183
|
+
pid: event.pid,
|
|
184
|
+
action_taken: event.action ?? "logged",
|
|
185
|
+
})),
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
if (!response.ok) {
|
|
189
|
+
return {
|
|
190
|
+
pushed: 0,
|
|
191
|
+
error: `HTTP ${response.status} ${response.statusText}`,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
cursor.ungoverned_last_key = eventKey(events[events.length - 1]);
|
|
195
|
+
writeCursor(dir, cursor);
|
|
196
|
+
return { pushed: events.length };
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
return {
|
|
200
|
+
pushed: 0,
|
|
201
|
+
error: err instanceof Error ? err.message : String(err),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
export function startGlobalUngovernedEventsPusher(cwd, credentialId, intervalMs) {
|
|
206
|
+
const tick = () => {
|
|
207
|
+
void pushGlobalUngovernedEvents(cwd, credentialId).then((outcome) => {
|
|
208
|
+
if (outcome.error) {
|
|
209
|
+
process.stderr.write(`[cortex-daemon] global host-events push failed: ${outcome.error}\n`);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
void Promise.resolve().then(tick);
|
|
214
|
+
const handle = setInterval(tick, intervalMs);
|
|
215
|
+
if (typeof handle.unref === "function")
|
|
216
|
+
handle.unref();
|
|
217
|
+
return {
|
|
218
|
+
stop() {
|
|
219
|
+
clearInterval(handle);
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|