codex-agent-view 0.4.7 → 0.4.8
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/.codex-plugin/plugin.json +1 -1
- package/README.ko.md +10 -10
- package/README.md +10 -10
- package/bin/codex-agent-view.mjs +297 -11
- package/package.json +1 -1
- package/public/app.js +339 -29
- package/skills/show-agents/SKILL.md +40 -64
- package/src/runtime/server.mjs +371 -14
package/README.ko.md
CHANGED
|
@@ -10,10 +10,10 @@ Codex Agent View는 Codex가 지금 어떤 작업을 수행하고 있고 어떤
|
|
|
10
10
|
|
|
11
11
|
### 빠른 시작: 설치 후에는 Codex 앱 안에서만 사용
|
|
12
12
|
|
|
13
|
-
이 README는 `codex-agent-view@0.4.
|
|
13
|
+
이 README는 `codex-agent-view@0.4.8` release candidate 사용법을 설명한다. 해당 version이 공개된 뒤 **최초 설치만** 일반 터미널에서 아래 exact-version 명령으로 진행한다.
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npm install --global codex-agent-view@0.4.
|
|
16
|
+
npm install --global codex-agent-view@0.4.8
|
|
17
17
|
codex-agent-view install
|
|
18
18
|
```
|
|
19
19
|
|
|
@@ -30,9 +30,9 @@ codex-agent-view install
|
|
|
30
30
|
|
|
31
31
|
**지금 사용해보기는 skill 호출이 아니다.** Codex plugin의 `interface.defaultPrompt`는 starter text이며, `$show-agents`처럼 보이는 text도 skill 선택으로 해석된다고 보장되지 않는다. 따라서 Codex Agent View는 plugin 카드 starter prompt를 정의하지 않는다. `@codex-agent-view`로 plugin을 선택한 다음 Codex 앱의 skill UI에서 `$show-agents`를 명시적으로 선택한다. 일반 사용에는 terminal command, 외부 browser 또는 localhost URL 관리가 필요 없다.
|
|
32
32
|
|
|
33
|
-
Trust된 첫 hook이 도착하면 plugin sender가 로컬 backend를 내부적으로 준비하고 같은 event 전달을 재시도한다. 사용자는 task ID를 등록하거나 `start`, `status`, `doctor`를 실행할 필요가 없다. **Show Agents**는
|
|
33
|
+
Trust된 첫 hook이 도착하면 plugin sender가 로컬 backend를 내부적으로 준비하고 같은 event 전달을 재시도한다. 사용자는 task ID를 등록하거나 `start`, `status`, `doctor`를 실행할 필요가 없다. 정상 경로의 **Show Agents**는 내부 `prepare-live-view` command 1회와 Codex in-app Browser open 요청 1회만 수행한다. Command는 runtime bearer를 보내기 전에 fresh nonce/HMAC ownership proof로 exact owned monitor를 검증하고, 그 process의 runtime token으로 서명한 1회용 60초 bootstrap grant를 발급받는다. URL fragment에는 이 bounded grant만 들어가며 installation-owned viewer credential과 runtime/control token은 들어가지 않는다. 모든 request는 exact `127.0.0.1:<port>` authority와 origin-form target을 사용한다. Cookie, CORS access, external browser와 사용자가 관리하는 localhost URL은 없다.
|
|
34
34
|
|
|
35
|
-
공개 Codex plugin API에는 prompt 없이 앱 시작과 동시에 sidebar, panel 또는 Browser tab을 생성하는 기능이 없다. 따라서 live 화면을 열 때 Codex 앱 안에서 `$show-agents` skill을 한 번 명시 선택해야 한다.
|
|
35
|
+
공개 Codex plugin API에는 prompt 없이 앱 시작과 동시에 sidebar, panel 또는 Browser tab을 생성하는 기능이 없다. 따라서 live 화면을 열 때 Codex 앱 안에서 `$show-agents` skill을 한 번 명시 선택해야 한다. Bootstrap은 access/recovery/refresh가 절대 연장할 수 없는 signed 30분 credential-family 만료 시각을 처음에 고정한다. 15분 access credential은 같은 family 안에서만 자동 갱신되어 그 tab이 family 끝까지 끊기지 않는다. Recovery는 `localStorage`가 아니라 tab-scoped `sessionStorage`에만 둔다. 인증 이력이 없는 tab에는 작동하지 않는 button이 없고, family가 만료되면 실제 `$show-agents` skill을 다시 호출해야 한다. Validated `CODEX_THREAD_ID`는 family에 signed binding된다. Bootstrap은 발급 process 안에서 1회만 쓸 수 있고 monitor가 재시작되면 즉시 무효가 된다.
|
|
36
36
|
|
|
37
37
|
Live UI의 기본 언어는 영어이며 language selector에서 **English**, **한국어**, **Español**을 고를 수 있다. 활동은 refresh 때 접히는 disclosure toggle 없이 계속 보이고, 2초 polling 간격도 유지한다. 각 작업에는 `UserPromptSubmit`에서 만든 첫 번째 유효 요청 요약을 표시할 수 있다. Sender는 원문 중 최대 4,096자만 검사하고 일반적인 credential, 이메일 주소, 링크와 절대 경로를 가린 뒤 한 줄·최대 180자로 제한하며 전체 요청 원문은 즉시 버린다. 이후의 짧은 follow-up은 이 첫 요약을 덮지 않는다. 실제 확인한 `SubagentStart` payload는 `agent_id`, `agent_type`만 제공하며 전용 할당 작업 설명 field가 없다. 따라서 작업 전체의 요청 요약은 보여주되 prompt나 tool input에서 에이전트별 할당 내용을 추측하지 않는다.
|
|
38
38
|
|
|
@@ -40,7 +40,7 @@ Live UI의 기본 언어는 영어이며 language selector에서 **English**, **
|
|
|
40
40
|
|
|
41
41
|
### 현재 상태
|
|
42
42
|
|
|
43
|
-
`0.4.
|
|
43
|
+
`0.4.8`은 더 빠르고 복구 가능하며 최소 권한인 live-view open을 목표로 하는 release candidate다. 정상 `$show-agents` 경로는 내부 준비 command 1회와 in-app Browser open 요청 1회로 줄어든다. Runtime bearer 전에는 ownership을 증명하고 URL에는 1회용 60초 process-signed bootstrap grant만 넣는다. Fixed 30분 signed family 안에서 15분 access를 자동 갱신하고 recovery는 tab-scoped이며 family deadline을 연장하지 않는다. Monitor restart는 사용 전 bootstrap만 무효화하고 이미 exchange된 family는 original expiry까지 새 in-memory 관찰 window에 재연결할 수 있다. Family 만료 뒤에는 actual skill을 다시 호출해야 한다. Source `npm run check`는 153개 test, plugin validation과 package dry-run을 통과했다. 공식 Codex in-app Browser에서는 grant 인증, fragment 제거, 같은 tab bare-root recovery button 성공과 새 tab의 recovery button 부재를 확인했다. Updated 공식 앱 hook의 실제 전달은 현재 app process 재시작 전이라 아직 미확인이다. npm publish, GitHub Release, CI와 public exact install은 아직 주장하지 않는다.
|
|
44
44
|
|
|
45
45
|
- 공식 Codex 앱의 내장 thread tools를 우선 사용하는 app-native active-task snapshot skill
|
|
46
46
|
- `.codex-plugin/plugin.json`, local marketplace catalog, genuine Codex skill
|
|
@@ -106,7 +106,7 @@ Codex Agent View는 historical audit이나 session replay 제품이 아니라
|
|
|
106
106
|
- Plugin 카드의 **지금 사용해보기**는 `@codex-agent-view`만 선택한다. Starter text를 덧붙이거나 skill을 dispatch한다고 주장하지 않는다. Live view를 열거나 다시 열 때 Codex 앱에서 `$show-agents`를 명시 선택하며, 별도 monitor 실행이나 task ID 등록은 필요 없다.
|
|
107
107
|
- npm은 plugin bundle, 내부 hook sender/runtime과 static UI를 사용자 machine에 배포하는 최초 설치 경로다.
|
|
108
108
|
- Live view는 사용자가 앱 안에서 `$show-agents`를 명시 호출했을 때만 열린다. 외부 website나 telemetry dashboard가 아니다.
|
|
109
|
-
- 공개 plugin API는 앱 시작 시 no-prompt sidebar/panel/Browser tab 생성을 제공하지 않는다. 최초 live view 열기에는 앱 안 skill 선택이 한 번
|
|
109
|
+
- 공개 plugin API는 앱 시작 시 no-prompt sidebar/panel/Browser tab 생성을 제공하지 않는다. 최초 live view 열기에는 앱 안 skill 선택이 한 번 필요하다. Current candidate의 tab은 fixed 30분 credential family 안에서 access를 자동 갱신한다. Monitor restart는 아직 교환하지 않은 bootstrap만 무효화하고 이미 발급된 family는 original deadline까지 같은 port에서 재연결할 수 있다. Family 만료 뒤에는 actual skill을 다시 호출해야 한다.
|
|
110
110
|
- Universal Plugins Directory는 npm의 대체재가 아니다. 공개 directory의 in-app custom UI 경로는 public HTTPS MCP server와 domain verification이 필요해 local-only/no-external-server 원칙과 충돌한다. 현재는 별도의 listing/skills 제출 가능성만 검토하며, 심사·publish 전에는 Codex plugin 검색으로 설치할 수 있다고 안내하지 않는다.
|
|
111
111
|
|
|
112
112
|
Hook event가 누락·중복·역순으로 올 수 있으므로 UI의 `unknown`, `stopped_without_start`, 빈 상태는 그대로 해석해야 한다. 빈 session 목록은 “이 monitor가 event를 관찰하지 못함”이며 “실행 중인 task가 없음”의 증거가 아니다.
|
|
@@ -121,7 +121,7 @@ Hook event가 누락·중복·역순으로 올 수 있으므로 UI의 `unknown`,
|
|
|
121
121
|
4. Language selector에서 **English**, **한국어**, **Español**을 선택한다. 기본값은 영어이고 언어 전환 뒤에도 2초 refresh는 유지된다.
|
|
122
122
|
5. 앱의 Browser capability 또는 permission을 사용할 수 없으면 private localhost URL을 노출하거나 외부 browser를 여는 대신 실패를 안내한다.
|
|
123
123
|
|
|
124
|
-
오른쪽 live 화면을 닫았다면 Codex 앱 task에서 `@codex-agent-view`를 선택하고 `$show-agents`를 다시 명시 호출한다. 붙여 넣은 `@codex-agent-view $show-agents` 문자열이 skill 선택으로 재해석된다고 가정하지 않는다.
|
|
124
|
+
오른쪽 live 화면을 닫았다면 Codex 앱 task에서 `@codex-agent-view`를 선택하고 `$show-agents`를 다시 명시 호출한다. 붙여 넣은 `@codex-agent-view $show-agents` 문자열이 skill 선택으로 재해석된다고 가정하지 않는다. Fixed 30분 family 동안 같은 tab은 recovery를 `sessionStorage`에만 보관하고 15분 access를 자동 갱신하며, page-level access가 없거나 거부될 때 **다시 연결** button을 표시한다. 다른 tab이나 인증 이력이 없는 tab에는 recovery가 없다. Family 만료 뒤에는 actual `$show-agents` skill을 다시 호출해야 한다. 복구에 terminal command, private URL 복사, cookie, CORS access 또는 외부 browser는 필요 없다. Monitor restart는 새 in-memory 관찰 window를 시작하고 이전 process의 미사용 bootstrap을 즉시 무효화한다.
|
|
125
125
|
|
|
126
126
|
### 요구사항과 검증 범위
|
|
127
127
|
|
|
@@ -214,16 +214,16 @@ Plugin enable/trust와 앱 재시작 뒤 생성되거나 재개되는 task는 tr
|
|
|
214
214
|
|
|
215
215
|
### npm 설치 명령 참고
|
|
216
216
|
|
|
217
|
-
아래 명령은 `0.4.
|
|
217
|
+
아래 명령은 공개 후 `0.4.8`을 exact version으로 설치한다.
|
|
218
218
|
|
|
219
219
|
```bash
|
|
220
|
-
npm install --global codex-agent-view@0.4.
|
|
220
|
+
npm install --global codex-agent-view@0.4.8
|
|
221
221
|
codex-agent-view install
|
|
222
222
|
```
|
|
223
223
|
|
|
224
224
|
이 두 명령 뒤에는 Codex 앱을 완전히 다시 열고 Plugins 화면에서 설치·활성화와 hook trust를 확인한 다음 새 task를 만든다. 첫 trusted hook이 backend 준비와 event 전달을 내부 처리하므로 사용자가 monitor CLI를 실행하지 않는다. Plugin 카드의 **지금 사용해보기**로 `@codex-agent-view`를 선택하고 앱에서 `$show-agents`를 명시 선택한다. Panel을 닫은 뒤에도 같은 방식으로 skill을 다시 선택한다.
|
|
225
225
|
|
|
226
|
-
`0.4.
|
|
226
|
+
`0.4.8` 설치 경로는 위의 global package 설치와 명시적인 `codex-agent-view install` command 조합이다. 이후 일반 사용은 Codex 앱 안에서 진행한다. Upgrade의 explicit `install`은 existing authenticated maintenance lifecycle로 healthy owned `0.4.7` monitor를 먼저 정지한 뒤 registration과 bundle을 교체한다. Installation-owned viewer credential과 `0.4.3`에서 검증한 legacy `0.4.2` migration 경계는 보존한다. 정상 Show Agents workflow는 persistent token을 출력하지 않고 Browser target에도 넣지 않는다.
|
|
227
227
|
|
|
228
228
|
Version별 npm, install, migration, CI, tag와 GitHub Release evidence는 [docs/distribution.md](docs/distribution.md)에 보존한다. 각 evidence는 실제 확인한 뒤에만 갱신한다.
|
|
229
229
|
|
package/README.md
CHANGED
|
@@ -8,12 +8,12 @@ Codex Agent View gives you a clear, read-only view of what Codex is working on a
|
|
|
8
8
|
|
|
9
9
|
## Quick start: install once, then stay inside the Codex app
|
|
10
10
|
|
|
11
|
-
This README documents `codex-agent-view@0.4.
|
|
11
|
+
This README documents the `codex-agent-view@0.4.8` release candidate. Use the exact-version command below for the one-time terminal installation after that version is published.
|
|
12
12
|
|
|
13
13
|
Universal Plugins Directory search installation is not available yet, so use a regular terminal for the **initial installation only**:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npm install --global codex-agent-view@0.4.
|
|
16
|
+
npm install --global codex-agent-view@0.4.8
|
|
17
17
|
codex-agent-view install
|
|
18
18
|
```
|
|
19
19
|
|
|
@@ -30,9 +30,9 @@ After installation:
|
|
|
30
30
|
|
|
31
31
|
**Quick start is not a skill invocation.** Codex plugin `interface.defaultPrompt` values are starter text, and text that looks like `$show-agents` is not guaranteed to be interpreted as a skill selection. Codex Agent View therefore defines no plugin-card starter prompt. Select the plugin with `@codex-agent-view`, then explicitly select `$show-agents` using the Codex app's skill UI. Routine use still requires neither a terminal command, an external browser, nor a localhost URL.
|
|
32
32
|
|
|
33
|
-
When the first trusted hook arrives, the plugin sender internally prepares the local backend and retries delivery of that same event. Users do not register task IDs or run `start`, `status`, or `doctor`. **Show Agents**
|
|
33
|
+
When the first trusted hook arrives, the plugin sender internally prepares the local backend and retries delivery of that same event. Users do not register task IDs or run `start`, `status`, or `doctor`. On its normal path, **Show Agents** runs one internal `prepare-live-view` command and makes one Codex in-app Browser open request. Before sending the runtime bearer, the command verifies a fresh nonce/HMAC ownership proof from the exact owned monitor. It then obtains a one-time, 60-second bootstrap grant signed by that process's runtime token. Only that bounded grant enters the URL fragment: the installation-owned viewer credential and runtime/control token do not. Every request uses the exact `127.0.0.1:<port>` authority and origin-form target. No cookie, CORS access, external browser, or user-managed localhost URL is involved.
|
|
34
34
|
|
|
35
|
-
The public Codex plugin API does not provide no-prompt app-start creation of a sidebar, panel, or Browser tab. Opening the live view therefore requires one explicit `$show-agents` skill selection inside the Codex app.
|
|
35
|
+
The public Codex plugin API does not provide no-prompt app-start creation of a sidebar, panel, or Browser tab. Opening the live view therefore requires one explicit `$show-agents` skill selection inside the Codex app. The bootstrap fixes one signed 30-minute credential-family expiry that access, recovery, and refresh can never extend. Fifteen-minute access credentials refresh automatically only inside that family, so the same tab remains connected until the family ends. Recovery is tab-scoped `sessionStorage`, not `localStorage`. A previously authenticated tab can therefore use **Reconnect** after losing page-level access, while a tab with no authentication history shows no nonfunctional button. When the family expires, the actual `$show-agents` skill must be invoked again. The invoking task's validated `CODEX_THREAD_ID` remains signed into the family. A bootstrap is one-use within its issuing process and becomes invalid immediately when that monitor restarts; a family already exchanged under the persistent viewer signing key can reconnect on the same origin until its original absolute expiry.
|
|
36
36
|
|
|
37
37
|
The live UI defaults to English and offers **English**, **Korean**, and **Spanish** in its language selector. Activity remains visible rather than hidden behind refresh-sensitive disclosure toggles, and the two-second polling interval is unchanged. Each work item can show its first valid short request summary derived from `UserPromptSubmit`: the sender inspects at most 4,096 characters, redacts common credentials, email addresses, links, and absolute paths, collapses the result to one line, bounds it to 180 characters, and immediately discards the full request. Later follow-ups do not replace that first valid summary. Verified `SubagentStart` payloads still provide only `agent_id` and `agent_type`; they do not provide a dedicated assignment description. The monitor therefore shows the work-level request summary but does not invent an agent-specific assignment from prompts or tool input.
|
|
38
38
|
|
|
@@ -40,7 +40,7 @@ In short: install once in a terminal; perform snapshot queries, status checks, l
|
|
|
40
40
|
|
|
41
41
|
## Status
|
|
42
42
|
|
|
43
|
-
Version `0.4.
|
|
43
|
+
Version `0.4.8` is a release candidate focused on faster, recoverable, least-privilege live-view opening. Its normal `$show-agents` path is reduced to one internal preparation command followed by one in-app Browser open request. Ownership is proven before the runtime bearer is sent; the URL carries only a one-use, 60-second process-signed bootstrap grant. A fixed 30-minute signed family supports automatic 15-minute access refresh and tab-scoped recovery without extending the family deadline. Monitor restart invalidates only an unused bootstrap; an exchanged family can reconnect to the new in-memory observation window until its original expiry. Family expiry requires the actual skill again. Source `npm run check` passes all 153 tests plus plugin validation and package dry-run. In the official Codex in-app Browser, grant authentication, fragment removal, same-tab bare-root recovery, and absence of a recovery button in a new tab were observed. Updated official-app hook delivery remains unverified until the current app process is restarted. npm publication, GitHub Release, CI, and public exact installation are not yet claimed.
|
|
44
44
|
|
|
45
45
|
Plugin installation and lifecycle payloads were verified with Homebrew Codex CLI and the Codex executable embedded in the official app. However, a real-use attempt that installed and enabled `0.2.0` in an already-running official app process delivered zero events while two subagents ran. The monitor, registration, enablement, and installed bundle were healthy, while app logs showed no sender invocation. Evidence indicates that the same process retained a pre-install `hooks/list` snapshot; persisted exact-hook trust is not exposed through CLI JSON, so the precise skip boundary remains unconfirmed.
|
|
46
46
|
|
|
@@ -80,7 +80,7 @@ Codex Agent View is a live companion, not a historical audit or session-replay p
|
|
|
80
80
|
- Hooks remain the source of truth for detailed lifecycle state in the trusted-hook auto-prepared local live backend. Its operational state exists only in bounded process memory; restart begins a new observation window. The separate private viewer credential is authentication metadata, not stored task history.
|
|
81
81
|
- `Stop` marks the observed root turn and the session/work-item summary `completed` immediately. If a child agent or tool was still active, its own row is separately marked `completion_not_observed` because no child stop/tool completion signal was observed. `SessionEnd` has terminal priority; any child agent, tool, or permission still open at that point is shown as `interrupted`, not silently completed.
|
|
82
82
|
- Official `SessionEnd` delivery may be delayed by up to 30 minutes. If no ending hook is observed while activity still appears open, five minutes without a new event changes it to `completion_not_observed` (**End not confirmed**), never inferred `completed`. This keeps a delayed or missing terminal event from turning stale activity into a false success.
|
|
83
|
-
- The viewer credential
|
|
83
|
+
- The installation-owned viewer credential remains private to local runtime files for ownership/legacy boundaries. The runtime/control token remains separate and process-scoped. The normal live-view URL contains neither credential; it contains only a one-use, process-signed 60-second bootstrap grant.
|
|
84
84
|
- After installation, hook trust, and an app restart, the first trusted hook automatically prepares the backend. This prepares a local process; it does not create app UI without a user action.
|
|
85
85
|
- There is no external telemetry, remote server, account, required SQLite/persistent event store, or remote control.
|
|
86
86
|
- Full prompt text, transcript paths, full tool input/output, and assistant messages are not retained or displayed by the monitor. Only the bounded, redacted one-line work summary described above may be retained in process memory.
|
|
@@ -109,7 +109,7 @@ Use this flow in a **new task** after completing installation and enablement in
|
|
|
109
109
|
4. Choose **English**, **Korean**, or **Spanish** from the language selector. English is the default, and changing language does not stop the two-second refresh.
|
|
110
110
|
5. If the app's Browser capability or permission is unavailable, the skill reports the failure without exposing a private localhost URL or opening an external browser.
|
|
111
111
|
|
|
112
|
-
If you close the right-side live view, select `@codex-agent-view` and explicitly invoke `$show-agents` again in a Codex app task. Do not rely on a pasted `@codex-agent-view $show-agents` string being reparsed as a skill selection.
|
|
112
|
+
If you close the right-side live view, select `@codex-agent-view` and explicitly invoke `$show-agents` again in a Codex app task. Do not rely on a pasted `@codex-agent-view $show-agents` string being reparsed as a skill selection. During its fixed 30-minute family, the same tab keeps recovery only in `sessionStorage`, refreshes 15-minute access automatically, and offers **Reconnect** when page-level access is missing or rejected. A different or never-authenticated tab has no recovery credential. Family expiry requires the actual `$show-agents` skill again. No terminal command, private URL copy, cookie, CORS access, or external browser is part of recovery. A restarted monitor presents a new in-memory observation window; an unused bootstrap issued by the old process is immediately invalid.
|
|
113
113
|
|
|
114
114
|
## Requirements and tested versions
|
|
115
115
|
|
|
@@ -166,16 +166,16 @@ After plugin enablement/trust and an app restart, the first trusted hook interna
|
|
|
166
166
|
|
|
167
167
|
## Install from npm
|
|
168
168
|
|
|
169
|
-
The commands below install `0.4.
|
|
169
|
+
The commands below install `0.4.8` by exact version after publication.
|
|
170
170
|
|
|
171
171
|
```bash
|
|
172
|
-
npm install --global codex-agent-view@0.4.
|
|
172
|
+
npm install --global codex-agent-view@0.4.8
|
|
173
173
|
codex-agent-view install
|
|
174
174
|
```
|
|
175
175
|
|
|
176
176
|
After these two commands, fully reopen the Codex app, verify installation, enablement, and hook trust, then create a new task. The first trusted hook prepares the backend and delivers its event internally, so users do not run monitor CLI commands. Use the plugin card's **Quick start** action to select `@codex-agent-view`, then explicitly select `$show-agents` in the app. Repeat that explicit skill selection after closing the panel.
|
|
177
177
|
|
|
178
|
-
The `0.4.
|
|
178
|
+
The `0.4.8` installation path is the global package install followed by the explicit `codex-agent-view install` command above. Routine use remains inside the Codex app afterward. During an upgrade, explicit `install` first stops a healthy owned `0.4.7` monitor through the existing authenticated maintenance lifecycle, then replaces registration and bundle files. It preserves the installation-owned viewer credential and the historical `0.4.2` migration boundary. The normal Show Agents workflow prints neither persistent token and does not put either one in the Browser target.
|
|
179
179
|
|
|
180
180
|
Version-specific npm, install, migration, CI, tag, and GitHub Release evidence is preserved in [Distribution](docs/distribution.md). That evidence is updated only after each item is actually verified.
|
|
181
181
|
|
package/bin/codex-agent-view.mjs
CHANGED
|
@@ -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,21 @@ 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 KNOWN_PRE_PROOF_MANAGED_VERSIONS = new Set([
|
|
46
|
+
"0.2.0", "0.2.1",
|
|
47
|
+
"0.3.0", "0.3.1", "0.3.2",
|
|
48
|
+
"0.4.0", "0.4.1", "0.4.2", "0.4.3", "0.4.4", "0.4.5", "0.4.6", "0.4.7",
|
|
49
|
+
]);
|
|
50
|
+
const CANONICAL_THREAD_ID_PATTERN =
|
|
51
|
+
/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i;
|
|
35
52
|
const INSTALL_ENTRIES = [
|
|
36
53
|
".agents",
|
|
37
54
|
".codex-plugin",
|
|
@@ -68,6 +85,13 @@ Start prints the local URL without opening an external browser unless --open is
|
|
|
68
85
|
`);
|
|
69
86
|
}
|
|
70
87
|
|
|
88
|
+
class LiveViewPreparationError extends Error {
|
|
89
|
+
constructor(code) {
|
|
90
|
+
super(code);
|
|
91
|
+
this.code = code;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
71
95
|
function parseStartArgs(args) {
|
|
72
96
|
let open = false;
|
|
73
97
|
let legacyNoOpen = false;
|
|
@@ -195,6 +219,9 @@ async function start(args) {
|
|
|
195
219
|
|
|
196
220
|
async function fetchState() {
|
|
197
221
|
const runtime = await readRuntimeInfo();
|
|
222
|
+
if ((await runtimeEndpointState(runtime)) !== "owned") {
|
|
223
|
+
throw new Error("the runtime endpoint was not identified as an owned monitor");
|
|
224
|
+
}
|
|
198
225
|
const response = await fetch(`http://${runtime.host}:${runtime.port}/api/state`, {
|
|
199
226
|
headers: { authorization: `Bearer ${runtime.token}` },
|
|
200
227
|
signal: AbortSignal.timeout(1_500),
|
|
@@ -483,7 +510,7 @@ async function revokeViewerCredential(preflight) {
|
|
|
483
510
|
};
|
|
484
511
|
}
|
|
485
512
|
|
|
486
|
-
async function
|
|
513
|
+
async function legacyRuntimeEndpointState(runtime) {
|
|
487
514
|
try {
|
|
488
515
|
const response = await fetch(`http://${runtime.host}:${runtime.port}/api/state`, {
|
|
489
516
|
headers: { authorization: `Bearer ${runtime.token}` },
|
|
@@ -502,15 +529,254 @@ async function runtimeEndpointState(runtime) {
|
|
|
502
529
|
}
|
|
503
530
|
}
|
|
504
531
|
|
|
505
|
-
async function
|
|
506
|
-
|
|
532
|
+
async function runtimeEndpointState(
|
|
533
|
+
runtime,
|
|
534
|
+
{ allowLegacyBearerProbe = false } = {},
|
|
535
|
+
) {
|
|
536
|
+
const ownership = await runtimeOwnershipState(runtime);
|
|
537
|
+
if (ownership !== "unrelated" || !allowLegacyBearerProbe) {
|
|
538
|
+
return ownership;
|
|
539
|
+
}
|
|
540
|
+
return legacyRuntimeEndpointState(runtime);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async function runtimeResponds(runtime, options) {
|
|
544
|
+
return (await runtimeEndpointState(runtime, options)) === "owned";
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function expectedOwnershipProof(nonce, runtimeToken) {
|
|
548
|
+
return createHmac("sha256", runtimeToken)
|
|
549
|
+
.update(OWNERSHIP_PROOF_DOMAIN)
|
|
550
|
+
.update("\0")
|
|
551
|
+
.update(nonce)
|
|
552
|
+
.digest("base64url");
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async function runtimeOwnershipState(runtime) {
|
|
556
|
+
const nonce = randomBytes(32).toString("base64url");
|
|
557
|
+
let response;
|
|
558
|
+
try {
|
|
559
|
+
response = await fetch(
|
|
560
|
+
`http://${runtime.host}:${runtime.port}/api/internal/ownership-proof`,
|
|
561
|
+
{
|
|
562
|
+
body: JSON.stringify({ nonce }),
|
|
563
|
+
headers: { "content-type": "application/json" },
|
|
564
|
+
method: "POST",
|
|
565
|
+
signal: AbortSignal.timeout(OWNERSHIP_PROOF_TIMEOUT_MS),
|
|
566
|
+
},
|
|
567
|
+
);
|
|
568
|
+
} catch {
|
|
569
|
+
return "absent";
|
|
570
|
+
}
|
|
571
|
+
if (response.status !== 200) {
|
|
572
|
+
await response.body?.cancel();
|
|
573
|
+
return "unrelated";
|
|
574
|
+
}
|
|
575
|
+
const payload = await response.json().catch(() => null);
|
|
576
|
+
if (
|
|
577
|
+
payload === null ||
|
|
578
|
+
typeof payload !== "object" ||
|
|
579
|
+
Array.isArray(payload) ||
|
|
580
|
+
Object.keys(payload).sort().join(",") !== "proof,status" ||
|
|
581
|
+
payload.status !== "owned" ||
|
|
582
|
+
typeof payload.proof !== "string" ||
|
|
583
|
+
!/^[A-Za-z0-9_-]{43}$/.test(payload.proof)
|
|
584
|
+
) {
|
|
585
|
+
return "unrelated";
|
|
586
|
+
}
|
|
587
|
+
const supplied = Buffer.from(payload.proof);
|
|
588
|
+
const expected = Buffer.from(expectedOwnershipProof(nonce, runtime.token));
|
|
589
|
+
return supplied.length === expected.length && timingSafeEqual(supplied, expected)
|
|
590
|
+
? "owned"
|
|
591
|
+
: "unrelated";
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async function inspectInstalledBundleForLiveView() {
|
|
595
|
+
const destination = join(runtimeDirectory(), "marketplace");
|
|
596
|
+
const stats = await pathExists(destination);
|
|
597
|
+
if (!stats) {
|
|
598
|
+
throw new LiveViewPreparationError("plugin_not_installed");
|
|
599
|
+
}
|
|
600
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
601
|
+
throw new LiveViewPreparationError("plugin_bundle_unowned");
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const marker = await readJsonRegularFile(join(destination, BUNDLE_MARKER));
|
|
605
|
+
if (
|
|
606
|
+
marker?.schema_version !== BUNDLE_MARKER_SCHEMA_VERSION ||
|
|
607
|
+
marker?.package !== MARKETPLACE_NAME ||
|
|
608
|
+
marker?.plugin_id !== PLUGIN_ID
|
|
609
|
+
) {
|
|
610
|
+
throw new LiveViewPreparationError("plugin_bundle_unowned");
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const manifest = await readJsonRegularFile(
|
|
614
|
+
join(destination, ".codex-plugin", "plugin.json"),
|
|
615
|
+
);
|
|
616
|
+
if (manifest?.name !== MARKETPLACE_NAME || typeof manifest.version !== "string") {
|
|
617
|
+
throw new LiveViewPreparationError("plugin_bundle_invalid");
|
|
618
|
+
}
|
|
619
|
+
if (manifest.version !== (await packageVersion())) {
|
|
620
|
+
throw new LiveViewPreparationError("plugin_version_mismatch");
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function autoStartEnvironment() {
|
|
625
|
+
const env = {};
|
|
626
|
+
for (const key of [
|
|
627
|
+
"CODEX_AGENT_VIEW_AUTO_START_PORT",
|
|
628
|
+
"CODEX_AGENT_VIEW_RUNTIME_DIR",
|
|
629
|
+
"SystemRoot",
|
|
630
|
+
]) {
|
|
631
|
+
if (typeof process.env[key] === "string") {
|
|
632
|
+
env[key] = process.env[key];
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return env;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function startMonitorDetached() {
|
|
639
|
+
const child = spawn(
|
|
640
|
+
process.execPath,
|
|
641
|
+
[fileURLToPath(new URL("../scripts/auto-start-monitor.mjs", import.meta.url))],
|
|
642
|
+
{
|
|
643
|
+
detached: true,
|
|
644
|
+
env: autoStartEnvironment(),
|
|
645
|
+
shell: false,
|
|
646
|
+
stdio: "ignore",
|
|
647
|
+
},
|
|
648
|
+
);
|
|
649
|
+
child.on("error", () => {});
|
|
650
|
+
child.unref();
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function liveViewRuntimeState() {
|
|
654
|
+
const runtime = await inspectRuntime();
|
|
655
|
+
if (runtime.kind === "unknown") {
|
|
656
|
+
throw new LiveViewPreparationError("runtime_record_invalid");
|
|
657
|
+
}
|
|
658
|
+
if (runtime.kind === "absent") {
|
|
659
|
+
return { kind: "not_running" };
|
|
660
|
+
}
|
|
661
|
+
const endpoint = await runtimeOwnershipState(runtime.info);
|
|
662
|
+
if (endpoint === "owned") {
|
|
663
|
+
return { info: runtime.info, kind: "owned" };
|
|
664
|
+
}
|
|
665
|
+
if (endpoint === "absent") {
|
|
666
|
+
return { kind: "not_running" };
|
|
667
|
+
}
|
|
668
|
+
throw new LiveViewPreparationError("unowned_runtime");
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function inheritedExcludedSessionId() {
|
|
672
|
+
const inheritedThreadId = process.env.CODEX_THREAD_ID;
|
|
673
|
+
return (
|
|
674
|
+
typeof inheritedThreadId === "string" &&
|
|
675
|
+
CANONICAL_THREAD_ID_PATTERN.test(inheritedThreadId)
|
|
676
|
+
? inheritedThreadId.toLowerCase()
|
|
677
|
+
: null
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async function requestViewerGrant(runtime, excludeSessionId) {
|
|
682
|
+
let response;
|
|
683
|
+
try {
|
|
684
|
+
response = await fetch(
|
|
685
|
+
`http://${runtime.host}:${runtime.port}/api/internal/viewer-grant`,
|
|
686
|
+
{
|
|
687
|
+
body: JSON.stringify({ exclude_session_id: excludeSessionId }),
|
|
688
|
+
headers: {
|
|
689
|
+
authorization: `Bearer ${runtime.token}`,
|
|
690
|
+
"content-type": "application/json",
|
|
691
|
+
},
|
|
692
|
+
method: "POST",
|
|
693
|
+
signal: AbortSignal.timeout(VIEWER_GRANT_TIMEOUT_MS),
|
|
694
|
+
},
|
|
695
|
+
);
|
|
696
|
+
} catch (error) {
|
|
697
|
+
throw new LiveViewPreparationError(
|
|
698
|
+
error?.name === "TimeoutError" || error?.name === "AbortError"
|
|
699
|
+
? "viewer_grant_timeout"
|
|
700
|
+
: "viewer_grant_unavailable",
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
if (response.status !== 201) {
|
|
704
|
+
await response.body?.cancel();
|
|
705
|
+
throw new LiveViewPreparationError("viewer_grant_rejected");
|
|
706
|
+
}
|
|
707
|
+
const payload = await response.json().catch(() => null);
|
|
708
|
+
const credential = payload?.bootstrap_credential;
|
|
709
|
+
if (
|
|
710
|
+
payload === null ||
|
|
711
|
+
typeof payload !== "object" ||
|
|
712
|
+
Array.isArray(payload) ||
|
|
713
|
+
Object.keys(payload).sort().join(",") !==
|
|
714
|
+
"bootstrap_credential,expires_in_ms,status" ||
|
|
715
|
+
payload.status !== "granted" ||
|
|
716
|
+
payload.expires_in_ms !== 60_000 ||
|
|
717
|
+
typeof credential !== "string" ||
|
|
718
|
+
credential.length > MAX_BOOTSTRAP_CREDENTIAL_LENGTH ||
|
|
719
|
+
!SIGNED_BOOTSTRAP_CREDENTIAL_PATTERN.test(credential) ||
|
|
720
|
+
credential === runtime.token ||
|
|
721
|
+
credential === runtime.viewer_token
|
|
722
|
+
) {
|
|
723
|
+
throw new LiveViewPreparationError("viewer_grant_invalid_response");
|
|
724
|
+
}
|
|
725
|
+
return credential;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function liveViewTarget(runtime, bootstrapCredential) {
|
|
729
|
+
return `http://${LOOPBACK_HOST}:${runtime.port}/#grant=${encodeURIComponent(bootstrapCredential)}`;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function prepareLiveView(args) {
|
|
733
|
+
try {
|
|
734
|
+
if (args.length > 0) {
|
|
735
|
+
throw new LiveViewPreparationError("invalid_arguments");
|
|
736
|
+
}
|
|
737
|
+
await inspectInstalledBundleForLiveView();
|
|
738
|
+
let runtime = await liveViewRuntimeState();
|
|
739
|
+
let reused = runtime.kind === "owned";
|
|
740
|
+
if (!reused) {
|
|
741
|
+
startMonitorDetached();
|
|
742
|
+
const deadline = Date.now() + PREPARE_LIVE_VIEW_WAIT_MS;
|
|
743
|
+
do {
|
|
744
|
+
await new Promise((resolvePromise) =>
|
|
745
|
+
setTimeout(resolvePromise, PREPARE_LIVE_VIEW_POLL_MS),
|
|
746
|
+
);
|
|
747
|
+
runtime = await liveViewRuntimeState();
|
|
748
|
+
if (runtime.kind === "owned") break;
|
|
749
|
+
} while (Date.now() < deadline);
|
|
750
|
+
}
|
|
751
|
+
if (runtime.kind !== "owned") {
|
|
752
|
+
throw new LiveViewPreparationError("monitor_start_timeout");
|
|
753
|
+
}
|
|
754
|
+
const bootstrapCredential = await requestViewerGrant(
|
|
755
|
+
runtime.info,
|
|
756
|
+
inheritedExcludedSessionId(),
|
|
757
|
+
);
|
|
758
|
+
process.stdout.write(
|
|
759
|
+
`${JSON.stringify({
|
|
760
|
+
ok: true,
|
|
761
|
+
reused,
|
|
762
|
+
target: liveViewTarget(runtime.info, bootstrapCredential),
|
|
763
|
+
})}\n`,
|
|
764
|
+
);
|
|
765
|
+
} catch (error) {
|
|
766
|
+
const code =
|
|
767
|
+
error instanceof LiveViewPreparationError
|
|
768
|
+
? error.code
|
|
769
|
+
: "live_view_preparation_failed";
|
|
770
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: { code } })}\n`);
|
|
771
|
+
process.exitCode = 1;
|
|
772
|
+
}
|
|
507
773
|
}
|
|
508
774
|
|
|
509
|
-
async function stopRunningRuntime(preflight) {
|
|
775
|
+
async function stopRunningRuntime(preflight, options = {}) {
|
|
510
776
|
if (preflight.kind !== "valid") {
|
|
511
777
|
return false;
|
|
512
778
|
}
|
|
513
|
-
const endpointState = await runtimeEndpointState(preflight.info);
|
|
779
|
+
const endpointState = await runtimeEndpointState(preflight.info, options);
|
|
514
780
|
if (endpointState === "absent") {
|
|
515
781
|
return false;
|
|
516
782
|
}
|
|
@@ -560,7 +826,7 @@ async function stopRunningRuntime(preflight) {
|
|
|
560
826
|
"runtime ownership changed during uninstall; new or unrecognized runtime data was preserved",
|
|
561
827
|
);
|
|
562
828
|
}
|
|
563
|
-
if (!(await runtimeResponds(current.info))) {
|
|
829
|
+
if (!(await runtimeResponds(current.info, options))) {
|
|
564
830
|
await removeRuntimeInfo(current.info.token);
|
|
565
831
|
return true;
|
|
566
832
|
}
|
|
@@ -590,7 +856,15 @@ async function inspectPluginBundle(destination) {
|
|
|
590
856
|
) {
|
|
591
857
|
return { kind: "unmanaged" };
|
|
592
858
|
}
|
|
593
|
-
return { kind: "managed" };
|
|
859
|
+
return { kind: "managed", version: manifest.version };
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function legacyBearerProbeOptions(bundle) {
|
|
863
|
+
return {
|
|
864
|
+
allowLegacyBearerProbe:
|
|
865
|
+
bundle.kind === "managed" &&
|
|
866
|
+
KNOWN_PRE_PROOF_MANAGED_VERSIONS.has(bundle.version),
|
|
867
|
+
};
|
|
594
868
|
}
|
|
595
869
|
|
|
596
870
|
function unmanagedBundleError(destination) {
|
|
@@ -670,6 +944,9 @@ async function install() {
|
|
|
670
944
|
? runtime.info.viewer_token || runtime.info.token
|
|
671
945
|
: undefined;
|
|
672
946
|
await ensureViewerToken(process.env, { seedToken });
|
|
947
|
+
if (runtime.kind === "valid") {
|
|
948
|
+
await stopRunningRuntime(runtime, legacyBearerProbeOptions(bundle));
|
|
949
|
+
}
|
|
673
950
|
await copyPluginBundle(destination);
|
|
674
951
|
if (!existing) {
|
|
675
952
|
await run("codex", ["plugin", "marketplace", "add", destination, "--json"]);
|
|
@@ -711,7 +988,7 @@ function isBroadRuntimeRoot(root) {
|
|
|
711
988
|
);
|
|
712
989
|
}
|
|
713
990
|
|
|
714
|
-
async function purgeStaleRuntime(preflight) {
|
|
991
|
+
async function purgeStaleRuntime(preflight, options = {}) {
|
|
715
992
|
if (preflight.kind !== "valid") {
|
|
716
993
|
return preflight.kind === "unknown";
|
|
717
994
|
}
|
|
@@ -723,7 +1000,7 @@ async function purgeStaleRuntime(preflight) {
|
|
|
723
1000
|
if (current.kind !== "valid" || current.info.token !== preflight.info.token) {
|
|
724
1001
|
return true;
|
|
725
1002
|
}
|
|
726
|
-
const endpointState = await runtimeEndpointState(current.info);
|
|
1003
|
+
const endpointState = await runtimeEndpointState(current.info, options);
|
|
727
1004
|
if (endpointState === "owned") {
|
|
728
1005
|
throw new Error("the Codex Agent View monitor started during uninstall; runtime data was preserved");
|
|
729
1006
|
}
|
|
@@ -790,7 +1067,11 @@ async function uninstall(args) {
|
|
|
790
1067
|
|
|
791
1068
|
const runtimePreflight = await inspectRuntime();
|
|
792
1069
|
const viewerPreflight = await inspectViewerCredential();
|
|
793
|
-
const
|
|
1070
|
+
const lifecycleProbeOptions = legacyBearerProbeOptions(bundlePreflight);
|
|
1071
|
+
const stoppedMonitor = await stopRunningRuntime(
|
|
1072
|
+
runtimePreflight,
|
|
1073
|
+
lifecycleProbeOptions,
|
|
1074
|
+
);
|
|
794
1075
|
|
|
795
1076
|
await run("codex", ["plugin", "remove", PLUGIN_ID, "--json"], { allowFailure: true });
|
|
796
1077
|
await run("codex", ["plugin", "marketplace", "remove", MARKETPLACE_NAME, "--json"], {
|
|
@@ -799,7 +1080,10 @@ async function uninstall(args) {
|
|
|
799
1080
|
await removeManagedPluginBundle(bundle);
|
|
800
1081
|
const viewerCredential = await revokeViewerCredential(viewerPreflight);
|
|
801
1082
|
if (purge) {
|
|
802
|
-
const preservedRuntimeFile = await purgeStaleRuntime(
|
|
1083
|
+
const preservedRuntimeFile = await purgeStaleRuntime(
|
|
1084
|
+
runtimePreflight,
|
|
1085
|
+
lifecycleProbeOptions,
|
|
1086
|
+
);
|
|
803
1087
|
const removedRoot = await removeRuntimeRootIfEmpty(root);
|
|
804
1088
|
if (removedRoot) {
|
|
805
1089
|
process.stdout.write(`Removed plugin, marketplace, and runtime data from ${root}.\n`);
|
|
@@ -842,6 +1126,8 @@ async function main() {
|
|
|
842
1126
|
await status(args);
|
|
843
1127
|
} else if (command === "doctor") {
|
|
844
1128
|
await doctor(args);
|
|
1129
|
+
} else if (command === "prepare-live-view") {
|
|
1130
|
+
await prepareLiveView(args);
|
|
845
1131
|
} else if (command === "install") {
|
|
846
1132
|
await install(args);
|
|
847
1133
|
} else if (command === "uninstall") {
|
package/package.json
CHANGED