codiedev 0.9.34 → 0.9.36
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/captureLog.d.ts +1 -1
- package/dist/cli.js +11 -1
- package/dist/codie.js +2 -1
- package/dist/codieLauncher.d.ts +7 -0
- package/dist/codieLauncher.js +14 -0
- package/dist/commands/doctor.js +5 -0
- package/dist/commands/security.d.ts +1 -0
- package/dist/commands/security.js +138 -0
- package/dist/connect.js +41 -0
- package/dist/hook.js +169 -2
- package/dist/security.d.ts +64 -1
- package/dist/security.js +287 -0
- package/dist/securityDeepScan.d.ts +41 -0
- package/dist/securityDeepScan.js +248 -0
- package/dist/securityDoctor.d.ts +32 -0
- package/dist/securityDoctor.js +63 -0
- package/dist/securityInstall.d.ts +61 -0
- package/dist/securityInstall.js +217 -0
- package/dist/securityInventory.d.ts +54 -0
- package/dist/securityInventory.js +546 -0
- package/dist/securityPilot.d.ts +19 -0
- package/dist/securityPilot.js +61 -0
- package/dist/securityPolicy.d.ts +23 -0
- package/dist/securityPolicy.js +161 -0
- package/dist/securitySession.d.ts +6 -0
- package/dist/securitySession.js +72 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.js +200 -2
- package/package.json +9 -2
- package/dist/scope.d.ts +0 -8
- package/dist/scope.js +0 -15
package/dist/captureLog.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "fs";
|
|
|
2
2
|
export declare const CAPTURE_LOG_MAX_BYTES: number;
|
|
3
3
|
export declare const CAPTURE_LOG_PATH: string;
|
|
4
4
|
export interface CaptureLogEntry {
|
|
5
|
-
stage: "capture-finish" | "upload" | "upload-deferred" | "stderr";
|
|
5
|
+
stage: "capture-finish" | "upload" | "upload-deferred" | "security" | "stderr";
|
|
6
6
|
sessionId: string | undefined;
|
|
7
7
|
outcome: string;
|
|
8
8
|
detail?: string;
|
package/dist/cli.js
CHANGED
|
@@ -34,6 +34,7 @@ const share_1 = require("./commands/share");
|
|
|
34
34
|
const send_1 = require("./commands/send");
|
|
35
35
|
const react_1 = require("./commands/react");
|
|
36
36
|
const capture_1 = require("./commands/capture");
|
|
37
|
+
const security_1 = require("./commands/security");
|
|
37
38
|
const version_1 = require("./version");
|
|
38
39
|
const HELP = `
|
|
39
40
|
Codiedev CLI
|
|
@@ -44,7 +45,13 @@ Codiedev CLI
|
|
|
44
45
|
|
|
45
46
|
Connect:
|
|
46
47
|
codiedev connect Link Claude Code / Codex to your org
|
|
47
|
-
codiedev doctor Verify
|
|
48
|
+
codiedev doctor Verify setup, capture, and Agent Security
|
|
49
|
+
codiedev security pilot Run safe synthetic policy checks
|
|
50
|
+
codiedev security inventory Inventory MCPs, skills, hooks, plugins,
|
|
51
|
+
instructions, and editor extensions
|
|
52
|
+
codiedev security scan --deep Run installed open-source scanners too
|
|
53
|
+
codiedev security sync Fetch administrator policy locally
|
|
54
|
+
codiedev security rollback --yes Restore pre-update hook configuration
|
|
48
55
|
|
|
49
56
|
Harness:
|
|
50
57
|
codie Launch the governed Codie harness.
|
|
@@ -453,6 +460,9 @@ async function main() {
|
|
|
453
460
|
case "verify":
|
|
454
461
|
await (0, doctor_1.runDoctor)(rest);
|
|
455
462
|
return;
|
|
463
|
+
case "security":
|
|
464
|
+
await (0, security_1.runSecurityCommand)(rest);
|
|
465
|
+
return;
|
|
456
466
|
case "push":
|
|
457
467
|
await (0, push_1.runPush)(rest);
|
|
458
468
|
return;
|
package/dist/codie.js
CHANGED
|
@@ -231,7 +231,8 @@ async function main() {
|
|
|
231
231
|
CODIEDEV_HEAD_SHA: git(["rev-parse", "HEAD"]),
|
|
232
232
|
OPENCODE_CONFIG_CONTENT: JSON.stringify(config),
|
|
233
233
|
};
|
|
234
|
-
const
|
|
234
|
+
const runtimeArgs = (0, codieLauncher_1.withCodieWorkingDirectory)(parsed.args, process.cwd());
|
|
235
|
+
const result = (0, child_process_1.spawnSync)(runtime, runtimeArgs, { stdio: "inherit", env });
|
|
235
236
|
if (result.error)
|
|
236
237
|
throw result.error;
|
|
237
238
|
process.exitCode = result.status === null ? 1 : result.status;
|
package/dist/codieLauncher.d.ts
CHANGED
|
@@ -2,6 +2,13 @@ export interface CatalogModel {
|
|
|
2
2
|
id: string;
|
|
3
3
|
name?: string;
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* Pin non-interactive runs to the directory where `codie` was launched.
|
|
7
|
+
* OpenCode can reuse a previously-known project instance; without an explicit
|
|
8
|
+
* --dir, a real smoke run launched in a throwaway repo resumed the caller's
|
|
9
|
+
* main checkout and executed there. TUI mode already owns its cwd directly.
|
|
10
|
+
*/
|
|
11
|
+
export declare function withCodieWorkingDirectory(args: string[], cwd: string): string[];
|
|
5
12
|
export declare function bareModel(value: unknown): string;
|
|
6
13
|
export declare function buildCodieConfig({ existing, catalog, env, plugin, plugins, }: {
|
|
7
14
|
existing?: Record<string, any>;
|
package/dist/codieLauncher.js
CHANGED
|
@@ -43,6 +43,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
43
43
|
})();
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
45
|
exports.HIDDEN_WORDMARK = exports.CODIEDEV_WORDMARK = exports.LEGACY_CODIEDEV_WORDMARK = exports.STOCK_WORDMARK = exports.BRAND_PLUGIN_SPEC = void 0;
|
|
46
|
+
exports.withCodieWorkingDirectory = withCodieWorkingDirectory;
|
|
46
47
|
exports.bareModel = bareModel;
|
|
47
48
|
exports.buildCodieConfig = buildCodieConfig;
|
|
48
49
|
exports.withBrandPlugin = withBrandPlugin;
|
|
@@ -61,6 +62,19 @@ const DEFAULT_NAMES = {
|
|
|
61
62
|
"z-ai/glm-5.2": "GLM 5.2 · Fireworks ZDR",
|
|
62
63
|
"moonshotai/kimi-k2.7-code": "Kimi K2.7 Code · Fireworks ZDR",
|
|
63
64
|
};
|
|
65
|
+
/**
|
|
66
|
+
* Pin non-interactive runs to the directory where `codie` was launched.
|
|
67
|
+
* OpenCode can reuse a previously-known project instance; without an explicit
|
|
68
|
+
* --dir, a real smoke run launched in a throwaway repo resumed the caller's
|
|
69
|
+
* main checkout and executed there. TUI mode already owns its cwd directly.
|
|
70
|
+
*/
|
|
71
|
+
function withCodieWorkingDirectory(args, cwd) {
|
|
72
|
+
if (args[0] !== "run")
|
|
73
|
+
return args;
|
|
74
|
+
if (args.some((arg) => arg === "--dir" || arg.startsWith("--dir=")))
|
|
75
|
+
return args;
|
|
76
|
+
return [...args, "--dir", cwd];
|
|
77
|
+
}
|
|
64
78
|
function cleanBaseUrl(value) {
|
|
65
79
|
return String(value || "https://gateway.codiedev.com/api/v1").replace(/\/+$/, "");
|
|
66
80
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -45,6 +45,7 @@ const version_1 = require("../version");
|
|
|
45
45
|
const upgradeNudge_1 = require("../upgradeNudge");
|
|
46
46
|
const captureReadiness_1 = require("../captureReadiness");
|
|
47
47
|
const captureLog_1 = require("../captureLog");
|
|
48
|
+
const securityDoctor_1 = require("../securityDoctor");
|
|
48
49
|
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
|
|
49
50
|
const CLAUDE_INSTRUCTIONS_PATH = path.join(os.homedir(), ".claude", "CLAUDE.md");
|
|
50
51
|
const CODEX_HOOKS_PATH = path.join(os.homedir(), ".codex", "hooks.json");
|
|
@@ -482,6 +483,10 @@ async function runDoctor(_args) {
|
|
|
482
483
|
detail: "not installed on this machine",
|
|
483
484
|
});
|
|
484
485
|
}
|
|
486
|
+
// Agent Security is part of installation health, not a separate setup
|
|
487
|
+
// concept. Keep `codiedev security doctor` as an alias for focused output,
|
|
488
|
+
// while the main doctor remains the one command users need to remember.
|
|
489
|
+
checks.push(...(0, securityDoctor_1.securityDoctorChecks)((0, securityDoctor_1.collectSecurityDoctorReport)()));
|
|
485
490
|
// 8. gh / glab CLIs (optional — only needed for reverse-ticket from a
|
|
486
491
|
// specific PR/MR URL; branch mode works without either).
|
|
487
492
|
checks.push({
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runSecurityCommand(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runSecurityCommand = runSecurityCommand;
|
|
4
|
+
const securityInstall_1 = require("../securityInstall");
|
|
5
|
+
const securityPilot_1 = require("../securityPilot");
|
|
6
|
+
const securityPolicy_1 = require("../securityPolicy");
|
|
7
|
+
const securityInventory_1 = require("../securityInventory");
|
|
8
|
+
const securityDeepScan_1 = require("../securityDeepScan");
|
|
9
|
+
const utils_1 = require("../utils");
|
|
10
|
+
const securityDoctor_1 = require("../securityDoctor");
|
|
11
|
+
function printPilot(json) {
|
|
12
|
+
const report = (0, securityPilot_1.runSyntheticSecurityPilot)();
|
|
13
|
+
if (json) {
|
|
14
|
+
console.log(JSON.stringify(report, null, 2));
|
|
15
|
+
return report.ok;
|
|
16
|
+
}
|
|
17
|
+
console.log("\nCodiedev security pilot — synthetic, side-effect-free validation\n");
|
|
18
|
+
for (const item of report.cases) {
|
|
19
|
+
console.log(`${item.passed ? "✓" : "✗"} ${item.surface}: ${item.name} (${item.actual})`);
|
|
20
|
+
}
|
|
21
|
+
console.log(`\n${report.ok ? "PASS" : "FAIL"}: ${report.cases.filter((item) => item.passed).length}/${report.cases.length} policy checks matched.`);
|
|
22
|
+
console.log("No commands were executed and no real credentials were used.\n");
|
|
23
|
+
return report.ok;
|
|
24
|
+
}
|
|
25
|
+
function printDoctor(json) {
|
|
26
|
+
const result = (0, securityDoctor_1.collectSecurityDoctorReport)();
|
|
27
|
+
if (json) {
|
|
28
|
+
console.log(JSON.stringify(result, null, 2));
|
|
29
|
+
return result.ok;
|
|
30
|
+
}
|
|
31
|
+
console.log("\nCodiedev doctor — Agent Security details\n");
|
|
32
|
+
for (const check of (0, securityDoctor_1.securityDoctorChecks)(result)) {
|
|
33
|
+
const mark = check.status === "pass" ? "✓" : check.status === "warn" ? "!" : "✗";
|
|
34
|
+
console.log(`${mark} ${check.name}: ${check.detail}`);
|
|
35
|
+
}
|
|
36
|
+
console.log(`\n${result.ok ? "PASS" : "FAIL"}: ${result.ok ? "endpoint security is healthy" : "run `codiedev connect`, then repeat this check"}.\n`);
|
|
37
|
+
return result.ok;
|
|
38
|
+
}
|
|
39
|
+
async function runSecurityCommand(args) {
|
|
40
|
+
const subcommand = args.find((arg) => !arg.startsWith("-")) ?? "doctor";
|
|
41
|
+
const json = args.includes("--json");
|
|
42
|
+
if (subcommand === "inventory" || subcommand === "scan") {
|
|
43
|
+
const workspaceFlag = args.indexOf("--workspace");
|
|
44
|
+
const workspace = workspaceFlag >= 0 && args[workspaceFlag + 1]
|
|
45
|
+
? args[workspaceFlag + 1]
|
|
46
|
+
: process.cwd();
|
|
47
|
+
const inventory = (0, securityInventory_1.buildSecurityInventory)({ workspace });
|
|
48
|
+
const deep = subcommand === "scan" && args.includes("--deep")
|
|
49
|
+
? (0, securityDeepScan_1.runDeepSecurityScan)(inventory, {
|
|
50
|
+
workspace,
|
|
51
|
+
connectMcp: args.includes("--connect-mcp"),
|
|
52
|
+
supplyChain: args.includes("--supply-chain"),
|
|
53
|
+
})
|
|
54
|
+
: undefined;
|
|
55
|
+
const uploadableInventory = deep ? (0, securityDeepScan_1.mergeDeepScanFindings)(inventory, deep) : inventory;
|
|
56
|
+
let upload;
|
|
57
|
+
const config = (0, utils_1.readConfig)();
|
|
58
|
+
if (config && !args.includes("--no-upload")) {
|
|
59
|
+
try {
|
|
60
|
+
upload = await (0, securityInventory_1.uploadSecurityInventory)(uploadableInventory, config);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (!json)
|
|
64
|
+
console.warn(`Inventory upload failed; local scan is still valid: ${error.message}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const output = { inventory: uploadableInventory, scanners: (0, securityInventory_1.scannerAvailability)(), ...(deep ? { deep } : {}), ...(upload ? { upload } : {}) };
|
|
68
|
+
if (json)
|
|
69
|
+
console.log(JSON.stringify(output, null, 2));
|
|
70
|
+
else {
|
|
71
|
+
const risky = inventory.components.filter((component) => component.findings.length > 0).length;
|
|
72
|
+
console.log(`\nCodiedev security inventory — ${inventory.components.length} components, ${risky} with local findings, ${inventory.drift.length} changed\n`);
|
|
73
|
+
const counts = new Map();
|
|
74
|
+
for (const component of inventory.components)
|
|
75
|
+
counts.set(component.kind, (counts.get(component.kind) ?? 0) + 1);
|
|
76
|
+
for (const [kind, count] of [...counts].sort())
|
|
77
|
+
console.log(` ${kind}: ${count}`);
|
|
78
|
+
if (deep)
|
|
79
|
+
for (const run of deep.runs)
|
|
80
|
+
console.log(` ${run.scanner}: ${run.status} — ${run.detail}`);
|
|
81
|
+
if (upload)
|
|
82
|
+
console.log(`\nUploaded ${upload.accepted} component records to the security workspace.`);
|
|
83
|
+
console.log("\nUse --json for machine-readable hashes and findings. Use scan --deep for installed OSS scanners; --connect-mcp and --supply-chain are explicit because they start or download third-party code.\n");
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (subcommand === "sync") {
|
|
88
|
+
const config = (0, utils_1.readConfig)();
|
|
89
|
+
if (!config) {
|
|
90
|
+
console.error("Not connected. Run `codiedev connect` first.");
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const policy = await (0, securityPolicy_1.syncEndpointSecurityPolicy)(config);
|
|
96
|
+
if (json)
|
|
97
|
+
console.log(JSON.stringify({ ok: true, policy }, null, 2));
|
|
98
|
+
else {
|
|
99
|
+
const verification = (0, securityPolicy_1.verifyEndpointSecurityPolicySignature)(policy);
|
|
100
|
+
console.log(`Security policy v${policy.policyVersion} synced (${policy.enforcementMode} mode; ${verification.detail}).`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.error(`Security policy sync failed: ${error.message}`);
|
|
105
|
+
process.exitCode = 1;
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (subcommand === "rollback") {
|
|
110
|
+
if (!args.includes("--yes")) {
|
|
111
|
+
console.error("Rollback overwrites the installed agent hook configurations. Re-run with `codiedev security rollback --yes` after confirming the snapshot shown by `codiedev doctor`.");
|
|
112
|
+
process.exitCode = 1;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const result = (0, securityInstall_1.restoreSecurityInstallSnapshot)((0, securityInstall_1.readSecurityInstallManifest)());
|
|
117
|
+
if (json)
|
|
118
|
+
console.log(JSON.stringify({ schemaVersion: 1, ok: true, ...result }, null, 2));
|
|
119
|
+
else
|
|
120
|
+
console.log(`Restored ${result.restored} hook configuration(s) from snapshot ${result.snapshotId}.`);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
console.error(`Rollback failed: ${error.message}`);
|
|
124
|
+
process.exitCode = 1;
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const ok = subcommand === "pilot" || subcommand === "self-test"
|
|
129
|
+
? printPilot(json)
|
|
130
|
+
: subcommand === "doctor" || subcommand === "status"
|
|
131
|
+
? printDoctor(json)
|
|
132
|
+
: false;
|
|
133
|
+
if (!["pilot", "self-test", "doctor", "status"].includes(subcommand)) {
|
|
134
|
+
console.error("Usage: codiedev security <doctor|pilot|inventory|scan|sync|rollback> [--json] [--deep] [--connect-mcp] [--supply-chain] [--no-upload] [--yes]");
|
|
135
|
+
}
|
|
136
|
+
if (!ok)
|
|
137
|
+
process.exitCode = 1;
|
|
138
|
+
}
|
package/dist/connect.js
CHANGED
|
@@ -39,11 +39,15 @@ const https = __importStar(require("https"));
|
|
|
39
39
|
const http = __importStar(require("http"));
|
|
40
40
|
const crypto = __importStar(require("crypto"));
|
|
41
41
|
const os = __importStar(require("os"));
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
42
44
|
const utils_1 = require("./utils");
|
|
43
45
|
const connectFlow_1 = require("./connectFlow");
|
|
44
46
|
const version_1 = require("./version");
|
|
45
47
|
const upgradeNudge_1 = require("./upgradeNudge");
|
|
46
48
|
const shared_1 = require("./commands/shared");
|
|
49
|
+
const securityInstall_1 = require("./securityInstall");
|
|
50
|
+
const securityPolicy_1 = require("./securityPolicy");
|
|
47
51
|
// cli.codiedev.com is a Convex custom domain on the prod deployment; the
|
|
48
52
|
// underlying judicious-falcon-861.convex.site URL keeps serving, so configs
|
|
49
53
|
// written by older CLI versions stay valid without a re-connect.
|
|
@@ -280,6 +284,15 @@ async function runConnect(args = []) {
|
|
|
280
284
|
// /api/cli/markInstalled so the portal's "Connected" state reflects
|
|
281
285
|
// what's actually wired up on this machine, not just "token validated."
|
|
282
286
|
const installedTargets = [];
|
|
287
|
+
const home = os.homedir();
|
|
288
|
+
const securityConfigPaths = [
|
|
289
|
+
path.join(home, ".claude", "settings.json"),
|
|
290
|
+
path.join(home, ".codex", "hooks.json"),
|
|
291
|
+
path.join(home, ".config", "opencode", "plugins", "codiedev.js"),
|
|
292
|
+
];
|
|
293
|
+
// Snapshot before any hook update. Rollback is intentionally not automatic:
|
|
294
|
+
// an administrator can inspect these 0600 copies before restoring them.
|
|
295
|
+
const securitySnapshot = (0, securityInstall_1.createSecurityInstallSnapshot)(securityConfigPaths);
|
|
283
296
|
// Wrap each per-target write so we emit one install_target row each. The
|
|
284
297
|
// existing user-visible behavior (console.warn on failure, continue) is
|
|
285
298
|
// unchanged — telemetry just observes the outcome.
|
|
@@ -317,6 +330,33 @@ async function runConnect(args = []) {
|
|
|
317
330
|
// a user connect Claude Code today and install the Codie harness tomorrow
|
|
318
331
|
// without needing a second setup ritual.
|
|
319
332
|
runInstall("opencode-capture-adapter", utils_1.installOpenCodeCapturePlugin, "OpenCode session capture adapter (~/.config/opencode/plugins/codiedev.js)", "opencode-capture");
|
|
333
|
+
// Record the exact installed hook bytes and the pre-update rollback files.
|
|
334
|
+
// This supplies health/integrity checks without requiring a package publish
|
|
335
|
+
// or trusting whichever `node` happens to be first on PATH later.
|
|
336
|
+
try {
|
|
337
|
+
const hookScriptPath = path.join(__dirname, "hook.js");
|
|
338
|
+
if (fs.existsSync(hookScriptPath)) {
|
|
339
|
+
const targets = [];
|
|
340
|
+
if (installedTargets.includes("claudeCode-hook"))
|
|
341
|
+
targets.push({ surface: "claude-code", configPath: securityConfigPaths[0] });
|
|
342
|
+
if (installedTargets.includes("codex-hook"))
|
|
343
|
+
targets.push({ surface: "codex", configPath: securityConfigPaths[1] });
|
|
344
|
+
if (installedTargets.includes("opencode-capture"))
|
|
345
|
+
targets.push({ surface: "codie-harness", configPath: securityConfigPaths[2] });
|
|
346
|
+
(0, securityInstall_1.writeSecurityInstallManifest)({ hookScriptPath, snapshot: securitySnapshot, targets });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
console.warn(`Warning: Security hooks installed, but integrity manifest could not be written — ${err.message}`);
|
|
351
|
+
}
|
|
352
|
+
// Prime the local cache after installation. Hooks read this file only, so
|
|
353
|
+
// an endpoint decision never depends on live network latency.
|
|
354
|
+
try {
|
|
355
|
+
await (0, securityPolicy_1.syncEndpointSecurityPolicy)(config);
|
|
356
|
+
}
|
|
357
|
+
catch (err) {
|
|
358
|
+
console.warn(`Warning: Agent-security policy will use safe defaults until sync succeeds — ${err.message}`);
|
|
359
|
+
}
|
|
320
360
|
// Tell the backend which targets we wired up so the portal's onboarding
|
|
321
361
|
// card flips to "Connected" only when something actually installed —
|
|
322
362
|
// not on bare token validation. Best-effort: a network failure here
|
|
@@ -355,5 +395,6 @@ async function runConnect(args = []) {
|
|
|
355
395
|
}
|
|
356
396
|
console.log();
|
|
357
397
|
console.log("Run `codiedev doctor` to verify everything's wired up.");
|
|
398
|
+
console.log("Run `codiedev security pilot` to validate blocking safely.");
|
|
358
399
|
console.log();
|
|
359
400
|
}
|
package/dist/hook.js
CHANGED
|
@@ -45,6 +45,10 @@ const utils_1 = require("./utils");
|
|
|
45
45
|
const limitEvent_1 = require("./limitEvent");
|
|
46
46
|
const version_1 = require("./version");
|
|
47
47
|
const captureLog_1 = require("./captureLog");
|
|
48
|
+
const security_1 = require("./security");
|
|
49
|
+
const securitySession_1 = require("./securitySession");
|
|
50
|
+
const securityPolicy_1 = require("./securityPolicy");
|
|
51
|
+
const securityInventory_1 = require("./securityInventory");
|
|
48
52
|
const captureExclusions_1 = require("./captureExclusions");
|
|
49
53
|
// Pure builder for the discovery additionalContext. Returns null when there are
|
|
50
54
|
// no matches. The suggested `codiedev pull` command carries the EXACT top key
|
|
@@ -218,7 +222,7 @@ timeoutMs) {
|
|
|
218
222
|
// postJson variant that returns the parsed response body — the upload-URL
|
|
219
223
|
// handshake needs the { uploadUrl } the server hands back. No timeout: runs
|
|
220
224
|
// only in detached uploader children, never on the shutdown clock.
|
|
221
|
-
function postJsonForResponse(url, body, bearerToken) {
|
|
225
|
+
function postJsonForResponse(url, body, bearerToken, timeoutMs) {
|
|
222
226
|
return new Promise((resolve, reject) => {
|
|
223
227
|
const parsed = new URL(url);
|
|
224
228
|
const data = JSON.stringify(body);
|
|
@@ -252,10 +256,114 @@ function postJsonForResponse(url, body, bearerToken) {
|
|
|
252
256
|
});
|
|
253
257
|
});
|
|
254
258
|
req.on("error", reject);
|
|
259
|
+
if (timeoutMs) {
|
|
260
|
+
req.setTimeout(timeoutMs, () => {
|
|
261
|
+
req.destroy(new Error(`timeout after ${timeoutMs}ms`));
|
|
262
|
+
});
|
|
263
|
+
}
|
|
255
264
|
req.write(data);
|
|
256
265
|
req.end();
|
|
257
266
|
});
|
|
258
267
|
}
|
|
268
|
+
// Local policy runs synchronously before a tool executes. Workflow telemetry
|
|
269
|
+
// is dispatched separately and fail-open, so a detector outage or timeout can
|
|
270
|
+
// never delay or override the deterministic endpoint decision.
|
|
271
|
+
async function runSecurityHook(hookInput, phase, source) {
|
|
272
|
+
const sessionId = hookInput.session_id ?? hookInput.sessionId;
|
|
273
|
+
const sessionSignal = phase === "pre" ? (0, securitySession_1.readSessionSecuritySignal)(sessionId) : undefined;
|
|
274
|
+
const localDecision = (0, security_1.evaluateToolSecurity)(hookInput, phase, (0, securityPolicy_1.readEndpointSecurityPolicy)(), source, sessionSignal);
|
|
275
|
+
if (phase === "post" && (0, security_1.securityInputHasSensitivePath)(hookInput)) {
|
|
276
|
+
(0, securitySession_1.recordSensitiveReadSignal)(sessionId);
|
|
277
|
+
}
|
|
278
|
+
const event = (0, security_1.buildSecurityWorkflowEvent)(hookInput, phase, source);
|
|
279
|
+
if (event) {
|
|
280
|
+
event.localDecision = localDecision.action;
|
|
281
|
+
event.localCategory = localDecision.category;
|
|
282
|
+
}
|
|
283
|
+
if (localDecision.action === "block" && phase === "pre") {
|
|
284
|
+
(0, captureLog_1.appendCaptureLog)({
|
|
285
|
+
stage: "security",
|
|
286
|
+
sessionId: hookInput.session_id ?? hookInput.sessionId ?? "unknown",
|
|
287
|
+
outcome: "ok",
|
|
288
|
+
detail: `phase=${phase} localDecision=block category=${localDecision.category}`,
|
|
289
|
+
});
|
|
290
|
+
dispatchSecurityEventUpload(event);
|
|
291
|
+
process.stdout.write(JSON.stringify({
|
|
292
|
+
hookSpecificOutput: {
|
|
293
|
+
hookEventName: "PreToolUse",
|
|
294
|
+
permissionDecision: "deny",
|
|
295
|
+
permissionDecisionReason: localDecision.reason,
|
|
296
|
+
},
|
|
297
|
+
}));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (!event)
|
|
301
|
+
return;
|
|
302
|
+
dispatchSecurityEventUpload(event);
|
|
303
|
+
}
|
|
304
|
+
function runPromptSecurityHook(hookInput, source) {
|
|
305
|
+
const decision = (0, security_1.evaluatePromptSecurity)(hookInput.prompt, (0, securityPolicy_1.readEndpointSecurityPolicy)(), source);
|
|
306
|
+
if (decision.action !== "block")
|
|
307
|
+
return false;
|
|
308
|
+
dispatchSecurityEventUpload((0, security_1.buildBlockedSecurityPromptEvent)(hookInput, source, decision.category ?? "secret_in_prompt"));
|
|
309
|
+
process.stdout.write(JSON.stringify({
|
|
310
|
+
decision: "block",
|
|
311
|
+
reason: decision.reason,
|
|
312
|
+
}));
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
function dispatchAllowedPromptSecurityEvent(hookInput, source) {
|
|
316
|
+
const event = (0, security_1.buildSecurityPromptEvent)(hookInput, source, Date.now, (0, securityPolicy_1.readEndpointSecurityPolicy)());
|
|
317
|
+
if (event)
|
|
318
|
+
dispatchSecurityEventUpload(event);
|
|
319
|
+
}
|
|
320
|
+
function dispatchSecurityEventUpload(event) {
|
|
321
|
+
if (!event || !(0, utils_1.readConfig)())
|
|
322
|
+
return;
|
|
323
|
+
try {
|
|
324
|
+
const child = (0, child_process_1.spawn)(process.execPath, [process.argv[1], "security-upload"], {
|
|
325
|
+
detached: true,
|
|
326
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
327
|
+
});
|
|
328
|
+
child.stdin?.end(JSON.stringify(event));
|
|
329
|
+
child.unref();
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
// Telemetry is best-effort and must never delay local enforcement.
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async function runSecurityEventUpload() {
|
|
336
|
+
const config = (0, utils_1.readConfig)();
|
|
337
|
+
if (!config)
|
|
338
|
+
return;
|
|
339
|
+
const startedAt = Date.now();
|
|
340
|
+
try {
|
|
341
|
+
const raw = await readStdin();
|
|
342
|
+
const event = JSON.parse(raw);
|
|
343
|
+
const eventWithEndpoint = (0, security_1.withEndpointIdentity)(event, {
|
|
344
|
+
endpointId: config.deviceId,
|
|
345
|
+
endpointName: config.deviceName,
|
|
346
|
+
cliVersion: version_1.CLI_VERSION,
|
|
347
|
+
});
|
|
348
|
+
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);
|
|
351
|
+
(0, captureLog_1.appendCaptureLog)({
|
|
352
|
+
stage: "security",
|
|
353
|
+
sessionId: event.sessionId,
|
|
354
|
+
outcome: "ok",
|
|
355
|
+
detail: `phase=${event.phase} elapsedMs=${Date.now() - startedAt} verdict=${String(result?.verdict ?? "unknown")}`,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
(0, captureLog_1.appendCaptureLog)({
|
|
360
|
+
stage: "security",
|
|
361
|
+
sessionId: "upload",
|
|
362
|
+
outcome: "ERROR",
|
|
363
|
+
detail: `elapsedMs=${Date.now() - startedAt} ${err.message}`,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
259
367
|
// Raw-bytes POST to a Convex storage upload URL (no auth header — the URL
|
|
260
368
|
// itself is the one-time credential). Returns the parsed { storageId }.
|
|
261
369
|
function postBytes(url, bytes, contentType) {
|
|
@@ -379,6 +487,20 @@ async function main() {
|
|
|
379
487
|
}
|
|
380
488
|
process.exit(0);
|
|
381
489
|
}
|
|
490
|
+
// Daily component drift scan runs in a detached hook process. The parent
|
|
491
|
+
// SessionStart path only performs a timestamp read, so agent startup never
|
|
492
|
+
// waits for filesystem traversal or an inventory upload.
|
|
493
|
+
if (process.argv[2] === "security-inventory-upload") {
|
|
494
|
+
const config = (0, utils_1.readConfig)();
|
|
495
|
+
if (config) {
|
|
496
|
+
const inventory = (0, securityInventory_1.buildSecurityInventory)({ workspace: process.argv[3] || process.cwd() });
|
|
497
|
+
try {
|
|
498
|
+
await (0, securityInventory_1.uploadSecurityInventory)(inventory, config);
|
|
499
|
+
}
|
|
500
|
+
catch { /* retry after the daily inventory window */ }
|
|
501
|
+
}
|
|
502
|
+
process.exit(0);
|
|
503
|
+
}
|
|
382
504
|
// Detached backup uploader child (spawned by runBackup below). The job file
|
|
383
505
|
// carries only { root, files }; the child re-reads config from disk. No stdin
|
|
384
506
|
// needed, so handle it before readStdin() like the capture uploader above.
|
|
@@ -415,6 +537,13 @@ async function main() {
|
|
|
415
537
|
}
|
|
416
538
|
process.exit(0);
|
|
417
539
|
}
|
|
540
|
+
// Detached, best-effort security telemetry. Local policy decisions have
|
|
541
|
+
// already completed in the parent hook; this child can never delay or alter
|
|
542
|
+
// the developer's action.
|
|
543
|
+
if (process.argv[2] === "security-upload") {
|
|
544
|
+
await runSecurityEventUpload();
|
|
545
|
+
process.exit(0);
|
|
546
|
+
}
|
|
418
547
|
let hookInput = {};
|
|
419
548
|
try {
|
|
420
549
|
const raw = await readStdin();
|
|
@@ -432,8 +561,32 @@ async function main() {
|
|
|
432
561
|
if ((0, captureExclusions_1.shouldIgnoreCaptureInput)((0, utils_1.readConfig)(), hookInput)) {
|
|
433
562
|
process.exit(0);
|
|
434
563
|
}
|
|
564
|
+
const securityCommand = process.argv[2] ?? "";
|
|
565
|
+
const securityMatch = securityCommand.match(/^security-(pre|post|failure)(?:-(codex|codie))?$/);
|
|
566
|
+
if (securityMatch) {
|
|
567
|
+
const source = securityMatch[2] === "codex"
|
|
568
|
+
? "codex"
|
|
569
|
+
: securityMatch[2] === "codie"
|
|
570
|
+
? "codie-harness"
|
|
571
|
+
: "claude-code";
|
|
572
|
+
await runSecurityHook(hookInput, securityMatch[1], source);
|
|
573
|
+
process.exit(0);
|
|
574
|
+
}
|
|
435
575
|
// UserPromptSubmit discovery path — separate from session capture.
|
|
436
|
-
|
|
576
|
+
const promptCommand = process.argv[2] ?? "";
|
|
577
|
+
if (process.argv[2] === "discover" ||
|
|
578
|
+
process.argv[2] === "discover-codex" ||
|
|
579
|
+
process.argv[2] === "discover-codie") {
|
|
580
|
+
const source = promptCommand === "discover-codex"
|
|
581
|
+
? "codex"
|
|
582
|
+
: promptCommand === "discover-codie"
|
|
583
|
+
? "codie-harness"
|
|
584
|
+
: "claude-code";
|
|
585
|
+
// Security runs first and locally. A blocked prompt is erased before it
|
|
586
|
+
// reaches the model, so discovery must not send it to the backend.
|
|
587
|
+
if (runPromptSecurityHook(hookInput, source))
|
|
588
|
+
return;
|
|
589
|
+
dispatchAllowedPromptSecurityEvent(hookInput, source);
|
|
437
590
|
await runDiscover(hookInput.prompt);
|
|
438
591
|
return;
|
|
439
592
|
}
|
|
@@ -444,6 +597,16 @@ async function main() {
|
|
|
444
597
|
}
|
|
445
598
|
// SessionStart: post the capture-coverage denominator ping. Tiny + fast.
|
|
446
599
|
if (process.argv[2] === "session-start" || process.argv[2] === "session-start-opencode") {
|
|
600
|
+
if ((0, securityInventory_1.securityInventoryDue)()) {
|
|
601
|
+
try {
|
|
602
|
+
const child = (0, child_process_1.spawn)(process.execPath, [process.argv[1], "security-inventory-upload", hookInput.cwd || process.cwd()], {
|
|
603
|
+
detached: true,
|
|
604
|
+
stdio: "ignore",
|
|
605
|
+
});
|
|
606
|
+
child.unref();
|
|
607
|
+
}
|
|
608
|
+
catch { /* daily inventory is best-effort */ }
|
|
609
|
+
}
|
|
447
610
|
await runSessionStart(hookInput, process.argv[2] === "session-start-opencode" ? "opencode" : "claude-code");
|
|
448
611
|
return;
|
|
449
612
|
}
|
|
@@ -661,6 +824,7 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
661
824
|
// worktree's branch+sha so all the resulting PRs attribute. branch/headSha
|
|
662
825
|
// remain the primary (most-recent) values for back-compat.
|
|
663
826
|
const keys = (0, utils_1.collectCaptureKeys)(captureDirs);
|
|
827
|
+
const gitRefs = (0, utils_1.collectCaptureGitRefs)(captureDirs);
|
|
664
828
|
// Union the worktree-dir branches with the branches the agent actually
|
|
665
829
|
// committed/pushed to (parsed from the transcript's git output). Like the
|
|
666
830
|
// SHAs below, these are independent of end-of-session filesystem state, so
|
|
@@ -678,6 +842,7 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
678
842
|
const authoredShas = (0, utils_1.parseAuthoredShas)(transcriptContent, workspaceRoot);
|
|
679
843
|
const headShas = [...new Set([...keys.headShas, ...authoredShas])];
|
|
680
844
|
const headSha = keys.headSha ?? headShas[0] ?? null;
|
|
845
|
+
const pullRequestUrls = (0, utils_1.parsePullRequestUrls)(transcriptContent);
|
|
681
846
|
// Cost/velocity/steering signals parsed straight from the transcript we're
|
|
682
847
|
// already holding — no extra hooks, no extra git/process calls. parseUsage
|
|
683
848
|
// keeps the literal mode (it has its own desktop parser) and reads the RAW
|
|
@@ -700,6 +865,8 @@ async function runCaptureFinishInner(input, config, session_id, transcript_path,
|
|
|
700
865
|
headSha,
|
|
701
866
|
branches,
|
|
702
867
|
headShas,
|
|
868
|
+
gitRefs,
|
|
869
|
+
pullRequestUrls,
|
|
703
870
|
inputTokens: usage.inputTokens,
|
|
704
871
|
outputTokens: usage.outputTokens,
|
|
705
872
|
cacheTokens: usage.cacheTokens,
|
package/dist/security.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type SecurityHookPhase = "pre" | "post" | "failure";
|
|
2
|
-
export type AgentSurface = "claude-code" | "codex";
|
|
2
|
+
export type AgentSurface = "claude-code" | "codex" | "codie-harness";
|
|
3
3
|
export interface RawSecurityHookInput {
|
|
4
4
|
session_id?: string;
|
|
5
5
|
sessionId?: string;
|
|
@@ -17,6 +17,7 @@ export interface RawSecurityHookInput {
|
|
|
17
17
|
duration_ms?: number;
|
|
18
18
|
durationMs?: number;
|
|
19
19
|
error?: unknown;
|
|
20
|
+
prompt?: string;
|
|
20
21
|
}
|
|
21
22
|
export interface SecurityWorkflowEvent {
|
|
22
23
|
schemaVersion: 1;
|
|
@@ -30,5 +31,67 @@ export interface SecurityWorkflowEvent {
|
|
|
30
31
|
toolDurationMs?: number;
|
|
31
32
|
payload: unknown;
|
|
32
33
|
redactionCount: number;
|
|
34
|
+
localDecision?: LocalSecurityAction;
|
|
35
|
+
localCategory?: LocalSecurityDecision["category"];
|
|
36
|
+
endpointId?: string;
|
|
37
|
+
endpointName?: string;
|
|
38
|
+
cliVersion?: string;
|
|
33
39
|
}
|
|
40
|
+
export type LocalSecurityAction = "allow" | "alert" | "block";
|
|
41
|
+
export type SecurityCategory = "secret_in_prompt" | "secret_exfiltration" | "destructive_outside_workspace" | "sensitive_file_read" | "sensitive_file_write" | "protected_agent_config_write" | "unapproved_network_egress" | "untrusted_mcp_tool" | "unapproved_command" | "obfuscated_shell_command" | "package_install" | "sensitive_read_then_egress";
|
|
42
|
+
export interface EndpointSecurityPolicy {
|
|
43
|
+
schemaVersion: 1;
|
|
44
|
+
policyVersion: number;
|
|
45
|
+
enforcementMode: "alert" | "block";
|
|
46
|
+
rules: Partial<Record<SecurityCategory, LocalSecurityAction>>;
|
|
47
|
+
constraints?: {
|
|
48
|
+
allowedMcpServers?: string[];
|
|
49
|
+
allowedHosts?: string[];
|
|
50
|
+
allowedCommands?: string[];
|
|
51
|
+
};
|
|
52
|
+
exceptions: Array<{
|
|
53
|
+
scope: "company" | "user" | "source" | "repository" | "tool" | "category";
|
|
54
|
+
scopeValue: string;
|
|
55
|
+
}>;
|
|
56
|
+
fetchedAt: number;
|
|
57
|
+
signature?: {
|
|
58
|
+
algorithm: "Ed25519";
|
|
59
|
+
keyId: string;
|
|
60
|
+
signedAt: number;
|
|
61
|
+
value: string;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export interface LocalSecurityDecision {
|
|
65
|
+
action: LocalSecurityAction;
|
|
66
|
+
category?: SecurityCategory;
|
|
67
|
+
reason?: string;
|
|
68
|
+
}
|
|
69
|
+
export declare function withEndpointIdentity(event: SecurityWorkflowEvent, identity: {
|
|
70
|
+
endpointId?: string;
|
|
71
|
+
endpointName?: string;
|
|
72
|
+
cliVersion?: string;
|
|
73
|
+
}): SecurityWorkflowEvent;
|
|
74
|
+
/**
|
|
75
|
+
* Runs before a user prompt reaches the model. This is intentionally narrow:
|
|
76
|
+
* block credential material, but allow safe references such as
|
|
77
|
+
* `$OPENAI_API_KEY` so developers can ask agents to use environment variables
|
|
78
|
+
* without pasting their values.
|
|
79
|
+
*/
|
|
80
|
+
export declare function evaluatePromptSecurity(prompt: string | undefined, policy?: EndpointSecurityPolicy, source?: AgentSurface): LocalSecurityDecision;
|
|
81
|
+
/** Runs synchronously before a tool executes. No network or model is involved. */
|
|
82
|
+
export declare function evaluateToolSecurity(input: RawSecurityHookInput, phase: SecurityHookPhase, policy?: EndpointSecurityPolicy, source?: AgentSurface, sessionContext?: {
|
|
83
|
+
sensitiveReadAt?: number;
|
|
84
|
+
now?: number;
|
|
85
|
+
}): LocalSecurityDecision;
|
|
86
|
+
export declare function securityInputHasSensitivePath(input: RawSecurityHookInput): boolean;
|
|
34
87
|
export declare function buildSecurityWorkflowEvent(input: RawSecurityHookInput, phase: SecurityHookPhase, source: AgentSurface, now?: () => number): SecurityWorkflowEvent | null;
|
|
88
|
+
/**
|
|
89
|
+
* Captures the intent side of the causal chain without weakening the local
|
|
90
|
+
* privacy boundary. A prompt that the deterministic policy blocks never
|
|
91
|
+
* becomes an uploadable event; allowed prompts are redacted and bounded using
|
|
92
|
+
* the same sanitizer as tool payloads.
|
|
93
|
+
*/
|
|
94
|
+
export declare function buildSecurityPromptEvent(input: Pick<RawSecurityHookInput, "session_id" | "sessionId" | "cwd" | "prompt">, source: AgentSurface, now?: () => number, policy?: EndpointSecurityPolicy): SecurityWorkflowEvent | null;
|
|
95
|
+
/** Audit-safe evidence for a blocked prompt. The secret-bearing prompt is
|
|
96
|
+
* intentionally omitted; administrators still see that prevention occurred. */
|
|
97
|
+
export declare function buildBlockedSecurityPromptEvent(input: Pick<RawSecurityHookInput, "session_id" | "sessionId" | "cwd">, source: AgentSurface, category: NonNullable<LocalSecurityDecision["category"]>, now?: () => number): SecurityWorkflowEvent | null;
|