@danielblomma/cortex-mcp 2.4.0 → 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 +19 -3
- package/bin/cortex.mjs +308 -75
- 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 +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
|
@@ -14,23 +14,32 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@huggingface/transformers": "^4.1.0",
|
|
17
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
18
18
|
"@types/js-yaml": "^4.0.9",
|
|
19
|
-
"js-yaml": "^4.
|
|
20
|
-
"minimatch": "^10.2.
|
|
19
|
+
"js-yaml": "^4.3.0",
|
|
20
|
+
"minimatch": "^10.2.6",
|
|
21
21
|
"ryugraph": "^25.9.1",
|
|
22
22
|
"zod": "^3.24.1"
|
|
23
23
|
},
|
|
24
24
|
"overrides": {
|
|
25
|
+
"@hono/node-server": "^2.0.12",
|
|
26
|
+
"adm-zip": "^0.6.0",
|
|
27
|
+
"body-parser": "^2.3.0",
|
|
28
|
+
"brace-expansion": "^5.0.8",
|
|
25
29
|
"cmake-js": "^8.0.0",
|
|
26
30
|
"express-rate-limit": "^8.5.1",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
31
|
+
"fast-uri": "^3.1.4",
|
|
32
|
+
"hono": "^4.12.32",
|
|
33
|
+
"protobufjs": "^7.6.5",
|
|
34
|
+
"sharp": "^0.35.3",
|
|
35
|
+
"tar": "^7.5.22"
|
|
30
36
|
},
|
|
31
37
|
"devDependencies": {
|
|
32
38
|
"@types/node": "^22.10.1",
|
|
33
39
|
"ts-node": "^10.9.2",
|
|
34
40
|
"typescript": "^5.7.2"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20.9.0"
|
|
35
44
|
}
|
|
36
45
|
}
|
|
@@ -1,11 +1,28 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { join } from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
|
|
3
|
+
import {
|
|
4
|
+
chmodSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import {
|
|
12
|
+
enterpriseCredentialId,
|
|
13
|
+
isAllowedLicenseEndpoint,
|
|
14
|
+
verifyLicense,
|
|
15
|
+
} from "../core/license.js";
|
|
16
|
+
import {
|
|
17
|
+
claimEnterpriseHostIdentity,
|
|
18
|
+
inspectEnterpriseHostIdentity,
|
|
19
|
+
} from "../core/enterprise-host-identity.js";
|
|
20
|
+
import { prepareEnterpriseCredentialRotation } from "../core/enterprise-rotation.js";
|
|
4
21
|
|
|
5
22
|
/**
|
|
6
23
|
* One-liner enterprise onboarding.
|
|
7
24
|
*
|
|
8
|
-
* $ cortex enterprise
|
|
25
|
+
* $ printenv CORTEX_API_KEY | cortex enterprise install --api-key-stdin
|
|
9
26
|
*
|
|
10
27
|
* Replaces the manual `.context/enterprise.yml` editing flow that's been
|
|
11
28
|
* the friction point for new users. Validates the key against the
|
|
@@ -33,7 +50,7 @@ const API_KEY_RE = /^(?:ctx|ent)_[A-Za-z0-9._-]{8,}$/;
|
|
|
33
50
|
|
|
34
51
|
function buildEnterpriseYaml(baseUrl: string, apiKey: string): string {
|
|
35
52
|
const lines = [
|
|
36
|
-
"# Cortex enterprise configuration. Generated by `cortex enterprise
|
|
53
|
+
"# Cortex enterprise configuration. Generated by `cortex enterprise install --api-key-stdin`.",
|
|
37
54
|
"# Single api_key is used for telemetry, policy, audit and govern services.",
|
|
38
55
|
"enterprise:",
|
|
39
56
|
` api_key: ${apiKey}`,
|
|
@@ -75,10 +92,11 @@ export async function runEnterpriseSetup(
|
|
|
75
92
|
};
|
|
76
93
|
}
|
|
77
94
|
|
|
78
|
-
if (
|
|
95
|
+
if (!isAllowedLicenseEndpoint(endpoint)) {
|
|
79
96
|
return {
|
|
80
97
|
ok: false,
|
|
81
|
-
message:
|
|
98
|
+
message:
|
|
99
|
+
"Endpoint must use HTTPS. Plain HTTP is allowed only for localhost loopback development.",
|
|
82
100
|
};
|
|
83
101
|
}
|
|
84
102
|
|
|
@@ -101,13 +119,33 @@ export async function runEnterpriseSetup(
|
|
|
101
119
|
message: `License rejected: ${license.reason} (source=${license.source}). Verify the API key and endpoint are correct.`,
|
|
102
120
|
};
|
|
103
121
|
}
|
|
104
|
-
|
|
105
122
|
// Write enterprise.yml.
|
|
106
123
|
const configPath = join(contextDir, "enterprise.yml");
|
|
124
|
+
const temporaryConfigPath = join(
|
|
125
|
+
contextDir,
|
|
126
|
+
`.enterprise.yml.${process.pid}.${randomUUID()}.tmp`,
|
|
127
|
+
);
|
|
107
128
|
try {
|
|
108
129
|
mkdirSync(contextDir, { recursive: true });
|
|
109
|
-
writeFileSync(
|
|
130
|
+
writeFileSync(
|
|
131
|
+
temporaryConfigPath,
|
|
132
|
+
buildEnterpriseYaml(endpoint, apiKey),
|
|
133
|
+
{
|
|
134
|
+
encoding: "utf8",
|
|
135
|
+
mode: 0o600,
|
|
136
|
+
flag: "wx",
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
chmodSync(temporaryConfigPath, 0o600);
|
|
140
|
+
renameSync(temporaryConfigPath, configPath);
|
|
141
|
+
// Preserve restrictive permissions even when replacing an existing file.
|
|
142
|
+
chmodSync(configPath, 0o600);
|
|
110
143
|
} catch (err) {
|
|
144
|
+
try {
|
|
145
|
+
unlinkSync(temporaryConfigPath);
|
|
146
|
+
} catch {
|
|
147
|
+
// The rename may already have completed, or the temp file was not made.
|
|
148
|
+
}
|
|
111
149
|
return {
|
|
112
150
|
ok: false,
|
|
113
151
|
message: `Failed to write ${configPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -122,3 +160,39 @@ export async function runEnterpriseSetup(
|
|
|
122
160
|
expiresAt: license.expires_at,
|
|
123
161
|
};
|
|
124
162
|
}
|
|
163
|
+
|
|
164
|
+
export function bindEnterpriseIdentity(options: {
|
|
165
|
+
apiKey: string;
|
|
166
|
+
endpoint?: string;
|
|
167
|
+
homeDir?: string;
|
|
168
|
+
owner?: { uid: number; gid: number };
|
|
169
|
+
}): boolean {
|
|
170
|
+
const endpoint = (options.endpoint ?? DEFAULT_ENDPOINT).replace(/\/$/, "");
|
|
171
|
+
const apiKey = options.apiKey.trim();
|
|
172
|
+
if (!API_KEY_RE.test(apiKey) || !isAllowedLicenseEndpoint(endpoint)) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
const credentialId = enterpriseCredentialId(endpoint, apiKey);
|
|
176
|
+
const identityStatus = inspectEnterpriseHostIdentity(
|
|
177
|
+
credentialId,
|
|
178
|
+
endpoint,
|
|
179
|
+
{ homeDir: options.homeDir, owner: options.owner },
|
|
180
|
+
);
|
|
181
|
+
if (identityStatus === "conflict" || identityStatus === "invalid") {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
if (
|
|
185
|
+
identityStatus === "rotation" &&
|
|
186
|
+
!prepareEnterpriseCredentialRotation(options.homeDir)
|
|
187
|
+
) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
return claimEnterpriseHostIdentity(
|
|
191
|
+
credentialId,
|
|
192
|
+
endpoint,
|
|
193
|
+
{
|
|
194
|
+
homeDir: options.homeDir,
|
|
195
|
+
owner: options.owner,
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
}
|
|
@@ -14,7 +14,15 @@ import { join, dirname } from "node:path";
|
|
|
14
14
|
import { platform, hostname } from "node:os";
|
|
15
15
|
import { randomUUID } from "node:crypto";
|
|
16
16
|
import { loadEnterpriseConfig, type ComplianceFramework } from "../core/config.js";
|
|
17
|
-
import {
|
|
17
|
+
import { isAllowedEnterpriseEndpoint } from "../core/secure-endpoint.js";
|
|
18
|
+
import { getGovernManagedPath } from "../core/govern-paths.js";
|
|
19
|
+
import { matchesEnterpriseHostIdentity } from "../core/enterprise-host-identity.js";
|
|
20
|
+
import { enterpriseCredentialId } from "../core/license.js";
|
|
21
|
+
import {
|
|
22
|
+
getDefaultCopilotShimPath,
|
|
23
|
+
installCopilotShim,
|
|
24
|
+
uninstallCopilotShim,
|
|
25
|
+
} from "./run.js";
|
|
18
26
|
import {
|
|
19
27
|
readTamperLock,
|
|
20
28
|
removeTamperLock,
|
|
@@ -28,21 +36,14 @@ const TIER1_CLIS: GovernCli[] = ["claude", "codex"];
|
|
|
28
36
|
|
|
29
37
|
export type ManagedSettingsPaths = Partial<Record<GovernCli, Partial<Record<NodeJS.Platform, string>>>>;
|
|
30
38
|
|
|
31
|
-
const DEFAULT_PATHS: ManagedSettingsPaths = {
|
|
32
|
-
claude: {
|
|
33
|
-
darwin: "/Library/Application Support/ClaudeCode/managed-settings.json",
|
|
34
|
-
linux: "/etc/claude-code/managed-settings.json",
|
|
35
|
-
},
|
|
36
|
-
codex: {
|
|
37
|
-
darwin: "/Library/Application Support/Codex/requirements.toml",
|
|
38
|
-
linux: "/etc/codex/requirements.toml",
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
|
|
42
39
|
export type GovernState = {
|
|
43
40
|
installs: Partial<Record<GovernCli, GovernInstallRecord>>;
|
|
44
41
|
};
|
|
45
42
|
|
|
43
|
+
export type ProjectOperationRunner = <T>(
|
|
44
|
+
operation: () => T | Promise<T>,
|
|
45
|
+
) => T | Promise<T>;
|
|
46
|
+
|
|
46
47
|
export type GovernInstallRecord = {
|
|
47
48
|
path: string;
|
|
48
49
|
version: string;
|
|
@@ -84,11 +85,10 @@ const SUPPORTED_CODEX_HOOK_EVENTS = new Set([
|
|
|
84
85
|
]);
|
|
85
86
|
|
|
86
87
|
export function getManagedSettingsPath(cli: GovernCli, os: NodeJS.Platform): string {
|
|
87
|
-
|
|
88
|
-
if (!path) {
|
|
88
|
+
if (cli === "copilot") {
|
|
89
89
|
throw new Error(`govern install for ${cli} not yet supported on ${os}`);
|
|
90
90
|
}
|
|
91
|
-
return
|
|
91
|
+
return getGovernManagedPath(cli, os);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
export function requireRoot(): void {
|
|
@@ -346,8 +346,33 @@ function loadState(cwd: string): GovernState {
|
|
|
346
346
|
|
|
347
347
|
function saveState(cwd: string, state: GovernState): void {
|
|
348
348
|
const path = getStatePath(cwd);
|
|
349
|
-
|
|
350
|
-
|
|
349
|
+
writeAtomic(path, JSON.stringify(state, null, 2) + "\n", 0o600);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function runProjectOperation<T>(
|
|
353
|
+
runner: ProjectOperationRunner | undefined,
|
|
354
|
+
operation: () => T | Promise<T>,
|
|
355
|
+
): Promise<T> {
|
|
356
|
+
return runner ? await runner(operation) : await operation();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function managedPathForCli(
|
|
360
|
+
cli: GovernCli,
|
|
361
|
+
options: {
|
|
362
|
+
skipRoot?: boolean;
|
|
363
|
+
pathOverride?: Partial<Record<GovernCli, string>>;
|
|
364
|
+
},
|
|
365
|
+
): string {
|
|
366
|
+
const override = options.pathOverride?.[cli];
|
|
367
|
+
if (override) {
|
|
368
|
+
if (!options.skipRoot) {
|
|
369
|
+
throw new Error("Managed path overrides are allowed only in non-root tests.");
|
|
370
|
+
}
|
|
371
|
+
return override;
|
|
372
|
+
}
|
|
373
|
+
return cli === "copilot"
|
|
374
|
+
? getDefaultCopilotShimPath(platform())
|
|
375
|
+
: getManagedSettingsPath(cli, platform());
|
|
351
376
|
}
|
|
352
377
|
|
|
353
378
|
async function fetchGovernConfig(
|
|
@@ -356,6 +381,9 @@ async function fetchGovernConfig(
|
|
|
356
381
|
cli: GovernCli,
|
|
357
382
|
frameworks: string[],
|
|
358
383
|
): Promise<{ config: FetchedConfig; etag: string | null }> {
|
|
384
|
+
if (!isAllowedEnterpriseEndpoint(baseUrl)) {
|
|
385
|
+
throw new Error("insecure or invalid enterprise endpoint");
|
|
386
|
+
}
|
|
359
387
|
const url = new URL(`${baseUrl.replace(/\/$/, "")}/api/v1/govern/config`);
|
|
360
388
|
url.searchParams.set("cli", cli);
|
|
361
389
|
url.searchParams.set("frameworks", frameworks.join(","));
|
|
@@ -383,6 +411,9 @@ async function postApplied(
|
|
|
383
411
|
instance_id?: string;
|
|
384
412
|
},
|
|
385
413
|
): Promise<void> {
|
|
414
|
+
if (!isAllowedEnterpriseEndpoint(baseUrl)) {
|
|
415
|
+
throw new Error("insecure or invalid enterprise endpoint");
|
|
416
|
+
}
|
|
386
417
|
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/api/v1/govern/applied`, {
|
|
387
418
|
method: "POST",
|
|
388
419
|
headers: {
|
|
@@ -406,6 +437,7 @@ export type GovernInstallOptions = {
|
|
|
406
437
|
skipRoot?: boolean;
|
|
407
438
|
apiKey?: string;
|
|
408
439
|
baseUrl?: string;
|
|
440
|
+
projectOperation?: ProjectOperationRunner;
|
|
409
441
|
};
|
|
410
442
|
|
|
411
443
|
export type GovernInstallResult = {
|
|
@@ -432,7 +464,10 @@ export async function runGovernInstall(
|
|
|
432
464
|
let frameworks = options.frameworks ?? [];
|
|
433
465
|
|
|
434
466
|
if (!apiKey || !baseUrl || frameworks.length === 0) {
|
|
435
|
-
const config =
|
|
467
|
+
const config = await runProjectOperation(
|
|
468
|
+
options.projectOperation,
|
|
469
|
+
() => loadEnterpriseConfig(contextDir),
|
|
470
|
+
);
|
|
436
471
|
if (!apiKey) apiKey = config.enterprise.api_key.trim();
|
|
437
472
|
if (!baseUrl) baseUrl = (config.enterprise.base_url || config.enterprise.endpoint).trim();
|
|
438
473
|
if (frameworks.length === 0) {
|
|
@@ -468,16 +503,17 @@ export async function runGovernInstall(
|
|
|
468
503
|
}
|
|
469
504
|
|
|
470
505
|
const mode = options.mode ?? "advisory";
|
|
471
|
-
const state =
|
|
506
|
+
const state = await runProjectOperation(
|
|
507
|
+
options.projectOperation,
|
|
508
|
+
() => loadState(cwd),
|
|
509
|
+
);
|
|
472
510
|
const installed: GovernCli[] = [];
|
|
473
511
|
|
|
474
512
|
for (const cli of targets) {
|
|
475
513
|
if (cli === "copilot") {
|
|
476
514
|
if (!options.skipRoot) requireRoot();
|
|
477
|
-
const shimPath = options
|
|
478
|
-
const shimResult = installCopilotShim(
|
|
479
|
-
shimPath ? { shimPath } : {},
|
|
480
|
-
);
|
|
515
|
+
const shimPath = managedPathForCli(cli, options);
|
|
516
|
+
const shimResult = installCopilotShim({ shimPath });
|
|
481
517
|
if (!shimResult.ok) {
|
|
482
518
|
console.log(`! ${cli}: ${shimResult.message}`);
|
|
483
519
|
continue;
|
|
@@ -495,7 +531,7 @@ export async function runGovernInstall(
|
|
|
495
531
|
}
|
|
496
532
|
if (!options.skipRoot) requireRoot();
|
|
497
533
|
|
|
498
|
-
const path =
|
|
534
|
+
const path = managedPathForCli(cli, options);
|
|
499
535
|
|
|
500
536
|
let merged: FetchedConfig;
|
|
501
537
|
let version: string;
|
|
@@ -564,7 +600,10 @@ export async function runGovernInstall(
|
|
|
564
600
|
console.log(`✓ ${cli}: managed-settings written to ${path} (version ${shortVersion}, mode=${mode})`);
|
|
565
601
|
}
|
|
566
602
|
|
|
567
|
-
|
|
603
|
+
await runProjectOperation(
|
|
604
|
+
options.projectOperation,
|
|
605
|
+
() => saveState(cwd, state),
|
|
606
|
+
);
|
|
568
607
|
return {
|
|
569
608
|
ok: true,
|
|
570
609
|
message: `Installed govern for ${installed.join(", ") || "(none)"}.`,
|
|
@@ -578,13 +617,18 @@ export type GovernUninstallOptions = {
|
|
|
578
617
|
reason?: string;
|
|
579
618
|
cwd?: string;
|
|
580
619
|
skipRoot?: boolean;
|
|
620
|
+
pathOverride?: Partial<Record<GovernCli, string>>;
|
|
621
|
+
projectOperation?: ProjectOperationRunner;
|
|
581
622
|
};
|
|
582
623
|
|
|
583
624
|
export async function runGovernUninstall(
|
|
584
625
|
options: GovernUninstallOptions,
|
|
585
626
|
): Promise<{ ok: boolean; message: string; uninstalled: GovernCli[] }> {
|
|
586
627
|
const cwd = options.cwd ?? process.cwd();
|
|
587
|
-
const state =
|
|
628
|
+
const state = await runProjectOperation(
|
|
629
|
+
options.projectOperation,
|
|
630
|
+
() => loadState(cwd),
|
|
631
|
+
);
|
|
588
632
|
|
|
589
633
|
// Filter out unknown CLI keys defensively — govern.local.json is a
|
|
590
634
|
// user-writable file and a corrupted/forward-compatible entry must not
|
|
@@ -620,8 +664,9 @@ export async function runGovernUninstall(
|
|
|
620
664
|
const inst = state.installs[cli];
|
|
621
665
|
if (!inst) continue;
|
|
622
666
|
if (!options.skipRoot) requireRoot();
|
|
667
|
+
const managedPath = managedPathForCli(cli, options);
|
|
623
668
|
if (cli === "copilot") {
|
|
624
|
-
const shimResult = uninstallCopilotShim(
|
|
669
|
+
const shimResult = uninstallCopilotShim(managedPath);
|
|
625
670
|
if (!shimResult.ok) {
|
|
626
671
|
console.log(`! ${cli}: ${shimResult.message}`);
|
|
627
672
|
continue;
|
|
@@ -635,19 +680,22 @@ export async function runGovernUninstall(
|
|
|
635
680
|
continue;
|
|
636
681
|
}
|
|
637
682
|
try {
|
|
638
|
-
unlinkSync(
|
|
683
|
+
unlinkSync(managedPath);
|
|
639
684
|
} catch {
|
|
640
685
|
// file already gone — proceed
|
|
641
686
|
}
|
|
642
687
|
delete state.installs[cli];
|
|
643
688
|
uninstalled.push(cli);
|
|
644
689
|
console.log(
|
|
645
|
-
`✓ ${cli}: managed-settings removed from ${
|
|
690
|
+
`✓ ${cli}: managed-settings removed from ${managedPath}` +
|
|
646
691
|
(options.breakGlass ? ` (break-glass: ${options.reason})` : ""),
|
|
647
692
|
);
|
|
648
693
|
}
|
|
649
694
|
|
|
650
|
-
|
|
695
|
+
await runProjectOperation(
|
|
696
|
+
options.projectOperation,
|
|
697
|
+
() => saveState(cwd, state),
|
|
698
|
+
);
|
|
651
699
|
return {
|
|
652
700
|
ok: true,
|
|
653
701
|
message: `Uninstalled govern for ${uninstalled.join(", ") || "(none)"}.`,
|
|
@@ -685,6 +733,7 @@ export type GovernStatusReport = {
|
|
|
685
733
|
generated_at: string;
|
|
686
734
|
enterprise: {
|
|
687
735
|
api_key_set: boolean;
|
|
736
|
+
host_identity_bound: boolean;
|
|
688
737
|
base_url: string;
|
|
689
738
|
frameworks_configured: ComplianceFramework[];
|
|
690
739
|
govern_mode_config: GovernConfigModeFromConfig;
|
|
@@ -933,14 +982,23 @@ export function buildGovernStatus(options: { cwd?: string; now?: Date } = {}): G
|
|
|
933
982
|
}
|
|
934
983
|
|
|
935
984
|
const { counts, sample } = readRecentEvents(cwd, 24 * 60 * 60 * 1000, now);
|
|
985
|
+
const enterpriseBaseUrl =
|
|
986
|
+
config.enterprise.base_url || config.enterprise.endpoint;
|
|
987
|
+
const enterpriseApiKey = config.enterprise.api_key.trim();
|
|
936
988
|
|
|
937
989
|
return {
|
|
938
990
|
cwd,
|
|
939
991
|
host_id: hostname(),
|
|
940
992
|
generated_at: now.toISOString(),
|
|
941
993
|
enterprise: {
|
|
942
|
-
api_key_set:
|
|
943
|
-
|
|
994
|
+
api_key_set: enterpriseApiKey !== "",
|
|
995
|
+
host_identity_bound:
|
|
996
|
+
enterpriseApiKey !== "" &&
|
|
997
|
+
enterpriseBaseUrl !== "" &&
|
|
998
|
+
matchesEnterpriseHostIdentity(
|
|
999
|
+
enterpriseCredentialId(enterpriseBaseUrl, enterpriseApiKey),
|
|
1000
|
+
),
|
|
1001
|
+
base_url: enterpriseBaseUrl,
|
|
944
1002
|
frameworks_configured: config.compliance.frameworks,
|
|
945
1003
|
govern_mode_config: config.govern.mode,
|
|
946
1004
|
},
|
|
@@ -964,7 +1022,10 @@ function formatCompact(report: GovernStatusReport): string {
|
|
|
964
1022
|
);
|
|
965
1023
|
lines.push(`Endpoint: ${report.enterprise.base_url || "(not set)"}`);
|
|
966
1024
|
lines.push(
|
|
967
|
-
`API key: ${report.enterprise.api_key_set ? "configured" : "NOT SET (run 'sudo cortex enterprise
|
|
1025
|
+
`API key: ${report.enterprise.api_key_set ? "configured" : "NOT SET (run 'sudo cortex enterprise install --api-key-stdin')"}`,
|
|
1026
|
+
);
|
|
1027
|
+
lines.push(
|
|
1028
|
+
`Host identity: ${report.enterprise.host_identity_bound ? "verified" : "NOT ENROLLED (re-run stdin install)"}`,
|
|
968
1029
|
);
|
|
969
1030
|
lines.push("");
|
|
970
1031
|
if (report.tamper_lock) {
|
|
@@ -986,7 +1047,7 @@ function formatCompact(report: GovernStatusReport): string {
|
|
|
986
1047
|
}
|
|
987
1048
|
if (report.installs.length === 0) {
|
|
988
1049
|
lines.push("No CLIs governed on this host.");
|
|
989
|
-
lines.push("Run: sudo cortex enterprise
|
|
1050
|
+
lines.push("Run: sudo cortex enterprise install --api-key-stdin");
|
|
990
1051
|
return lines.join("\n");
|
|
991
1052
|
}
|
|
992
1053
|
lines.push("AI CLIs on this host:");
|
|
@@ -1065,6 +1126,8 @@ export type GovernRepairOptions = {
|
|
|
1065
1126
|
cwd?: string;
|
|
1066
1127
|
skipRoot?: boolean;
|
|
1067
1128
|
reason?: string;
|
|
1129
|
+
pathOverride?: Partial<Record<GovernCli, string>>;
|
|
1130
|
+
projectOperation?: ProjectOperationRunner;
|
|
1068
1131
|
};
|
|
1069
1132
|
|
|
1070
1133
|
export type GovernRepairResult = {
|
|
@@ -1087,7 +1150,10 @@ export async function runGovernRepair(
|
|
|
1087
1150
|
options: GovernRepairOptions = {},
|
|
1088
1151
|
): Promise<GovernRepairResult> {
|
|
1089
1152
|
const cwd = options.cwd ?? process.cwd();
|
|
1090
|
-
const state =
|
|
1153
|
+
const state = await runProjectOperation(
|
|
1154
|
+
options.projectOperation,
|
|
1155
|
+
() => loadState(cwd),
|
|
1156
|
+
);
|
|
1091
1157
|
// Filter to known CLIs so a corrupt or forward-compatible install
|
|
1092
1158
|
// record doesn't crash the repair walk.
|
|
1093
1159
|
const installed: Array<[GovernCli, GovernInstallRecord]> = Object.entries(
|
|
@@ -1100,7 +1166,7 @@ export async function runGovernRepair(
|
|
|
1100
1166
|
return {
|
|
1101
1167
|
ok: false,
|
|
1102
1168
|
message:
|
|
1103
|
-
"No CLIs governed on this host — nothing to repair. Run 'cortex enterprise
|
|
1169
|
+
"No CLIs governed on this host — nothing to repair. Run 'cortex enterprise install --api-key-stdin' first.",
|
|
1104
1170
|
reverified: [],
|
|
1105
1171
|
};
|
|
1106
1172
|
}
|
|
@@ -1109,21 +1175,22 @@ export async function runGovernRepair(
|
|
|
1109
1175
|
|
|
1110
1176
|
const verified: GovernCli[] = [];
|
|
1111
1177
|
const missing: string[] = [];
|
|
1112
|
-
for (const [cli
|
|
1113
|
-
|
|
1114
|
-
|
|
1178
|
+
for (const [cli] of installed) {
|
|
1179
|
+
const managedPath = managedPathForCli(cli, options);
|
|
1180
|
+
if (!existsSync(managedPath)) {
|
|
1181
|
+
missing.push(`${cli}: ${managedPath} is missing`);
|
|
1115
1182
|
continue;
|
|
1116
1183
|
}
|
|
1117
1184
|
if (cli === "copilot") {
|
|
1118
1185
|
// Verify the file is still our shim (not replaced by a real binary).
|
|
1119
1186
|
try {
|
|
1120
|
-
const raw = readFileSync(
|
|
1187
|
+
const raw = readFileSync(managedPath, "utf8");
|
|
1121
1188
|
if (!raw.includes("# cortex-shim-v1")) {
|
|
1122
|
-
missing.push(`${cli}: ${
|
|
1189
|
+
missing.push(`${cli}: ${managedPath} is no longer a cortex shim`);
|
|
1123
1190
|
continue;
|
|
1124
1191
|
}
|
|
1125
1192
|
} catch {
|
|
1126
|
-
missing.push(`${cli}: ${
|
|
1193
|
+
missing.push(`${cli}: ${managedPath} could not be read`);
|
|
1127
1194
|
continue;
|
|
1128
1195
|
}
|
|
1129
1196
|
}
|
|
@@ -1141,7 +1208,10 @@ export async function runGovernRepair(
|
|
|
1141
1208
|
};
|
|
1142
1209
|
}
|
|
1143
1210
|
|
|
1144
|
-
const lock =
|
|
1211
|
+
const lock = await runProjectOperation(
|
|
1212
|
+
options.projectOperation,
|
|
1213
|
+
() => readTamperLock(cwd),
|
|
1214
|
+
);
|
|
1145
1215
|
if (!lock) {
|
|
1146
1216
|
return {
|
|
1147
1217
|
ok: true,
|
|
@@ -1152,14 +1222,20 @@ export async function runGovernRepair(
|
|
|
1152
1222
|
};
|
|
1153
1223
|
}
|
|
1154
1224
|
|
|
1155
|
-
const removed =
|
|
1225
|
+
const removed = await runProjectOperation(
|
|
1226
|
+
options.projectOperation,
|
|
1227
|
+
() => removeTamperLock(cwd),
|
|
1228
|
+
);
|
|
1156
1229
|
if (removed) {
|
|
1157
|
-
await
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1230
|
+
await runProjectOperation(
|
|
1231
|
+
options.projectOperation,
|
|
1232
|
+
() => emitTamperAudit(cwd, {
|
|
1233
|
+
...lock,
|
|
1234
|
+
detected_at: new Date().toISOString(),
|
|
1235
|
+
hook_name: "tamper_repaired",
|
|
1236
|
+
missing_seconds: 0,
|
|
1237
|
+
}).catch(() => undefined),
|
|
1238
|
+
);
|
|
1163
1239
|
}
|
|
1164
1240
|
|
|
1165
1241
|
return {
|
|
@@ -1172,9 +1248,17 @@ export async function runGovernRepair(
|
|
|
1172
1248
|
};
|
|
1173
1249
|
}
|
|
1174
1250
|
|
|
1175
|
-
export async function runGovernSync(
|
|
1251
|
+
export async function runGovernSync(
|
|
1252
|
+
options: {
|
|
1253
|
+
cwd?: string;
|
|
1254
|
+
projectOperation?: ProjectOperationRunner;
|
|
1255
|
+
} = {},
|
|
1256
|
+
): Promise<void> {
|
|
1176
1257
|
const cwd = options.cwd ?? process.cwd();
|
|
1177
|
-
const state =
|
|
1258
|
+
const state = await runProjectOperation(
|
|
1259
|
+
options.projectOperation,
|
|
1260
|
+
() => loadState(cwd),
|
|
1261
|
+
);
|
|
1178
1262
|
// Drop unknown CLI keys: runGovernInstall would throw on an unsupported
|
|
1179
1263
|
// cli, but silently skipping is the right behaviour when sync runs in
|
|
1180
1264
|
// the daemon — it's not a user typo to surface, just stale/forward
|
|
@@ -1192,6 +1276,7 @@ export async function runGovernSync(options: { cwd?: string } = {}): Promise<voi
|
|
|
1192
1276
|
cli,
|
|
1193
1277
|
cwd,
|
|
1194
1278
|
mode: previous?.mode,
|
|
1279
|
+
projectOperation: options.projectOperation,
|
|
1195
1280
|
});
|
|
1196
1281
|
if (!result.ok) {
|
|
1197
1282
|
console.log(`! sync ${cli} failed: ${result.message}`);
|
|
@@ -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)}`,
|