codiedev 0.9.36 → 0.9.38
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/dist/commands/doctor.js +4 -1
- package/dist/commands/security.js +16 -3
- package/dist/connect.js +17 -10
- package/dist/hook.js +21 -9
- package/dist/securityDoctor.d.ts +1 -1
- package/dist/securityDoctor.js +2 -2
- package/dist/securityFeature.d.ts +5 -0
- package/dist/securityFeature.js +7 -0
- package/dist/securityInstall.js +1 -1
- package/dist/securityPolicy.d.ts +4 -3
- package/dist/securityPolicy.js +13 -7
- package/dist/utils.d.ts +7 -4
- package/dist/utils.js +67 -14
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -46,6 +46,7 @@ const upgradeNudge_1 = require("../upgradeNudge");
|
|
|
46
46
|
const captureReadiness_1 = require("../captureReadiness");
|
|
47
47
|
const captureLog_1 = require("../captureLog");
|
|
48
48
|
const securityDoctor_1 = require("../securityDoctor");
|
|
49
|
+
const securityFeature_1 = require("../securityFeature");
|
|
49
50
|
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
|
|
50
51
|
const CLAUDE_INSTRUCTIONS_PATH = path.join(os.homedir(), ".claude", "CLAUDE.md");
|
|
51
52
|
const CODEX_HOOKS_PATH = path.join(os.homedir(), ".codex", "hooks.json");
|
|
@@ -486,7 +487,9 @@ async function runDoctor(_args) {
|
|
|
486
487
|
// Agent Security is part of installation health, not a separate setup
|
|
487
488
|
// concept. Keep `codiedev security doctor` as an alias for focused output,
|
|
488
489
|
// while the main doctor remains the one command users need to remember.
|
|
489
|
-
|
|
490
|
+
if ((0, securityFeature_1.isAgentSecurityEnabled)(config)) {
|
|
491
|
+
checks.push(...(0, securityDoctor_1.securityDoctorChecks)((0, securityDoctor_1.collectSecurityDoctorReport)(config.companyId)));
|
|
492
|
+
}
|
|
490
493
|
// 8. gh / glab CLIs (optional — only needed for reverse-ticket from a
|
|
491
494
|
// specific PR/MR URL; branch mode works without either).
|
|
492
495
|
checks.push({
|
|
@@ -8,6 +8,7 @@ const securityInventory_1 = require("../securityInventory");
|
|
|
8
8
|
const securityDeepScan_1 = require("../securityDeepScan");
|
|
9
9
|
const utils_1 = require("../utils");
|
|
10
10
|
const securityDoctor_1 = require("../securityDoctor");
|
|
11
|
+
const securityFeature_1 = require("../securityFeature");
|
|
11
12
|
function printPilot(json) {
|
|
12
13
|
const report = (0, securityPilot_1.runSyntheticSecurityPilot)();
|
|
13
14
|
if (json) {
|
|
@@ -22,8 +23,8 @@ function printPilot(json) {
|
|
|
22
23
|
console.log("No commands were executed and no real credentials were used.\n");
|
|
23
24
|
return report.ok;
|
|
24
25
|
}
|
|
25
|
-
function printDoctor(json) {
|
|
26
|
-
const result = (0, securityDoctor_1.collectSecurityDoctorReport)();
|
|
26
|
+
function printDoctor(json, companyId) {
|
|
27
|
+
const result = (0, securityDoctor_1.collectSecurityDoctorReport)(companyId);
|
|
27
28
|
if (json) {
|
|
28
29
|
console.log(JSON.stringify(result, null, 2));
|
|
29
30
|
return result.ok;
|
|
@@ -39,6 +40,18 @@ function printDoctor(json) {
|
|
|
39
40
|
async function runSecurityCommand(args) {
|
|
40
41
|
const subcommand = args.find((arg) => !arg.startsWith("-")) ?? "doctor";
|
|
41
42
|
const json = args.includes("--json");
|
|
43
|
+
const connectedConfig = (0, utils_1.readConfig)();
|
|
44
|
+
const requiresEntitlement = !["pilot", "self-test"].includes(subcommand);
|
|
45
|
+
if (requiresEntitlement && (!connectedConfig || !(0, securityFeature_1.isAgentSecurityEnabled)(connectedConfig))) {
|
|
46
|
+
const detail = connectedConfig
|
|
47
|
+
? `Agent Security is not enabled for ${connectedConfig.companyName}.`
|
|
48
|
+
: "Not connected. Run `codiedev connect` first.";
|
|
49
|
+
if (json)
|
|
50
|
+
console.log(JSON.stringify({ schemaVersion: 1, enabled: false, detail }, null, 2));
|
|
51
|
+
else
|
|
52
|
+
console.log(`\n${detail}\n`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
42
55
|
if (subcommand === "inventory" || subcommand === "scan") {
|
|
43
56
|
const workspaceFlag = args.indexOf("--workspace");
|
|
44
57
|
const workspace = workspaceFlag >= 0 && args[workspaceFlag + 1]
|
|
@@ -128,7 +141,7 @@ async function runSecurityCommand(args) {
|
|
|
128
141
|
const ok = subcommand === "pilot" || subcommand === "self-test"
|
|
129
142
|
? printPilot(json)
|
|
130
143
|
: subcommand === "doctor" || subcommand === "status"
|
|
131
|
-
? printDoctor(json)
|
|
144
|
+
? printDoctor(json, connectedConfig.companyId)
|
|
132
145
|
: false;
|
|
133
146
|
if (!["pilot", "self-test", "doctor", "status"].includes(subcommand)) {
|
|
134
147
|
console.error("Usage: codiedev security <doctor|pilot|inventory|scan|sync|rollback> [--json] [--deep] [--connect-mcp] [--supply-chain] [--no-upload] [--yes]");
|
package/dist/connect.js
CHANGED
|
@@ -48,6 +48,7 @@ const upgradeNudge_1 = require("./upgradeNudge");
|
|
|
48
48
|
const shared_1 = require("./commands/shared");
|
|
49
49
|
const securityInstall_1 = require("./securityInstall");
|
|
50
50
|
const securityPolicy_1 = require("./securityPolicy");
|
|
51
|
+
const securityFeature_1 = require("./securityFeature");
|
|
51
52
|
// cli.codiedev.com is a Convex custom domain on the prod deployment; the
|
|
52
53
|
// underlying judicious-falcon-861.convex.site URL keeps serving, so configs
|
|
53
54
|
// written by older CLI versions stay valid without a re-connect.
|
|
@@ -204,6 +205,7 @@ async function runConnect(args = []) {
|
|
|
204
205
|
// Extract fields from response
|
|
205
206
|
const companyId = responseData.companyId;
|
|
206
207
|
const companyName = responseData.companyName;
|
|
208
|
+
const agentSecurityEnabled = responseData.agentSecurityEnabled === true;
|
|
207
209
|
const repos = responseData.repos ?? [];
|
|
208
210
|
if (!companyId || !companyName) {
|
|
209
211
|
console.error("\nError: Invalid response from server. Token may be invalid.");
|
|
@@ -216,6 +218,7 @@ async function runConnect(args = []) {
|
|
|
216
218
|
repos,
|
|
217
219
|
lastSynced: new Date().toISOString(),
|
|
218
220
|
backendUrl: BACKEND_URL,
|
|
221
|
+
agentSecurityEnabled,
|
|
219
222
|
deviceId: saved?.deviceId || crypto.randomUUID(),
|
|
220
223
|
deviceName: saved?.deviceName || os.hostname(),
|
|
221
224
|
captureIgnoredPaths: saved?.captureIgnoredPaths ?? [],
|
|
@@ -315,11 +318,11 @@ async function runConnect(args = []) {
|
|
|
315
318
|
}
|
|
316
319
|
};
|
|
317
320
|
if (hasClaude) {
|
|
318
|
-
runInstall("claude-code-hook", utils_1.installHook, "Claude Code SessionEnd (capture) + UserPromptSubmit (skill discovery) + Stop (auto-backup) hooks (~/.claude/settings.json)", "claudeCode-hook");
|
|
321
|
+
runInstall("claude-code-hook", () => (0, utils_1.installHook)(agentSecurityEnabled), "Claude Code SessionEnd (capture) + UserPromptSubmit (skill discovery) + Stop (auto-backup) hooks (~/.claude/settings.json)", "claudeCode-hook");
|
|
319
322
|
runInstall("claude-code-instructions", utils_1.installClaudeCodeInstructions, "Claude Code agent instructions (~/.claude/CLAUDE.md)", "claudeCode-instructions");
|
|
320
323
|
}
|
|
321
324
|
if (hasCodex) {
|
|
322
|
-
runInstall("codex-hook", utils_1.installCodexHook, "Codex Stop (capture + auto-backup) hooks (~/.codex/hooks.json)", "codex-hook");
|
|
325
|
+
runInstall("codex-hook", () => (0, utils_1.installCodexHook)(agentSecurityEnabled), "Codex Stop (capture + auto-backup) hooks (~/.codex/hooks.json)", "codex-hook");
|
|
323
326
|
runInstall("codex-instructions", utils_1.installCodexInstructions, "Codex agent instructions (~/.codex/AGENTS.md)", "codex-instructions");
|
|
324
327
|
}
|
|
325
328
|
if (hasCursor) {
|
|
@@ -329,13 +332,13 @@ async function runConnect(args = []) {
|
|
|
329
332
|
// Always stage the adapter. It is dormant until OpenCode starts, which lets
|
|
330
333
|
// a user connect Claude Code today and install the Codie harness tomorrow
|
|
331
334
|
// without needing a second setup ritual.
|
|
332
|
-
runInstall("opencode-capture-adapter", utils_1.installOpenCodeCapturePlugin, "OpenCode session capture adapter (~/.config/opencode/plugins/codiedev.js)", "opencode-capture");
|
|
335
|
+
runInstall("opencode-capture-adapter", () => (0, utils_1.installOpenCodeCapturePlugin)(agentSecurityEnabled), "OpenCode session capture adapter (~/.config/opencode/plugins/codiedev.js)", "opencode-capture");
|
|
333
336
|
// Record the exact installed hook bytes and the pre-update rollback files.
|
|
334
337
|
// This supplies health/integrity checks without requiring a package publish
|
|
335
338
|
// or trusting whichever `node` happens to be first on PATH later.
|
|
336
339
|
try {
|
|
337
340
|
const hookScriptPath = path.join(__dirname, "hook.js");
|
|
338
|
-
if (fs.existsSync(hookScriptPath)) {
|
|
341
|
+
if (agentSecurityEnabled && fs.existsSync(hookScriptPath)) {
|
|
339
342
|
const targets = [];
|
|
340
343
|
if (installedTargets.includes("claudeCode-hook"))
|
|
341
344
|
targets.push({ surface: "claude-code", configPath: securityConfigPaths[0] });
|
|
@@ -351,11 +354,13 @@ async function runConnect(args = []) {
|
|
|
351
354
|
}
|
|
352
355
|
// Prime the local cache after installation. Hooks read this file only, so
|
|
353
356
|
// an endpoint decision never depends on live network latency.
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
357
|
+
if (agentSecurityEnabled) {
|
|
358
|
+
try {
|
|
359
|
+
await (0, securityPolicy_1.syncEndpointSecurityPolicy)(config);
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
console.warn(`Warning: Agent-security policy will use safe defaults until sync succeeds — ${err.message}`);
|
|
363
|
+
}
|
|
359
364
|
}
|
|
360
365
|
// Tell the backend which targets we wired up so the portal's onboarding
|
|
361
366
|
// card flips to "Connected" only when something actually installed —
|
|
@@ -395,6 +400,8 @@ async function runConnect(args = []) {
|
|
|
395
400
|
}
|
|
396
401
|
console.log();
|
|
397
402
|
console.log("Run `codiedev doctor` to verify everything's wired up.");
|
|
398
|
-
|
|
403
|
+
if ((0, securityFeature_1.isAgentSecurityEnabled)(config)) {
|
|
404
|
+
console.log("Run `codiedev security pilot` to validate blocking safely.");
|
|
405
|
+
}
|
|
399
406
|
console.log();
|
|
400
407
|
}
|
package/dist/hook.js
CHANGED
|
@@ -48,6 +48,7 @@ const captureLog_1 = require("./captureLog");
|
|
|
48
48
|
const security_1 = require("./security");
|
|
49
49
|
const securitySession_1 = require("./securitySession");
|
|
50
50
|
const securityPolicy_1 = require("./securityPolicy");
|
|
51
|
+
const securityFeature_1 = require("./securityFeature");
|
|
51
52
|
const securityInventory_1 = require("./securityInventory");
|
|
52
53
|
const captureExclusions_1 = require("./captureExclusions");
|
|
53
54
|
// Pure builder for the discovery additionalContext. Returns null when there are
|
|
@@ -269,9 +270,13 @@ function postJsonForResponse(url, body, bearerToken, timeoutMs) {
|
|
|
269
270
|
// is dispatched separately and fail-open, so a detector outage or timeout can
|
|
270
271
|
// never delay or override the deterministic endpoint decision.
|
|
271
272
|
async function runSecurityHook(hookInput, phase, source) {
|
|
273
|
+
const config = (0, utils_1.readConfig)();
|
|
274
|
+
if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
|
|
275
|
+
return;
|
|
276
|
+
const policy = (0, securityPolicy_1.readEndpointSecurityPolicy)(undefined, undefined, config.companyId);
|
|
272
277
|
const sessionId = hookInput.session_id ?? hookInput.sessionId;
|
|
273
278
|
const sessionSignal = phase === "pre" ? (0, securitySession_1.readSessionSecuritySignal)(sessionId) : undefined;
|
|
274
|
-
const localDecision = (0, security_1.evaluateToolSecurity)(hookInput, phase,
|
|
279
|
+
const localDecision = (0, security_1.evaluateToolSecurity)(hookInput, phase, policy, source, sessionSignal);
|
|
275
280
|
if (phase === "post" && (0, security_1.securityInputHasSensitivePath)(hookInput)) {
|
|
276
281
|
(0, securitySession_1.recordSensitiveReadSignal)(sessionId);
|
|
277
282
|
}
|
|
@@ -302,7 +307,10 @@ async function runSecurityHook(hookInput, phase, source) {
|
|
|
302
307
|
dispatchSecurityEventUpload(event);
|
|
303
308
|
}
|
|
304
309
|
function runPromptSecurityHook(hookInput, source) {
|
|
305
|
-
const
|
|
310
|
+
const config = (0, utils_1.readConfig)();
|
|
311
|
+
if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
|
|
312
|
+
return false;
|
|
313
|
+
const decision = (0, security_1.evaluatePromptSecurity)(hookInput.prompt, (0, securityPolicy_1.readEndpointSecurityPolicy)(undefined, undefined, config.companyId), source);
|
|
306
314
|
if (decision.action !== "block")
|
|
307
315
|
return false;
|
|
308
316
|
dispatchSecurityEventUpload((0, security_1.buildBlockedSecurityPromptEvent)(hookInput, source, decision.category ?? "secret_in_prompt"));
|
|
@@ -313,12 +321,15 @@ function runPromptSecurityHook(hookInput, source) {
|
|
|
313
321
|
return true;
|
|
314
322
|
}
|
|
315
323
|
function dispatchAllowedPromptSecurityEvent(hookInput, source) {
|
|
316
|
-
const
|
|
324
|
+
const config = (0, utils_1.readConfig)();
|
|
325
|
+
if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
|
|
326
|
+
return;
|
|
327
|
+
const event = (0, security_1.buildSecurityPromptEvent)(hookInput, source, Date.now, (0, securityPolicy_1.readEndpointSecurityPolicy)(undefined, undefined, config.companyId));
|
|
317
328
|
if (event)
|
|
318
329
|
dispatchSecurityEventUpload(event);
|
|
319
330
|
}
|
|
320
331
|
function dispatchSecurityEventUpload(event) {
|
|
321
|
-
if (!event || !(0, utils_1.readConfig)())
|
|
332
|
+
if (!event || !(0, securityFeature_1.isAgentSecurityEnabled)((0, utils_1.readConfig)()))
|
|
322
333
|
return;
|
|
323
334
|
try {
|
|
324
335
|
const child = (0, child_process_1.spawn)(process.execPath, [process.argv[1], "security-upload"], {
|
|
@@ -334,7 +345,7 @@ function dispatchSecurityEventUpload(event) {
|
|
|
334
345
|
}
|
|
335
346
|
async function runSecurityEventUpload() {
|
|
336
347
|
const config = (0, utils_1.readConfig)();
|
|
337
|
-
if (!config)
|
|
348
|
+
if (!config || !(0, securityFeature_1.isAgentSecurityEnabled)(config))
|
|
338
349
|
return;
|
|
339
350
|
const startedAt = Date.now();
|
|
340
351
|
try {
|
|
@@ -346,8 +357,9 @@ async function runSecurityEventUpload() {
|
|
|
346
357
|
cliVersion: version_1.CLI_VERSION,
|
|
347
358
|
});
|
|
348
359
|
const result = await postJsonForResponse(`${config.backendUrl}/api/cli/securityEvent`, { ...eventWithEndpoint }, config.token, 1500);
|
|
349
|
-
if (result?.policy?.schemaVersion === 1)
|
|
350
|
-
(0, securityPolicy_1.writeEndpointSecurityPolicy)(result.policy);
|
|
360
|
+
if (result?.policy?.schemaVersion === 1) {
|
|
361
|
+
(0, securityPolicy_1.writeEndpointSecurityPolicy)(result.policy, undefined, undefined, config.companyId);
|
|
362
|
+
}
|
|
351
363
|
(0, captureLog_1.appendCaptureLog)({
|
|
352
364
|
stage: "security",
|
|
353
365
|
sessionId: event.sessionId,
|
|
@@ -492,7 +504,7 @@ async function main() {
|
|
|
492
504
|
// waits for filesystem traversal or an inventory upload.
|
|
493
505
|
if (process.argv[2] === "security-inventory-upload") {
|
|
494
506
|
const config = (0, utils_1.readConfig)();
|
|
495
|
-
if (config) {
|
|
507
|
+
if (config && (0, securityFeature_1.isAgentSecurityEnabled)(config)) {
|
|
496
508
|
const inventory = (0, securityInventory_1.buildSecurityInventory)({ workspace: process.argv[3] || process.cwd() });
|
|
497
509
|
try {
|
|
498
510
|
await (0, securityInventory_1.uploadSecurityInventory)(inventory, config);
|
|
@@ -597,7 +609,7 @@ async function main() {
|
|
|
597
609
|
}
|
|
598
610
|
// SessionStart: post the capture-coverage denominator ping. Tiny + fast.
|
|
599
611
|
if (process.argv[2] === "session-start" || process.argv[2] === "session-start-opencode") {
|
|
600
|
-
if ((0, securityInventory_1.securityInventoryDue)()) {
|
|
612
|
+
if ((0, securityFeature_1.isAgentSecurityEnabled)((0, utils_1.readConfig)()) && (0, securityInventory_1.securityInventoryDue)()) {
|
|
601
613
|
try {
|
|
602
614
|
const child = (0, child_process_1.spawn)(process.execPath, [process.argv[1], "security-inventory-upload", hookInput.cwd || process.cwd()], {
|
|
603
615
|
detached: true,
|
package/dist/securityDoctor.d.ts
CHANGED
|
@@ -27,6 +27,6 @@ export interface SecurityDoctorReport {
|
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
/** One security-health model shared by `doctor` and its legacy alias. */
|
|
30
|
-
export declare function collectSecurityDoctorReport(): SecurityDoctorReport;
|
|
30
|
+
export declare function collectSecurityDoctorReport(companyId?: string): SecurityDoctorReport;
|
|
31
31
|
/** Convert security health into the same compact rows used by core doctor. */
|
|
32
32
|
export declare function securityDoctorChecks(report: SecurityDoctorReport): SecurityDoctorCheck[];
|
package/dist/securityDoctor.js
CHANGED
|
@@ -6,12 +6,12 @@ const securityInstall_1 = require("./securityInstall");
|
|
|
6
6
|
const securityPilot_1 = require("./securityPilot");
|
|
7
7
|
const securityPolicy_1 = require("./securityPolicy");
|
|
8
8
|
/** One security-health model shared by `doctor` and its legacy alias. */
|
|
9
|
-
function collectSecurityDoctorReport() {
|
|
9
|
+
function collectSecurityDoctorReport(companyId) {
|
|
10
10
|
const manifest = (0, securityInstall_1.readSecurityInstallManifest)();
|
|
11
11
|
const integrity = (0, securityInstall_1.verifySecurityInstallManifest)(manifest);
|
|
12
12
|
const installations = (0, securityInstall_1.inspectSecurityInstallation)();
|
|
13
13
|
const pilot = (0, securityPilot_1.runSyntheticSecurityPilot)();
|
|
14
|
-
const policy = (0, securityPolicy_1.inspectEndpointSecurityPolicy)();
|
|
14
|
+
const policy = (0, securityPolicy_1.inspectEndpointSecurityPolicy)(undefined, undefined, companyId);
|
|
15
15
|
const ok = integrity.ok &&
|
|
16
16
|
policy.verification.ok &&
|
|
17
17
|
pilot.ok &&
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export interface AgentSecurityEntitlement {
|
|
2
|
+
agentSecurityEnabled?: boolean;
|
|
3
|
+
}
|
|
4
|
+
/** Security is opt-in per company. Missing/stale entitlement data is dormant. */
|
|
5
|
+
export declare function isAgentSecurityEnabled(config: AgentSecurityEntitlement | null | undefined): boolean;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isAgentSecurityEnabled = isAgentSecurityEnabled;
|
|
4
|
+
/** Security is opt-in per company. Missing/stale entitlement data is dormant. */
|
|
5
|
+
function isAgentSecurityEnabled(config) {
|
|
6
|
+
return config?.agentSecurityEnabled === true;
|
|
7
|
+
}
|
package/dist/securityInstall.js
CHANGED
|
@@ -177,7 +177,7 @@ function inspectJsonSurface(surface, filePath) {
|
|
|
177
177
|
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
178
178
|
const suffix = surface === "codex" ? "-codex" : "";
|
|
179
179
|
const expected = [
|
|
180
|
-
["UserPromptSubmit",
|
|
180
|
+
["UserPromptSubmit", `discover${suffix}`],
|
|
181
181
|
["PreToolUse", `security-pre${suffix}`],
|
|
182
182
|
["PostToolUse", `security-post${suffix}`],
|
|
183
183
|
["PostToolUseFailure", `security-failure${suffix}`],
|
package/dist/securityPolicy.d.ts
CHANGED
|
@@ -6,8 +6,8 @@ export declare function verifyEndpointSecurityPolicySignature(policy: EndpointSe
|
|
|
6
6
|
trusted: boolean;
|
|
7
7
|
detail: string;
|
|
8
8
|
};
|
|
9
|
-
export declare function readEndpointSecurityPolicy(stateDir?: string, trustedKeys?: TrustedPolicyKeys): EndpointSecurityPolicy | undefined;
|
|
10
|
-
export declare function inspectEndpointSecurityPolicy(stateDir?: string, trustedKeys?: TrustedPolicyKeys): {
|
|
9
|
+
export declare function readEndpointSecurityPolicy(stateDir?: string, trustedKeys?: TrustedPolicyKeys, companyId?: string): EndpointSecurityPolicy | undefined;
|
|
10
|
+
export declare function inspectEndpointSecurityPolicy(stateDir?: string, trustedKeys?: TrustedPolicyKeys, companyId?: string): {
|
|
11
11
|
exists: boolean;
|
|
12
12
|
policy?: EndpointSecurityPolicy;
|
|
13
13
|
verification: {
|
|
@@ -16,8 +16,9 @@ export declare function inspectEndpointSecurityPolicy(stateDir?: string, trusted
|
|
|
16
16
|
detail: string;
|
|
17
17
|
};
|
|
18
18
|
};
|
|
19
|
-
export declare function writeEndpointSecurityPolicy(policy: EndpointSecurityPolicy, stateDir?: string, trustedKeys?: TrustedPolicyKeys): void;
|
|
19
|
+
export declare function writeEndpointSecurityPolicy(policy: EndpointSecurityPolicy, stateDir?: string, trustedKeys?: TrustedPolicyKeys, companyId?: string): void;
|
|
20
20
|
export declare function syncEndpointSecurityPolicy(config: {
|
|
21
21
|
backendUrl: string;
|
|
22
22
|
token: string;
|
|
23
|
+
companyId: string;
|
|
23
24
|
}, source?: AgentSurface, timeoutMs?: number): Promise<EndpointSecurityPolicy>;
|
package/dist/securityPolicy.js
CHANGED
|
@@ -46,6 +46,12 @@ const https = __importStar(require("https"));
|
|
|
46
46
|
const path = __importStar(require("path"));
|
|
47
47
|
const configDir_1 = require("./configDir");
|
|
48
48
|
const POLICY_FILE = "security-policy.json";
|
|
49
|
+
function policyFile(companyId) {
|
|
50
|
+
if (!companyId)
|
|
51
|
+
return POLICY_FILE;
|
|
52
|
+
const scope = companyId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 128);
|
|
53
|
+
return `security-policy.${scope}.json`;
|
|
54
|
+
}
|
|
49
55
|
function stableJson(value) {
|
|
50
56
|
if (Array.isArray(value))
|
|
51
57
|
return `[${value.map(stableJson).join(",")}]`;
|
|
@@ -96,12 +102,12 @@ function verifyEndpointSecurityPolicySignature(policy, trustedKeys = trustedPoli
|
|
|
96
102
|
return { ok: false, trusted: false, detail: `policy signature could not be verified: ${error.message}` };
|
|
97
103
|
}
|
|
98
104
|
}
|
|
99
|
-
function readEndpointSecurityPolicy(stateDir = (0, configDir_1.codiedevStateDir)(), trustedKeys = trustedPolicyKeysFromEnvironment()) {
|
|
100
|
-
const inspection = inspectEndpointSecurityPolicy(stateDir, trustedKeys);
|
|
105
|
+
function readEndpointSecurityPolicy(stateDir = (0, configDir_1.codiedevStateDir)(), trustedKeys = trustedPolicyKeysFromEnvironment(), companyId) {
|
|
106
|
+
const inspection = inspectEndpointSecurityPolicy(stateDir, trustedKeys, companyId);
|
|
101
107
|
return inspection.verification.ok ? inspection.policy : undefined;
|
|
102
108
|
}
|
|
103
|
-
function inspectEndpointSecurityPolicy(stateDir = (0, configDir_1.codiedevStateDir)(), trustedKeys = trustedPolicyKeysFromEnvironment()) {
|
|
104
|
-
const target = path.join(stateDir,
|
|
109
|
+
function inspectEndpointSecurityPolicy(stateDir = (0, configDir_1.codiedevStateDir)(), trustedKeys = trustedPolicyKeysFromEnvironment(), companyId) {
|
|
110
|
+
const target = path.join(stateDir, policyFile(companyId));
|
|
105
111
|
if (!fs.existsSync(target)) {
|
|
106
112
|
return { exists: false, verification: { ok: true, trusted: false, detail: "no cached endpoint policy" } };
|
|
107
113
|
}
|
|
@@ -117,12 +123,12 @@ function inspectEndpointSecurityPolicy(stateDir = (0, configDir_1.codiedevStateD
|
|
|
117
123
|
return { exists: true, verification: { ok: false, trusted: false, detail: `cached endpoint policy could not be read: ${error.message}` } };
|
|
118
124
|
}
|
|
119
125
|
}
|
|
120
|
-
function writeEndpointSecurityPolicy(policy, stateDir = (0, configDir_1.codiedevStateDir)(), trustedKeys = trustedPolicyKeysFromEnvironment()) {
|
|
126
|
+
function writeEndpointSecurityPolicy(policy, stateDir = (0, configDir_1.codiedevStateDir)(), trustedKeys = trustedPolicyKeysFromEnvironment(), companyId) {
|
|
121
127
|
const verification = verifyEndpointSecurityPolicySignature(policy, trustedKeys);
|
|
122
128
|
if (!verification.ok)
|
|
123
129
|
throw new Error(`Refusing untrusted endpoint policy: ${verification.detail}`);
|
|
124
130
|
fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
125
|
-
const target = path.join(stateDir,
|
|
131
|
+
const target = path.join(stateDir, policyFile(companyId));
|
|
126
132
|
const temporary = `${target}.${process.pid}.tmp`;
|
|
127
133
|
fs.writeFileSync(temporary, `${JSON.stringify(policy, null, 2)}\n`, { mode: 0o600 });
|
|
128
134
|
fs.chmodSync(temporary, 0o600);
|
|
@@ -156,6 +162,6 @@ async function syncEndpointSecurityPolicy(config, source, timeoutMs = 3_000) {
|
|
|
156
162
|
request.on("error", reject);
|
|
157
163
|
request.end();
|
|
158
164
|
});
|
|
159
|
-
writeEndpointSecurityPolicy(policy);
|
|
165
|
+
writeEndpointSecurityPolicy(policy, undefined, undefined, config.companyId);
|
|
160
166
|
return policy;
|
|
161
167
|
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface CodiedevConfig {
|
|
|
14
14
|
}>;
|
|
15
15
|
lastSynced: string;
|
|
16
16
|
backendUrl: string;
|
|
17
|
+
agentSecurityEnabled?: boolean;
|
|
17
18
|
deviceId?: string;
|
|
18
19
|
deviceName?: string;
|
|
19
20
|
captureIgnoredPaths?: string[];
|
|
@@ -96,7 +97,7 @@ export declare function openCodeInstalled(): boolean;
|
|
|
96
97
|
/** Install a dormant global OpenCode adapter. Installing it unconditionally
|
|
97
98
|
* keeps `codiedev connect` as the only setup step even when Codie/OpenCode is
|
|
98
99
|
* installed later. OpenCode loads this file from its documented plugin dir. */
|
|
99
|
-
export declare function installOpenCodeCapturePlugin(): void;
|
|
100
|
+
export declare function installOpenCodeCapturePlugin(agentSecurityEnabled?: boolean): void;
|
|
100
101
|
/**
|
|
101
102
|
* VS Code with the GitHub Copilot Chat extension installed. The extension
|
|
102
103
|
* directory pattern is `~/.vscode/extensions/github.copilot-chat-*`. We treat
|
|
@@ -104,11 +105,13 @@ export declare function installOpenCodeCapturePlugin(): void;
|
|
|
104
105
|
*/
|
|
105
106
|
export declare function vscodeCopilotInstalled(): boolean;
|
|
106
107
|
export declare function upsertCodiedevHook(hooks: Record<string, unknown>, eventName: string, subcommand: string, matcher?: string): void;
|
|
107
|
-
|
|
108
|
+
/** Remove Codiedev's entries for one event while preserving foreign hooks. */
|
|
109
|
+
export declare function removeCodiedevHook(hooks: Record<string, unknown>, eventName: string): boolean;
|
|
110
|
+
export declare function installHook(agentSecurityEnabled?: boolean): void;
|
|
108
111
|
export declare function ensureDiscoveryHookInstalled(): void;
|
|
109
112
|
export declare function ensureBackupHookInstalled(): void;
|
|
110
113
|
export declare function ensureLimitHookInstalled(): void;
|
|
111
|
-
export declare function ensureAllCodiedevHooksInstalled(): void;
|
|
114
|
+
export declare function ensureAllCodiedevHooksInstalled(agentSecurityEnabled?: boolean): void;
|
|
112
115
|
export declare function ensureSessionStartHookInstalled(): void;
|
|
113
116
|
export declare function installClaudeCodeInstructions(): void;
|
|
114
117
|
export declare function installCodexInstructions(): void;
|
|
@@ -166,7 +169,7 @@ export declare function cleanupCodexMcp(): void;
|
|
|
166
169
|
* so we append a well-marked block if one isn't already present. Idempotent.
|
|
167
170
|
*/
|
|
168
171
|
export declare function installCodexMcp(): boolean;
|
|
169
|
-
export declare function installCodexHook(): void;
|
|
172
|
+
export declare function installCodexHook(agentSecurityEnabled?: boolean): void;
|
|
170
173
|
export declare function installCursorHook(): void;
|
|
171
174
|
export interface ParsedStats {
|
|
172
175
|
messageCount: number;
|
package/dist/utils.js
CHANGED
|
@@ -64,6 +64,7 @@ exports.openCodeInstalled = openCodeInstalled;
|
|
|
64
64
|
exports.installOpenCodeCapturePlugin = installOpenCodeCapturePlugin;
|
|
65
65
|
exports.vscodeCopilotInstalled = vscodeCopilotInstalled;
|
|
66
66
|
exports.upsertCodiedevHook = upsertCodiedevHook;
|
|
67
|
+
exports.removeCodiedevHook = removeCodiedevHook;
|
|
67
68
|
exports.installHook = installHook;
|
|
68
69
|
exports.ensureDiscoveryHookInstalled = ensureDiscoveryHookInstalled;
|
|
69
70
|
exports.ensureBackupHookInstalled = ensureBackupHookInstalled;
|
|
@@ -1050,7 +1051,7 @@ function openCodeInstalled() {
|
|
|
1050
1051
|
/** Install a dormant global OpenCode adapter. Installing it unconditionally
|
|
1051
1052
|
* keeps `codiedev connect` as the only setup step even when Codie/OpenCode is
|
|
1052
1053
|
* installed later. OpenCode loads this file from its documented plugin dir. */
|
|
1053
|
-
function installOpenCodeCapturePlugin() {
|
|
1054
|
+
function installOpenCodeCapturePlugin(agentSecurityEnabled = true) {
|
|
1054
1055
|
const start = resolveHookInvocation("session-start-opencode");
|
|
1055
1056
|
const capture = resolveHookInvocation("capture-opencode");
|
|
1056
1057
|
const finish = resolveHookInvocation("capture-opencode-final");
|
|
@@ -1072,6 +1073,7 @@ function installOpenCodeCapturePlugin() {
|
|
|
1072
1073
|
`const SECURITY_PRE = ${JSON.stringify(securityPre)};\n` +
|
|
1073
1074
|
`const SECURITY_POST = ${JSON.stringify(securityPost)};\n` +
|
|
1074
1075
|
`const SECURITY_FAILURE = ${JSON.stringify(securityFailure)};\n` +
|
|
1076
|
+
`const AGENT_SECURITY_ENABLED = ${JSON.stringify(agentSecurityEnabled)};\n` +
|
|
1075
1077
|
`const fingerprints = new Map();\n\n` +
|
|
1076
1078
|
`const transcriptPaths = new Map();\n\n` +
|
|
1077
1079
|
`function sessionId(event) {\n` +
|
|
@@ -1115,10 +1117,12 @@ function installOpenCodeCapturePlugin() {
|
|
|
1115
1117
|
` enforceSync(PROMPT, { sessionId: id, cwd, prompt });\n` +
|
|
1116
1118
|
` },\n` +
|
|
1117
1119
|
` "tool.execute.before": async (input, output) => {\n` +
|
|
1120
|
+
` if (!AGENT_SECURITY_ENABLED) return;\n` +
|
|
1118
1121
|
` const cwd = worktree || directory || process.cwd();\n` +
|
|
1119
1122
|
` enforceSync(SECURITY_PRE, { sessionId: input?.sessionID ?? input?.sessionId ?? "unknown", cwd, toolName: input?.tool ?? "unknown", toolUseId: input?.callID, toolInput: output?.args ?? output });\n` +
|
|
1120
1123
|
` },\n` +
|
|
1121
1124
|
` "tool.execute.after": async (input, output) => {\n` +
|
|
1125
|
+
` if (!AGENT_SECURITY_ENABLED) return;\n` +
|
|
1122
1126
|
` const cwd = worktree || directory || process.cwd();\n` +
|
|
1123
1127
|
` const payload = { sessionId: input?.sessionID ?? input?.sessionId ?? "unknown", cwd, toolName: input?.tool ?? "unknown", toolUseId: input?.callID, toolInput: input?.args, toolResponse: output };\n` +
|
|
1124
1128
|
` dispatch(output?.error ? SECURITY_FAILURE : SECURITY_POST, payload);\n` +
|
|
@@ -1217,7 +1221,36 @@ function upsertCodiedevHook(hooks, eventName, subcommand, matcher = ".*") {
|
|
|
1217
1221
|
});
|
|
1218
1222
|
hooks[eventName] = filtered;
|
|
1219
1223
|
}
|
|
1220
|
-
|
|
1224
|
+
/** Remove Codiedev's entries for one event while preserving foreign hooks. */
|
|
1225
|
+
function removeCodiedevHook(hooks, eventName) {
|
|
1226
|
+
const entries = hooks[eventName];
|
|
1227
|
+
if (!Array.isArray(entries))
|
|
1228
|
+
return false;
|
|
1229
|
+
let changed = false;
|
|
1230
|
+
const filtered = entries.flatMap((entry) => {
|
|
1231
|
+
const record = entry;
|
|
1232
|
+
const inner = record.hooks;
|
|
1233
|
+
if (!Array.isArray(inner))
|
|
1234
|
+
return [record];
|
|
1235
|
+
const remaining = inner.filter((hook) => {
|
|
1236
|
+
const remove = (0, detection_1.isCodiedevHookCommand)(hook.command);
|
|
1237
|
+
if (remove)
|
|
1238
|
+
changed = true;
|
|
1239
|
+
return !remove;
|
|
1240
|
+
});
|
|
1241
|
+
if (remaining.length === 0)
|
|
1242
|
+
return [];
|
|
1243
|
+
return [{ ...record, hooks: remaining }];
|
|
1244
|
+
});
|
|
1245
|
+
if (!changed)
|
|
1246
|
+
return false;
|
|
1247
|
+
if (filtered.length === 0)
|
|
1248
|
+
delete hooks[eventName];
|
|
1249
|
+
else
|
|
1250
|
+
hooks[eventName] = filtered;
|
|
1251
|
+
return true;
|
|
1252
|
+
}
|
|
1253
|
+
function installHook(agentSecurityEnabled = true) {
|
|
1221
1254
|
let settings = {};
|
|
1222
1255
|
try {
|
|
1223
1256
|
if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
@@ -1236,9 +1269,16 @@ function installHook() {
|
|
|
1236
1269
|
upsertCodiedevHook(hooks, "SessionEnd", "capture");
|
|
1237
1270
|
// Tool boundaries → ADR shadow detection. PreToolUse is the eventual
|
|
1238
1271
|
// enforcement point; alert mode always returns allow.
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1272
|
+
if (agentSecurityEnabled) {
|
|
1273
|
+
upsertCodiedevHook(hooks, "PreToolUse", "security-pre");
|
|
1274
|
+
upsertCodiedevHook(hooks, "PostToolUse", "security-post");
|
|
1275
|
+
upsertCodiedevHook(hooks, "PostToolUseFailure", "security-failure");
|
|
1276
|
+
}
|
|
1277
|
+
else {
|
|
1278
|
+
removeCodiedevHook(hooks, "PreToolUse");
|
|
1279
|
+
removeCodiedevHook(hooks, "PostToolUse");
|
|
1280
|
+
removeCodiedevHook(hooks, "PostToolUseFailure");
|
|
1281
|
+
}
|
|
1242
1282
|
// UserPromptSubmit → discover (surface a matching team skill for the task).
|
|
1243
1283
|
upsertCodiedevHook(hooks, "UserPromptSubmit", "discover");
|
|
1244
1284
|
// Stop → backup (auto-back-up changed artifact files to your private library).
|
|
@@ -1340,7 +1380,7 @@ function ensureLimitHookInstalled() {
|
|
|
1340
1380
|
// shrinks that window and removes the intra-process version entirely.
|
|
1341
1381
|
// The per-hook ensure* functions below are kept for tests/back-compat but are
|
|
1342
1382
|
// no longer called from the hook entrypoint.
|
|
1343
|
-
function ensureAllCodiedevHooksInstalled() {
|
|
1383
|
+
function ensureAllCodiedevHooksInstalled(agentSecurityEnabled = readConfig()?.agentSecurityEnabled === true) {
|
|
1344
1384
|
try {
|
|
1345
1385
|
if (!fs.existsSync(CLAUDE_SETTINGS_PATH))
|
|
1346
1386
|
return; // not a Claude Code user
|
|
@@ -1348,16 +1388,22 @@ function ensureAllCodiedevHooksInstalled() {
|
|
|
1348
1388
|
const hooks = (settings.hooks ?? {});
|
|
1349
1389
|
const wanted = [
|
|
1350
1390
|
["SessionEnd", "capture"],
|
|
1351
|
-
["PreToolUse", "security-pre"],
|
|
1352
|
-
["PostToolUse", "security-post"],
|
|
1353
|
-
["PostToolUseFailure", "security-failure"],
|
|
1354
1391
|
["UserPromptSubmit", "discover"],
|
|
1355
1392
|
["Stop", "backup"],
|
|
1356
1393
|
["StopFailure", "limit", "rate_limit"],
|
|
1357
1394
|
["SessionStart", "session-start"],
|
|
1358
1395
|
];
|
|
1396
|
+
if (agentSecurityEnabled) {
|
|
1397
|
+
wanted.splice(1, 0, ["PreToolUse", "security-pre"], ["PostToolUse", "security-post"], ["PostToolUseFailure", "security-failure"]);
|
|
1398
|
+
}
|
|
1399
|
+
let changed = false;
|
|
1400
|
+
if (!agentSecurityEnabled) {
|
|
1401
|
+
for (const eventName of ["PreToolUse", "PostToolUse", "PostToolUseFailure"]) {
|
|
1402
|
+
changed = removeCodiedevHook(hooks, eventName) || changed;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1359
1405
|
const missing = wanted.filter(([eventName]) => !hasCodiedevHook(hooks, eventName));
|
|
1360
|
-
if (missing.length === 0)
|
|
1406
|
+
if (missing.length === 0 && !changed)
|
|
1361
1407
|
return; // steady state: no write, no churn
|
|
1362
1408
|
settings.hooks = hooks;
|
|
1363
1409
|
for (const [eventName, subcommand, matcher] of missing) {
|
|
@@ -1798,7 +1844,7 @@ function installCodexMcp() {
|
|
|
1798
1844
|
// Codex doesn't have a SessionEnd event yet — Stop fires after every turn.
|
|
1799
1845
|
// The backend dedupes on (companyId, sessionId) and replaces the stored
|
|
1800
1846
|
// transcript on re-upload, so the latest turn's snapshot wins.
|
|
1801
|
-
function installCodexHook() {
|
|
1847
|
+
function installCodexHook(agentSecurityEnabled = true) {
|
|
1802
1848
|
let hooksFile = {};
|
|
1803
1849
|
try {
|
|
1804
1850
|
if (fs.existsSync(CODEX_HOOKS_PATH)) {
|
|
@@ -1845,9 +1891,16 @@ function installCodexHook() {
|
|
|
1845
1891
|
// Codex uses the same Claude-style matcher-group shape. Keep these as
|
|
1846
1892
|
// separate entries because Codex hook coverage is still evolving and a
|
|
1847
1893
|
// malformed/unavailable post hook must not interfere with PreToolUse.
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1894
|
+
if (agentSecurityEnabled) {
|
|
1895
|
+
upsertCodiedevHook(hooks, "PreToolUse", "security-pre-codex");
|
|
1896
|
+
upsertCodiedevHook(hooks, "PostToolUse", "security-post-codex");
|
|
1897
|
+
upsertCodiedevHook(hooks, "PostToolUseFailure", "security-failure-codex");
|
|
1898
|
+
}
|
|
1899
|
+
else {
|
|
1900
|
+
removeCodiedevHook(hooks, "PreToolUse");
|
|
1901
|
+
removeCodiedevHook(hooks, "PostToolUse");
|
|
1902
|
+
removeCodiedevHook(hooks, "PostToolUseFailure");
|
|
1903
|
+
}
|
|
1851
1904
|
// Prompt guard + skill discovery. The guard runs locally before any prompt
|
|
1852
1905
|
// content can reach either the model or Codiedev's discovery endpoint.
|
|
1853
1906
|
upsertCodiedevHook(hooks, "UserPromptSubmit", "discover-codex");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codiedev",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.38",
|
|
4
4
|
"description": "Connect Claude Code, Codex, Cursor, OpenCode, or VS Code Copilot to Codiedev for team session capture and artifact collaboration — and run the governed Codie harness",
|
|
5
5
|
"bin": {
|
|
6
6
|
"codiedev": "dist/cli.js",
|