@tunnelbox/codex 0.1.9 → 0.1.11

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/README.md CHANGED
@@ -1,138 +1,138 @@
1
- # @tunnelbox/codex
2
-
3
- [English](README.md) · [简体中文](README.zh-CN.md)
4
-
5
- **Official website:** [www.tunnelbox.top](https://www.tunnelbox.top)
6
-
7
- **tunnelbox Codex adapter (standalone daemon):** drive the Codex CLI on your computer from your phone — sessions, streaming, abort, delete, workspaces. It runs as a background Node process that dials **out** to a relay only (no public IP, no port forwarding).
8
-
9
- ```
10
- Phone app ──WSS──► relay ──WSS──► tunnelbox-codex (this adapter)
11
- └─ codex exec --json (spawns a codex subprocess per message)
12
- ```
13
-
14
- ## Quick start (install → run → pair → use)
15
-
16
- Follow these steps once on a computer that has Codex installed and signed in.
17
-
18
- ### Step 0 — Prepare your phone
19
-
20
- Install the tunnelbox App (Android & iOS) from the official website: **https://www.tunnelbox.top**
21
-
22
- - Sign in / create an account when prompted.
23
-
24
- ### Step 1 — Install the adapter on your computer
25
-
26
- Prerequisites: Node.js ≥ 22, and the **Codex CLI** installed & signed in (`codex --version`, `codex login` / API key configured).
27
-
28
- ```bash
29
- npm install -g @tunnelbox/codex
30
- ```
31
-
32
- ### Step 2 — Run it
33
-
34
- ```bash
35
- tunnelbox-codex
36
- ```
37
-
38
- On first start the adapter connects to the relay (`wss://relay.tunnelbox.top` by default) and prints a **pairing QR code + pairing code** in the terminal.
39
-
40
- ### Step 3 — Pair with your phone
41
-
42
- In the phone app tap **Scan to pair** and scan the terminal QR code (or type the pairing code manually). The code is single-use and valid for about 10 minutes.
43
-
44
- - Once bound, the adapter prints "connected account — open the app and pick this computer" on later starts, and does not refresh the code.
45
- - Want a fresh code any time? Stop and run:
46
-
47
- ```bash
48
- tunnelbox-codex --pair
49
- ```
50
-
51
- ### Step 4 — Use it from your phone
52
-
53
- In the app, open this computer and you can start new sessions, send messages, watch streaming output, approve risky tool calls, and abort a running session.
54
-
55
- ## Configuration
56
-
57
- | Env var | Default | Description |
58
- |---|---|---|
59
- | `TUNNELBOX_RELAY_URL` | saved relay URL | Relay address (e.g. `wss://chat.example.com`); set it if you self-host |
60
- | `TUNNELBOX_CWD` | `process.cwd()` | Default workspace; switch per-session from the phone workspace picker |
61
- | `TUNNELBOX_CODEX_SANDBOX` | `workspace-write` | `read-only` / `workspace-write` / `danger-full-access` (passed as `codex exec --sandbox`) |
62
- | `TUNNELBOX_CODEX_MODEL` | codex default | `-m <model>` |
63
- | `TUNNELBOX_CODEX_BIN` | `codex` | Codex executable name / absolute path |
64
- | `TUNNELBOX_CODEX_APPROVAL` | `on` | Phone approval switch `on`/`off`. When on, tool requests are pushed to the phone approval card via the Codex `PermissionRequest` hook (**experimental**) |
65
- | `TUNNELBOX_CODEX_HOOK_PORT` | `18790` | Local approval HTTP port (hook subprocess ↔ adapter) |
66
-
67
- ```bash
68
- TUNNELBOX_RELAY_URL=wss://chat.example.com TUNNELBOX_CODEX_SANDBOX=workspace-write tunnelbox-codex
69
- ```
70
-
71
- ## Capabilities
72
-
73
- | Capability | Value | Notes |
74
- |---|---|---|
75
- | streaming | ✅ | `codex exec --json` NDJSON events → live `message.part` deltas |
76
- | thinking | ✅ | `agent_reasoning` → thinking parts (when present) |
77
- | permission | ✅ default | `PermissionRequest` hook → phone approval card (**experimental**) |
78
- | commands | ✅ | `/new`, `/help` |
79
- | abort | ✅ | kill subprocess (SIGTERM → SIGKILL) |
80
- | workspaces | ✅ | per-session `cwd` |
81
-
82
- ## Phone approval (experimental)
83
-
84
- On startup the adapter idempotently installs a `PermissionRequest` hook into the Codex config (`~/.codex/hooks.json`). Each tool request goes hook → local HTTP (`127.0.0.1:18790`) → relay → your phone's approval card. Allow/deny from the phone is written back; denials block the tool (exit 2). When approval is on, `codex exec` runs with `-c approval_policy=on-request`.
85
-
86
- **Fail-closed:** if the relay is offline, the request times out (120s), or anything errors, the tool is **denied** — never auto-approved. Scope is still bounded by `--sandbox`.
87
-
88
- > ⚠️ Experimental: Codex's hooks system is still evolving. If anything misbehaves, disable with `TUNNELBOX_CODEX_APPROVAL=off` (restores the built-in fail-closed sandbox behavior, `capability.permission=false`).
89
-
90
- ## Sessions
91
-
92
- - **New phone sessions = adapter UUIDs.** The transcript id from the first `codex exec` is stored in an alias table (`~/.tunnelbox/codex-aliases.json`); follow-ups resume that session (`codex exec --resume <codexId>`), stable across processes.
93
- - **Native codex sessions** you started in a terminal also appear in the list (scanned from `CODEX_HOME/sessions`, default `~/.codex/sessions`) and can be resumed by their native id.
94
- - History is read from the transcript JSONL; deleting a session deletes the file.
95
- - Workspaces = session `cwd`; sessions run in parallel (one subprocess each); a running session can't receive another message.
96
-
97
- ## Security notes
98
-
99
- - Phone approval defaults **on** and is **fail-closed** (offline/timeout/error ⇒ deny). Enforcement boundary still follows `--sandbox`:
100
- - `read-only` — no writes;
101
- - `workspace-write` (default) — writes inside the workspace; writes/network outside need higher policy;
102
- - `danger-full-access` — full access (dangerous, trusted workspaces only).
103
- - Set `TUNNELBOX_CODEX_APPROVAL=off` to go back to `capability.permission=false`: no approval card, gating decided by `--sandbox` alone.
104
- - Don't expose sensitive projects to an untrusted phone.
105
-
106
- ## Troubleshooting
107
-
108
- | Problem | Fix |
109
- |---|---|
110
- | No QR / pairing code on screen | Run `tunnelbox-codex --pair` to print a fresh one |
111
- | Need to pair a different phone | Unbind in the app settings, then run with `--pair` again |
112
- | Already bound but phone doesn't connect | Confirm both use the same relay; check `~/.tunnelbox/tunnelbox.log` |
113
- | Use your own relay | Start with `TUNNELBOX_RELAY_URL=wss://<your-relay>` |
114
-
115
- ## State files
116
-
117
- Independent tunnelbox state lives in `~/.tunnelbox/`:
118
-
119
- - `remote-state.codex.json` — agentID / relay / bound state / language
120
- - `codex-aliases.json` — adapter-UUID → codex transcript-id aliases
121
- - `remote-pairing.codex.txt` + `remote-pairing.json` — latest pairing info
122
- - `tunnelbox.log` — adapter log
123
-
124
- ## Multilingual
125
-
126
- User-visible output supports 8 languages (`zh-CN` / `zh-TW` / `en-US` / `ja-JP` / `ko-KR` / `fr-FR` / `de-DE` / `es-ES`). Resolution order: `TUNNELBOX_LANG` > saved `remote-state.codex.json.lang` > system locale > `en-US`.
127
-
128
- ## Uninstall
129
-
130
- ```bash
131
- npm uninstall -g @tunnelbox/codex
132
- ```
133
-
134
- Remove `~/.tunnelbox` (all adapters' shared state) only if you no longer use any tunnelbox adapter.
135
-
136
- ## Development
137
-
138
- Implementation details (protocol mapping, source layout, design decisions, untested items) are kept in the source repository as `DEV.md` next to this package, plus the protocol spec under the repo's `docs/`.
1
+ # @tunnelbox/codex
2
+
3
+ [English](README.md) · [简体中文](README.zh-CN.md)
4
+
5
+ **Official website:** [www.tunnelbox.top](https://www.tunnelbox.top)
6
+
7
+ **tunnelbox Codex adapter (standalone daemon):** drive the Codex CLI on your computer from your phone — sessions, streaming, abort, delete, workspaces. It runs as a background Node process that dials **out** to a relay only (no public IP, no port forwarding).
8
+
9
+ ```
10
+ Phone app ──WSS──► relay ──WSS──► tunnelbox-codex (this adapter)
11
+ └─ codex exec --json (spawns a codex subprocess per message)
12
+ ```
13
+
14
+ ## Quick start (install → run → pair → use)
15
+
16
+ Follow these steps once on a computer that has Codex installed and signed in.
17
+
18
+ ### Step 0 — Prepare your phone
19
+
20
+ Install the tunnelbox App (Android & iOS) from the official website: **https://www.tunnelbox.top**
21
+
22
+ - Sign in / create an account when prompted.
23
+
24
+ ### Step 1 — Install the adapter on your computer
25
+
26
+ Prerequisites: Node.js ≥ 22, and the **Codex CLI** installed & signed in (`codex --version`, `codex login` / API key configured).
27
+
28
+ ```bash
29
+ npm install -g @tunnelbox/codex
30
+ ```
31
+
32
+ ### Step 2 — Run it
33
+
34
+ ```bash
35
+ tunnelbox-codex
36
+ ```
37
+
38
+ On first start the adapter connects to the relay (`wss://relay.tunnelbox.top` by default) and prints a **pairing QR code + pairing code** in the terminal.
39
+
40
+ ### Step 3 — Pair with your phone
41
+
42
+ In the phone app tap **Scan to pair** and scan the terminal QR code (or type the pairing code manually). The code is single-use and valid for about 10 minutes.
43
+
44
+ - Once bound, the adapter prints "connected account — open the app and pick this computer" on later starts, and does not refresh the code.
45
+ - Want a fresh code any time? Stop and run:
46
+
47
+ ```bash
48
+ tunnelbox-codex --pair
49
+ ```
50
+
51
+ ### Step 4 — Use it from your phone
52
+
53
+ In the app, open this computer and you can start new sessions, send messages, watch streaming output, approve risky tool calls, and abort a running session.
54
+
55
+ ## Configuration
56
+
57
+ | Env var | Default | Description |
58
+ |---|---|---|
59
+ | `TUNNELBOX_RELAY_URL` | saved relay URL | Relay address (e.g. `wss://chat.example.com`); set it if you self-host |
60
+ | `TUNNELBOX_CWD` | `process.cwd()` | Default workspace; switch per-session from the phone workspace picker |
61
+ | `TUNNELBOX_CODEX_SANDBOX` | `workspace-write` | `read-only` / `workspace-write` / `danger-full-access` (passed as `codex exec --sandbox`) |
62
+ | `TUNNELBOX_CODEX_MODEL` | codex default | `-m <model>` |
63
+ | `TUNNELBOX_CODEX_BIN` | `codex` | Codex executable name / absolute path |
64
+ | `TUNNELBOX_CODEX_APPROVAL` | `on` | Phone approval switch `on`/`off`. When on, tool requests are pushed to the phone approval card via the Codex `PermissionRequest` hook (**experimental**) |
65
+ | `TUNNELBOX_CODEX_HOOK_PORT` | `18790` | Local approval HTTP port (hook subprocess ↔ adapter) |
66
+
67
+ ```bash
68
+ TUNNELBOX_RELAY_URL=wss://chat.example.com TUNNELBOX_CODEX_SANDBOX=workspace-write tunnelbox-codex
69
+ ```
70
+
71
+ ## Capabilities
72
+
73
+ | Capability | Value | Notes |
74
+ |---|---|---|
75
+ | streaming | ✅ | `codex exec --json` NDJSON events → live `message.part` deltas |
76
+ | thinking | ✅ | `agent_reasoning` → thinking parts (when present) |
77
+ | permission | ✅ default | `PermissionRequest` hook → phone approval card (**experimental**) |
78
+ | commands | ✅ | `/new`, `/help` |
79
+ | abort | ✅ | kill subprocess (SIGTERM → SIGKILL) |
80
+ | workspaces | ✅ | per-session `cwd` |
81
+
82
+ ## Phone approval (experimental)
83
+
84
+ On startup the adapter idempotently installs a `PermissionRequest` hook into the Codex config (`~/.codex/hooks.json`). Each tool request goes hook → local HTTP (`127.0.0.1:18790`) → relay → your phone's approval card. Allow/deny from the phone is written back; denials block the tool (exit 2). When approval is on, `codex exec` runs with `-c approval_policy=on-request`.
85
+
86
+ **Fail-closed:** if the relay is offline, the request times out (120s), or anything errors, the tool is **denied** — never auto-approved. Scope is still bounded by `--sandbox`.
87
+
88
+ > ⚠️ Experimental: Codex's hooks system is still evolving. If anything misbehaves, disable with `TUNNELBOX_CODEX_APPROVAL=off` (restores the built-in fail-closed sandbox behavior, `capability.permission=false`).
89
+
90
+ ## Sessions
91
+
92
+ - **New phone sessions = adapter UUIDs.** The transcript id from the first `codex exec` is stored in an alias table (`~/.tunnelbox/codex-aliases.json`); follow-ups resume that session (`codex exec --resume <codexId>`), stable across processes.
93
+ - **Native codex sessions** you started in a terminal also appear in the list (scanned from `CODEX_HOME/sessions`, default `~/.codex/sessions`) and can be resumed by their native id.
94
+ - History is read from the transcript JSONL; deleting a session deletes the file.
95
+ - Workspaces = session `cwd`; sessions run in parallel (one subprocess each); a running session can't receive another message.
96
+
97
+ ## Security notes
98
+
99
+ - Phone approval defaults **on** and is **fail-closed** (offline/timeout/error ⇒ deny). Enforcement boundary still follows `--sandbox`:
100
+ - `read-only` — no writes;
101
+ - `workspace-write` (default) — writes inside the workspace; writes/network outside need higher policy;
102
+ - `danger-full-access` — full access (dangerous, trusted workspaces only).
103
+ - Set `TUNNELBOX_CODEX_APPROVAL=off` to go back to `capability.permission=false`: no approval card, gating decided by `--sandbox` alone.
104
+ - Don't expose sensitive projects to an untrusted phone.
105
+
106
+ ## Troubleshooting
107
+
108
+ | Problem | Fix |
109
+ |---|---|
110
+ | No QR / pairing code on screen | Run `tunnelbox-codex --pair` to print a fresh one |
111
+ | Need to pair a different phone | Unbind in the app settings, then run with `--pair` again |
112
+ | Already bound but phone doesn't connect | Confirm both use the same relay; check `~/.tunnelbox/tunnelbox.log` |
113
+ | Use your own relay | Start with `TUNNELBOX_RELAY_URL=wss://<your-relay>` |
114
+
115
+ ## State files
116
+
117
+ Independent tunnelbox state lives in `~/.tunnelbox/`:
118
+
119
+ - `remote-state.codex.json` — agentID / relay / bound state / language
120
+ - `codex-aliases.json` — adapter-UUID → codex transcript-id aliases
121
+ - `remote-pairing.codex.txt` + `remote-pairing.json` — latest pairing info
122
+ - `tunnelbox.log` — adapter log
123
+
124
+ ## Multilingual
125
+
126
+ User-visible output supports 8 languages (`zh-CN` / `zh-TW` / `en-US` / `ja-JP` / `ko-KR` / `fr-FR` / `de-DE` / `es-ES`). Resolution order: `TUNNELBOX_LANG` > saved `remote-state.codex.json.lang` > system locale > `en-US`.
127
+
128
+ ## Uninstall
129
+
130
+ ```bash
131
+ npm uninstall -g @tunnelbox/codex
132
+ ```
133
+
134
+ Remove `~/.tunnelbox` (all adapters' shared state) only if you no longer use any tunnelbox adapter.
135
+
136
+ ## Development
137
+
138
+ Implementation details (protocol mapping, source layout, design decisions, untested items) are kept in the source repository as `DEV.md` next to this package, plus the protocol spec under the repo's `docs/`.
package/README.zh-CN.md CHANGED
@@ -1,138 +1,138 @@
1
- # @tunnelbox/codex
2
-
3
- [English](README.md) · [简体中文](README.zh-CN.md)
4
-
5
- **官网:** [www.tunnelbox.top](https://www.tunnelbox.top)
6
-
7
- **tunnelbox Codex 适配器(独立进程)**:用手机远程驱动电脑上的 Codex CLI——会话 / 流式 / 中止 / 删除 / 工作区。它以常驻 Node 进程运行,只向中继发出站连接(无需公网 IP / 端口映射)。
8
-
9
- ```
10
- 手机 App ──WSS──► relay ──WSS──► tunnelbox-codex(本适配器)
11
- └─ codex exec --json(每条消息拉起一个 codex 子进程)
12
- ```
13
-
14
- ## 快速上手(安装 → 运行 → 配对 → 使用)
15
-
16
- 在装有 Codex 并已登录的电脑上,按以下步骤操作一次即可。
17
-
18
- ### 第 0 步 — 准备手机
19
-
20
- 在官网下载并安装 tunnelbox App(Android / iOS 均已上架):**https://www.tunnelbox.top**
21
-
22
- - 按提示登录/注册账号。
23
-
24
- ### 第 1 步 — 电脑上安装适配器
25
-
26
- 前置条件:Node.js ≥ 22,且本机已安装并登录 **Codex CLI**(`codex --version` 可用、已 `codex login` 或配好 API key)。
27
-
28
- ```bash
29
- npm install -g @tunnelbox/codex
30
- ```
31
-
32
- ### 第 2 步 — 运行
33
-
34
- ```bash
35
- tunnelbox-codex
36
- ```
37
-
38
- 首次启动会连接中继(默认 `wss://relay.tunnelbox.top`),并在终端打印**配对二维码 + 配对码**。
39
-
40
- ### 第 3 步 — 与手机配对
41
-
42
- 在手机 App 点「扫一扫配对」,扫描终端二维码(或手动输入配对码)。配对码一次性有效,约 10 分钟。
43
-
44
- - 绑定后再次启动会打印「已绑定账号,从手机「我的电脑」直接连接」,不再刷新码;
45
- - 随时需要新码:停止后运行:
46
-
47
- ```bash
48
- tunnelbox-codex --pair
49
- ```
50
-
51
- ### 第 4 步 — 在手机上使用
52
-
53
- 在 App 里打开这台电脑,即可新建会话、发消息、看流式输出、审批危险工具调用、中止运行中的会话。
54
-
55
- ## 配置
56
-
57
- | 环境变量 | 默认 | 说明 |
58
- |---|---|---|
59
- | `TUNNELBOX_RELAY_URL` | state 保存的地址 | 中继地址(如 `wss://chat.example.com`);自建中继时设置 |
60
- | `TUNNELBOX_CWD` | `process.cwd()` | 默认工作区;手机上可按会话切换工作区 |
61
- | `TUNNELBOX_CODEX_SANDBOX` | `workspace-write` | `read-only` / `workspace-write` / `danger-full-access`(传给 `codex exec --sandbox`) |
62
- | `TUNNELBOX_CODEX_MODEL` | codex 默认 | `-m <model>` |
63
- | `TUNNELBOX_CODEX_BIN` | `codex` | codex 可执行名/绝对路径 |
64
- | `TUNNELBOX_CODEX_APPROVAL` | `on` | 手机审批开关 `on`/`off`。开启时经 Codex `PermissionRequest` hook 把工具请求推到手机审批卡(**实验**) |
65
- | `TUNNELBOX_CODEX_HOOK_PORT` | `18790` | 本地审批 HTTP 端口(hook 子进程与适配器通信用) |
66
-
67
- ```bash
68
- TUNNELBOX_RELAY_URL=wss://chat.example.com TUNNELBOX_CODEX_SANDBOX=workspace-write tunnelbox-codex
69
- ```
70
-
71
- ## 能力位
72
-
73
- | 能力 | 值 | 说明 |
74
- |---|---|---|
75
- | streaming | ✅ | `codex exec --json` NDJSON 事件 → 实时 `message.part` 增量 |
76
- | thinking | ✅ | `agent_reasoning` → thinking 部件(如有) |
77
- | permission | ✅ 默认 | `PermissionRequest` hook → 手机审批卡(**实验**) |
78
- | commands | ✅ | `/new`、`/help` |
79
- | abort | ✅ | kill 子进程(SIGTERM → SIGKILL) |
80
- | workspaces | ✅ | 每会话一个 `cwd` |
81
-
82
- ## 手机审批(实验)
83
-
84
- 启动时把 `PermissionRequest` hook 幂等写入 Codex 配置(`~/.codex/hooks.json`)。每次工具请求走 hook → 本地 HTTP(`127.0.0.1:18790`)→ 中继 → 手机审批卡;手机 allow/deny 回填,deny 阻止工具(exit 2)。审批开启时 `codex exec` 以 `-c approval_policy=on-request` 运行。
85
-
86
- **fail-closed**:中继离线 / 请求超时(120s)/ 任何异常 → 一律**拒绝**,绝不自动放行;放行边界仍由 `--sandbox` 决定。
87
-
88
- > ⚠️ 实验性:Codex hooks 体系仍在演进。异常时可 `TUNNELBOX_CODEX_APPROVAL=off` 关闭(恢复内置 fail-closed 沙箱行为,`capability.permission=false`)。
89
-
90
- ## 会话模型
91
-
92
- - **手机新建会话 = 适配器 UUID**。首次 `codex exec` 的 transcript id 记入别名表(`~/.tunnelbox/codex-aliases.json`),后续用 `codex exec --resume <codexId>` 续聊(跨进程稳定)。
93
- - **codex 原生会话**:终端里开过的会话也会出现在列表(扫描 `CODEX_HOME/sessions`,默认 `~/.codex/sessions`),以原生 id resume。
94
- - 历史 = 读 transcript JSONL;删除会话即删对应 jsonl。
95
- - 工作区 = 会话 `cwd`;会话并行(每会话一个子进程),运行中的会话不能再发消息。
96
-
97
- ## 安全说明
98
-
99
- - 手机审批默认**开启**且 **fail-closed**(离线/超时/异常一律拒绝)。放行边界由 `--sandbox` 决定:
100
- - `read-only`:只读,不写文件;
101
- - `workspace-write`(默认):允许写工作区内;工作区外的写/网络需更高策略;
102
- - `danger-full-access`:全放行(危险,仅可信工作区)。
103
- - `TUNNELBOX_CODEX_APPROVAL=off` 回到 `capability.permission=false`:手机不弹审批卡,完全由 `--sandbox` 门控。
104
- - 请勿把敏感工程目录暴露给不受信手机。
105
-
106
- ## 故障排查
107
-
108
- | 问题 | 解决 |
109
- |---|---|
110
- | 终端没有二维码/配对码 | 运行 `tunnelbox-codex --pair` 打印新码 |
111
- | 需要换手机配对 | 在 App 设置里解绑,再带 `--pair` 运行 |
112
- | 已绑定但手机连不上 | 确认两端同一中继;查看 `~/.tunnelbox/tunnelbox.log` |
113
- | 使用自建中继 | 启动时带 `TUNNELBOX_RELAY_URL=wss://<你的中继>` |
114
-
115
- ## 状态文件
116
-
117
- tunnelbox 独立状态存于 `~/.tunnelbox/`:
118
-
119
- - `remote-state.codex.json` — agentID / 中继地址 / 绑定状态 / 语言
120
- - `codex-aliases.json` — 适配器 UUID → codex transcript id 别名表
121
- - `remote-pairing.codex.txt` + `remote-pairing.json` — 最新配对信息
122
- - `tunnelbox.log` — 适配器日志
123
-
124
- ## 多语言
125
-
126
- 用户可见输出支持 8 种语言(`zh-CN` / `zh-TW` / `en-US` / `ja-JP` / `ko-KR` / `fr-FR` / `de-DE` / `es-ES`)。解析顺序:`TUNNELBOX_LANG` > `remote-state.codex.json` 的 `lang` > 系统 locale > 默认 `en-US`。
127
-
128
- ## 卸载
129
-
130
- ```bash
131
- npm uninstall -g @tunnelbox/codex
132
- ```
133
-
134
- 仅当不再使用任何 tunnelbox 适配器时,才删除 `~/.tunnelbox`(各适配器共享状态)。
135
-
136
- ## 开发
137
-
138
- 实现细节(协议映射、源码结构、决策记录、待实测项)保存在源码仓库中本包旁的 `DEV.md` 与仓库 `docs/` 下。
1
+ # @tunnelbox/codex
2
+
3
+ [English](README.md) · [简体中文](README.zh-CN.md)
4
+
5
+ **官网:** [www.tunnelbox.top](https://www.tunnelbox.top)
6
+
7
+ **tunnelbox Codex 适配器(独立进程)**:用手机远程驱动电脑上的 Codex CLI——会话 / 流式 / 中止 / 删除 / 工作区。它以常驻 Node 进程运行,只向中继发出站连接(无需公网 IP / 端口映射)。
8
+
9
+ ```
10
+ 手机 App ──WSS──► relay ──WSS──► tunnelbox-codex(本适配器)
11
+ └─ codex exec --json(每条消息拉起一个 codex 子进程)
12
+ ```
13
+
14
+ ## 快速上手(安装 → 运行 → 配对 → 使用)
15
+
16
+ 在装有 Codex 并已登录的电脑上,按以下步骤操作一次即可。
17
+
18
+ ### 第 0 步 — 准备手机
19
+
20
+ 在官网下载并安装 tunnelbox App(Android / iOS 均已上架):**https://www.tunnelbox.top**
21
+
22
+ - 按提示登录/注册账号。
23
+
24
+ ### 第 1 步 — 电脑上安装适配器
25
+
26
+ 前置条件:Node.js ≥ 22,且本机已安装并登录 **Codex CLI**(`codex --version` 可用、已 `codex login` 或配好 API key)。
27
+
28
+ ```bash
29
+ npm install -g @tunnelbox/codex
30
+ ```
31
+
32
+ ### 第 2 步 — 运行
33
+
34
+ ```bash
35
+ tunnelbox-codex
36
+ ```
37
+
38
+ 首次启动会连接中继(默认 `wss://relay.tunnelbox.top`),并在终端打印**配对二维码 + 配对码**。
39
+
40
+ ### 第 3 步 — 与手机配对
41
+
42
+ 在手机 App 点「扫一扫配对」,扫描终端二维码(或手动输入配对码)。配对码一次性有效,约 10 分钟。
43
+
44
+ - 绑定后再次启动会打印「已绑定账号,从手机「我的电脑」直接连接」,不再刷新码;
45
+ - 随时需要新码:停止后运行:
46
+
47
+ ```bash
48
+ tunnelbox-codex --pair
49
+ ```
50
+
51
+ ### 第 4 步 — 在手机上使用
52
+
53
+ 在 App 里打开这台电脑,即可新建会话、发消息、看流式输出、审批危险工具调用、中止运行中的会话。
54
+
55
+ ## 配置
56
+
57
+ | 环境变量 | 默认 | 说明 |
58
+ |---|---|---|
59
+ | `TUNNELBOX_RELAY_URL` | state 保存的地址 | 中继地址(如 `wss://chat.example.com`);自建中继时设置 |
60
+ | `TUNNELBOX_CWD` | `process.cwd()` | 默认工作区;手机上可按会话切换工作区 |
61
+ | `TUNNELBOX_CODEX_SANDBOX` | `workspace-write` | `read-only` / `workspace-write` / `danger-full-access`(传给 `codex exec --sandbox`) |
62
+ | `TUNNELBOX_CODEX_MODEL` | codex 默认 | `-m <model>` |
63
+ | `TUNNELBOX_CODEX_BIN` | `codex` | codex 可执行名/绝对路径 |
64
+ | `TUNNELBOX_CODEX_APPROVAL` | `on` | 手机审批开关 `on`/`off`。开启时经 Codex `PermissionRequest` hook 把工具请求推到手机审批卡(**实验**) |
65
+ | `TUNNELBOX_CODEX_HOOK_PORT` | `18790` | 本地审批 HTTP 端口(hook 子进程与适配器通信用) |
66
+
67
+ ```bash
68
+ TUNNELBOX_RELAY_URL=wss://chat.example.com TUNNELBOX_CODEX_SANDBOX=workspace-write tunnelbox-codex
69
+ ```
70
+
71
+ ## 能力位
72
+
73
+ | 能力 | 值 | 说明 |
74
+ |---|---|---|
75
+ | streaming | ✅ | `codex exec --json` NDJSON 事件 → 实时 `message.part` 增量 |
76
+ | thinking | ✅ | `agent_reasoning` → thinking 部件(如有) |
77
+ | permission | ✅ 默认 | `PermissionRequest` hook → 手机审批卡(**实验**) |
78
+ | commands | ✅ | `/new`、`/help` |
79
+ | abort | ✅ | kill 子进程(SIGTERM → SIGKILL) |
80
+ | workspaces | ✅ | 每会话一个 `cwd` |
81
+
82
+ ## 手机审批(实验)
83
+
84
+ 启动时把 `PermissionRequest` hook 幂等写入 Codex 配置(`~/.codex/hooks.json`)。每次工具请求走 hook → 本地 HTTP(`127.0.0.1:18790`)→ 中继 → 手机审批卡;手机 allow/deny 回填,deny 阻止工具(exit 2)。审批开启时 `codex exec` 以 `-c approval_policy=on-request` 运行。
85
+
86
+ **fail-closed**:中继离线 / 请求超时(120s)/ 任何异常 → 一律**拒绝**,绝不自动放行;放行边界仍由 `--sandbox` 决定。
87
+
88
+ > ⚠️ 实验性:Codex hooks 体系仍在演进。异常时可 `TUNNELBOX_CODEX_APPROVAL=off` 关闭(恢复内置 fail-closed 沙箱行为,`capability.permission=false`)。
89
+
90
+ ## 会话模型
91
+
92
+ - **手机新建会话 = 适配器 UUID**。首次 `codex exec` 的 transcript id 记入别名表(`~/.tunnelbox/codex-aliases.json`),后续用 `codex exec --resume <codexId>` 续聊(跨进程稳定)。
93
+ - **codex 原生会话**:终端里开过的会话也会出现在列表(扫描 `CODEX_HOME/sessions`,默认 `~/.codex/sessions`),以原生 id resume。
94
+ - 历史 = 读 transcript JSONL;删除会话即删对应 jsonl。
95
+ - 工作区 = 会话 `cwd`;会话并行(每会话一个子进程),运行中的会话不能再发消息。
96
+
97
+ ## 安全说明
98
+
99
+ - 手机审批默认**开启**且 **fail-closed**(离线/超时/异常一律拒绝)。放行边界由 `--sandbox` 决定:
100
+ - `read-only`:只读,不写文件;
101
+ - `workspace-write`(默认):允许写工作区内;工作区外的写/网络需更高策略;
102
+ - `danger-full-access`:全放行(危险,仅可信工作区)。
103
+ - `TUNNELBOX_CODEX_APPROVAL=off` 回到 `capability.permission=false`:手机不弹审批卡,完全由 `--sandbox` 门控。
104
+ - 请勿把敏感工程目录暴露给不受信手机。
105
+
106
+ ## 故障排查
107
+
108
+ | 问题 | 解决 |
109
+ |---|---|
110
+ | 终端没有二维码/配对码 | 运行 `tunnelbox-codex --pair` 打印新码 |
111
+ | 需要换手机配对 | 在 App 设置里解绑,再带 `--pair` 运行 |
112
+ | 已绑定但手机连不上 | 确认两端同一中继;查看 `~/.tunnelbox/tunnelbox.log` |
113
+ | 使用自建中继 | 启动时带 `TUNNELBOX_RELAY_URL=wss://<你的中继>` |
114
+
115
+ ## 状态文件
116
+
117
+ tunnelbox 独立状态存于 `~/.tunnelbox/`:
118
+
119
+ - `remote-state.codex.json` — agentID / 中继地址 / 绑定状态 / 语言
120
+ - `codex-aliases.json` — 适配器 UUID → codex transcript id 别名表
121
+ - `remote-pairing.codex.txt` + `remote-pairing.json` — 最新配对信息
122
+ - `tunnelbox.log` — 适配器日志
123
+
124
+ ## 多语言
125
+
126
+ 用户可见输出支持 8 种语言(`zh-CN` / `zh-TW` / `en-US` / `ja-JP` / `ko-KR` / `fr-FR` / `de-DE` / `es-ES`)。解析顺序:`TUNNELBOX_LANG` > `remote-state.codex.json` 的 `lang` > 系统 locale > 默认 `en-US`。
127
+
128
+ ## 卸载
129
+
130
+ ```bash
131
+ npm uninstall -g @tunnelbox/codex
132
+ ```
133
+
134
+ 仅当不再使用任何 tunnelbox 适配器时,才删除 `~/.tunnelbox`(各适配器共享状态)。
135
+
136
+ ## 开发
137
+
138
+ 实现细节(协议映射、源码结构、决策记录、待实测项)保存在源码仓库中本包旁的 `DEV.md` 与仓库 `docs/` 下。
package/dist/index.mjs CHANGED
@@ -20,6 +20,8 @@ function envelope(type, payload, id) {
20
20
  }
21
21
  var BASE_DELAY = 1e3;
22
22
  var MAX_DELAY = 15e3;
23
+ var KEEPALIVE_INTERVAL_MS = 15e3;
24
+ var KEEPALIVE_TIMEOUT_MS = 25e3;
23
25
  var RelayClient = class {
24
26
  ws = null;
25
27
  url = "";
@@ -29,6 +31,11 @@ var RelayClient = class {
29
31
  retry = 0;
30
32
  stopped = false;
31
33
  connected = false;
34
+ reconnectScheduled = false;
35
+ /** 保活心跳定时器(onopen 启动 / onclose 停止) */
36
+ keepaliveTimer = null;
37
+ /** 最近一次收到服务端帧的时间(含心跳回执 pong),用于静默断流检测 */
38
+ lastInbound = 0;
32
39
  get isConnected() {
33
40
  return this.connected;
34
41
  }
@@ -38,6 +45,8 @@ var RelayClient = class {
38
45
  this.handlers = handlers;
39
46
  this.stopped = false;
40
47
  this.retry = 0;
48
+ this.reconnectScheduled = false;
49
+ this.stopKeepalive();
41
50
  this.open();
42
51
  }
43
52
  send(env) {
@@ -53,6 +62,8 @@ var RelayClient = class {
53
62
  }
54
63
  close() {
55
64
  this.stopped = true;
65
+ this.reconnectScheduled = false;
66
+ this.stopKeepalive();
56
67
  if (this.timer) clearTimeout(this.timer);
57
68
  this.timer = null;
58
69
  if (this.ws) {
@@ -72,11 +83,14 @@ var RelayClient = class {
72
83
  ws.onopen = () => {
73
84
  this.connected = true;
74
85
  this.retry = 0;
86
+ this.startKeepalive();
75
87
  this.handlers?.onOpen();
76
88
  };
77
89
  ws.onmessage = (ev) => {
90
+ this.lastInbound = Date.now();
78
91
  try {
79
92
  const msg = JSON.parse(String(ev.data));
93
+ if (msg.type === "pong") return;
80
94
  this.handlers?.onMessage(msg);
81
95
  } catch {
82
96
  }
@@ -85,6 +99,7 @@ var RelayClient = class {
85
99
  ws.onclose = () => {
86
100
  this.connected = false;
87
101
  this.ws = null;
102
+ this.stopKeepalive();
88
103
  this.handlers?.onClose();
89
104
  this.scheduleReconnect();
90
105
  };
@@ -94,10 +109,52 @@ var RelayClient = class {
94
109
  }
95
110
  }
96
111
  scheduleReconnect() {
97
- if (this.stopped) return;
112
+ if (this.stopped || this.reconnectScheduled) return;
113
+ this.reconnectScheduled = true;
98
114
  const delay = Math.min(BASE_DELAY * 2 ** this.retry, MAX_DELAY);
99
115
  this.retry++;
100
- this.timer = setTimeout(() => this.open(), delay);
116
+ this.timer = setTimeout(() => {
117
+ this.reconnectScheduled = false;
118
+ this.open();
119
+ }, delay);
120
+ }
121
+ /** 启动保活心跳:每周期发 ping,长时间收不到任何帧则判定断流主动重连。 */
122
+ startKeepalive() {
123
+ this.stopKeepalive();
124
+ this.lastInbound = Date.now();
125
+ this.keepaliveTimer = setInterval(() => {
126
+ if (!this.connected || !this.ws) return;
127
+ const now = Date.now();
128
+ if (now - this.lastInbound > KEEPALIVE_TIMEOUT_MS) {
129
+ this.forceReconnect();
130
+ return;
131
+ }
132
+ try {
133
+ this.ws.send(JSON.stringify({ type: "ping", payload: {}, ts: now }));
134
+ } catch {
135
+ }
136
+ }, KEEPALIVE_INTERVAL_MS);
137
+ }
138
+ stopKeepalive() {
139
+ if (this.keepaliveTimer !== null) {
140
+ clearInterval(this.keepaliveTimer);
141
+ this.keepaliveTimer = null;
142
+ }
143
+ }
144
+ /** 判定连接已死:关闭当前 socket 并调度重连(沿用指数退避,onclose 去重)。 */
145
+ forceReconnect() {
146
+ if (this.stopped) return;
147
+ this.connected = false;
148
+ this.stopKeepalive();
149
+ const w = this.ws;
150
+ this.ws = null;
151
+ if (w) {
152
+ try {
153
+ w.close();
154
+ } catch {
155
+ }
156
+ }
157
+ this.scheduleReconnect();
101
158
  }
102
159
  };
103
160
  var zh_CN_default = {
@@ -116,6 +173,7 @@ var zh_CN_default = {
116
173
  "agent.log.pendingResent": "session.pending: \u8865\u53D1 {session} \u79EF\u538B\u8BF7\u6C42 {count} \u6761",
117
174
  "agent.log.unhandledType": "\u672A\u5904\u7406\u7684\u6D88\u606F\u7C7B\u578B: {type}",
118
175
  "events.log.fwdPermission": "\u8F6C\u53D1\u6743\u9650\u8BF7\u6C42\u5230\u624B\u673A (id={id} tool={tool} {version})",
176
+ "events.log.sessionDeleted": "\u8F6C\u53D1 session.deleted \u5230\u624B\u673A (session={session})",
119
177
  "events.log.permissionReplied": "opencode \u56DE\u6267 permission.replied (id={id} {response})",
120
178
  "events.log.questionRejected": "opencode \u56DE\u6267 question.rejected (id={id})",
121
179
  "events.log.questionReplied": "opencode \u56DE\u6267 question.replied (id={id})",
@@ -281,6 +339,7 @@ var zh_TW_default = {
281
339
  "agent.log.pendingResent": "session.pending: \u88DC\u767C {session} \u7A4D\u58D3\u8ACB\u6C42 {count} \u689D",
282
340
  "agent.log.unhandledType": "\u672A\u8655\u7406\u7684\u8A0A\u606F\u985E\u578B: {type}",
283
341
  "events.log.fwdPermission": "\u8F49\u767C\u6B0A\u9650\u8ACB\u6C42\u5230\u624B\u6A5F (id={id} tool={tool} {version})",
342
+ "events.log.sessionDeleted": "\u8F49\u767C session.deleted \u5230\u624B\u6A5F (session={session})",
284
343
  "events.log.permissionReplied": "opencode \u56DE\u57F7 permission.replied (id={id} {response})",
285
344
  "events.log.questionRejected": "opencode \u56DE\u57F7 question.rejected (id={id})",
286
345
  "events.log.questionReplied": "opencode \u56DE\u57F7 question.replied (id={id})",
@@ -446,6 +505,7 @@ var en_US_default = {
446
505
  "agent.log.pendingResent": "session.pending: resent {count} pending request(s) for {session}",
447
506
  "agent.log.unhandledType": "Unhandled message type: {type}",
448
507
  "events.log.fwdPermission": "Forwarded permission request to phone (id={id} tool={tool} {version})",
508
+ "events.log.sessionDeleted": "Forwarded session.deleted to phone (session={session})",
449
509
  "events.log.permissionReplied": "opencode acked permission.replied (id={id} {response})",
450
510
  "events.log.questionRejected": "opencode acked question.rejected (id={id})",
451
511
  "events.log.questionReplied": "opencode acked question.replied (id={id})",
@@ -611,6 +671,7 @@ var ja_JP_default = {
611
671
  "agent.log.pendingResent": "session.pending: {session} \u306E\u4FDD\u7559\u30EA\u30AF\u30A8\u30B9\u30C8 {count} \u4EF6\u3092\u518D\u9001",
612
672
  "agent.log.unhandledType": "\u672A\u51E6\u7406\u306E\u30E1\u30C3\u30BB\u30FC\u30B8\u30BF\u30A4\u30D7: {type}",
613
673
  "events.log.fwdPermission": "\u6A29\u9650\u30EA\u30AF\u30A8\u30B9\u30C8\u3092\u643A\u5E2F\u3078\u8EE2\u9001 (id={id} tool={tool} {version})",
674
+ "events.log.sessionDeleted": "session.deleted \u3092\u643A\u5E2F\u3078\u8EE2\u9001 (session={session})",
614
675
  "events.log.permissionReplied": "opencode \u5FDC\u7B54 permission.replied (id={id} {response})",
615
676
  "events.log.questionRejected": "opencode \u5FDC\u7B54 question.rejected (id={id})",
616
677
  "events.log.questionReplied": "opencode \u5FDC\u7B54 question.replied (id={id})",
@@ -776,6 +837,7 @@ var ko_KR_default = {
776
837
  "agent.log.pendingResent": "session.pending: {session} \uBCF4\uB958 \uC694\uCCAD {count}\uAC74 \uC7AC\uC804\uC1A1",
777
838
  "agent.log.unhandledType": "\uCC98\uB9AC\uB418\uC9C0 \uC54A\uC740 \uBA54\uC2DC\uC9C0 \uC720\uD615: {type}",
778
839
  "events.log.fwdPermission": "\uAD8C\uD55C \uC694\uCCAD\uC744 \uD734\uB300\uD3F0\uC73C\uB85C \uC804\uB2EC (id={id} tool={tool} {version})",
840
+ "events.log.sessionDeleted": "session.deleted\uB97C \uD734\uB300\uD3F0\uC73C\uB85C \uC804\uB2EC (session={session})",
779
841
  "events.log.permissionReplied": "opencode \uC751\uB2F5 permission.replied (id={id} {response})",
780
842
  "events.log.questionRejected": "opencode \uC751\uB2F5 question.rejected (id={id})",
781
843
  "events.log.questionReplied": "opencode \uC751\uB2F5 question.replied (id={id})",
@@ -941,6 +1003,7 @@ var fr_FR_default = {
941
1003
  "agent.log.pendingResent": "session.pending : renvoi de {count} demande(s) en attente pour {session}",
942
1004
  "agent.log.unhandledType": "Type de message non g\xE9r\xE9 : {type}",
943
1005
  "events.log.fwdPermission": "Demande d'autorisation transmise au t\xE9l\xE9phone (id={id} tool={tool} {version})",
1006
+ "events.log.sessionDeleted": "session.deleted transmise au t\xE9l\xE9phone (session={session})",
944
1007
  "events.log.permissionReplied": "opencode a accus\xE9 permission.replied (id={id} {response})",
945
1008
  "events.log.questionRejected": "opencode a accus\xE9 question.rejected (id={id})",
946
1009
  "events.log.questionReplied": "opencode a accus\xE9 question.replied (id={id})",
@@ -1106,6 +1169,7 @@ var de_DE_default = {
1106
1169
  "agent.log.pendingResent": "session.pending: {count} ausstehende Anfrage(n) f\xFCr {session} erneut gesendet",
1107
1170
  "agent.log.unhandledType": "Nicht behandelter Nachrichtentyp: {type}",
1108
1171
  "events.log.fwdPermission": "Berechtigungsanfrage an Telefon weitergeleitet (id={id} tool={tool} {version})",
1172
+ "events.log.sessionDeleted": "session.deleted an Telefon weitergeleitet (session={session})",
1109
1173
  "events.log.permissionReplied": "opencode quittierte permission.replied (id={id} {response})",
1110
1174
  "events.log.questionRejected": "opencode quittierte question.rejected (id={id})",
1111
1175
  "events.log.questionReplied": "opencode quittierte question.replied (id={id})",
@@ -1271,6 +1335,7 @@ var es_ES_default = {
1271
1335
  "agent.log.pendingResent": "session.pending: reenviadas {count} solicitud(es) pendientes para {session}",
1272
1336
  "agent.log.unhandledType": "Tipo de mensaje no gestionado: {type}",
1273
1337
  "events.log.fwdPermission": "Solicitud de permiso reenviada al tel\xE9fono (id={id} tool={tool} {version})",
1338
+ "events.log.sessionDeleted": "session.deleted reenviada al tel\xE9fono (session={session})",
1274
1339
  "events.log.permissionReplied": "opencode confirm\xF3 permission.replied (id={id} {response})",
1275
1340
  "events.log.questionRejected": "opencode confirm\xF3 question.rejected (id={id})",
1276
1341
  "events.log.questionReplied": "opencode confirm\xF3 question.replied (id={id})",
@@ -1585,7 +1650,7 @@ async function printPairing(code, type, name, lang) {
1585
1650
  console.log(" " + tt("qr.fileWritten", { path: file }));
1586
1651
  console.log("");
1587
1652
  }
1588
- var DEFAULT_PAGE_SIZE = 50;
1653
+ var DEFAULT_PAGE_SIZE = 20;
1589
1654
  var MAX_PART_TEXT = 16e3;
1590
1655
  var MAX_PART_ARGS = 4e3;
1591
1656
  var MAX_PAGE_BYTES = 15e5;
@@ -2462,7 +2527,7 @@ function newSessionId() {
2462
2527
  }
2463
2528
 
2464
2529
  // src/bridge.ts
2465
- var VERSION = true ? "0.1.9" : "0.0.0-dev";
2530
+ var VERSION = true ? "0.1.11" : "0.0.0-dev";
2466
2531
  var AGENT_TYPE = "codex";
2467
2532
  var ADAPTER_COMMANDS = [
2468
2533
  { name: "new", description: "\u65B0\u5EFA\u4F1A\u8BDD" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tunnelbox/codex",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "tunnelbox 的 Codex 适配器(B 类):手机远程驱动本机 Codex CLI(codex exec --json,会话/流式/中止/删除)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -31,7 +31,7 @@
31
31
  "qrcode": "^1.5.3"
32
32
  },
33
33
  "devDependencies": {
34
- "@tunnelbox/core": "0.1.9",
34
+ "@tunnelbox/core": "0.1.11",
35
35
  "@types/node": "^22.0.0",
36
36
  "@types/qrcode": "^1.5.5",
37
37
  "esbuild": "^0.24.0",