@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
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
2
|
existsSync,
|
|
3
|
+
lstatSync,
|
|
3
4
|
mkdirSync,
|
|
4
5
|
readFileSync,
|
|
5
|
-
|
|
6
|
+
readdirSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
rmdirSync,
|
|
10
|
+
unlinkSync,
|
|
6
11
|
writeFileSync,
|
|
7
12
|
} from "node:fs";
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
8
14
|
import { homedir, hostname } from "node:os";
|
|
9
|
-
import { join } from "node:path";
|
|
15
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
10
16
|
import { loadEnterpriseConfig } from "../core/config.js";
|
|
17
|
+
import { matchesEnterpriseHostIdentity } from "../core/enterprise-host-identity.js";
|
|
18
|
+
import {
|
|
19
|
+
enterpriseCredentialId,
|
|
20
|
+
isAllowedLicenseEndpoint,
|
|
21
|
+
} from "../core/license.js";
|
|
11
22
|
import { writeHostAuditEvent } from "./ungoverned-scanner.js";
|
|
12
23
|
import { daemonDir } from "./paths.js";
|
|
13
24
|
|
|
@@ -35,10 +46,14 @@ import { daemonDir } from "./paths.js";
|
|
|
35
46
|
|
|
36
47
|
const STATE_FILENAME = "skills.local.json";
|
|
37
48
|
const NOTIFICATION_FILENAME = ".skills-update-applied.json";
|
|
49
|
+
const OWNERSHIP_FILENAME = ".cortex-managed.json";
|
|
50
|
+
const OWNERSHIP_VERSION = 1;
|
|
38
51
|
|
|
39
52
|
const SUPPORTED_CLIS = ["claude", "codex"] as const;
|
|
40
53
|
type SkillCli = (typeof SUPPORTED_CLIS)[number];
|
|
41
54
|
|
|
55
|
+
const SKILL_NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,78}[a-z0-9])?$/;
|
|
56
|
+
|
|
42
57
|
type ManifestEntry = {
|
|
43
58
|
name: string;
|
|
44
59
|
scope: string;
|
|
@@ -46,6 +61,7 @@ type ManifestEntry = {
|
|
|
46
61
|
};
|
|
47
62
|
|
|
48
63
|
type LocalSkillRecord = {
|
|
64
|
+
credential_id?: string;
|
|
49
65
|
cli: SkillCli;
|
|
50
66
|
scope: string;
|
|
51
67
|
updated_at: string;
|
|
@@ -53,6 +69,7 @@ type LocalSkillRecord = {
|
|
|
53
69
|
};
|
|
54
70
|
|
|
55
71
|
type LocalSkillsState = {
|
|
72
|
+
credential_id?: string;
|
|
56
73
|
skills: Record<string, LocalSkillRecord>;
|
|
57
74
|
last_synced_at?: string;
|
|
58
75
|
};
|
|
@@ -102,6 +119,10 @@ function readState(): LocalSkillsState {
|
|
|
102
119
|
: inferredCli;
|
|
103
120
|
const normalizedKey = key.includes(":") ? key : `${cli}:${key}`;
|
|
104
121
|
normalizedSkills[normalizedKey] = {
|
|
122
|
+
credential_id:
|
|
123
|
+
typeof record.credential_id === "string"
|
|
124
|
+
? record.credential_id
|
|
125
|
+
: undefined,
|
|
105
126
|
cli,
|
|
106
127
|
scope: String(record.scope ?? "global"),
|
|
107
128
|
updated_at: String(record.updated_at ?? ""),
|
|
@@ -109,6 +130,10 @@ function readState(): LocalSkillsState {
|
|
|
109
130
|
};
|
|
110
131
|
}
|
|
111
132
|
return {
|
|
133
|
+
credential_id:
|
|
134
|
+
typeof parsed.credential_id === "string"
|
|
135
|
+
? parsed.credential_id
|
|
136
|
+
: undefined,
|
|
112
137
|
skills: normalizedSkills,
|
|
113
138
|
last_synced_at: parsed.last_synced_at,
|
|
114
139
|
};
|
|
@@ -130,12 +155,65 @@ function writeState(state: LocalSkillsState): void {
|
|
|
130
155
|
* skills are installed once per CLI, so the destination root depends on the
|
|
131
156
|
* active sync target rather than just the stored scope.
|
|
132
157
|
*/
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
158
|
+
function isSafeSkillName(name: unknown): name is string {
|
|
159
|
+
return typeof name === "string" && SKILL_NAME_RE.test(name);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function assertPathContained(root: string, target: string): void {
|
|
163
|
+
const rel = relative(root, target);
|
|
164
|
+
if (
|
|
165
|
+
rel === ".." ||
|
|
166
|
+
rel.startsWith(`..${sep}`) ||
|
|
167
|
+
isAbsolute(rel)
|
|
168
|
+
) {
|
|
169
|
+
throw new Error("skill target escapes managed root");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function configuredSkillRoot(cli: SkillCli): string {
|
|
174
|
+
return join(
|
|
175
|
+
realpathSync(homedir()),
|
|
176
|
+
cli === "codex" ? ".codex" : ".claude",
|
|
177
|
+
"skills",
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function assertNotSymlink(path: string, description: string): void {
|
|
182
|
+
if (existsSync(path) && lstatSync(path).isSymbolicLink()) {
|
|
183
|
+
throw new Error(`${description} must not be a symbolic link`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function resolveSkillRoot(cli: SkillCli, create: boolean): string | null {
|
|
188
|
+
const root = configuredSkillRoot(cli);
|
|
189
|
+
const cliRoot = resolve(root, "..");
|
|
190
|
+
assertNotSymlink(cliRoot, `${cli} configuration directory`);
|
|
191
|
+
if (!existsSync(cliRoot)) {
|
|
192
|
+
if (!create) return null;
|
|
193
|
+
mkdirSync(cliRoot, { mode: 0o700 });
|
|
194
|
+
}
|
|
195
|
+
if (!lstatSync(cliRoot).isDirectory()) {
|
|
196
|
+
throw new Error(`${cli} configuration path is not a directory`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
assertNotSymlink(root, `${cli} skills root`);
|
|
200
|
+
if (!existsSync(root)) {
|
|
201
|
+
if (!create) return null;
|
|
202
|
+
mkdirSync(root, { mode: 0o700 });
|
|
203
|
+
}
|
|
204
|
+
if (!lstatSync(root).isDirectory()) {
|
|
205
|
+
throw new Error(`${cli} skills root is not a directory`);
|
|
206
|
+
}
|
|
207
|
+
return realpathSync(root);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function skillFilePath(root: string, name: string): string {
|
|
211
|
+
if (!isSafeSkillName(name)) {
|
|
212
|
+
throw new Error("invalid skill name");
|
|
213
|
+
}
|
|
214
|
+
const target = resolve(root, name, "SKILL.md");
|
|
215
|
+
assertPathContained(resolve(root), target);
|
|
216
|
+
return target;
|
|
139
217
|
}
|
|
140
218
|
|
|
141
219
|
function stateSkillKey(cli: SkillCli, name: string): string {
|
|
@@ -151,7 +229,7 @@ async function fetchManifest(
|
|
|
151
229
|
baseUrl: string,
|
|
152
230
|
apiKey: string,
|
|
153
231
|
cli: SkillCli,
|
|
154
|
-
): Promise<
|
|
232
|
+
): Promise<unknown[]> {
|
|
155
233
|
const url = new URL(
|
|
156
234
|
baseUrl.replace(/\/$/, "") + "/api/v1/govern/skills/manifest",
|
|
157
235
|
);
|
|
@@ -162,8 +240,12 @@ async function fetchManifest(
|
|
|
162
240
|
if (!res.ok) {
|
|
163
241
|
throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
164
242
|
}
|
|
165
|
-
const body = (await res.json()) as { skills?:
|
|
166
|
-
|
|
243
|
+
const body = (await res.json()) as { skills?: unknown };
|
|
244
|
+
if (body.skills === undefined) return [];
|
|
245
|
+
if (!Array.isArray(body.skills)) {
|
|
246
|
+
throw new Error("invalid skill manifest: skills must be an array");
|
|
247
|
+
}
|
|
248
|
+
return body.skills;
|
|
167
249
|
}
|
|
168
250
|
|
|
169
251
|
async function fetchSkillBody(
|
|
@@ -185,19 +267,219 @@ async function fetchSkillBody(
|
|
|
185
267
|
return res.text();
|
|
186
268
|
}
|
|
187
269
|
|
|
188
|
-
function
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
270
|
+
function validateManifest(
|
|
271
|
+
manifest: unknown[],
|
|
272
|
+
cli: SkillCli,
|
|
273
|
+
): ManifestEntry[] {
|
|
274
|
+
const validated: ManifestEntry[] = [];
|
|
275
|
+
const names = new Set<string>();
|
|
276
|
+
for (let index = 0; index < manifest.length; index += 1) {
|
|
277
|
+
const raw = manifest[index];
|
|
278
|
+
if (!raw || typeof raw !== "object") {
|
|
279
|
+
throw new Error(`invalid skill manifest entry at index ${index}`);
|
|
280
|
+
}
|
|
281
|
+
const entry = raw as Record<string, unknown>;
|
|
282
|
+
if (!isSafeSkillName(entry.name)) {
|
|
283
|
+
throw new Error(`invalid skill name at manifest index ${index}`);
|
|
284
|
+
}
|
|
285
|
+
if (
|
|
286
|
+
entry.scope !== "global" &&
|
|
287
|
+
entry.scope !== "cli:claude" &&
|
|
288
|
+
entry.scope !== "cli:codex"
|
|
289
|
+
) {
|
|
290
|
+
throw new Error(`invalid skill scope at manifest index ${index}`);
|
|
291
|
+
}
|
|
292
|
+
if (typeof entry.updated_at !== "string" || !entry.updated_at.trim()) {
|
|
293
|
+
throw new Error(`invalid skill timestamp at manifest index ${index}`);
|
|
294
|
+
}
|
|
295
|
+
if (shouldSyncForCli(entry.scope, cli)) {
|
|
296
|
+
if (names.has(entry.name)) {
|
|
297
|
+
throw new Error(`duplicate skill name at manifest index ${index}`);
|
|
298
|
+
}
|
|
299
|
+
names.add(entry.name);
|
|
300
|
+
}
|
|
301
|
+
validated.push({
|
|
302
|
+
name: entry.name,
|
|
303
|
+
scope: entry.scope,
|
|
304
|
+
updated_at: entry.updated_at,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return validated;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
type OwnershipMarker = {
|
|
311
|
+
version: number;
|
|
312
|
+
manager: string;
|
|
313
|
+
cli: SkillCli;
|
|
314
|
+
name: string;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
function ownershipMarker(cli: SkillCli, name: string): OwnershipMarker {
|
|
318
|
+
return {
|
|
319
|
+
version: OWNERSHIP_VERSION,
|
|
320
|
+
manager: "cortex",
|
|
321
|
+
cli,
|
|
322
|
+
name,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function assertCortexOwnedSkillDir(
|
|
327
|
+
skillDir: string,
|
|
328
|
+
cli: SkillCli,
|
|
329
|
+
name: string,
|
|
330
|
+
): void {
|
|
331
|
+
const markerPath = join(skillDir, OWNERSHIP_FILENAME);
|
|
332
|
+
if (!existsSync(markerPath) || lstatSync(markerPath).isSymbolicLink()) {
|
|
333
|
+
throw new Error("skill directory is not owned by Cortex");
|
|
334
|
+
}
|
|
335
|
+
let parsed: Partial<OwnershipMarker>;
|
|
336
|
+
try {
|
|
337
|
+
parsed = JSON.parse(readFileSync(markerPath, "utf8")) as Partial<OwnershipMarker>;
|
|
338
|
+
} catch {
|
|
339
|
+
throw new Error("skill directory has an invalid Cortex ownership marker");
|
|
340
|
+
}
|
|
341
|
+
if (
|
|
342
|
+
parsed.version !== OWNERSHIP_VERSION ||
|
|
343
|
+
parsed.manager !== "cortex" ||
|
|
344
|
+
parsed.cli !== cli ||
|
|
345
|
+
parsed.name !== name
|
|
346
|
+
) {
|
|
347
|
+
throw new Error("skill directory has an invalid Cortex ownership marker");
|
|
348
|
+
}
|
|
192
349
|
}
|
|
193
350
|
|
|
194
|
-
function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
351
|
+
function preflightSkillWrite(
|
|
352
|
+
cli: SkillCli,
|
|
353
|
+
name: string,
|
|
354
|
+
): { root: string; skillDir: string; target: string } {
|
|
355
|
+
const root = resolveSkillRoot(cli, true);
|
|
356
|
+
if (!root) throw new Error("skill root could not be created");
|
|
357
|
+
const target = skillFilePath(root, name);
|
|
358
|
+
const skillDir = resolve(root, name);
|
|
359
|
+
assertPathContained(root, skillDir);
|
|
360
|
+
if (!existsSync(skillDir)) return { root, skillDir, target };
|
|
361
|
+
|
|
362
|
+
const stat = lstatSync(skillDir);
|
|
363
|
+
if (stat.isSymbolicLink()) {
|
|
364
|
+
throw new Error("skill directory must not be a symbolic link");
|
|
365
|
+
}
|
|
366
|
+
if (!stat.isDirectory()) {
|
|
367
|
+
throw new Error("managed skill target is not a directory");
|
|
368
|
+
}
|
|
369
|
+
assertCortexOwnedSkillDir(skillDir, cli, name);
|
|
370
|
+
const existingSkillFile = join(skillDir, "SKILL.md");
|
|
371
|
+
if (
|
|
372
|
+
existsSync(existingSkillFile) &&
|
|
373
|
+
lstatSync(existingSkillFile).isSymbolicLink()
|
|
374
|
+
) {
|
|
375
|
+
throw new Error("skill file must not be a symbolic link");
|
|
376
|
+
}
|
|
377
|
+
return { root, skillDir, target };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function writeSkillFile(
|
|
381
|
+
cli: SkillCli,
|
|
382
|
+
name: string,
|
|
383
|
+
content: string,
|
|
384
|
+
): string {
|
|
385
|
+
const {
|
|
386
|
+
root,
|
|
387
|
+
skillDir,
|
|
388
|
+
target: lexicalTarget,
|
|
389
|
+
} = preflightSkillWrite(cli, name);
|
|
390
|
+
|
|
391
|
+
let created = false;
|
|
392
|
+
if (existsSync(skillDir)) {
|
|
393
|
+
const stat = lstatSync(skillDir);
|
|
394
|
+
if (stat.isSymbolicLink()) {
|
|
395
|
+
throw new Error("skill directory must not be a symbolic link");
|
|
396
|
+
}
|
|
397
|
+
if (!stat.isDirectory()) {
|
|
398
|
+
throw new Error("managed skill target is not a directory");
|
|
399
|
+
}
|
|
400
|
+
assertCortexOwnedSkillDir(skillDir, cli, name);
|
|
401
|
+
} else {
|
|
402
|
+
mkdirSync(skillDir, { mode: 0o700 });
|
|
403
|
+
created = true;
|
|
404
|
+
writeFileSync(
|
|
405
|
+
join(skillDir, OWNERSHIP_FILENAME),
|
|
406
|
+
JSON.stringify(ownershipMarker(cli, name), null, 2) + "\n",
|
|
407
|
+
{ encoding: "utf8", mode: 0o600, flag: "wx" },
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const realSkillDir = realpathSync(skillDir);
|
|
412
|
+
assertPathContained(root, realSkillDir);
|
|
413
|
+
const target = join(realSkillDir, "SKILL.md");
|
|
414
|
+
assertPathContained(root, target);
|
|
415
|
+
if (existsSync(target) && lstatSync(target).isSymbolicLink()) {
|
|
416
|
+
throw new Error("skill file must not be a symbolic link");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const temporaryTarget = join(
|
|
420
|
+
realSkillDir,
|
|
421
|
+
`.SKILL.md.${process.pid}.${randomUUID()}.tmp`,
|
|
422
|
+
);
|
|
423
|
+
try {
|
|
424
|
+
writeFileSync(temporaryTarget, content, {
|
|
425
|
+
encoding: "utf8",
|
|
426
|
+
mode: 0o600,
|
|
427
|
+
flag: "wx",
|
|
428
|
+
});
|
|
429
|
+
renameSync(temporaryTarget, target);
|
|
430
|
+
} catch (err) {
|
|
431
|
+
if (existsSync(temporaryTarget)) unlinkSync(temporaryTarget);
|
|
432
|
+
if (created) {
|
|
433
|
+
const entries = readdirSync(skillDir);
|
|
434
|
+
if (
|
|
435
|
+
entries.length === 1 &&
|
|
436
|
+
entries[0] === OWNERSHIP_FILENAME
|
|
437
|
+
) {
|
|
438
|
+
unlinkSync(join(skillDir, OWNERSHIP_FILENAME));
|
|
439
|
+
rmdirSync(skillDir);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
throw err;
|
|
443
|
+
}
|
|
444
|
+
return lexicalTarget;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function removeSkillFile(cli: SkillCli, name: string): void {
|
|
448
|
+
if (!isSafeSkillName(name)) {
|
|
449
|
+
throw new Error("invalid skill name");
|
|
450
|
+
}
|
|
451
|
+
const root = resolveSkillRoot(cli, false);
|
|
452
|
+
if (!root) return;
|
|
453
|
+
const skillDir = resolve(root, name);
|
|
454
|
+
assertPathContained(root, skillDir);
|
|
455
|
+
if (!existsSync(skillDir)) return;
|
|
456
|
+
|
|
457
|
+
const stat = lstatSync(skillDir);
|
|
458
|
+
if (stat.isSymbolicLink()) {
|
|
459
|
+
throw new Error("skill directory must not be a symbolic link");
|
|
460
|
+
}
|
|
461
|
+
if (!stat.isDirectory()) {
|
|
462
|
+
throw new Error("managed skill target is not a directory");
|
|
463
|
+
}
|
|
464
|
+
assertPathContained(root, realpathSync(skillDir));
|
|
465
|
+
assertCortexOwnedSkillDir(skillDir, cli, name);
|
|
466
|
+
|
|
467
|
+
const entries = readdirSync(skillDir);
|
|
468
|
+
const unexpected = entries.filter(
|
|
469
|
+
(entry) => entry !== OWNERSHIP_FILENAME && entry !== "SKILL.md",
|
|
470
|
+
);
|
|
471
|
+
if (unexpected.length > 0) {
|
|
472
|
+
throw new Error("managed skill directory contains unowned files");
|
|
473
|
+
}
|
|
474
|
+
const skillPath = join(skillDir, "SKILL.md");
|
|
475
|
+
if (existsSync(skillPath)) {
|
|
476
|
+
if (lstatSync(skillPath).isSymbolicLink()) {
|
|
477
|
+
throw new Error("skill file must not be a symbolic link");
|
|
478
|
+
}
|
|
479
|
+
unlinkSync(skillPath);
|
|
480
|
+
}
|
|
481
|
+
unlinkSync(join(skillDir, OWNERSHIP_FILENAME));
|
|
482
|
+
rmdirSync(skillDir);
|
|
201
483
|
}
|
|
202
484
|
|
|
203
485
|
function writeNotification(data: {
|
|
@@ -224,10 +506,30 @@ export async function runSkillSyncForCli(
|
|
|
224
506
|
if (!apiKey || !baseUrl) {
|
|
225
507
|
return { kind: "failed", cli, error: "enterprise not configured" };
|
|
226
508
|
}
|
|
509
|
+
if (!isAllowedLicenseEndpoint(baseUrl)) {
|
|
510
|
+
return { kind: "failed", cli, error: "insecure or invalid enterprise endpoint" };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const credentialId = enterpriseCredentialId(baseUrl, apiKey);
|
|
514
|
+
if (!matchesEnterpriseHostIdentity(credentialId)) {
|
|
515
|
+
return {
|
|
516
|
+
kind: "failed",
|
|
517
|
+
cli,
|
|
518
|
+
error:
|
|
519
|
+
"enterprise identity conflict: this user profile is already enrolled to another endpoint or API key",
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
const state = readState();
|
|
523
|
+
const identityChanged = state.credential_id !== credentialId;
|
|
524
|
+
let stateSanitized = identityChanged;
|
|
525
|
+
state.credential_id = credentialId;
|
|
227
526
|
|
|
228
527
|
let manifest: ManifestEntry[];
|
|
229
528
|
try {
|
|
230
|
-
manifest =
|
|
529
|
+
manifest = validateManifest(
|
|
530
|
+
await fetchManifest(baseUrl, apiKey, cli),
|
|
531
|
+
cli,
|
|
532
|
+
);
|
|
231
533
|
} catch (err) {
|
|
232
534
|
return {
|
|
233
535
|
kind: "failed",
|
|
@@ -236,7 +538,6 @@ export async function runSkillSyncForCli(
|
|
|
236
538
|
};
|
|
237
539
|
}
|
|
238
540
|
|
|
239
|
-
const state = readState();
|
|
240
541
|
const relevantManifest = manifest.filter((entry) =>
|
|
241
542
|
shouldSyncForCli(entry.scope, cli),
|
|
242
543
|
);
|
|
@@ -245,7 +546,6 @@ export async function runSkillSyncForCli(
|
|
|
245
546
|
const added: string[] = [];
|
|
246
547
|
const changed: string[] = [];
|
|
247
548
|
const removed: string[] = [];
|
|
248
|
-
|
|
249
549
|
// Detect adds + changes
|
|
250
550
|
for (const entry of relevantManifest) {
|
|
251
551
|
const skillKey = stateSkillKey(cli, entry.name);
|
|
@@ -253,9 +553,27 @@ export async function runSkillSyncForCli(
|
|
|
253
553
|
const isNew = !local;
|
|
254
554
|
const isChanged =
|
|
255
555
|
Boolean(local) &&
|
|
256
|
-
(
|
|
556
|
+
(
|
|
557
|
+
identityChanged ||
|
|
558
|
+
local.credential_id !== credentialId ||
|
|
559
|
+
local.updated_at !== entry.updated_at ||
|
|
560
|
+
local.scope !== entry.scope
|
|
561
|
+
);
|
|
257
562
|
if (!isNew && !isChanged) continue;
|
|
258
563
|
|
|
564
|
+
try {
|
|
565
|
+
preflightSkillWrite(cli, entry.name);
|
|
566
|
+
} catch (err) {
|
|
567
|
+
return {
|
|
568
|
+
kind: "failed",
|
|
569
|
+
cli,
|
|
570
|
+
error:
|
|
571
|
+
err instanceof Error
|
|
572
|
+
? `preflight ${entry.name}: ${err.message}`
|
|
573
|
+
: `preflight ${entry.name}: ${String(err)}`,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
259
577
|
let body: string;
|
|
260
578
|
try {
|
|
261
579
|
body = await fetchSkillBody(baseUrl, apiKey, entry.name);
|
|
@@ -270,9 +588,9 @@ export async function runSkillSyncForCli(
|
|
|
270
588
|
};
|
|
271
589
|
}
|
|
272
590
|
|
|
273
|
-
|
|
591
|
+
let path: string;
|
|
274
592
|
try {
|
|
275
|
-
writeSkillFile(
|
|
593
|
+
path = writeSkillFile(cli, entry.name, body);
|
|
276
594
|
} catch (err) {
|
|
277
595
|
return {
|
|
278
596
|
kind: "failed",
|
|
@@ -285,6 +603,7 @@ export async function runSkillSyncForCli(
|
|
|
285
603
|
}
|
|
286
604
|
|
|
287
605
|
state.skills[skillKey] = {
|
|
606
|
+
credential_id: credentialId,
|
|
288
607
|
cli,
|
|
289
608
|
scope: entry.scope,
|
|
290
609
|
updated_at: entry.updated_at,
|
|
@@ -301,12 +620,26 @@ export async function runSkillSyncForCli(
|
|
|
301
620
|
if (record.cli !== cli) continue;
|
|
302
621
|
const [, name] = skillKey.split(":", 2);
|
|
303
622
|
if (!name) continue;
|
|
623
|
+
if (!isSafeSkillName(name)) {
|
|
624
|
+
// State from an older vulnerable version is not deletion authority.
|
|
625
|
+
// Drop the record without touching its persisted absolute path.
|
|
626
|
+
delete state.skills[skillKey];
|
|
627
|
+
stateSanitized = true;
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
304
630
|
if (!shouldSyncForCli(record.scope, cli)) continue;
|
|
305
631
|
if (remoteByName.has(name)) continue;
|
|
306
632
|
try {
|
|
307
|
-
removeSkillFile(
|
|
308
|
-
} catch {
|
|
309
|
-
|
|
633
|
+
removeSkillFile(cli, name);
|
|
634
|
+
} catch (err) {
|
|
635
|
+
return {
|
|
636
|
+
kind: "failed",
|
|
637
|
+
cli,
|
|
638
|
+
error:
|
|
639
|
+
err instanceof Error
|
|
640
|
+
? `remove ${name}: ${err.message}`
|
|
641
|
+
: `remove ${name}: ${String(err)}`,
|
|
642
|
+
};
|
|
310
643
|
}
|
|
311
644
|
delete state.skills[skillKey];
|
|
312
645
|
removed.push(name);
|
|
@@ -314,6 +647,10 @@ export async function runSkillSyncForCli(
|
|
|
314
647
|
|
|
315
648
|
const totalChanged = added.length + changed.length + removed.length;
|
|
316
649
|
if (totalChanged === 0) {
|
|
650
|
+
if (stateSanitized) {
|
|
651
|
+
state.last_synced_at = new Date().toISOString();
|
|
652
|
+
writeState(state);
|
|
653
|
+
}
|
|
317
654
|
return { kind: "unchanged", cli, count: relevantManifest.length };
|
|
318
655
|
}
|
|
319
656
|
|
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
|
2
2
|
import { hostname } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { loadEnterpriseConfig } from "../core/config.js";
|
|
5
|
+
import { isAllowedEnterpriseEndpoint } from "../core/secure-endpoint.js";
|
|
5
6
|
import { writeHostAuditEvent } from "./ungoverned-scanner.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -84,6 +85,9 @@ export async function checkSyncForCli(
|
|
|
84
85
|
if (!apiKey || !baseUrl) {
|
|
85
86
|
return { kind: "failed", error: "enterprise not configured" };
|
|
86
87
|
}
|
|
88
|
+
if (!isAllowedEnterpriseEndpoint(baseUrl)) {
|
|
89
|
+
return { kind: "failed", error: "insecure or invalid enterprise endpoint" };
|
|
90
|
+
}
|
|
87
91
|
const frameworks = activeFrameworks(cwd);
|
|
88
92
|
if (frameworks.length === 0) {
|
|
89
93
|
return { kind: "failed", error: "no active frameworks" };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { appendFile, mkdir } from "node:fs/promises";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { userInfo } from "node:os";
|
|
4
|
+
import { platform, userInfo } from "node:os";
|
|
5
5
|
import {
|
|
6
6
|
detectUngoverned,
|
|
7
7
|
enforceFinding,
|
|
@@ -9,12 +9,16 @@ import {
|
|
|
9
9
|
type EnforcementMode,
|
|
10
10
|
type UngovernedFinding,
|
|
11
11
|
} from "../cli/ungoverned-detector.js";
|
|
12
|
+
import { writeGlobalUngovernedEvent } from "./global-host-events.js";
|
|
13
|
+
import { getGovernManagedPath } from "../core/govern-paths.js";
|
|
12
14
|
|
|
13
15
|
export type ScannerOptions = {
|
|
14
16
|
cwd: string;
|
|
15
17
|
intervalMs?: number;
|
|
16
18
|
mode?: EnforcementMode;
|
|
17
19
|
detectorOptions?: DetectorOptions;
|
|
20
|
+
credentialId?: string;
|
|
21
|
+
managedPathOverrides?: Partial<Record<"claude" | "codex", string>>;
|
|
18
22
|
onFinding?: (finding: UngovernedFinding & { action: string }) => void;
|
|
19
23
|
};
|
|
20
24
|
|
|
@@ -38,7 +42,28 @@ function readMode(cwd: string): EnforcementMode {
|
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
function
|
|
45
|
+
function isExpectedManagedArtifact(
|
|
46
|
+
cli: "claude" | "codex",
|
|
47
|
+
path: string,
|
|
48
|
+
): boolean {
|
|
49
|
+
try {
|
|
50
|
+
const stat = lstatSync(path);
|
|
51
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return false;
|
|
52
|
+
const raw = readFileSync(path, "utf8");
|
|
53
|
+
if (cli === "codex") {
|
|
54
|
+
return raw.startsWith("# Cortex govern — codex requirements");
|
|
55
|
+
}
|
|
56
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
57
|
+
return parsed.allowManagedHooksOnly === true;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readManagedTier1Clis(
|
|
64
|
+
cwd: string,
|
|
65
|
+
overrides?: Partial<Record<"claude" | "codex", string>>,
|
|
66
|
+
): Set<string> {
|
|
42
67
|
const stateFile = join(cwd, ".context", "govern.local.json");
|
|
43
68
|
const managed = new Set<string>();
|
|
44
69
|
if (!existsSync(stateFile)) return managed;
|
|
@@ -49,7 +74,13 @@ function readManagedTier1Clis(cwd: string): Set<string> {
|
|
|
49
74
|
};
|
|
50
75
|
for (const [cli, inst] of Object.entries(parsed.installs ?? {})) {
|
|
51
76
|
if (!TIER1_CLIS.has(cli)) continue;
|
|
52
|
-
|
|
77
|
+
const tier1Cli = cli as "claude" | "codex";
|
|
78
|
+
const expectedPath =
|
|
79
|
+
overrides?.[tier1Cli] ??
|
|
80
|
+
getGovernManagedPath(tier1Cli, platform());
|
|
81
|
+
if (!isExpectedManagedArtifact(tier1Cli, expectedPath)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
53
84
|
managed.add(cli);
|
|
54
85
|
}
|
|
55
86
|
} catch {
|
|
@@ -79,7 +110,10 @@ export async function writeHostAuditEvent(
|
|
|
79
110
|
|
|
80
111
|
export async function runScanOnce(options: ScannerOptions): Promise<UngovernedFinding[]> {
|
|
81
112
|
const mode = options.mode ?? readMode(options.cwd);
|
|
82
|
-
const managedTier1Clis = readManagedTier1Clis(
|
|
113
|
+
const managedTier1Clis = readManagedTier1Clis(
|
|
114
|
+
options.cwd,
|
|
115
|
+
options.managedPathOverrides,
|
|
116
|
+
);
|
|
83
117
|
const findings = filterManagedTier1Findings(
|
|
84
118
|
detectUngoverned(options.detectorOptions),
|
|
85
119
|
managedTier1Clis,
|
|
@@ -87,26 +121,24 @@ export async function runScanOnce(options: ScannerOptions): Promise<UngovernedFi
|
|
|
87
121
|
const me = userInfo().username;
|
|
88
122
|
for (const finding of findings) {
|
|
89
123
|
const action = enforceFinding(finding, { mode, currentUser: me });
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
`[cortex-daemon] failed to write ungoverned audit: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
109
|
-
);
|
|
124
|
+
// A host process snapshot has no trustworthy project attribution. Store
|
|
125
|
+
// it only in the credential-bound user-global queue; never in whichever
|
|
126
|
+
// project happened to register first.
|
|
127
|
+
if (options.credentialId) {
|
|
128
|
+
writeGlobalUngovernedEvent(options.credentialId, {
|
|
129
|
+
event_type: "ungoverned_ai_session_detected",
|
|
130
|
+
timestamp: finding.detected_at,
|
|
131
|
+
host_id: finding.host_id,
|
|
132
|
+
cli: finding.cli,
|
|
133
|
+
binary: finding.binary,
|
|
134
|
+
pid: finding.pid,
|
|
135
|
+
ppid: finding.ppid,
|
|
136
|
+
user: finding.user,
|
|
137
|
+
args: finding.args,
|
|
138
|
+
parent_chain: finding.parent_chain,
|
|
139
|
+
mode,
|
|
140
|
+
action,
|
|
141
|
+
});
|
|
110
142
|
}
|
|
111
143
|
options.onFinding?.({ ...finding, action });
|
|
112
144
|
}
|