codiedev 0.9.56 → 0.10.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +5 -4
  2. package/dist/captureLog.d.ts +1 -1
  3. package/dist/cli.js +11 -1
  4. package/dist/codexLimitScan.d.ts +0 -2
  5. package/dist/codexLimitScan.js +2 -15
  6. package/dist/codie.js +146 -1
  7. package/dist/codieLauncher.d.ts +5 -0
  8. package/dist/codieLauncher.js +23 -1
  9. package/dist/codieRuntime.d.ts +11 -1
  10. package/dist/codieRuntime.js +20 -3
  11. package/dist/codieRuntimeInstall.d.ts +12 -11
  12. package/dist/codieRuntimeInstall.js +3 -1
  13. package/dist/codieStartup.d.ts +26 -0
  14. package/dist/codieStartup.js +30 -0
  15. package/dist/commands/doctor.js +8 -0
  16. package/dist/commands/security.d.ts +1 -0
  17. package/dist/commands/security.js +156 -0
  18. package/dist/connect.js +56 -4
  19. package/dist/cursorHooks.d.ts +16 -2
  20. package/dist/cursorHooks.js +31 -6
  21. package/dist/hook.js +232 -22
  22. package/dist/limitEvent.d.ts +5 -1
  23. package/dist/limitEvent.js +6 -27
  24. package/dist/security.d.ts +102 -0
  25. package/dist/security.js +437 -0
  26. package/dist/securityDeepScan.d.ts +41 -0
  27. package/dist/securityDeepScan.js +248 -0
  28. package/dist/securityDoctor.d.ts +32 -0
  29. package/dist/securityDoctor.js +63 -0
  30. package/dist/securityFeature.d.ts +5 -0
  31. package/dist/securityFeature.js +7 -0
  32. package/dist/securityInstall.d.ts +63 -0
  33. package/dist/securityInstall.js +239 -0
  34. package/dist/securityInventory.d.ts +54 -0
  35. package/dist/securityInventory.js +556 -0
  36. package/dist/securityPilot.d.ts +19 -0
  37. package/dist/securityPilot.js +61 -0
  38. package/dist/securityPolicy.d.ts +33 -0
  39. package/dist/securityPolicy.js +188 -0
  40. package/dist/securitySession.d.ts +6 -0
  41. package/dist/securitySession.js +72 -0
  42. package/dist/upgradeNudge.d.ts +1 -0
  43. package/dist/upgradeNudge.js +1 -0
  44. package/dist/utils.d.ts +12 -6
  45. package/dist/utils.js +201 -38
  46. package/package.json +11 -2
package/README.md CHANGED
@@ -12,7 +12,7 @@ The CLI will prompt you for an API token (get one at https://codiedev.com/portal
12
12
 
13
13
  - **Claude Code** — `SessionEnd` hook in `~/.claude/settings.json`
14
14
  - **Codex** — `Stop` hook in `~/.codex/hooks.json`
15
- - **Cursor** — native session, backup, and capture hooks in `~/.cursor/hooks.json`
15
+ - **Cursor** — native session, prompt-security, tool-security, backup, and capture hooks in `~/.cursor/hooks.json`
16
16
  - **OpenCode / Codie** — plugin in `~/.config/opencode/plugins/codiedev.js`
17
17
 
18
18
  Installed clients are wired automatically. The OpenCode adapter is staged even
@@ -51,9 +51,10 @@ Codex doesn't currently expose a `SessionEnd` event, so the hook runs on `Stop`
51
51
 
52
52
  ## Cursor note
53
53
 
54
- Cursor uses its native hook events for session start/end and stop-time backup.
55
- Cursor's `beforeSubmitPrompt` currently cannot inject dynamic context, so task-
56
- specific discovery remains unavailable there. Cursor sessions keep their source
54
+ Cursor uses its native hook events for session start/end, prompt blocking,
55
+ pre/post-tool security, and stop-time backup. Cursor's `beforeSubmitPrompt`
56
+ cannot inject dynamic context today, so task-specific discovery is not claimed.
57
+ Cursor sessions keep their source
57
58
  label and real transcript statistics through the shared capture pipeline.
58
59
 
59
60
  ## OpenCode note
@@ -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" | "limit";
5
+ stage: "capture-finish" | "upload" | "upload-deferred" | "security" | "stderr" | "limit";
6
6
  sessionId: string | undefined;
7
7
  outcome: string;
8
8
  detail?: string;
package/dist/cli.js CHANGED
@@ -33,6 +33,7 @@ const share_1 = require("./commands/share");
33
33
  const send_1 = require("./commands/send");
34
34
  const react_1 = require("./commands/react");
35
35
  const capture_1 = require("./commands/capture");
36
+ const security_1 = require("./commands/security");
36
37
  const version_1 = require("./version");
37
38
  const HELP = `
38
39
  Codiedev CLI
@@ -43,7 +44,13 @@ Codiedev CLI
43
44
 
44
45
  Connect:
45
46
  codiedev connect Link Claude Code / Codex / Cursor to your org
46
- codiedev doctor Verify your setup after connecting
47
+ codiedev doctor Verify setup, capture, and Agent Security
48
+ codiedev security pilot Run safe synthetic policy checks
49
+ codiedev security inventory Inventory MCPs, skills, hooks, plugins,
50
+ instructions, and editor extensions
51
+ codiedev security scan --deep Run installed open-source scanners too
52
+ codiedev security sync Fetch administrator policy locally
53
+ codiedev security rollback --yes Restore pre-update hook configuration
47
54
 
48
55
  Harness:
49
56
  codie Launch the governed Codie harness.
@@ -451,6 +458,9 @@ async function main() {
451
458
  case "verify":
452
459
  await (0, doctor_1.runDoctor)(rest);
453
460
  return;
461
+ case "security":
462
+ await (0, security_1.runSecurityCommand)(rest);
463
+ return;
454
464
  case "push":
455
465
  await (0, push_1.runPush)(rest);
456
466
  return;
@@ -45,8 +45,6 @@ export interface SweepArgs {
45
45
  scanOptions?: ScanOptions;
46
46
  /** How many rollouts may actually be READ in one sweep. */
47
47
  maxScans?: number;
48
- /** Stop starting new scans once this many ms have elapsed. */
49
- deadlineMs?: number;
50
48
  }
51
49
  /**
52
50
  * Report every Codex block this machine has not reported yet. Returns how many
@@ -382,20 +382,8 @@ function recordCodexLimitReported(filePath, at, statePath = exports.CODEX_LIMIT_
382
382
  // Candidates are listed generously and settled by one statSync each; only
383
383
  // files that actually changed cost a read. The budget therefore bounds work
384
384
  // without letting idle sessions bury an unreported block.
385
- //
386
- // The budget binds only on a COLD START, where nothing has been seen before,
387
- // so it must be big enough to finish that first pass. A budget of 8 silently
388
- // starved the older blocked rollouts on a real machine: 49 candidates in the
389
- // window, the newest 8 all unblocked, nothing reported. A full pass over
390
- // those 49 costs ~205ms — the backward scan stops at the marker rather than
391
- // reading each file to its cap — so the budget can afford to cover them.
392
385
  const DEFAULT_CANDIDATES = 200;
393
- const DEFAULT_MAX_SCANS = 64;
394
- // The budget bounds how MANY files are read; this bounds how LONG. A count is
395
- // not enough on a slow or network-mounted disk, and this runs on the user's
396
- // prompt clock. Whatever is not reached stays unrecorded, so the next prompt
397
- // picks it up rather than skipping it.
398
- const DEFAULT_DEADLINE_MS = 1_500;
386
+ const DEFAULT_MAX_SCANS = 8;
399
387
  /**
400
388
  * Report every Codex block this machine has not reported yet. Returns how many
401
389
  * events were posted. Never throws — it runs inside a hook.
@@ -419,7 +407,6 @@ async function sweepCodexLimits(args) {
419
407
  const now = Date.now();
420
408
  const touched = {};
421
409
  let budget = args.maxScans ?? DEFAULT_MAX_SCANS;
422
- const deadline = now + (args.deadlineMs ?? DEFAULT_DEADLINE_MS);
423
410
  const pending = [];
424
411
  for (const file of files) {
425
412
  const print = fingerprint(file);
@@ -430,7 +417,7 @@ async function sweepCodexLimits(args) {
430
417
  // it holds nothing new and does not spend the read budget.
431
418
  if (record && record.size === print.size && record.mtime === print.mtime)
432
419
  continue;
433
- if (budget <= 0 || Date.now() >= deadline)
420
+ if (budget <= 0)
434
421
  break;
435
422
  budget -= 1;
436
423
  const hit = scanCodexRolloutForLimit(file, args.scanOptions);
package/dist/codie.js CHANGED
@@ -53,6 +53,7 @@ const configDir_1 = require("./configDir");
53
53
  const codieLauncher_1 = require("./codieLauncher");
54
54
  const codieRuntime_1 = require("./codieRuntime");
55
55
  const codieRuntimeInstall_1 = require("./codieRuntimeInstall");
56
+ const codieStartup_1 = require("./codieStartup");
56
57
  const upgradeNudge_1 = require("./upgradeNudge");
57
58
  function git(args) {
58
59
  const result = (0, child_process_1.spawnSync)("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -169,6 +170,120 @@ function installBrandWordmarkUnsafe(runtime) {
169
170
  (0, child_process_1.spawnSync)("codesign", ["--force", "--sign", "-", runtime], { stdio: "ignore" });
170
171
  }
171
172
  }
173
+ async function runNonInteractive(runtime, runtimeArgs, env) {
174
+ const detached = process.platform !== "win32";
175
+ const child = (0, child_process_1.spawn)(runtime, runtimeArgs, {
176
+ stdio: ["pipe", "pipe", "pipe"],
177
+ env,
178
+ detached,
179
+ });
180
+ if (child.stdin)
181
+ process.stdin.pipe(child.stdin);
182
+ const signalChild = (signal) => {
183
+ if (detached && child.pid) {
184
+ try {
185
+ process.kill(-child.pid, signal);
186
+ return;
187
+ }
188
+ catch {
189
+ // Fall back to the direct child if its process group already exited.
190
+ }
191
+ }
192
+ child.kill(signal);
193
+ };
194
+ const forwardInterrupt = () => signalChild("SIGINT");
195
+ const forwardTermination = () => signalChild("SIGTERM");
196
+ process.once("SIGINT", forwardInterrupt);
197
+ process.once("SIGTERM", forwardTermination);
198
+ return await new Promise((resolve, reject) => {
199
+ let blockReason;
200
+ const errorPrefixes = [Buffer.from("\x1b[91m\x1b[1mError:"), Buffer.from("Error: {")];
201
+ const retainedPrefixBytes = Math.max(...errorPrefixes.map((prefix) => prefix.length)) - 1;
202
+ // OpenCode's generic UnknownError and the adapter's private reason can be
203
+ // delivered in either order — and the generic block can arrive on stdout
204
+ // or stderr. Gate both streams by content, retaining only a possible final
205
+ // error block. Ordinary output stays streaming apart from the few bytes
206
+ // needed to recognize a split prefix.
207
+ const createOutputGate = (write) => {
208
+ let tail = Buffer.alloc(0);
209
+ let possibleError;
210
+ return {
211
+ push(chunk, blocked) {
212
+ if (blocked) {
213
+ if (possibleError)
214
+ possibleError.push(Buffer.from(chunk));
215
+ return;
216
+ }
217
+ if (possibleError) {
218
+ possibleError.push(Buffer.from(chunk));
219
+ return;
220
+ }
221
+ const combined = Buffer.concat([tail, chunk]);
222
+ let errorStart = -1;
223
+ for (const prefix of errorPrefixes) {
224
+ const index = combined.indexOf(prefix);
225
+ if (index >= 0 && (errorStart < 0 || index < errorStart))
226
+ errorStart = index;
227
+ }
228
+ if (errorStart >= 0) {
229
+ if (errorStart > 0)
230
+ write(combined.subarray(0, errorStart));
231
+ possibleError = [combined.subarray(errorStart)];
232
+ tail = Buffer.alloc(0);
233
+ return;
234
+ }
235
+ const flushBytes = Math.max(0, combined.length - retainedPrefixBytes);
236
+ if (flushBytes > 0)
237
+ write(combined.subarray(0, flushBytes));
238
+ tail = combined.subarray(flushBytes);
239
+ },
240
+ finish(blocked) {
241
+ if (tail.length > 0)
242
+ write(tail);
243
+ const heldError = possibleError ? Buffer.concat(possibleError).toString() : "";
244
+ const genericBlockError = heldError.includes("UnknownError") &&
245
+ heldError.includes("Unexpected server error");
246
+ if (heldError && (!blocked || !genericBlockError))
247
+ write(heldError);
248
+ },
249
+ };
250
+ };
251
+ const stdoutGate = createOutputGate((chunk) => process.stdout.write(chunk));
252
+ const stderrGate = createOutputGate((chunk) => process.stderr.write(chunk));
253
+ child.stdout?.on("data", (chunk) => {
254
+ stdoutGate.push(Buffer.from(chunk), Boolean(blockReason));
255
+ });
256
+ child.stderr?.on("data", (chunk) => {
257
+ const text = chunk.toString();
258
+ const reason = (0, codieLauncher_1.extractCodieSecurityBlock)(text);
259
+ if (reason) {
260
+ blockReason = reason;
261
+ signalChild("SIGTERM");
262
+ return;
263
+ }
264
+ stderrGate.push(Buffer.from(chunk), Boolean(blockReason));
265
+ });
266
+ child.once("error", (error) => {
267
+ if (child.stdin)
268
+ process.stdin.unpipe(child.stdin);
269
+ process.removeListener("SIGINT", forwardInterrupt);
270
+ process.removeListener("SIGTERM", forwardTermination);
271
+ reject(error);
272
+ });
273
+ child.once("close", (status) => {
274
+ if (child.stdin)
275
+ process.stdin.unpipe(child.stdin);
276
+ process.removeListener("SIGINT", forwardInterrupt);
277
+ process.removeListener("SIGTERM", forwardTermination);
278
+ stdoutGate.finish(Boolean(blockReason));
279
+ stderrGate.finish(Boolean(blockReason));
280
+ if (blockReason) {
281
+ console.error(`Codie Security blocked this action:\n${blockReason}`);
282
+ }
283
+ resolve(status);
284
+ });
285
+ });
286
+ }
172
287
  async function main() {
173
288
  (0, codieRuntime_1.assertCodieNodeVersion)(process.versions.node);
174
289
  const connected = connectedCodiedevConfig();
@@ -186,6 +301,10 @@ async function main() {
186
301
  process.exitCode = update.status ?? 0;
187
302
  return;
188
303
  }
304
+ const policyRefresh = await (0, codieStartup_1.refreshCodieSecurityPolicy)(connected);
305
+ if (policyRefresh.status === "cached") {
306
+ console.error(`codie: security policy refresh failed; using the cached policy (${policyRefresh.error})`);
307
+ }
189
308
  const gatewayUrl = process.env.CODIEDEV_GATEWAY_URL || "https://gateway.codiedev.com/api/v1";
190
309
  const catalog = await (0, codieLauncher_1.fetchApprovedCatalog)({ baseUrl: gatewayUrl, key });
191
310
  const parsed = (0, codieLauncher_1.parseCodieArgs)(process.argv.slice(2));
@@ -196,11 +315,20 @@ async function main() {
196
315
  }
197
316
  (0, codieLauncher_1.validateCodieArgs)(parsed.args, catalog);
198
317
  const stateDir = (0, configDir_1.codiedevStateDir)();
199
- const runtime = (0, codieRuntimeInstall_1.ensureCodieRuntimeBinary)({
318
+ const runtime = await (0, codieRuntimeInstall_1.ensureCodieRuntimeBinary)({
200
319
  resolve: () => (0, codieRuntime_1.resolveCodieRuntime)({
201
320
  env: process.env,
202
321
  stateDir,
203
322
  existsSync: (candidate) => fs.existsSync(candidate),
323
+ readInstalledVersion: (candidate) => {
324
+ try {
325
+ const value = JSON.parse(fs.readFileSync(candidate, "utf8"));
326
+ return typeof value.version === "string" ? value.version : null;
327
+ }
328
+ catch {
329
+ return null;
330
+ }
331
+ },
204
332
  resolveBundled: () => {
205
333
  try {
206
334
  return require.resolve("opencode-ai/bin/opencode.exe");
@@ -211,6 +339,16 @@ async function main() {
211
339
  },
212
340
  }),
213
341
  install: (missing) => (0, codieRuntimeInstall_1.installCodieRuntimeOnDisk)(missing, stateDir),
342
+ confirmUpdate: async (outdated) => {
343
+ console.error("");
344
+ console.error(`Codie harness ${outdated.expectedVersion} is available (you're on ${outdated.currentVersion}).`);
345
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
346
+ console.error("Restart `codie` in an interactive terminal to update the harness.");
347
+ console.error("");
348
+ return false;
349
+ }
350
+ return await (0, upgradeNudge_1.promptYesNo)(" Update the harness now? [y/N] ");
351
+ },
214
352
  });
215
353
  const metadataPlugin = (0, url_1.pathToFileURL)(assetPath("opencode-plugin.mjs")).href;
216
354
  const capturePlugin = (0, codieLauncher_1.resolveCapturePlugin)({ homeDir: os.homedir(), existsSync: fs.existsSync });
@@ -232,6 +370,13 @@ async function main() {
232
370
  OPENCODE_CONFIG_CONTENT: JSON.stringify(config),
233
371
  };
234
372
  const runtimeArgs = (0, codieLauncher_1.withCodieWorkingDirectory)(parsed.args, process.cwd());
373
+ const nonInteractiveRun = runtimeArgs[0] === "run";
374
+ if (nonInteractiveRun)
375
+ env.CODIEDEV_SECURITY_BLOCK_OUTPUT = "marker";
376
+ if (nonInteractiveRun) {
377
+ process.exitCode = (await runNonInteractive(runtime, runtimeArgs, env)) ?? 1;
378
+ return;
379
+ }
235
380
  const result = (0, child_process_1.spawnSync)(runtime, runtimeArgs, { stdio: "inherit", env });
236
381
  if (result.error)
237
382
  throw result.error;
@@ -2,6 +2,11 @@ export interface CatalogModel {
2
2
  id: string;
3
3
  name?: string;
4
4
  }
5
+ export declare const CODIE_SECURITY_BLOCK_MARKER = "__CODIEDEV_SECURITY_BLOCK__";
6
+ /** Extract the private adapter marker used to carry a local block reason
7
+ * across OpenCode's generic 500 boundary. The marker never contains prompt or
8
+ * tool input — only the deterministic policy reason. */
9
+ export declare function extractCodieSecurityBlock(stderr: string): string | undefined;
5
10
  /**
6
11
  * Pin non-interactive runs to the directory where `codie` was launched.
7
12
  * OpenCode can reuse a previously-known project instance; without an explicit
@@ -42,7 +42,8 @@ var __importStar = (this && this.__importStar) || (function () {
42
42
  };
43
43
  })();
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
- exports.HIDDEN_WORDMARK = exports.CODIEDEV_WORDMARK = exports.LEGACY_CODIEDEV_WORDMARK = exports.STOCK_WORDMARK = exports.BRAND_PLUGIN_SPEC = void 0;
45
+ exports.HIDDEN_WORDMARK = exports.CODIEDEV_WORDMARK = exports.LEGACY_CODIEDEV_WORDMARK = exports.STOCK_WORDMARK = exports.BRAND_PLUGIN_SPEC = exports.CODIE_SECURITY_BLOCK_MARKER = void 0;
46
+ exports.extractCodieSecurityBlock = extractCodieSecurityBlock;
46
47
  exports.withCodieWorkingDirectory = withCodieWorkingDirectory;
47
48
  exports.bareModel = bareModel;
48
49
  exports.buildCodieConfig = buildCodieConfig;
@@ -55,6 +56,7 @@ exports.validateCodieArgs = validateCodieArgs;
55
56
  const path = __importStar(require("path"));
56
57
  const url_1 = require("url");
57
58
  const DEFAULT_NAMES = {
59
+ "openai/gpt-5.6-luna": "GPT-5.6 Luna",
58
60
  "openai/gpt-5.3-codex": "GPT-5.3 Codex",
59
61
  "openai/gpt-5.2-codex": "GPT-5.2 Codex",
60
62
  "openai/gpt-5.1-codex-mini": "GPT-5.1 Codex Mini",
@@ -62,6 +64,26 @@ const DEFAULT_NAMES = {
62
64
  "z-ai/glm-5.2": "GLM 5.2 · Fireworks ZDR",
63
65
  "moonshotai/kimi-k2.7-code": "Kimi K2.7 Code · Fireworks ZDR",
64
66
  };
67
+ exports.CODIE_SECURITY_BLOCK_MARKER = "__CODIEDEV_SECURITY_BLOCK__";
68
+ /** Extract the private adapter marker used to carry a local block reason
69
+ * across OpenCode's generic 500 boundary. The marker never contains prompt or
70
+ * tool input — only the deterministic policy reason. */
71
+ function extractCodieSecurityBlock(stderr) {
72
+ for (const line of stderr.split(/\r?\n/)) {
73
+ const markerIndex = line.indexOf(exports.CODIE_SECURITY_BLOCK_MARKER);
74
+ if (markerIndex < 0)
75
+ continue;
76
+ try {
77
+ const parsed = JSON.parse(line.slice(markerIndex + exports.CODIE_SECURITY_BLOCK_MARKER.length));
78
+ if (typeof parsed?.reason === "string" && parsed.reason.trim())
79
+ return parsed.reason.trim();
80
+ }
81
+ catch {
82
+ // Ignore malformed or foreign stderr and preserve the original runtime error.
83
+ }
84
+ }
85
+ return undefined;
86
+ }
65
87
  /**
66
88
  * Pin non-interactive runs to the directory where `codie` was launched.
67
89
  * OpenCode can reuse a previously-known project instance; without an explicit
@@ -1,4 +1,5 @@
1
1
  export declare const OPENCODE_RUNTIME_SPEC = "opencode-ai@1.18.4";
2
+ export declare const OPENCODE_RUNTIME_VERSION: string;
2
3
  export declare const CODIE_MIN_NODE_MAJOR = 20;
3
4
  export type CodieRuntime = {
4
5
  kind: "ready";
@@ -8,12 +9,21 @@ export type CodieRuntime = {
8
9
  installDir: string;
9
10
  binary: string;
10
11
  spec: string;
12
+ } | {
13
+ kind: "outdated";
14
+ installDir: string;
15
+ binary: string;
16
+ spec: string;
17
+ currentVersion: string;
18
+ expectedVersion: string;
11
19
  };
12
20
  export declare function codieRuntimeDir(stateDir: string): string;
13
- export declare function resolveCodieRuntime({ env, stateDir, existsSync, resolveBundled, }: {
21
+ export declare function installedRuntimePackageJson(stateDir: string): string;
22
+ export declare function resolveCodieRuntime({ env, stateDir, existsSync, readInstalledVersion, resolveBundled, }: {
14
23
  env: Record<string, string | undefined>;
15
24
  stateDir: string;
16
25
  existsSync: (candidate: string) => boolean;
26
+ readInstalledVersion?: (packageJson: string) => string | null;
17
27
  resolveBundled: () => string | null;
18
28
  }): CodieRuntime;
19
29
  export declare function assertCodieNodeVersion(version: string): void;
@@ -43,8 +43,9 @@ var __importStar = (this && this.__importStar) || (function () {
43
43
  };
44
44
  })();
45
45
  Object.defineProperty(exports, "__esModule", { value: true });
46
- exports.CODIE_MIN_NODE_MAJOR = exports.OPENCODE_RUNTIME_SPEC = void 0;
46
+ exports.CODIE_MIN_NODE_MAJOR = exports.OPENCODE_RUNTIME_VERSION = exports.OPENCODE_RUNTIME_SPEC = void 0;
47
47
  exports.codieRuntimeDir = codieRuntimeDir;
48
+ exports.installedRuntimePackageJson = installedRuntimePackageJson;
48
49
  exports.resolveCodieRuntime = resolveCodieRuntime;
49
50
  exports.assertCodieNodeVersion = assertCodieNodeVersion;
50
51
  const path = __importStar(require("path"));
@@ -52,6 +53,7 @@ const path = __importStar(require("path"));
52
53
  // recognizes byte-for-byte, so a floating version would silently regress the
53
54
  // terminal branding whenever OpenCode ships a new build.
54
55
  exports.OPENCODE_RUNTIME_SPEC = "opencode-ai@1.18.4";
56
+ exports.OPENCODE_RUNTIME_VERSION = exports.OPENCODE_RUNTIME_SPEC.slice(exports.OPENCODE_RUNTIME_SPEC.lastIndexOf("@") + 1);
55
57
  // The harness needs 20; capture does not. package.json `engines` stays >=18 so
56
58
  // `codiedev connect` still installs for Node 18 users — this floor is enforced
57
59
  // at `codie` launch instead.
@@ -62,7 +64,10 @@ function codieRuntimeDir(stateDir) {
62
64
  function installedBinary(stateDir) {
63
65
  return path.join(codieRuntimeDir(stateDir), "node_modules", "opencode-ai", "bin", "opencode.exe");
64
66
  }
65
- function resolveCodieRuntime({ env, stateDir, existsSync, resolveBundled, }) {
67
+ function installedRuntimePackageJson(stateDir) {
68
+ return path.join(codieRuntimeDir(stateDir), "node_modules", "opencode-ai", "package.json");
69
+ }
70
+ function resolveCodieRuntime({ env, stateDir, existsSync, readInstalledVersion, resolveBundled, }) {
66
71
  // 1. Explicit override — the air-gapped / pre-provisioned escape hatch.
67
72
  const override = env.CODIEDEV_OPENCODE_BINARY?.trim();
68
73
  if (override) {
@@ -78,8 +83,20 @@ function resolveCodieRuntime({ env, stateDir, existsSync, resolveBundled, }) {
78
83
  return { kind: "ready", binary: bundled };
79
84
  // 3. The lazily installed engine.
80
85
  const binary = installedBinary(stateDir);
81
- if (existsSync(binary))
86
+ if (existsSync(binary)) {
87
+ const currentVersion = readInstalledVersion?.(installedRuntimePackageJson(stateDir)) ?? null;
88
+ if (currentVersion && currentVersion !== exports.OPENCODE_RUNTIME_VERSION) {
89
+ return {
90
+ kind: "outdated",
91
+ installDir: codieRuntimeDir(stateDir),
92
+ binary,
93
+ spec: exports.OPENCODE_RUNTIME_SPEC,
94
+ currentVersion,
95
+ expectedVersion: exports.OPENCODE_RUNTIME_VERSION,
96
+ };
97
+ }
82
98
  return { kind: "ready", binary };
99
+ }
83
100
  return { kind: "missing", installDir: codieRuntimeDir(stateDir), binary, spec: exports.OPENCODE_RUNTIME_SPEC };
84
101
  }
85
102
  function assertCodieNodeVersion(version) {
@@ -1,9 +1,10 @@
1
1
  import { CodieRuntime } from "./codieRuntime";
2
+ type InstallableCodieRuntime = Exclude<CodieRuntime, {
3
+ kind: "ready";
4
+ }>;
2
5
  export declare function buildRuntimeInstallArgs(stagingDir: string, spec: string): string[];
3
6
  export interface InstallDeps {
4
- runtime: Extract<CodieRuntime, {
5
- kind: "missing";
6
- }>;
7
+ runtime: InstallableCodieRuntime;
7
8
  makeStagingDir: () => string;
8
9
  run: (args: string[]) => {
9
10
  status: number | null;
@@ -15,13 +16,13 @@ export interface InstallDeps {
15
16
  log: (message: string) => void;
16
17
  }
17
18
  export declare function installCodieRuntime(deps: InstallDeps): void;
18
- export declare function ensureCodieRuntimeBinary({ resolve, install, }: {
19
+ export declare function ensureCodieRuntimeBinary({ resolve, install, confirmUpdate, }: {
19
20
  resolve: () => CodieRuntime;
20
- install: (runtime: Extract<CodieRuntime, {
21
- kind: "missing";
22
- }>) => void;
23
- }): string;
24
- export declare function installCodieRuntimeOnDisk(runtime: Extract<CodieRuntime, {
25
- kind: "missing";
26
- }>, stateDir: string): void;
21
+ install: (runtime: InstallableCodieRuntime) => void;
22
+ confirmUpdate?: (runtime: Extract<CodieRuntime, {
23
+ kind: "outdated";
24
+ }>) => Promise<boolean>;
25
+ }): Promise<string>;
26
+ export declare function installCodieRuntimeOnDisk(runtime: InstallableCodieRuntime, stateDir: string): void;
27
27
  export declare function defaultStagingRoot(): string;
28
+ export {};
@@ -98,10 +98,12 @@ function installCodieRuntime(deps) {
98
98
  }
99
99
  }
100
100
  // Resolve the engine, downloading it once if this is the first `codie` run.
101
- function ensureCodieRuntimeBinary({ resolve, install, }) {
101
+ async function ensureCodieRuntimeBinary({ resolve, install, confirmUpdate = async () => false, }) {
102
102
  const runtime = resolve();
103
103
  if (runtime.kind === "ready")
104
104
  return runtime.binary;
105
+ if (runtime.kind === "outdated" && !(await confirmUpdate(runtime)))
106
+ return runtime.binary;
105
107
  install(runtime);
106
108
  return runtime.binary;
107
109
  }
@@ -0,0 +1,26 @@
1
+ import type { AgentSurface, EndpointSecurityPolicy } from "./security";
2
+ import type { AgentSecurityEntitlement } from "./securityFeature";
3
+ export interface CodieStartupConfig extends AgentSecurityEntitlement {
4
+ backendUrl?: string;
5
+ token?: string;
6
+ companyId?: string;
7
+ deviceId?: string;
8
+ }
9
+ export type CodiePolicyRefreshResult = {
10
+ status: "disabled";
11
+ } | {
12
+ status: "synced";
13
+ policy: EndpointSecurityPolicy;
14
+ } | {
15
+ status: "cached";
16
+ error: string;
17
+ };
18
+ /** Refresh the endpoint policy before Codie hands the terminal to OpenCode.
19
+ * Enforcement remains available offline because hooks keep using the last
20
+ * verified company-scoped policy when this best-effort refresh cannot land. */
21
+ export declare function refreshCodieSecurityPolicy(config: CodieStartupConfig, syncPolicy?: (config: {
22
+ backendUrl: string;
23
+ token: string;
24
+ companyId: string;
25
+ deviceId?: string;
26
+ }, source?: AgentSurface) => Promise<EndpointSecurityPolicy>): Promise<CodiePolicyRefreshResult>;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.refreshCodieSecurityPolicy = refreshCodieSecurityPolicy;
4
+ const securityFeature_1 = require("./securityFeature");
5
+ const securityPolicy_1 = require("./securityPolicy");
6
+ /** Refresh the endpoint policy before Codie hands the terminal to OpenCode.
7
+ * Enforcement remains available offline because hooks keep using the last
8
+ * verified company-scoped policy when this best-effort refresh cannot land. */
9
+ async function refreshCodieSecurityPolicy(config, syncPolicy = securityPolicy_1.syncEndpointSecurityPolicy) {
10
+ if (!(0, securityFeature_1.isAgentSecurityEnabled)(config))
11
+ return { status: "disabled" };
12
+ if (!config.backendUrl || !config.token || !config.companyId) {
13
+ return { status: "cached", error: "connected account details are incomplete" };
14
+ }
15
+ try {
16
+ const policy = await syncPolicy({
17
+ backendUrl: config.backendUrl,
18
+ token: config.token,
19
+ companyId: config.companyId,
20
+ ...(config.deviceId ? { deviceId: config.deviceId } : {}),
21
+ }, "codie-harness");
22
+ return { status: "synced", policy };
23
+ }
24
+ catch (error) {
25
+ return {
26
+ status: "cached",
27
+ error: error instanceof Error ? error.message : String(error),
28
+ };
29
+ }
30
+ }
@@ -45,6 +45,8 @@ 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");
49
+ const securityFeature_1 = require("../securityFeature");
48
50
  const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
49
51
  const CLAUDE_INSTRUCTIONS_PATH = path.join(os.homedir(), ".claude", "CLAUDE.md");
50
52
  const CODEX_HOOKS_PATH = path.join(os.homedir(), ".codex", "hooks.json");
@@ -482,6 +484,12 @@ async function runDoctor(_args) {
482
484
  detail: "not installed on this machine",
483
485
  });
484
486
  }
487
+ // Agent Security is part of installation health, not a separate setup
488
+ // concept. Keep `codiedev security doctor` as an alias for focused output,
489
+ // while the main doctor remains the one command users need to remember.
490
+ if ((0, securityFeature_1.isAgentSecurityEnabled)(config)) {
491
+ checks.push(...(0, securityDoctor_1.securityDoctorChecks)((0, securityDoctor_1.collectSecurityDoctorReport)(config.companyId)));
492
+ }
485
493
  // 8. gh / glab CLIs (optional — only needed for reverse-ticket from a
486
494
  // specific PR/MR URL; branch mode works without either).
487
495
  checks.push({
@@ -0,0 +1 @@
1
+ export declare function runSecurityCommand(args: string[]): Promise<void>;