@danielblomma/cortex-mcp 2.2.5 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +201 -0
- package/README.md +73 -3
- package/bin/cortex.mjs +339 -242
- package/bin/daemon-control.mjs +162 -0
- package/package.json +7 -3
- package/scaffold/mcp/dist/.cortex-build-hash +1 -0
- package/scaffold/mcp/dist/cli/enterprise-setup.js +134 -0
- package/scaffold/mcp/dist/cli/govern.js +937 -0
- package/scaffold/mcp/dist/cli/query.js +409 -0
- package/scaffold/mcp/dist/cli/run.js +295 -0
- package/scaffold/mcp/dist/cli/stage.js +308 -0
- package/scaffold/mcp/dist/cli/telemetry-test.js +141 -0
- package/scaffold/mcp/dist/cli/ungoverned-detector.js +133 -0
- package/scaffold/mcp/dist/contextEntities.js +282 -0
- package/scaffold/mcp/dist/core/audit/query.js +72 -0
- package/scaffold/mcp/dist/core/audit/writer.js +28 -0
- package/scaffold/mcp/dist/core/config.js +235 -0
- package/scaffold/mcp/dist/core/enterprise-host-identity.js +193 -0
- package/scaffold/mcp/dist/core/enterprise-identity.js +18 -0
- package/scaffold/mcp/dist/core/enterprise-rotation.js +158 -0
- package/scaffold/mcp/dist/core/govern-paths.js +21 -0
- package/scaffold/mcp/dist/core/index.js +14 -0
- package/scaffold/mcp/dist/core/license.js +278 -0
- package/scaffold/mcp/dist/core/policy/enforce.js +66 -0
- package/scaffold/mcp/dist/core/policy/injection.js +172 -0
- package/scaffold/mcp/dist/core/policy/store.js +179 -0
- package/scaffold/mcp/dist/core/rbac/check.js +30 -0
- package/scaffold/mcp/dist/core/secure-endpoint.js +25 -0
- package/scaffold/mcp/dist/core/telemetry/collector.js +285 -0
- package/scaffold/mcp/dist/core/telemetry/state-dir.js +31 -0
- package/scaffold/mcp/dist/core/validators/builtins.js +632 -0
- package/scaffold/mcp/dist/core/validators/config.js +44 -0
- package/scaffold/mcp/dist/core/validators/engine.js +120 -0
- package/scaffold/mcp/dist/core/validators/evaluators/code_comments.js +236 -0
- package/scaffold/mcp/dist/core/validators/evaluators/regex.js +116 -0
- package/scaffold/mcp/dist/core/workflow/artifact-io.js +103 -0
- package/scaffold/mcp/dist/core/workflow/capabilities.js +88 -0
- package/scaffold/mcp/dist/core/workflow/default-workflows.js +102 -0
- package/scaffold/mcp/dist/core/workflow/enforcement.js +164 -0
- package/scaffold/mcp/dist/core/workflow/envelope.js +132 -0
- package/scaffold/mcp/dist/core/workflow/index.js +11 -0
- package/scaffold/mcp/dist/core/workflow/mcp-tools.js +175 -0
- package/scaffold/mcp/dist/core/workflow/resolution.js +69 -0
- package/scaffold/mcp/dist/core/workflow/run-lifecycle.js +123 -0
- package/scaffold/mcp/dist/core/workflow/schemas.js +141 -0
- package/scaffold/mcp/dist/core/workflow/synced-capability-registry.js +60 -0
- package/scaffold/mcp/dist/core/workflow/synced-registry.js +60 -0
- package/scaffold/mcp/dist/daemon/capability-sync-checker.js +242 -0
- package/scaffold/mcp/dist/daemon/client.js +130 -0
- package/scaffold/mcp/dist/daemon/egress-proxy.js +288 -0
- package/scaffold/mcp/dist/daemon/global-host-events.js +222 -0
- package/scaffold/mcp/dist/daemon/heartbeat-pusher.js +116 -0
- package/scaffold/mcp/dist/daemon/heartbeat-tracker.js +165 -0
- package/scaffold/mcp/dist/daemon/host-events-pusher.js +249 -0
- package/scaffold/mcp/dist/daemon/main.js +449 -0
- package/scaffold/mcp/dist/daemon/paths.js +36 -0
- package/scaffold/mcp/dist/daemon/project-service-registry.js +78 -0
- package/scaffold/mcp/dist/daemon/protocol.js +8 -0
- package/scaffold/mcp/dist/daemon/server.js +180 -0
- package/scaffold/mcp/dist/daemon/skill-sync-checker.js +557 -0
- package/scaffold/mcp/dist/daemon/sync-checker.js +165 -0
- package/scaffold/mcp/dist/daemon/ungoverned-scanner.js +136 -0
- package/scaffold/mcp/dist/daemon/workflow-sync-checker.js +249 -0
- package/scaffold/mcp/dist/defaults.js +6 -0
- package/scaffold/mcp/dist/embed.js +627 -0
- package/scaffold/mcp/dist/embedScheduler.js +479 -0
- package/scaffold/mcp/dist/embeddings.js +167 -0
- package/scaffold/mcp/dist/enterprise/audit/push.js +66 -0
- package/scaffold/mcp/dist/enterprise/index.js +327 -0
- package/scaffold/mcp/dist/enterprise/model/deploy.js +27 -0
- package/scaffold/mcp/dist/enterprise/policy/sync.js +129 -0
- package/scaffold/mcp/dist/enterprise/privacy/boundary.js +184 -0
- package/scaffold/mcp/dist/enterprise/reviews/changed-files.js +33 -0
- package/scaffold/mcp/dist/enterprise/reviews/pattern-context.js +202 -0
- package/scaffold/mcp/dist/enterprise/reviews/policy-selection.js +46 -0
- package/scaffold/mcp/dist/enterprise/reviews/push.js +102 -0
- package/scaffold/mcp/dist/enterprise/reviews/trust-state.js +186 -0
- package/scaffold/mcp/dist/enterprise/telemetry/sync.js +57 -0
- package/scaffold/mcp/dist/enterprise/tools/enterprise.js +826 -0
- package/scaffold/mcp/dist/enterprise/tools/harness.js +40 -0
- package/scaffold/mcp/dist/enterprise/tools/walk.js +73 -0
- package/scaffold/mcp/dist/enterprise/violations/push.js +77 -0
- package/scaffold/mcp/dist/enterprise/workflow/push.js +44 -0
- package/scaffold/mcp/dist/enterprise/workflow/state.js +329 -0
- package/scaffold/mcp/dist/frontmatter.js +33 -0
- package/scaffold/mcp/dist/graph.js +769 -0
- package/scaffold/mcp/dist/graphCsv.js +55 -0
- package/scaffold/mcp/dist/graphMetrics.js +8 -0
- package/scaffold/mcp/dist/hooks/permission-request.js +89 -0
- package/scaffold/mcp/dist/hooks/post-tool-use.js +105 -0
- package/scaffold/mcp/dist/hooks/pre-compact.js +29 -0
- package/scaffold/mcp/dist/hooks/pre-tool-use.js +78 -0
- package/scaffold/mcp/dist/hooks/session-end.js +43 -0
- package/scaffold/mcp/dist/hooks/session-start.js +41 -0
- package/scaffold/mcp/dist/hooks/shared.js +194 -0
- package/scaffold/mcp/dist/hooks/stop.js +28 -0
- package/scaffold/mcp/dist/hooks/user-prompt-submit.js +33 -0
- package/scaffold/mcp/dist/impactPresentation.js +137 -0
- package/scaffold/mcp/dist/impactRanking.js +191 -0
- package/scaffold/mcp/dist/impactResponse.js +30 -0
- package/scaffold/mcp/dist/impactResults.js +105 -0
- package/scaffold/mcp/dist/impactSeed.js +20 -0
- package/scaffold/mcp/dist/impactTraversal.js +64 -0
- package/scaffold/mcp/dist/jsonl.js +77 -0
- package/scaffold/mcp/dist/loadGraph.js +759 -0
- package/scaffold/mcp/dist/lruCache.js +38 -0
- package/scaffold/mcp/dist/paths.js +97 -0
- package/scaffold/mcp/dist/patternEvidence.js +272 -0
- package/scaffold/mcp/dist/plugin.js +81 -0
- package/scaffold/mcp/dist/presets.js +78 -0
- package/scaffold/mcp/dist/relatedResponse.js +18 -0
- package/scaffold/mcp/dist/relatedTraversal.js +78 -0
- package/scaffold/mcp/dist/rules.js +23 -0
- package/scaffold/mcp/dist/search.js +212 -0
- package/scaffold/mcp/dist/searchCore.js +457 -0
- package/scaffold/mcp/dist/searchResults.js +230 -0
- package/scaffold/mcp/dist/server.js +317 -0
- package/scaffold/mcp/dist/types.js +1 -0
- package/scaffold/mcp/package-lock.json +336 -212
- 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/query.ts +36 -2
- 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/embed.ts +211 -10
- package/scaffold/mcp/src/enterprise/audit/push.ts +8 -0
- package/scaffold/mcp/src/enterprise/index.ts +1 -1
- package/scaffold/mcp/src/enterprise/policy/sync.ts +12 -0
- package/scaffold/mcp/src/enterprise/reviews/changed-files.ts +34 -0
- package/scaffold/mcp/src/enterprise/reviews/pattern-context.ts +231 -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/tools/enterprise.ts +49 -36
- package/scaffold/mcp/src/enterprise/violations/push.ts +9 -0
- package/scaffold/mcp/src/enterprise/workflow/push.ts +7 -0
- package/scaffold/mcp/src/paths.ts +3 -5
- package/scaffold/mcp/src/patternEvidence.ts +347 -0
- package/scaffold/mcp/src/plugin.ts +20 -0
- package/scaffold/mcp/src/search.ts +35 -9
- package/scaffold/mcp/src/searchCore.ts +254 -12
- package/scaffold/mcp/src/searchResults.ts +96 -9
- package/scaffold/mcp/src/types.ts +7 -0
- package/scaffold/mcp/tests/changed-files.test.mjs +41 -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/embed-entities.test.mjs +109 -1
- package/scaffold/mcp/tests/enterprise-identity-sync.test.mjs +401 -0
- package/scaffold/mcp/tests/enterprise-pattern-context.test.mjs +322 -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/paths.test.mjs +11 -3
- package/scaffold/mcp/tests/pattern-evidence.test.mjs +321 -0
- package/scaffold/mcp/tests/project-service-registry.test.mjs +172 -0
- package/scaffold/mcp/tests/query-cli.test.mjs +73 -1
- package/scaffold/mcp/tests/review-trust-contract.test.mjs +18 -0
- package/scaffold/mcp/tests/search-graph-score.test.mjs +167 -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/scripts/dashboard.mjs +29 -18
- package/scaffold/scripts/doctor.sh +26 -8
- package/scaffold/scripts/ingest.mjs +37 -4
- package/scaffold/scripts/status.sh +13 -6
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import {
|
|
3
3
|
existsSync,
|
|
4
|
+
closeSync,
|
|
5
|
+
fchmodSync,
|
|
6
|
+
lstatSync,
|
|
7
|
+
openSync,
|
|
4
8
|
readFileSync,
|
|
9
|
+
renameSync,
|
|
5
10
|
writeFileSync,
|
|
6
11
|
unlinkSync,
|
|
7
12
|
mkdirSync,
|
|
@@ -10,6 +15,7 @@ import {
|
|
|
10
15
|
import { join, dirname } from "node:path";
|
|
11
16
|
import { platform, tmpdir } from "node:os";
|
|
12
17
|
import { randomUUID } from "node:crypto";
|
|
18
|
+
import { getGovernManagedPath } from "../core/govern-paths.js";
|
|
13
19
|
|
|
14
20
|
export type RunCli = "claude" | "codex" | "copilot";
|
|
15
21
|
export const RUN_CLIS: RunCli[] = ["claude", "codex", "copilot"];
|
|
@@ -186,17 +192,8 @@ export async function runAiCli(options: RunOptions): Promise<number> {
|
|
|
186
192
|
return 1;
|
|
187
193
|
}
|
|
188
194
|
|
|
189
|
-
const DEFAULT_COPILOT_SHIM_PATHS: Partial<Record<NodeJS.Platform, string>> = {
|
|
190
|
-
darwin: "/usr/local/bin/copilot",
|
|
191
|
-
linux: "/usr/local/bin/copilot",
|
|
192
|
-
};
|
|
193
|
-
|
|
194
195
|
export function getDefaultCopilotShimPath(os: NodeJS.Platform): string {
|
|
195
|
-
|
|
196
|
-
if (!path) {
|
|
197
|
-
throw new Error(`copilot shim install not yet supported on ${os}`);
|
|
198
|
-
}
|
|
199
|
-
return path;
|
|
196
|
+
return getGovernManagedPath("copilot", os);
|
|
200
197
|
}
|
|
201
198
|
|
|
202
199
|
export function buildCopilotShim(realBinary: string): string {
|
|
@@ -240,19 +237,56 @@ export function installCopilotShim(options: InstallShimOptions = {}): InstallShi
|
|
|
240
237
|
"Install GitHub Copilot CLI first, then re-run cortex enterprise sync.",
|
|
241
238
|
};
|
|
242
239
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
240
|
+
try {
|
|
241
|
+
const existing = lstatSync(shimPath);
|
|
242
|
+
if (
|
|
243
|
+
existing.isSymbolicLink() ||
|
|
244
|
+
!existing.isFile() ||
|
|
245
|
+
!isCortexShim(shimPath)
|
|
246
|
+
) {
|
|
247
|
+
return {
|
|
248
|
+
ok: false,
|
|
249
|
+
message:
|
|
250
|
+
`${shimPath} exists and is not a regular cortex shim — refusing to overwrite. ` +
|
|
251
|
+
"Move/rename the existing path, then re-run.",
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
} catch (err) {
|
|
255
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
256
|
+
return {
|
|
257
|
+
ok: false,
|
|
258
|
+
message:
|
|
259
|
+
`Failed to inspect shim path ${shimPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
250
262
|
}
|
|
263
|
+
const temporaryPath = `${shimPath}.tmp.${process.pid}.${randomUUID()}`;
|
|
264
|
+
let fd: number | null = null;
|
|
251
265
|
try {
|
|
252
266
|
mkdirSync(dirname(shimPath), { recursive: true });
|
|
253
|
-
|
|
254
|
-
|
|
267
|
+
fd = openSync(temporaryPath, "wx", 0o700);
|
|
268
|
+
writeFileSync(fd, buildCopilotShim(real));
|
|
269
|
+
fchmodSync(fd, 0o755);
|
|
270
|
+
closeSync(fd);
|
|
271
|
+
fd = null;
|
|
272
|
+
renameSync(temporaryPath, shimPath);
|
|
273
|
+
const installed = lstatSync(shimPath);
|
|
274
|
+
if (!installed.isFile() || installed.isSymbolicLink()) {
|
|
275
|
+
throw new Error("installed shim is not a regular file");
|
|
276
|
+
}
|
|
255
277
|
} catch (err) {
|
|
278
|
+
if (fd !== null) {
|
|
279
|
+
try {
|
|
280
|
+
closeSync(fd);
|
|
281
|
+
} catch {
|
|
282
|
+
// Best effort.
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
unlinkSync(temporaryPath);
|
|
287
|
+
} catch {
|
|
288
|
+
// Best effort.
|
|
289
|
+
}
|
|
256
290
|
return {
|
|
257
291
|
ok: false,
|
|
258
292
|
message: `Failed to write shim at ${shimPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -35,7 +35,7 @@ import { isEnforcedMode, isEnterpriseProject } from "../hooks/shared.js";
|
|
|
35
35
|
const ENTERPRISE_REQUIRED_MESSAGE =
|
|
36
36
|
"cortex stage is part of the Cortex Harness — an enterprise-only feature. " +
|
|
37
37
|
"This project does not have an enterprise license configured (no enterprise.api_key " +
|
|
38
|
-
"in .context/enterprise.yml). Run 'cortex enterprise
|
|
38
|
+
"in .context/enterprise.yml). Run 'cortex enterprise install --api-key-stdin' to enable it, or " +
|
|
39
39
|
"contact your org admin.";
|
|
40
40
|
|
|
41
41
|
export async function runStageCommand(args: string[]): Promise<void> {
|
|
@@ -142,6 +142,7 @@ async function runStart(args: string[]): Promise<void> {
|
|
|
142
142
|
typeof flags.workflow === "string" ? flags.workflow : "secure-build";
|
|
143
143
|
|
|
144
144
|
const resolved = resolveWorkflowDefinition(workflowId, {
|
|
145
|
+
cwd: projectRoot(),
|
|
145
146
|
emitBundledFallbackWarning: true,
|
|
146
147
|
bundledFallbackPolicy: isEnforcedMode(projectRoot()) ? "block" : "warn",
|
|
147
148
|
});
|
|
@@ -183,7 +184,9 @@ async function runEnvelope(args: string[]): Promise<void> {
|
|
|
183
184
|
`No run state for task ${taskId}. Start one with 'cortex stage start'.`,
|
|
184
185
|
);
|
|
185
186
|
}
|
|
186
|
-
const workflow = resolveWorkflowDefinition(state.workflow_id
|
|
187
|
+
const workflow = resolveWorkflowDefinition(state.workflow_id, {
|
|
188
|
+
cwd: projectRoot(),
|
|
189
|
+
}).workflow;
|
|
187
190
|
const envelope = composeStageEnvelope({
|
|
188
191
|
cwd: projectRoot(),
|
|
189
192
|
taskId,
|
|
@@ -243,7 +246,9 @@ async function runAdvance(args: string[]): Promise<void> {
|
|
|
243
246
|
`No run state for task ${taskId}. Start one with 'cortex stage start'.`,
|
|
244
247
|
);
|
|
245
248
|
}
|
|
246
|
-
const workflow = resolveWorkflowDefinition(state.workflow_id
|
|
249
|
+
const workflow = resolveWorkflowDefinition(state.workflow_id, {
|
|
250
|
+
cwd: projectRoot(),
|
|
251
|
+
}).workflow;
|
|
247
252
|
const stage = workflow.stages.find((s) => s.name === stageName);
|
|
248
253
|
if (!stage) {
|
|
249
254
|
throw new Error(
|
|
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type { Role, RBACConfig } from "./rbac/check.js";
|
|
4
4
|
import { parseValidatorsConfig, type ValidatorsConfig } from "./validators/config.js";
|
|
5
|
+
import { isAllowedEnterpriseEndpoint } from "./secure-endpoint.js";
|
|
5
6
|
|
|
6
7
|
export type TelemetryConfig = {
|
|
7
8
|
enabled: boolean;
|
|
@@ -201,7 +202,10 @@ function isLikelyHttpUrl(value: string): boolean {
|
|
|
201
202
|
|
|
202
203
|
export function resolveEnterpriseActivation(config: EnterpriseConfig): EnterpriseActivation {
|
|
203
204
|
const apiKey = config.enterprise.api_key.trim();
|
|
204
|
-
const endpoint =
|
|
205
|
+
const endpoint = (
|
|
206
|
+
config.enterprise.base_url ||
|
|
207
|
+
config.enterprise.endpoint
|
|
208
|
+
).trim();
|
|
205
209
|
|
|
206
210
|
if (!apiKey) {
|
|
207
211
|
return { active: false, reason: "missing_api_key", api_key: null, endpoint: endpoint || null };
|
|
@@ -215,7 +219,10 @@ export function resolveEnterpriseActivation(config: EnterpriseConfig): Enterpris
|
|
|
215
219
|
return { active: false, reason: "invalid_api_key_format", api_key: apiKey, endpoint };
|
|
216
220
|
}
|
|
217
221
|
|
|
218
|
-
if (
|
|
222
|
+
if (
|
|
223
|
+
!isLikelyHttpUrl(endpoint) ||
|
|
224
|
+
!isAllowedEnterpriseEndpoint(endpoint)
|
|
225
|
+
) {
|
|
219
226
|
return { active: false, reason: "invalid_endpoint_format", api_key: apiKey, endpoint };
|
|
220
227
|
}
|
|
221
228
|
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chownSync,
|
|
3
|
+
chmodSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
unlinkSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
const HOST_IDENTITY_FILENAME = "enterprise-host-identity.json";
|
|
18
|
+
const HOST_IDENTITY_VERSION = 2;
|
|
19
|
+
|
|
20
|
+
type EnterpriseHostIdentity = {
|
|
21
|
+
version: 2;
|
|
22
|
+
credential_id: string;
|
|
23
|
+
endpoint_sha256: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type HostIdentityOwner = {
|
|
27
|
+
uid: number;
|
|
28
|
+
gid: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type HostIdentityOptions = {
|
|
32
|
+
homeDir?: string;
|
|
33
|
+
owner?: HostIdentityOwner;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type EnterpriseHostIdentityStatus =
|
|
37
|
+
| "unbound"
|
|
38
|
+
| "match"
|
|
39
|
+
| "rotation"
|
|
40
|
+
| "conflict"
|
|
41
|
+
| "invalid";
|
|
42
|
+
|
|
43
|
+
function configuredHomeDir(homeDir?: string): string {
|
|
44
|
+
return homeDir ?? process.env.HOME?.trim() ?? homedir();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hostIdentityDir(
|
|
48
|
+
options: HostIdentityOptions = {},
|
|
49
|
+
create = false,
|
|
50
|
+
): string | null {
|
|
51
|
+
try {
|
|
52
|
+
const home = realpathSync(configuredHomeDir(options.homeDir));
|
|
53
|
+
const dir = join(home, ".cortex");
|
|
54
|
+
if (existsSync(dir)) {
|
|
55
|
+
const stat = lstatSync(dir);
|
|
56
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) return null;
|
|
57
|
+
if (realpathSync(dir) !== dir) return null;
|
|
58
|
+
if (options.owner) {
|
|
59
|
+
if (stat.uid !== options.owner.uid && stat.uid !== 0) return null;
|
|
60
|
+
chownSync(dir, options.owner.uid, options.owner.gid);
|
|
61
|
+
chmodSync(dir, 0o700);
|
|
62
|
+
}
|
|
63
|
+
return dir;
|
|
64
|
+
}
|
|
65
|
+
if (!create) return null;
|
|
66
|
+
mkdirSync(dir, { mode: 0o700 });
|
|
67
|
+
if (options.owner) {
|
|
68
|
+
chownSync(dir, options.owner.uid, options.owner.gid);
|
|
69
|
+
}
|
|
70
|
+
chmodSync(dir, 0o700);
|
|
71
|
+
return dir;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function hostIdentityPath(
|
|
78
|
+
options: HostIdentityOptions = {},
|
|
79
|
+
createDir = false,
|
|
80
|
+
): string | null {
|
|
81
|
+
const dir = hostIdentityDir(options, createDir);
|
|
82
|
+
return dir ? join(dir, HOST_IDENTITY_FILENAME) : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readHostIdentity(
|
|
86
|
+
options: HostIdentityOptions = {},
|
|
87
|
+
): EnterpriseHostIdentity | null {
|
|
88
|
+
const path = hostIdentityPath(options);
|
|
89
|
+
if (!path) return null;
|
|
90
|
+
if (!existsSync(path)) return null;
|
|
91
|
+
try {
|
|
92
|
+
const stat = lstatSync(path);
|
|
93
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return null;
|
|
94
|
+
if (
|
|
95
|
+
options.owner &&
|
|
96
|
+
stat.uid !== options.owner.uid &&
|
|
97
|
+
stat.uid !== 0
|
|
98
|
+
) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const parsed = JSON.parse(
|
|
102
|
+
readFileSync(path, "utf8"),
|
|
103
|
+
) as Partial<EnterpriseHostIdentity>;
|
|
104
|
+
if (
|
|
105
|
+
parsed.version !== HOST_IDENTITY_VERSION ||
|
|
106
|
+
typeof parsed.credential_id !== "string" ||
|
|
107
|
+
!/^[a-f0-9]{64}$/.test(parsed.credential_id) ||
|
|
108
|
+
typeof parsed.endpoint_sha256 !== "string" ||
|
|
109
|
+
!/^[a-f0-9]{64}$/.test(parsed.endpoint_sha256)
|
|
110
|
+
) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return parsed as EnterpriseHostIdentity;
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Atomically bind user-global Enterprise services and artifacts to one
|
|
121
|
+
* endpoint/API-key identity. Malformed existing enrollment fails closed.
|
|
122
|
+
*/
|
|
123
|
+
export function claimEnterpriseHostIdentity(
|
|
124
|
+
credentialId: string,
|
|
125
|
+
endpoint: string,
|
|
126
|
+
options: HostIdentityOptions = {},
|
|
127
|
+
): boolean {
|
|
128
|
+
if (!/^[a-f0-9]{64}$/.test(credentialId)) return false;
|
|
129
|
+
const endpointSha256 = createHash("sha256")
|
|
130
|
+
.update(endpoint.trim().replace(/\/+$/, ""), "utf8")
|
|
131
|
+
.digest("hex");
|
|
132
|
+
const path = hostIdentityPath(options, true);
|
|
133
|
+
if (!path) return false;
|
|
134
|
+
if (existsSync(path)) {
|
|
135
|
+
const existing = readHostIdentity(options);
|
|
136
|
+
if (!existing || existing.endpoint_sha256 !== endpointSha256) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
if (existing.credential_id === credentialId) {
|
|
140
|
+
try {
|
|
141
|
+
if (options.owner) {
|
|
142
|
+
chownSync(path, options.owner.uid, options.owner.gid);
|
|
143
|
+
}
|
|
144
|
+
chmodSync(path, 0o600);
|
|
145
|
+
return true;
|
|
146
|
+
} catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return replaceHostIdentity(path, {
|
|
151
|
+
version: HOST_IDENTITY_VERSION,
|
|
152
|
+
credential_id: credentialId,
|
|
153
|
+
endpoint_sha256: endpointSha256,
|
|
154
|
+
}, options.owner);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
writeFileSync(
|
|
159
|
+
path,
|
|
160
|
+
JSON.stringify(
|
|
161
|
+
{
|
|
162
|
+
version: HOST_IDENTITY_VERSION,
|
|
163
|
+
credential_id: credentialId,
|
|
164
|
+
endpoint_sha256: endpointSha256,
|
|
165
|
+
},
|
|
166
|
+
null,
|
|
167
|
+
2,
|
|
168
|
+
) + "\n",
|
|
169
|
+
{ encoding: "utf8", mode: 0o600, flag: "wx" },
|
|
170
|
+
);
|
|
171
|
+
if (options.owner) {
|
|
172
|
+
chownSync(path, options.owner.uid, options.owner.gid);
|
|
173
|
+
}
|
|
174
|
+
chmodSync(path, 0o600);
|
|
175
|
+
return true;
|
|
176
|
+
} catch {
|
|
177
|
+
// Another process may have won the create race. Only the same identity
|
|
178
|
+
// is accepted after re-reading the completed enrollment record.
|
|
179
|
+
const existing = readHostIdentity(options);
|
|
180
|
+
return (
|
|
181
|
+
existing?.credential_id === credentialId &&
|
|
182
|
+
existing.endpoint_sha256 === endpointSha256
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function replaceHostIdentity(
|
|
188
|
+
path: string,
|
|
189
|
+
identity: EnterpriseHostIdentity,
|
|
190
|
+
owner?: HostIdentityOwner,
|
|
191
|
+
): boolean {
|
|
192
|
+
const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
193
|
+
try {
|
|
194
|
+
writeFileSync(
|
|
195
|
+
temporaryPath,
|
|
196
|
+
JSON.stringify(identity, null, 2) + "\n",
|
|
197
|
+
{ encoding: "utf8", mode: 0o600, flag: "wx" },
|
|
198
|
+
);
|
|
199
|
+
if (owner) {
|
|
200
|
+
chownSync(temporaryPath, owner.uid, owner.gid);
|
|
201
|
+
}
|
|
202
|
+
chmodSync(temporaryPath, 0o600);
|
|
203
|
+
renameSync(temporaryPath, path);
|
|
204
|
+
chmodSync(path, 0o600);
|
|
205
|
+
return true;
|
|
206
|
+
} catch {
|
|
207
|
+
try {
|
|
208
|
+
unlinkSync(temporaryPath);
|
|
209
|
+
} catch {
|
|
210
|
+
// Best effort.
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function matchesEnterpriseHostIdentity(
|
|
217
|
+
credentialId: string,
|
|
218
|
+
options: HostIdentityOptions = {},
|
|
219
|
+
): boolean {
|
|
220
|
+
if (!/^[a-f0-9]{64}$/.test(credentialId)) return false;
|
|
221
|
+
return readHostIdentity(options)?.credential_id === credentialId;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function inspectEnterpriseHostIdentity(
|
|
225
|
+
credentialId: string,
|
|
226
|
+
endpoint: string,
|
|
227
|
+
options: HostIdentityOptions = {},
|
|
228
|
+
): EnterpriseHostIdentityStatus {
|
|
229
|
+
if (!/^[a-f0-9]{64}$/.test(credentialId)) return "invalid";
|
|
230
|
+
let home: string;
|
|
231
|
+
try {
|
|
232
|
+
home = realpathSync(configuredHomeDir(options.homeDir));
|
|
233
|
+
} catch {
|
|
234
|
+
return "invalid";
|
|
235
|
+
}
|
|
236
|
+
const dir = join(home, ".cortex");
|
|
237
|
+
if (!existsSync(dir)) return "unbound";
|
|
238
|
+
try {
|
|
239
|
+
const dirStat = lstatSync(dir);
|
|
240
|
+
if (
|
|
241
|
+
!dirStat.isDirectory() ||
|
|
242
|
+
dirStat.isSymbolicLink() ||
|
|
243
|
+
realpathSync(dir) !== dir
|
|
244
|
+
) {
|
|
245
|
+
return "invalid";
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
return "invalid";
|
|
249
|
+
}
|
|
250
|
+
const path = join(dir, HOST_IDENTITY_FILENAME);
|
|
251
|
+
if (!existsSync(path)) return "unbound";
|
|
252
|
+
const existing = readHostIdentity(options);
|
|
253
|
+
if (!existing) return "invalid";
|
|
254
|
+
const endpointSha256 = createHash("sha256")
|
|
255
|
+
.update(endpoint.trim().replace(/\/+$/, ""), "utf8")
|
|
256
|
+
.digest("hex");
|
|
257
|
+
if (existing.endpoint_sha256 !== endpointSha256) return "conflict";
|
|
258
|
+
return existing.credential_id === credentialId ? "match" : "rotation";
|
|
259
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { loadEnterpriseConfig } from "./config.js";
|
|
3
|
+
import { enterpriseCredentialId } from "./license.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the opaque per-user Enterprise identity for a project.
|
|
7
|
+
*
|
|
8
|
+
* The identifier binds the normalized endpoint and API key without
|
|
9
|
+
* persisting either credential in shared daemon metadata.
|
|
10
|
+
*/
|
|
11
|
+
export function configuredEnterpriseCredentialId(cwd: string): string | null {
|
|
12
|
+
const config = loadEnterpriseConfig(join(cwd, ".context"));
|
|
13
|
+
const apiKey = config.enterprise.api_key.trim();
|
|
14
|
+
const endpoint = (
|
|
15
|
+
config.enterprise.base_url ||
|
|
16
|
+
config.enterprise.endpoint
|
|
17
|
+
).trim();
|
|
18
|
+
if (!apiKey || !endpoint) return null;
|
|
19
|
+
return enterpriseCredentialId(endpoint, apiKey);
|
|
20
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
lstatSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
realpathSync,
|
|
7
|
+
rmdirSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { join, resolve } from "node:path";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
|
|
13
|
+
const SKILL_NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,78}[a-z0-9])?$/;
|
|
14
|
+
const OWNERSHIP_FILENAME = ".cortex-managed.json";
|
|
15
|
+
const STATE_FILES = [
|
|
16
|
+
"skills.local.json",
|
|
17
|
+
"workflows.local.json",
|
|
18
|
+
"capabilities.local.json",
|
|
19
|
+
".skills-update-applied.json",
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
type SkillCli = "claude" | "codex";
|
|
23
|
+
|
|
24
|
+
type PurgeTarget = {
|
|
25
|
+
dir: string;
|
|
26
|
+
marker: string;
|
|
27
|
+
skill: string | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function configuredHome(homeDir?: string): string {
|
|
31
|
+
return homeDir ?? process.env.HOME?.trim() ?? homedir();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function safeDirectory(path: string): string | null {
|
|
35
|
+
if (!existsSync(path)) return null;
|
|
36
|
+
const stat = lstatSync(path);
|
|
37
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
38
|
+
throw new Error(`${path} is not a safe directory`);
|
|
39
|
+
}
|
|
40
|
+
const real = realpathSync(path);
|
|
41
|
+
if (real !== path) throw new Error(`${path} is not a canonical directory`);
|
|
42
|
+
return real;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function inspectManagedSkill(
|
|
46
|
+
root: string,
|
|
47
|
+
cli: SkillCli,
|
|
48
|
+
name: string,
|
|
49
|
+
): PurgeTarget | null {
|
|
50
|
+
if (!SKILL_NAME_RE.test(name)) {
|
|
51
|
+
throw new Error("invalid managed skill name");
|
|
52
|
+
}
|
|
53
|
+
const dir = resolve(root, name);
|
|
54
|
+
if (!dir.startsWith(`${root}/`)) {
|
|
55
|
+
throw new Error("managed skill escapes its root");
|
|
56
|
+
}
|
|
57
|
+
const stat = lstatSync(dir);
|
|
58
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
59
|
+
throw new Error("managed skill target is not a safe directory");
|
|
60
|
+
}
|
|
61
|
+
if (realpathSync(dir) !== dir) {
|
|
62
|
+
throw new Error("managed skill target is not canonical");
|
|
63
|
+
}
|
|
64
|
+
const marker = join(dir, OWNERSHIP_FILENAME);
|
|
65
|
+
if (!existsSync(marker)) return null;
|
|
66
|
+
const markerStat = lstatSync(marker);
|
|
67
|
+
if (!markerStat.isFile() || markerStat.isSymbolicLink()) {
|
|
68
|
+
throw new Error("managed skill ownership marker is unsafe");
|
|
69
|
+
}
|
|
70
|
+
const parsed = JSON.parse(readFileSync(marker, "utf8")) as {
|
|
71
|
+
version?: unknown;
|
|
72
|
+
manager?: unknown;
|
|
73
|
+
cli?: unknown;
|
|
74
|
+
name?: unknown;
|
|
75
|
+
};
|
|
76
|
+
if (
|
|
77
|
+
parsed.version !== 1 ||
|
|
78
|
+
parsed.manager !== "cortex" ||
|
|
79
|
+
parsed.cli !== cli ||
|
|
80
|
+
parsed.name !== name
|
|
81
|
+
) {
|
|
82
|
+
throw new Error("managed skill ownership marker is invalid");
|
|
83
|
+
}
|
|
84
|
+
const entries = readdirSync(dir);
|
|
85
|
+
const unexpected = entries.filter(
|
|
86
|
+
(entry) => entry !== OWNERSHIP_FILENAME && entry !== "SKILL.md",
|
|
87
|
+
);
|
|
88
|
+
if (unexpected.length > 0) {
|
|
89
|
+
throw new Error("managed skill directory contains unowned files");
|
|
90
|
+
}
|
|
91
|
+
const skill = join(dir, "SKILL.md");
|
|
92
|
+
if (existsSync(skill)) {
|
|
93
|
+
const skillStat = lstatSync(skill);
|
|
94
|
+
if (!skillStat.isFile() || skillStat.isSymbolicLink()) {
|
|
95
|
+
throw new Error("managed skill file is unsafe");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { dir, marker, skill: existsSync(skill) ? skill : null };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function recordedSkills(home: string): Array<{ cli: SkillCli; name: string }> {
|
|
102
|
+
const statePath = join(home, ".cortex", "skills.local.json");
|
|
103
|
+
if (!existsSync(statePath)) return [];
|
|
104
|
+
const stat = lstatSync(statePath);
|
|
105
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
106
|
+
throw new Error("skills state is unsafe");
|
|
107
|
+
}
|
|
108
|
+
const parsed = JSON.parse(readFileSync(statePath, "utf8")) as {
|
|
109
|
+
skills?: Record<string, { cli?: unknown }>;
|
|
110
|
+
};
|
|
111
|
+
if (!parsed.skills || typeof parsed.skills !== "object") return [];
|
|
112
|
+
const records: Array<{ cli: SkillCli; name: string }> = [];
|
|
113
|
+
for (const [key, value] of Object.entries(parsed.skills)) {
|
|
114
|
+
const [keyCli, name] = key.includes(":")
|
|
115
|
+
? key.split(":", 2)
|
|
116
|
+
: [String(value?.cli ?? ""), key];
|
|
117
|
+
if (
|
|
118
|
+
(keyCli !== "claude" && keyCli !== "codex") ||
|
|
119
|
+
!SKILL_NAME_RE.test(name)
|
|
120
|
+
) {
|
|
121
|
+
throw new Error("skills state contains an unsafe record");
|
|
122
|
+
}
|
|
123
|
+
records.push({ cli: keyCli, name });
|
|
124
|
+
}
|
|
125
|
+
return records;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Purge credential-specific artifacts before an explicit API-key rotation.
|
|
130
|
+
* Every path is derived from the target user's home and every skill deletion
|
|
131
|
+
* requires the Cortex ownership marker. Any ambiguity aborts rotation.
|
|
132
|
+
*/
|
|
133
|
+
export function prepareEnterpriseCredentialRotation(homeDir?: string): boolean {
|
|
134
|
+
try {
|
|
135
|
+
const home = realpathSync(configuredHome(homeDir));
|
|
136
|
+
const cortexDir = safeDirectory(join(home, ".cortex"));
|
|
137
|
+
if (!cortexDir) return true;
|
|
138
|
+
|
|
139
|
+
const targets = new Map<string, PurgeTarget>();
|
|
140
|
+
const recorded = recordedSkills(home);
|
|
141
|
+
for (const cli of ["claude", "codex"] as const) {
|
|
142
|
+
const cliDir = safeDirectory(join(home, cli === "codex" ? ".codex" : ".claude"));
|
|
143
|
+
if (!cliDir) {
|
|
144
|
+
if (recorded.some((record) => record.cli === cli)) return false;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const root = safeDirectory(join(cliDir, "skills"));
|
|
148
|
+
if (!root) {
|
|
149
|
+
if (recorded.some((record) => record.cli === cli)) return false;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
for (const name of readdirSync(root)) {
|
|
153
|
+
const candidate = join(root, name);
|
|
154
|
+
const stat = lstatSync(candidate);
|
|
155
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
|
|
156
|
+
const target = inspectManagedSkill(root, cli, name);
|
|
157
|
+
if (target) targets.set(target.dir, target);
|
|
158
|
+
}
|
|
159
|
+
for (const record of recorded.filter((entry) => entry.cli === cli)) {
|
|
160
|
+
const candidate = join(root, record.name);
|
|
161
|
+
if (!existsSync(candidate)) continue;
|
|
162
|
+
const target = inspectManagedSkill(root, cli, record.name);
|
|
163
|
+
if (!target) {
|
|
164
|
+
// Legacy state without an ownership marker is not deletion
|
|
165
|
+
// authority. Abort and require the operator to resolve it.
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
targets.set(target.dir, target);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const target of targets.values()) {
|
|
173
|
+
if (target.skill) unlinkSync(target.skill);
|
|
174
|
+
unlinkSync(target.marker);
|
|
175
|
+
rmdirSync(target.dir);
|
|
176
|
+
}
|
|
177
|
+
for (const filename of STATE_FILES) {
|
|
178
|
+
const path = join(cortexDir, filename);
|
|
179
|
+
if (existsSync(path)) unlinkSync(path);
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
} catch {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type GovernManagedCli = "claude" | "codex" | "copilot";
|
|
2
|
+
|
|
3
|
+
const MANAGED_PATHS: Record<
|
|
4
|
+
GovernManagedCli,
|
|
5
|
+
Partial<Record<NodeJS.Platform, string>>
|
|
6
|
+
> = {
|
|
7
|
+
claude: {
|
|
8
|
+
darwin: "/Library/Application Support/ClaudeCode/managed-settings.json",
|
|
9
|
+
linux: "/etc/claude-code/managed-settings.json",
|
|
10
|
+
},
|
|
11
|
+
codex: {
|
|
12
|
+
darwin: "/Library/Application Support/Codex/requirements.toml",
|
|
13
|
+
linux: "/etc/codex/requirements.toml",
|
|
14
|
+
},
|
|
15
|
+
copilot: {
|
|
16
|
+
darwin: "/usr/local/bin/copilot",
|
|
17
|
+
linux: "/usr/local/bin/copilot",
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function getGovernManagedPath(
|
|
22
|
+
cli: GovernManagedCli,
|
|
23
|
+
os: NodeJS.Platform,
|
|
24
|
+
): string {
|
|
25
|
+
const path = MANAGED_PATHS[cli][os];
|
|
26
|
+
if (!path) {
|
|
27
|
+
throw new Error(`govern install for ${cli} not yet supported on ${os}`);
|
|
28
|
+
}
|
|
29
|
+
return path;
|
|
30
|
+
}
|