codex-agent-view 0.4.7 → 0.5.0

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.
@@ -10,6 +10,7 @@ import {
10
10
  writeFile,
11
11
  } from "node:fs/promises";
12
12
  import { spawn } from "node:child_process";
13
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
13
14
  import { homedir } from "node:os";
14
15
  import { dirname, join, resolve } from "node:path";
15
16
  import { fileURLToPath } from "node:url";
@@ -17,6 +18,7 @@ import { fileURLToPath } from "node:url";
17
18
  import { startMonitorServer } from "../src/runtime/server.mjs";
18
19
  import {
19
20
  DEFAULT_PORT,
21
+ LOOPBACK_HOST,
20
22
  ensureViewerToken,
21
23
  readRuntimeInfo,
22
24
  readViewerToken,
@@ -32,6 +34,22 @@ const PLUGIN_ID = "codex-agent-view@codex-agent-view";
32
34
  const MARKETPLACE_NAME = "codex-agent-view";
33
35
  const BUNDLE_MARKER = ".codex-agent-view-owned.json";
34
36
  const BUNDLE_MARKER_SCHEMA_VERSION = 1;
37
+ const PREPARE_LIVE_VIEW_WAIT_MS = 2_000;
38
+ const PREPARE_LIVE_VIEW_POLL_MS = 40;
39
+ const VIEWER_GRANT_TIMEOUT_MS = 1_000;
40
+ const MAX_BOOTSTRAP_CREDENTIAL_LENGTH = 1_024;
41
+ const SIGNED_BOOTSTRAP_CREDENTIAL_PATTERN =
42
+ /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{43}$/;
43
+ const OWNERSHIP_PROOF_DOMAIN = "codex-agent-view/runtime-ownership/v1";
44
+ const OWNERSHIP_PROOF_TIMEOUT_MS = 1_000;
45
+ const EXTERNAL_BROWSER_OPEN_TIMEOUT_MS = 3_000;
46
+ const KNOWN_PRE_PROOF_MANAGED_VERSIONS = new Set([
47
+ "0.2.0", "0.2.1",
48
+ "0.3.0", "0.3.1", "0.3.2",
49
+ "0.4.0", "0.4.1", "0.4.2", "0.4.3", "0.4.4", "0.4.5", "0.4.6", "0.4.7",
50
+ ]);
51
+ const CANONICAL_THREAD_ID_PATTERN =
52
+ /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i;
35
53
  const INSTALL_ENTRIES = [
36
54
  ".agents",
37
55
  ".codex-plugin",
@@ -57,6 +75,7 @@ function printHelp() {
57
75
 
58
76
  Usage:
59
77
  codex-agent-view start [--port <port>] [--open]
78
+ codex-agent-view open
60
79
  codex-agent-view status [--json]
61
80
  codex-agent-view doctor [--json]
62
81
  codex-agent-view install
@@ -65,9 +84,17 @@ Usage:
65
84
 
66
85
  The monitor is read-only and binds only to 127.0.0.1.
67
86
  Start prints the local URL without opening an external browser unless --open is set.
87
+ Open prepares an authenticated live view and launches it in the default browser.
68
88
  `);
69
89
  }
70
90
 
91
+ class LiveViewPreparationError extends Error {
92
+ constructor(code) {
93
+ super(code);
94
+ this.code = code;
95
+ }
96
+ }
97
+
71
98
  function parseStartArgs(args) {
72
99
  let open = false;
73
100
  let legacyNoOpen = false;
@@ -142,20 +169,43 @@ function run(command, args, { allowFailure = false } = {}) {
142
169
  });
143
170
  }
144
171
 
145
- function openBrowser(url) {
146
- const command =
147
- process.platform === "darwin"
148
- ? ["open", [url]]
149
- : process.platform === "win32"
150
- ? ["cmd", ["/c", "start", "", url]]
151
- : ["xdg-open", [url]];
152
- const child = spawn(command[0], command[1], {
153
- detached: true,
154
- shell: false,
155
- stdio: "ignore",
172
+ function externalBrowserCommand(url) {
173
+ if (process.platform === "darwin") return ["open", [url]];
174
+ if (process.platform === "win32") {
175
+ return ["rundll32.exe", ["url.dll,FileProtocolHandler", url]];
176
+ }
177
+ return ["xdg-open", [url]];
178
+ }
179
+
180
+ function openExternalBrowser(url) {
181
+ const [command, args] = externalBrowserCommand(url);
182
+ return new Promise((resolvePromise, reject) => {
183
+ const child = spawn(command, args, {
184
+ shell: false,
185
+ stdio: "ignore",
186
+ });
187
+ let settled = false;
188
+ const settle = (callback, value) => {
189
+ if (settled) return;
190
+ settled = true;
191
+ clearTimeout(timeout);
192
+ callback(value);
193
+ };
194
+ const timeout = setTimeout(() => {
195
+ child.kill();
196
+ settle(reject, new LiveViewPreparationError("browser_open_timeout"));
197
+ }, EXTERNAL_BROWSER_OPEN_TIMEOUT_MS);
198
+ child.once("error", () => {
199
+ settle(reject, new LiveViewPreparationError("browser_open_failed"));
200
+ });
201
+ child.once("close", (code) => {
202
+ if (code === 0) {
203
+ settle(resolvePromise);
204
+ return;
205
+ }
206
+ settle(reject, new LiveViewPreparationError("browser_open_failed"));
207
+ });
156
208
  });
157
- child.unref();
158
- child.on("error", () => {});
159
209
  }
160
210
 
161
211
  async function start(args) {
@@ -189,12 +239,20 @@ async function start(args) {
189
239
  process.stdout.write(`Codex Agent View is running at ${monitor.url}\n`);
190
240
  process.stdout.write("Press Ctrl+C to stop the in-memory monitor.\n");
191
241
  if (options.open) {
192
- openBrowser(monitor.url);
242
+ try {
243
+ await openExternalBrowser(monitor.url);
244
+ } catch (error) {
245
+ await monitor.close();
246
+ throw error;
247
+ }
193
248
  }
194
249
  }
195
250
 
196
251
  async function fetchState() {
197
252
  const runtime = await readRuntimeInfo();
253
+ if ((await runtimeEndpointState(runtime)) !== "owned") {
254
+ throw new Error("the runtime endpoint was not identified as an owned monitor");
255
+ }
198
256
  const response = await fetch(`http://${runtime.host}:${runtime.port}/api/state`, {
199
257
  headers: { authorization: `Bearer ${runtime.token}` },
200
258
  signal: AbortSignal.timeout(1_500),
@@ -483,7 +541,7 @@ async function revokeViewerCredential(preflight) {
483
541
  };
484
542
  }
485
543
 
486
- async function runtimeEndpointState(runtime) {
544
+ async function legacyRuntimeEndpointState(runtime) {
487
545
  try {
488
546
  const response = await fetch(`http://${runtime.host}:${runtime.port}/api/state`, {
489
547
  headers: { authorization: `Bearer ${runtime.token}` },
@@ -502,15 +560,252 @@ async function runtimeEndpointState(runtime) {
502
560
  }
503
561
  }
504
562
 
505
- async function runtimeResponds(runtime) {
506
- return (await runtimeEndpointState(runtime)) === "owned";
563
+ async function runtimeEndpointState(
564
+ runtime,
565
+ { allowLegacyBearerProbe = false } = {},
566
+ ) {
567
+ const ownership = await runtimeOwnershipState(runtime);
568
+ if (ownership !== "unrelated" || !allowLegacyBearerProbe) {
569
+ return ownership;
570
+ }
571
+ return legacyRuntimeEndpointState(runtime);
572
+ }
573
+
574
+ async function runtimeResponds(runtime, options) {
575
+ return (await runtimeEndpointState(runtime, options)) === "owned";
576
+ }
577
+
578
+ function expectedOwnershipProof(nonce, runtimeToken) {
579
+ return createHmac("sha256", runtimeToken)
580
+ .update(OWNERSHIP_PROOF_DOMAIN)
581
+ .update("\0")
582
+ .update(nonce)
583
+ .digest("base64url");
584
+ }
585
+
586
+ async function runtimeOwnershipState(runtime) {
587
+ const nonce = randomBytes(32).toString("base64url");
588
+ let response;
589
+ try {
590
+ response = await fetch(
591
+ `http://${runtime.host}:${runtime.port}/api/internal/ownership-proof`,
592
+ {
593
+ body: JSON.stringify({ nonce }),
594
+ headers: { "content-type": "application/json" },
595
+ method: "POST",
596
+ signal: AbortSignal.timeout(OWNERSHIP_PROOF_TIMEOUT_MS),
597
+ },
598
+ );
599
+ } catch {
600
+ return "absent";
601
+ }
602
+ if (response.status !== 200) {
603
+ await response.body?.cancel();
604
+ return "unrelated";
605
+ }
606
+ const payload = await response.json().catch(() => null);
607
+ if (
608
+ payload === null ||
609
+ typeof payload !== "object" ||
610
+ Array.isArray(payload) ||
611
+ Object.keys(payload).sort().join(",") !== "proof,status" ||
612
+ payload.status !== "owned" ||
613
+ typeof payload.proof !== "string" ||
614
+ !/^[A-Za-z0-9_-]{43}$/.test(payload.proof)
615
+ ) {
616
+ return "unrelated";
617
+ }
618
+ const supplied = Buffer.from(payload.proof);
619
+ const expected = Buffer.from(expectedOwnershipProof(nonce, runtime.token));
620
+ return supplied.length === expected.length && timingSafeEqual(supplied, expected)
621
+ ? "owned"
622
+ : "unrelated";
623
+ }
624
+
625
+ async function inspectInstalledBundleForLiveView() {
626
+ const destination = join(runtimeDirectory(), "marketplace");
627
+ const stats = await pathExists(destination);
628
+ if (!stats) {
629
+ throw new LiveViewPreparationError("plugin_not_installed");
630
+ }
631
+ if (!stats.isDirectory() || stats.isSymbolicLink()) {
632
+ throw new LiveViewPreparationError("plugin_bundle_unowned");
633
+ }
634
+
635
+ const marker = await readJsonRegularFile(join(destination, BUNDLE_MARKER));
636
+ if (
637
+ marker?.schema_version !== BUNDLE_MARKER_SCHEMA_VERSION ||
638
+ marker?.package !== MARKETPLACE_NAME ||
639
+ marker?.plugin_id !== PLUGIN_ID
640
+ ) {
641
+ throw new LiveViewPreparationError("plugin_bundle_unowned");
642
+ }
643
+
644
+ const manifest = await readJsonRegularFile(
645
+ join(destination, ".codex-plugin", "plugin.json"),
646
+ );
647
+ if (manifest?.name !== MARKETPLACE_NAME || typeof manifest.version !== "string") {
648
+ throw new LiveViewPreparationError("plugin_bundle_invalid");
649
+ }
650
+ if (manifest.version !== (await packageVersion())) {
651
+ throw new LiveViewPreparationError("plugin_version_mismatch");
652
+ }
653
+ }
654
+
655
+ function autoStartEnvironment() {
656
+ const env = {};
657
+ for (const key of [
658
+ "CODEX_AGENT_VIEW_AUTO_START_PORT",
659
+ "CODEX_AGENT_VIEW_RUNTIME_DIR",
660
+ "SystemRoot",
661
+ ]) {
662
+ if (typeof process.env[key] === "string") {
663
+ env[key] = process.env[key];
664
+ }
665
+ }
666
+ return env;
667
+ }
668
+
669
+ function startMonitorDetached() {
670
+ const child = spawn(
671
+ process.execPath,
672
+ [fileURLToPath(new URL("../scripts/auto-start-monitor.mjs", import.meta.url))],
673
+ {
674
+ detached: true,
675
+ env: autoStartEnvironment(),
676
+ shell: false,
677
+ stdio: "ignore",
678
+ },
679
+ );
680
+ child.on("error", () => {});
681
+ child.unref();
682
+ }
683
+
684
+ async function liveViewRuntimeState() {
685
+ const runtime = await inspectRuntime();
686
+ if (runtime.kind === "unknown") {
687
+ throw new LiveViewPreparationError("runtime_record_invalid");
688
+ }
689
+ if (runtime.kind === "absent") {
690
+ return { kind: "not_running" };
691
+ }
692
+ const endpoint = await runtimeOwnershipState(runtime.info);
693
+ if (endpoint === "owned") {
694
+ return { info: runtime.info, kind: "owned" };
695
+ }
696
+ if (endpoint === "absent") {
697
+ return { kind: "not_running" };
698
+ }
699
+ throw new LiveViewPreparationError("unowned_runtime");
700
+ }
701
+
702
+ function inheritedExcludedSessionId() {
703
+ const inheritedThreadId = process.env.CODEX_THREAD_ID;
704
+ return (
705
+ typeof inheritedThreadId === "string" &&
706
+ CANONICAL_THREAD_ID_PATTERN.test(inheritedThreadId)
707
+ ? inheritedThreadId.toLowerCase()
708
+ : null
709
+ );
710
+ }
711
+
712
+ async function requestViewerGrant(runtime, excludeSessionId) {
713
+ let response;
714
+ try {
715
+ response = await fetch(
716
+ `http://${runtime.host}:${runtime.port}/api/internal/viewer-grant`,
717
+ {
718
+ body: JSON.stringify({ exclude_session_id: excludeSessionId }),
719
+ headers: {
720
+ authorization: `Bearer ${runtime.token}`,
721
+ "content-type": "application/json",
722
+ },
723
+ method: "POST",
724
+ signal: AbortSignal.timeout(VIEWER_GRANT_TIMEOUT_MS),
725
+ },
726
+ );
727
+ } catch (error) {
728
+ throw new LiveViewPreparationError(
729
+ error?.name === "TimeoutError" || error?.name === "AbortError"
730
+ ? "viewer_grant_timeout"
731
+ : "viewer_grant_unavailable",
732
+ );
733
+ }
734
+ if (response.status !== 201) {
735
+ await response.body?.cancel();
736
+ throw new LiveViewPreparationError("viewer_grant_rejected");
737
+ }
738
+ const payload = await response.json().catch(() => null);
739
+ const credential = payload?.bootstrap_credential;
740
+ if (
741
+ payload === null ||
742
+ typeof payload !== "object" ||
743
+ Array.isArray(payload) ||
744
+ Object.keys(payload).sort().join(",") !==
745
+ "bootstrap_credential,expires_in_ms,status" ||
746
+ payload.status !== "granted" ||
747
+ payload.expires_in_ms !== 60_000 ||
748
+ typeof credential !== "string" ||
749
+ credential.length > MAX_BOOTSTRAP_CREDENTIAL_LENGTH ||
750
+ !SIGNED_BOOTSTRAP_CREDENTIAL_PATTERN.test(credential) ||
751
+ credential === runtime.token ||
752
+ credential === runtime.viewer_token
753
+ ) {
754
+ throw new LiveViewPreparationError("viewer_grant_invalid_response");
755
+ }
756
+ return credential;
757
+ }
758
+
759
+ function liveViewTarget(runtime, bootstrapCredential) {
760
+ return `http://${LOOPBACK_HOST}:${runtime.port}/#grant=${encodeURIComponent(bootstrapCredential)}`;
761
+ }
762
+
763
+ async function prepareLiveViewTarget(args) {
764
+ if (args.length > 0) {
765
+ throw new LiveViewPreparationError("invalid_arguments");
766
+ }
767
+ await inspectInstalledBundleForLiveView();
768
+ let runtime = await liveViewRuntimeState();
769
+ if (runtime.kind !== "owned") {
770
+ startMonitorDetached();
771
+ const deadline = Date.now() + PREPARE_LIVE_VIEW_WAIT_MS;
772
+ do {
773
+ await new Promise((resolvePromise) =>
774
+ setTimeout(resolvePromise, PREPARE_LIVE_VIEW_POLL_MS),
775
+ );
776
+ runtime = await liveViewRuntimeState();
777
+ if (runtime.kind === "owned") break;
778
+ } while (Date.now() < deadline);
779
+ }
780
+ if (runtime.kind !== "owned") {
781
+ throw new LiveViewPreparationError("monitor_start_timeout");
782
+ }
783
+ const bootstrapCredential = await requestViewerGrant(
784
+ runtime.info,
785
+ inheritedExcludedSessionId(),
786
+ );
787
+ return liveViewTarget(runtime.info, bootstrapCredential);
788
+ }
789
+
790
+ async function openLiveView(args) {
791
+ try {
792
+ const target = await prepareLiveViewTarget(args);
793
+ await openExternalBrowser(target);
794
+ process.stdout.write("Codex Agent View opened in the default browser.\n");
795
+ } catch (error) {
796
+ const code = error instanceof LiveViewPreparationError
797
+ ? error.code
798
+ : "live_view_open_failed";
799
+ process.stderr.write(`codex-agent-view: live view open failed (${code})\n`);
800
+ process.exitCode = 1;
801
+ }
507
802
  }
508
803
 
509
- async function stopRunningRuntime(preflight) {
804
+ async function stopRunningRuntime(preflight, options = {}) {
510
805
  if (preflight.kind !== "valid") {
511
806
  return false;
512
807
  }
513
- const endpointState = await runtimeEndpointState(preflight.info);
808
+ const endpointState = await runtimeEndpointState(preflight.info, options);
514
809
  if (endpointState === "absent") {
515
810
  return false;
516
811
  }
@@ -560,7 +855,7 @@ async function stopRunningRuntime(preflight) {
560
855
  "runtime ownership changed during uninstall; new or unrecognized runtime data was preserved",
561
856
  );
562
857
  }
563
- if (!(await runtimeResponds(current.info))) {
858
+ if (!(await runtimeResponds(current.info, options))) {
564
859
  await removeRuntimeInfo(current.info.token);
565
860
  return true;
566
861
  }
@@ -590,7 +885,15 @@ async function inspectPluginBundle(destination) {
590
885
  ) {
591
886
  return { kind: "unmanaged" };
592
887
  }
593
- return { kind: "managed" };
888
+ return { kind: "managed", version: manifest.version };
889
+ }
890
+
891
+ function legacyBearerProbeOptions(bundle) {
892
+ return {
893
+ allowLegacyBearerProbe:
894
+ bundle.kind === "managed" &&
895
+ KNOWN_PRE_PROOF_MANAGED_VERSIONS.has(bundle.version),
896
+ };
594
897
  }
595
898
 
596
899
  function unmanagedBundleError(destination) {
@@ -670,6 +973,9 @@ async function install() {
670
973
  ? runtime.info.viewer_token || runtime.info.token
671
974
  : undefined;
672
975
  await ensureViewerToken(process.env, { seedToken });
976
+ if (runtime.kind === "valid") {
977
+ await stopRunningRuntime(runtime, legacyBearerProbeOptions(bundle));
978
+ }
673
979
  await copyPluginBundle(destination);
674
980
  if (!existing) {
675
981
  await run("codex", ["plugin", "marketplace", "add", destination, "--json"]);
@@ -711,7 +1017,7 @@ function isBroadRuntimeRoot(root) {
711
1017
  );
712
1018
  }
713
1019
 
714
- async function purgeStaleRuntime(preflight) {
1020
+ async function purgeStaleRuntime(preflight, options = {}) {
715
1021
  if (preflight.kind !== "valid") {
716
1022
  return preflight.kind === "unknown";
717
1023
  }
@@ -723,7 +1029,7 @@ async function purgeStaleRuntime(preflight) {
723
1029
  if (current.kind !== "valid" || current.info.token !== preflight.info.token) {
724
1030
  return true;
725
1031
  }
726
- const endpointState = await runtimeEndpointState(current.info);
1032
+ const endpointState = await runtimeEndpointState(current.info, options);
727
1033
  if (endpointState === "owned") {
728
1034
  throw new Error("the Codex Agent View monitor started during uninstall; runtime data was preserved");
729
1035
  }
@@ -790,7 +1096,11 @@ async function uninstall(args) {
790
1096
 
791
1097
  const runtimePreflight = await inspectRuntime();
792
1098
  const viewerPreflight = await inspectViewerCredential();
793
- const stoppedMonitor = await stopRunningRuntime(runtimePreflight);
1099
+ const lifecycleProbeOptions = legacyBearerProbeOptions(bundlePreflight);
1100
+ const stoppedMonitor = await stopRunningRuntime(
1101
+ runtimePreflight,
1102
+ lifecycleProbeOptions,
1103
+ );
794
1104
 
795
1105
  await run("codex", ["plugin", "remove", PLUGIN_ID, "--json"], { allowFailure: true });
796
1106
  await run("codex", ["plugin", "marketplace", "remove", MARKETPLACE_NAME, "--json"], {
@@ -799,7 +1109,10 @@ async function uninstall(args) {
799
1109
  await removeManagedPluginBundle(bundle);
800
1110
  const viewerCredential = await revokeViewerCredential(viewerPreflight);
801
1111
  if (purge) {
802
- const preservedRuntimeFile = await purgeStaleRuntime(runtimePreflight);
1112
+ const preservedRuntimeFile = await purgeStaleRuntime(
1113
+ runtimePreflight,
1114
+ lifecycleProbeOptions,
1115
+ );
803
1116
  const removedRoot = await removeRuntimeRootIfEmpty(root);
804
1117
  if (removedRoot) {
805
1118
  process.stdout.write(`Removed plugin, marketplace, and runtime data from ${root}.\n`);
@@ -838,6 +1151,8 @@ async function main() {
838
1151
  process.stdout.write(`${await packageVersion()}\n`);
839
1152
  } else if (command === "start") {
840
1153
  await start(args);
1154
+ } else if (command === "open") {
1155
+ await openLiveView(args);
841
1156
  } else if (command === "status") {
842
1157
  await status(args);
843
1158
  } else if (command === "doctor") {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "codex-agent-view",
3
- "version": "0.4.7",
4
- "description": "Follow Codex work and participating agent progress in a clear, read-only live view inside the official Codex app.",
3
+ "version": "0.5.0",
4
+ "description": "Follow Codex work and participating agent progress in a clear, read-only live view opened from the official Codex app.",
5
5
  "type": "module",
6
6
  "main": "./src/core/index.mjs",
7
7
  "exports": "./src/core/index.mjs",