@orion-agents/orion-code 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +75 -3
  2. package/README.zh-CN.md +79 -4
  3. package/dist/commands/index.d.ts.map +1 -1
  4. package/dist/commands/index.js +168 -54
  5. package/dist/commands/index.js.map +1 -1
  6. package/dist/commands/types.d.ts +3 -1
  7. package/dist/commands/types.d.ts.map +1 -1
  8. package/dist/commands/types.js.map +1 -1
  9. package/dist/framework/query.d.ts +3 -0
  10. package/dist/framework/query.d.ts.map +1 -1
  11. package/dist/framework/query.js +1 -0
  12. package/dist/framework/query.js.map +1 -1
  13. package/dist/framework/tool-scheduler.d.ts +61 -1
  14. package/dist/framework/tool-scheduler.d.ts.map +1 -1
  15. package/dist/framework/tool-scheduler.js +165 -47
  16. package/dist/framework/tool-scheduler.js.map +1 -1
  17. package/dist/framework/tool.d.ts +6 -0
  18. package/dist/framework/tool.d.ts.map +1 -1
  19. package/dist/framework/tool.js +2 -0
  20. package/dist/framework/tool.js.map +1 -1
  21. package/dist/ink-ui/screens/ReplScreen.d.ts +2 -1
  22. package/dist/ink-ui/screens/ReplScreen.d.ts.map +1 -1
  23. package/dist/ink-ui/screens/ReplScreen.js +65 -0
  24. package/dist/ink-ui/screens/ReplScreen.js.map +1 -1
  25. package/dist/print-ui/launch.d.ts +2 -1
  26. package/dist/print-ui/launch.d.ts.map +1 -1
  27. package/dist/print-ui/launch.js +7 -0
  28. package/dist/print-ui/launch.js.map +1 -1
  29. package/dist/runtime/agent-runtime-controller.d.ts +1 -0
  30. package/dist/runtime/agent-runtime-controller.d.ts.map +1 -1
  31. package/dist/runtime/agent-runtime-controller.js +24 -3
  32. package/dist/runtime/agent-runtime-controller.js.map +1 -1
  33. package/dist/runtime/agent-runtime-protocol.d.ts +13 -1
  34. package/dist/runtime/agent-runtime-protocol.d.ts.map +1 -1
  35. package/dist/runtime/agent-runtime-protocol.js +6 -0
  36. package/dist/runtime/agent-runtime-protocol.js.map +1 -1
  37. package/dist/runtime/chat-controller.d.ts +6 -0
  38. package/dist/runtime/chat-controller.d.ts.map +1 -1
  39. package/dist/runtime/chat-controller.js +13 -0
  40. package/dist/runtime/chat-controller.js.map +1 -1
  41. package/dist/runtime/goals/tools.d.ts.map +1 -1
  42. package/dist/runtime/goals/tools.js +14 -14
  43. package/dist/runtime/goals/tools.js.map +1 -1
  44. package/dist/runtime/goals/types.d.ts +6 -0
  45. package/dist/runtime/goals/types.d.ts.map +1 -1
  46. package/dist/runtime/goals/types.js.map +1 -1
  47. package/dist/runtime/turn-controller.d.ts.map +1 -1
  48. package/dist/runtime/turn-controller.js +3 -1
  49. package/dist/runtime/turn-controller.js.map +1 -1
  50. package/dist/runtime/ui-events.d.ts +17 -0
  51. package/dist/runtime/ui-events.d.ts.map +1 -1
  52. package/dist/runtime/ui-events.js.map +1 -1
  53. package/dist/services/config.d.ts.map +1 -1
  54. package/dist/services/config.js +2 -1
  55. package/dist/services/config.js.map +1 -1
  56. package/dist/services/global-config.d.ts +31 -0
  57. package/dist/services/global-config.d.ts.map +1 -1
  58. package/dist/services/global-config.js.map +1 -1
  59. package/dist/services/model-catalog.d.ts +2 -0
  60. package/dist/services/model-catalog.d.ts.map +1 -1
  61. package/dist/services/model-catalog.js +25 -15
  62. package/dist/services/model-catalog.js.map +1 -1
  63. package/dist/services/model-context.d.ts.map +1 -1
  64. package/dist/services/model-context.js +36 -0
  65. package/dist/services/model-context.js.map +1 -1
  66. package/dist/services/model-registry.d.ts.map +1 -1
  67. package/dist/services/model-registry.js +32 -0
  68. package/dist/services/model-registry.js.map +1 -1
  69. package/dist/services/tool-allowlist.d.ts +100 -0
  70. package/dist/services/tool-allowlist.d.ts.map +1 -0
  71. package/dist/services/tool-allowlist.js +250 -0
  72. package/dist/services/tool-allowlist.js.map +1 -0
  73. package/dist/terminal-ui/launch.d.ts +22 -2
  74. package/dist/terminal-ui/launch.d.ts.map +1 -1
  75. package/dist/terminal-ui/launch.js +145 -1
  76. package/dist/terminal-ui/launch.js.map +1 -1
  77. package/dist/tools/git.d.ts +4 -0
  78. package/dist/tools/git.d.ts.map +1 -1
  79. package/dist/tools/git.js +310 -3
  80. package/dist/tools/git.js.map +1 -1
  81. package/dist/tools/index.d.ts.map +1 -1
  82. package/dist/tools/index.js +56 -7
  83. package/dist/tools/index.js.map +1 -1
  84. package/dist/tools/sandbox.d.ts +139 -0
  85. package/dist/tools/sandbox.d.ts.map +1 -0
  86. package/dist/tools/sandbox.js +485 -0
  87. package/dist/tools/sandbox.js.map +1 -0
  88. package/dist/tools/web.d.ts.map +1 -1
  89. package/dist/tools/web.js +113 -42
  90. package/dist/tools/web.js.map +1 -1
  91. package/dist/tui-ui/launch.d.ts.map +1 -1
  92. package/dist/tui-ui/launch.js +46 -8
  93. package/dist/tui-ui/launch.js.map +1 -1
  94. package/dist/tui-ui/layout.d.ts.map +1 -1
  95. package/dist/tui-ui/layout.js +62 -0
  96. package/dist/tui-ui/layout.js.map +1 -1
  97. package/dist/tui-ui/runner.d.ts +6 -0
  98. package/dist/tui-ui/runner.d.ts.map +1 -1
  99. package/dist/tui-ui/runner.js +81 -1
  100. package/dist/tui-ui/runner.js.map +1 -1
  101. package/dist/tui-ui/state.d.ts +19 -1
  102. package/dist/tui-ui/state.d.ts.map +1 -1
  103. package/dist/tui-ui/state.js +35 -7
  104. package/dist/tui-ui/state.js.map +1 -1
  105. package/dist/tui-ui/terminal-image.d.ts +3 -13
  106. package/dist/tui-ui/terminal-image.d.ts.map +1 -1
  107. package/dist/tui-ui/terminal-image.js +5 -160
  108. package/dist/tui-ui/terminal-image.js.map +1 -1
  109. package/dist/utils/mask.d.ts +28 -0
  110. package/dist/utils/mask.d.ts.map +1 -0
  111. package/dist/utils/mask.js +54 -0
  112. package/dist/utils/mask.js.map +1 -0
  113. package/package.json +2 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **Goal-driven coding agent for the terminal.**
4
4
  >
5
- > v0.1.2 — Goal continuity and evidence audit
5
+ > v0.1.3 — Goal continuity, model config & shell sandbox POC
6
6
 
7
7
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
8
  [![Node.js](https://img.shields.io/badge/node-%3E%3D20.0-green.svg)](https://nodejs.org)
@@ -35,8 +35,8 @@ Orion Code is a terminal-based coding agent. It wraps LLM APIs in a harness of s
35
35
  ### Install & Run
36
36
 
37
37
  ```bash
38
- # Install the v0.1.2 release:
39
- npm install -g @orion-agents/orion-code@0.1.2
38
+ # Install the v0.1.3 release:
39
+ npm install -g @orion-agents/orion-code@0.1.3
40
40
 
41
41
  # Or run from a checked-out source tree:
42
42
  npm ci
@@ -147,6 +147,78 @@ Configuration: `~/.orion-code/orion.json` | Priority: `CLI flags > config > env
147
147
  The renderer is a runtime choice rather than a persisted setting: `orion` starts
148
148
  the TUI, while `--ui terminal` selects the technical renderer.
149
149
 
150
+ ### Project tool rules (`allowedTools`)
151
+
152
+ Per-project permission rules live under `projects["<absolute path>"].allowedTools`.
153
+ They are scoped to a single repository and are evaluated on top of each tool's own
154
+ policy — they can tighten the gate, never loosen it.
155
+
156
+ ```json
157
+ {
158
+ "projects": {
159
+ "/Users/me/work/api": {
160
+ "allowedTools": [
161
+ "exec_command(git status*)",
162
+ "exec_command(npm test*)",
163
+ "ask:exec_command(git push*)",
164
+ "deny:read_file(*.env)"
165
+ ]
166
+ }
167
+ }
168
+ }
169
+ ```
170
+
171
+ - Grammar: `[allow|ask|deny:]tool[(subject glob)]`; `*` matches any run of characters,
172
+ `?` a single one, and `*` as the tool name matches every tool.
173
+ - The subject is the call's `command` / `file_path` / `path` / `url` / `pattern` / `query`
174
+ argument, whichever comes first.
175
+ - Conflicts resolve most-restrictive-first (`deny` > `ask` > `allow`), independent of order.
176
+ - `allow` only skips an interactive confirmation. It never overrides a tool's own `deny`,
177
+ never escapes plan mode, and never auto-approves a destructive invocation.
178
+ - Malformed entries are ignored and listed by `/config` rather than silently applied.
179
+
180
+ ### Shell execution sandbox (`sandbox`)
181
+
182
+ `exec_command` can run inside an OS-level sandbox. This is a **security POC**: the
183
+ backend is probed at runtime and a configured-but-unusable sandbox **fails closed**
184
+ (refuses to run) rather than silently degrading to an unsandboxed execution.
185
+
186
+ ```json
187
+ {
188
+ "sandbox": {
189
+ "profile": "none",
190
+ "backend": "auto",
191
+ "allowNetwork": false,
192
+ "writableRoots": [],
193
+ "image": "alpine:latest"
194
+ }
195
+ }
196
+ ```
197
+
198
+ - **Ownership** — `sandbox` lives in the global config (`~/.orion-code/orion.json`)
199
+ and can be overridden per project under `projects["<path>"].sandbox`. Project
200
+ keys win individually; absent keys inherit the global value.
201
+ - **`profile`** — `none` (default, no isolation, identical to the legacy `sh -c`),
202
+ `read-only` (no writes anywhere, no network), or `workspace-write` (writes
203
+ confined to the workspace + temp dirs, network still blocked unless
204
+ `allowNetwork: true`). An unknown value (e.g. written by a newer Orion) is a
205
+ hard failure, never a downgrade to `none`.
206
+ - **`backend`** — `auto` (default, picks the first usable backend), `seatbelt`
207
+ (macOS `sandbox-exec`), `bubblewrap` (Linux `bwrap`), or `docker`. Each backend
208
+ is validated with a real execution probe; `which`-based guessing is never used.
209
+ - **`image`** — required only for `docker`; names the container image to run in.
210
+ - **`writableRoots`** — extra host roots the `workspace-write` profile may write
211
+ to (in addition to the cwd and temp). Non-string / empty entries are dropped.
212
+ - **Migration / rollback** — adding the `sandbox` key is backward compatible: the
213
+ default `profile: "none"` reproduces the previous behaviour exactly. Removing the
214
+ key (or reverting to a pre-sandbox version) is a safe rollback; older Orion
215
+ simply ignores the unknown field. No secrets are stored, so no redaction is
216
+ required.
217
+ - **Known limitations** — `seatbelt` cannot be nested inside another sandbox (e.g.
218
+ when Orion itself runs inside an app sandbox); `docker` runs the command in a
219
+ separate process tree and only constrains what is bind-mounted (the image rootfs
220
+ is made read-only via `--read-only`); none of the backends constrain CPU/memory.
221
+
150
222
  ## Interactive Commands
151
223
 
152
224
  | Command | Description |
package/README.zh-CN.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > **Orion Code — 通用 Agent 驾驭框架**
4
4
  > 一个 CLI 驱动的编码 Agent,具备安全边界、工具编排、记忆系统和上下文管理。
5
5
  >
6
- > v0.1.2 — Goal 连续性与证据审计
6
+ > v0.1.3 — Goal 连续性、模型配置与命令沙箱 POC
7
7
 
8
8
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
9
  [![Node.js](https://img.shields.io/badge/node-%3E%3D20.0-green.svg)](https://nodejs.org)
@@ -104,10 +104,10 @@ npm start -- --print "review the current git diff"
104
104
 
105
105
  ### 全局安装
106
106
 
107
- 固定安装 v0.1.2
107
+ 固定安装 v0.1.3
108
108
 
109
109
  ```bash
110
- npm install -g @orion-agents/orion-code@0.1.2
110
+ npm install -g @orion-agents/orion-code@0.1.3
111
111
  # 任意目录运行
112
112
  orion
113
113
  ```
@@ -173,6 +173,69 @@ v0.1.2 只覆盖单 Session、单 Active Goal,不承诺多 Goal 调度或无
173
173
  CLI 参数 > ~/.orion-code/orion.json > 环境变量 > 内部默认
174
174
  ```
175
175
 
176
+ ### 项目级工具规则(`allowedTools`)
177
+
178
+ 权限规则写在 `projects["<项目绝对路径>"].allowedTools`,只作用于该仓库。规则叠加在
179
+ 工具自身的权限策略之上:**只能收紧,不能放宽**。
180
+
181
+ ```json
182
+ {
183
+ "projects": {
184
+ "/Users/me/work/api": {
185
+ "allowedTools": [
186
+ "exec_command(git status*)",
187
+ "exec_command(npm test*)",
188
+ "ask:exec_command(git push*)",
189
+ "deny:read_file(*.env)"
190
+ ]
191
+ }
192
+ }
193
+ }
194
+ ```
195
+
196
+ - 语法:`[allow|ask|deny:]工具名[(参数通配)]`;`*` 匹配任意字符串,`?` 匹配单个字符,
197
+ 工具名写 `*` 表示匹配所有工具。
198
+ - 参数通配匹配的是调用参数中的 `command` / `file_path` / `path` / `url` / `pattern` /
199
+ `query`(按此顺序取第一个非空字符串)。
200
+ - 冲突时按“最严格优先”解析:`deny` > `ask` > `allow`,与书写顺序无关。
201
+ - `allow` 仅用于免去交互确认,**不会**覆盖工具自身的 `deny`、**不会**突破 plan 模式的
202
+ 只读约束,也**不会**自动放行破坏性调用。
203
+ - 无法解析的条目会被忽略而不是静默放行,可通过 `/config` 查看解析结果与非法条目。
204
+
205
+ ### 命令执行沙箱(`sandbox`)
206
+
207
+ `exec_command` 可在操作系统级沙箱内执行。这是**安全 POC**:后端在运行
208
+ 时实测探测,已配置但不可用的沙箱会**fail closed**(拒绝执行),而不会静默降级为
209
+ 无沙箱执行。
210
+
211
+ ```json
212
+ {
213
+ "sandbox": {
214
+ "profile": "none",
215
+ "backend": "auto",
216
+ "allowNetwork": false,
217
+ "writableRoots": [],
218
+ "image": "alpine:latest"
219
+ }
220
+ }
221
+ ```
222
+
223
+ - **归属** — `sandbox` 位于全局配置(`~/.orion-code/orion.json`),并可在项目级
224
+ `projects["<项目绝对路径>"].sandbox` 覆盖。项目级按 key 单独覆盖,缺省 key 继承全局。
225
+ - **`profile`** — `none`(默认,不隔离,等价于旧版 `sh -c`)、`read-only`(任何位置不可写、无网络)、
226
+ `workspace-write`(写权限限定在工作区与临时目录,除非 `allowNetwork: true` 否则仍禁网)。
227
+ 未知取值(如新版 Orion 写入的更严格值)一律硬失败,绝不降级为 `none`。
228
+ - **`backend`** — `auto`(默认,自动选首个可用后端)、`seatbelt`(macOS `sandbox-exec`)、
229
+ `bubblewrap`(Linux `bwrap`)或 `docker`。每个后端都用真实执行探针校验,从不靠 `which` 推断。
230
+ - **`image`** — 仅 `docker` 需要,指定运行所用的容器镜像。
231
+ - **`writableRoots`** — `workspace-write` 可写入的额外宿主根目录(除工作区与临时目录外)。
232
+ 非字符串 / 空项会被丢弃。
233
+ - **迁移 / 回滚** — 新增 `sandbox` 完全向后兼容:默认 `profile: "none"` 与旧行为完全一致。
234
+ 删除该 key(或回退到沙箱前的版本)是安全回滚;旧版 Orion 会忽略该未知字段。不涉及任何敏感信息,无需脱敏。
235
+ - **已知限制** — `seatbelt` 不能嵌套在另一沙箱内(例如 Orion 自身运行在应用沙箱中时);
236
+ `docker` 在独立进程树中运行命令,只约束被 bind-mount 的部分(镜像根文件系统已通过 `--read-only` 设为只读);
237
+ 各后端均不限制 CPU / 内存。
238
+
176
239
  ### 环境变量
177
240
 
178
241
  | 变量 | 默认值 | 说明 |
@@ -399,7 +462,16 @@ orion-code/
399
462
 
400
463
  ## 版本历史
401
464
 
402
- ### v0.1.2(当前版本)
465
+ ### v0.1.3(当前版本)
466
+
467
+ - `/model` 与 `/models` 拆分:`/model` 只显示当前模型信息,`/models` 承接交互式切换;
468
+ - `modelRegistry` 成为模型展示与切换的唯一事实源,静态 catalog 退为 legacy fallback;
469
+ - 内置 catalog 补全(火山方舟 / xf-yun / deepseek 等),未配置上下文时正确回退;
470
+ - 命令沙箱 POC、只读 Git 工具与工具 allowlist 的安全基线。
471
+
472
+ > 完整变更与交付状态(已发布 / 已合并 / 候选)以 `CHANGELOG.md` 为准。
473
+
474
+ ### v0.1.2(已发布)
403
475
 
404
476
  - 单 Session、单 Active Goal 的 typed continuation;
405
477
  - restart/resume 后安全暂停,必须显式恢复;
@@ -439,6 +511,9 @@ npm run lint
439
511
 
440
512
  # 格式化
441
513
  npm run format
514
+
515
+ # 发布前只读检查(不会 tag / push / publish)
516
+ npm run release:check
442
517
  ```
443
518
 
444
519
  ---
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/commands/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAIL,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,aAAa,EAEnB,MAAM,SAAS,CAAC;AA8RjB,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,eAAe,GAAG,SAAS,GAAG,MAAM,CAErF;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,YAAY,EAAE,CASrE;AAikDD,iBAAe,UAAU,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAoVpF;AAixDD,wBAAgB,WAAW,IAAI,YAAY,EAAE,CAE5C;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAC,YAAY,CAAC,GAAG,YAAY,EAAE,CAQ1F;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAElE;AAED,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAE1C;AAED,OAAO,EAAE,UAAU,IAAI,WAAW,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/commands/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAIL,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,aAAa,EAEnB,MAAM,SAAS,CAAC;AA4UjB,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,eAAe,GAAG,SAAS,GAAG,MAAM,CAErF;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,YAAY,EAAE,CASrE;AA+nDD,iBAAe,UAAU,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAqVpF;AAyyDD,wBAAgB,WAAW,IAAI,YAAY,EAAE,CAE5C;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAC,YAAY,CAAC,GAAG,YAAY,EAAE,CAQ1F;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAElE;AAED,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAE1C;AAED,OAAO,EAAE,UAAU,IAAI,WAAW,EAAE,CAAC"}
@@ -21,6 +21,7 @@ const ui_view_model_1 = require("../runtime/ui-view-model");
21
21
  const task_manager_1 = require("../services/task-manager");
22
22
  const agent_runner_1 = require("../services/agent-runner");
23
23
  const config_1 = require("../services/config");
24
+ const tool_allowlist_1 = require("../services/tool-allowlist");
24
25
  const box_1 = require("../ui/box");
25
26
  const stream_markdown_1 = require("../ui/stream-markdown");
26
27
  const progress_1 = require("../ui/progress");
@@ -40,7 +41,9 @@ const prompt_context_1 = require("../services/prompt-context");
40
41
  const doctor_1 = require("../services/doctor");
41
42
  const model_context_1 = require("../services/model-context");
42
43
  const token_estimate_1 = require("../utils/token-estimate");
44
+ const mask_1 = require("../utils/mask");
43
45
  const model_catalog_1 = require("../services/model-catalog");
46
+ const model_registry_1 = require("../services/model-registry");
44
47
  const workspace_diff_1 = require("../services/workspace-diff");
45
48
  const commit_plan_1 = require("../services/commit-plan");
46
49
  const tool_artifacts_1 = require("../core/tool-artifacts");
@@ -107,8 +110,42 @@ function formatRendererStatus(ctx) {
107
110
  : DIM('custom');
108
111
  return `${BRAND(snapshot.renderer.name)} ${status} ${DIM(snapshot.renderer.capabilityLabels.join(', '))}`;
109
112
  }
110
- function formatModelAliasHelp() {
111
- return Object.keys((0, model_catalog_1.getModelCatalogAliases)()).sort().join(', ');
113
+ function getProfileByModelId(registry, modelId) {
114
+ if (!registry)
115
+ return undefined;
116
+ const normalized = modelId.trim();
117
+ if (!normalized)
118
+ return undefined;
119
+ if (registry.profiles.has(normalized))
120
+ return registry.profiles.get(normalized);
121
+ for (const profile of registry.profiles.values()) {
122
+ if (modelId === profile.model)
123
+ return profile;
124
+ }
125
+ return undefined;
126
+ }
127
+ function resolveModelFromRegistry(config, selector) {
128
+ if (!config.modelRegistry)
129
+ return undefined;
130
+ const trimmed = selector.trim();
131
+ if (!trimmed)
132
+ return undefined;
133
+ return (0, model_registry_1.lookupProfile)(config.modelRegistry, trimmed) ?? undefined;
134
+ }
135
+ function listConfiguredModelCatalogEntries(registry) {
136
+ if (!registry)
137
+ return [];
138
+ return registry.enabledProfiles.map(profile => {
139
+ const provider = registry.providers.get(profile.provider);
140
+ return {
141
+ name: profile.id,
142
+ alias: profile.aliases?.[0],
143
+ provider: provider?.displayName ?? profile.provider,
144
+ contextWindow: profile.resolvedContextWindow,
145
+ maxOutputTokens: profile.resolvedMaxOutputTokens,
146
+ source: `${profile.contextSource}/${profile.outputSource}`,
147
+ };
148
+ });
112
149
  }
113
150
  function formatLoopBudgetSource(stats) {
114
151
  const source = stats.loopBudgetSource ?? 'unknown';
@@ -1319,19 +1356,35 @@ function showConfig(ctx) {
1319
1356
  console.log();
1320
1357
  console.log(HEADER('Configuration'));
1321
1358
  console.log(DIM('─'.repeat(40)));
1359
+ const allowlist = (0, tool_allowlist_1.resolveProjectToolAllowlist)(ctx.cwd);
1360
+ const allowlistSummary = allowlist.rules.length === 0 && allowlist.invalid.length === 0
1361
+ ? '(none)'
1362
+ : `${allowlist.rules.length} rule(s)` +
1363
+ (allowlist.invalid.length > 0 ? `, ${allowlist.invalid.length} invalid (ignored)` : '');
1322
1364
  const summary = {
1323
1365
  name: ctx.config.name,
1324
1366
  model: ctx.config.model,
1325
1367
  apiBaseUrl: ctx.config.apiBaseUrl || '(default OpenAI)',
1326
- apiKey: ctx.config.apiKey ? `${ctx.config.apiKey.slice(0, 7)}***` : '(not set)',
1368
+ apiKey: (0, mask_1.maskSecret)(ctx.config.apiKey),
1327
1369
  mode: ctx.config.mode,
1328
1370
  logLevel: ctx.config.logLevel,
1329
1371
  toolConfirmation: ctx.config.toolConfirmation,
1372
+ allowedTools: allowlistSummary,
1330
1373
  };
1331
1374
  const llmSummary = ctx.llm?.getConfigSummary() ?? {};
1332
1375
  for (const [key, val] of Object.entries(summary)) {
1333
1376
  console.log(` ${ACCENT(key.padEnd(16))} ${DIM(val)}`);
1334
1377
  }
1378
+ if (allowlist.rules.length > 0 || allowlist.invalid.length > 0) {
1379
+ console.log();
1380
+ console.log(HEADER(' Project allowedTools:'));
1381
+ for (const rule of allowlist.rules) {
1382
+ console.log(` ${ACCENT(rule.effect.padEnd(6))} ${DIM(rule.tool)}${DIM(rule.pattern ? `(${rule.pattern})` : '')}`);
1383
+ }
1384
+ for (const entry of allowlist.invalid) {
1385
+ console.log(` ${WARN('invalid')} ${DIM(entry)}`);
1386
+ }
1387
+ }
1335
1388
  console.log();
1336
1389
  console.log(HEADER(' LLM Settings:'));
1337
1390
  for (const [key, val] of Object.entries(llmSummary)) {
@@ -1346,14 +1399,29 @@ function handleModel(ctx, args) {
1346
1399
  if (!args || trimmedArgs === '?' || trimmedArgs === 'info') {
1347
1400
  console.log();
1348
1401
  if (ctx.llm) {
1349
- const currentModel = ctx.llm.getModel();
1402
+ const currentModel = ctx.store.getSnapshot().currentModel || ctx.llm.getModel();
1403
+ const profile = getProfileByModelId(ctx.config.modelRegistry, currentModel);
1350
1404
  const aliasEntry = (0, model_catalog_1.getModelCatalogEntry)(currentModel);
1351
- const contextInfo = (0, model_context_1.resolveModelContext)(currentModel);
1405
+ const contextInfo = profile
1406
+ ? {
1407
+ id: profile.model,
1408
+ label: profile.displayName || profile.id,
1409
+ contextWindow: profile.resolvedContextWindow,
1410
+ maxOutputTokens: profile.resolvedMaxOutputTokens,
1411
+ source: 'config',
1412
+ matchedId: profile.model,
1413
+ }
1414
+ : (0, model_context_1.resolveModelContext)(currentModel);
1352
1415
  const compactStats = getCommandAutoCompact(ctx, currentModel).getStats();
1353
1416
  console.log(HEADER('Current Model'));
1354
1417
  console.log(DIM('─'.repeat(40)));
1355
1418
  console.log(` Model ${BRAND(currentModel)}`);
1356
- if (aliasEntry) {
1419
+ if (profile) {
1420
+ console.log(` Alias ${ACCENT(profile.aliases?.[0] ? `(${profile.aliases[0]})` : 'none')}`);
1421
+ const provider = ctx.config.modelRegistry?.providers.get(profile.provider);
1422
+ console.log(` Provider ${DIM(provider?.displayName || profile.provider)}`);
1423
+ }
1424
+ else if (aliasEntry) {
1357
1425
  console.log(` Alias ${ACCENT(aliasEntry.alias)}`);
1358
1426
  console.log(` Provider ${DIM(aliasEntry.provider)}`);
1359
1427
  }
@@ -1370,49 +1438,12 @@ function handleModel(ctx, args) {
1370
1438
  console.log();
1371
1439
  return { success: true };
1372
1440
  }
1373
- // 显示模型列表
1374
- if (trimmedArgs === 'list' || trimmedArgs === 'ls') {
1375
- console.log();
1376
- console.log(HEADER('Available Models'));
1377
- console.log(DIM('─'.repeat(40)));
1378
- const currentModel = ctx.llm?.getModel() || '';
1379
- const modelPicker = (0, ui_view_model_1.createModelPickerState)({
1380
- currentModel,
1381
- models: (0, model_catalog_1.listModelCatalogEntries)().map(model => {
1382
- const contextInfo = (0, model_context_1.resolveModelContext)(model.name);
1383
- return {
1384
- ...model,
1385
- contextWindow: contextInfo.contextWindow,
1386
- maxOutputTokens: contextInfo.maxOutputTokens,
1387
- source: contextInfo.source,
1388
- };
1389
- }),
1390
- });
1391
- for (const item of modelPicker.visibleItems) {
1392
- const marker = item.isCurrent ? SUCCESS('●') : DIM('○');
1393
- const alias = item.alias ? `(${item.alias})` : '';
1394
- const context = `${formatTokenCount(item.contextWindow ?? 0)} ctx`;
1395
- const current = item.isCurrent ? BRAND('(current)') : '';
1396
- console.log(` ${marker} ${ACCENT(item.name)} ${DIM(alias)} ${DIM(context)} ${current}`);
1397
- console.log(` ${DIM(item.provider ?? 'unknown')}`);
1398
- }
1441
+ // /model list|ls|help 已移除,统一由 /models 承接(交互式选择切换)
1442
+ if (trimmedArgs === 'list' || trimmedArgs === 'ls' || trimmedArgs === 'help') {
1399
1443
  console.log();
1400
- console.log(DIM('Use /model <name|alias> to switch, e.g. /model sonnet'));
1401
- console.log();
1402
- return { success: true };
1403
- }
1404
- // 显示帮助
1405
- if (trimmedArgs === 'help') {
1406
- console.log();
1407
- console.log(HEADER('/model Command Help'));
1408
- console.log(DIM('─'.repeat(40)));
1409
- console.log();
1410
- console.log(` ${ACCENT('/model')} Show current model`);
1411
- console.log(` ${ACCENT('/model list')} Show available models`);
1412
- console.log(` ${ACCENT('/model <name>')} Switch to specific model`);
1413
- console.log(` ${ACCENT('/model <alias>')} Switch using alias (opus, sonnet, haiku)`);
1414
- console.log();
1415
- console.log(DIM(`Aliases: ${formatModelAliasHelp()}`));
1444
+ console.log(WARN(`/model ${trimmedArgs} was removed.`));
1445
+ console.log(DIM('Use /models to switch models interactively,'));
1446
+ console.log(DIM('or /model <name|alias> to switch directly, e.g. /model sonnet'));
1416
1447
  console.log();
1417
1448
  return { success: true };
1418
1449
  }
@@ -1423,16 +1454,75 @@ function handleModel(ctx, args) {
1423
1454
  return { success: false };
1424
1455
  }
1425
1456
  // 解析别名
1426
- const resolvedModel = (0, model_catalog_1.resolveModelAlias)(args);
1457
+ const registryProfile = resolveModelFromRegistry(ctx.config, args);
1458
+ const resolvedModel = registryProfile ? registryProfile.model : (0, model_catalog_1.resolveModelAlias)(args);
1459
+ const resolvedProfileId = registryProfile ? registryProfile.id : resolvedModel;
1427
1460
  ctx.llm.setModel(resolvedModel);
1428
1461
  getCommandAutoCompact(ctx, resolvedModel);
1429
- ctx.store.setState({ currentModel: resolvedModel });
1430
- console.log(SUCCESS(`✔ Model changed to ${BRAND(resolvedModel)}`));
1431
- const contextInfo = (0, model_context_1.resolveModelContext)(resolvedModel);
1462
+ ctx.store.setState({ currentModel: resolvedProfileId });
1463
+ console.log(SUCCESS(`✔ Model changed to ${BRAND(resolvedProfileId)}`));
1464
+ const contextInfo = registryProfile
1465
+ ? {
1466
+ contextWindow: registryProfile.resolvedContextWindow,
1467
+ maxOutputTokens: registryProfile.resolvedMaxOutputTokens,
1468
+ source: 'resolved',
1469
+ }
1470
+ : (0, model_context_1.resolveModelContext)(resolvedModel);
1432
1471
  console.log(DIM(` Context window ${formatTokenCount(contextInfo.contextWindow)} tokens (${contextInfo.source})`));
1433
1472
  console.log();
1434
1473
  return { success: true };
1435
1474
  }
1475
+ /**
1476
+ * /models — 交互式选择切换模型。
1477
+ * 交互式渲染器返回结构化 modelPicker 请求(渲染器弹出选择层,选中即 /model <id>);
1478
+ * 非交互式渲染器直接打印候选列表并提示 /model <name|alias>。
1479
+ */
1480
+ function handleModels(ctx, _args) {
1481
+ console.log();
1482
+ const currentModel = ctx.store.getSnapshot().currentModel || ctx.llm?.getModel() || '';
1483
+ const configuredModels = listConfiguredModelCatalogEntries(ctx.config.modelRegistry);
1484
+ const candidates = configuredModels.length > 0
1485
+ ? configuredModels
1486
+ : (0, model_catalog_1.listModelCatalogEntries)().map(model => {
1487
+ const contextInfo = (0, model_context_1.resolveModelContext)(model.name);
1488
+ return {
1489
+ name: model.name,
1490
+ alias: model.alias,
1491
+ provider: model.provider,
1492
+ contextWindow: contextInfo.contextWindow,
1493
+ maxOutputTokens: contextInfo.maxOutputTokens,
1494
+ source: contextInfo.source,
1495
+ };
1496
+ });
1497
+ const ui = commandUICapabilities(ctx);
1498
+ if (ui.structuredPickers) {
1499
+ return {
1500
+ success: true,
1501
+ modelPicker: {
1502
+ models: candidates,
1503
+ currentModel,
1504
+ title: 'Switch Model',
1505
+ maxVisibleItems: 12,
1506
+ },
1507
+ };
1508
+ }
1509
+ // 非交互式渲染器:打印候选列表
1510
+ console.log(HEADER('Available Models'));
1511
+ console.log(DIM('─'.repeat(40)));
1512
+ const modelPicker = (0, ui_view_model_1.createModelPickerState)({ currentModel, models: candidates });
1513
+ for (const item of modelPicker.visibleItems) {
1514
+ const marker = item.isCurrent ? SUCCESS('●') : DIM('○');
1515
+ const alias = item.alias ? `(${item.alias})` : '';
1516
+ const context = `${formatTokenCount(item.contextWindow ?? 0)} ctx`;
1517
+ const current = item.isCurrent ? BRAND('(current)') : '';
1518
+ console.log(` ${marker} ${ACCENT(item.name)} ${DIM(alias)} ${DIM(context)} ${current}`);
1519
+ console.log(` ${DIM(item.provider ?? 'unknown')}`);
1520
+ }
1521
+ console.log();
1522
+ console.log(DIM('Use /model <name|alias> to switch, e.g. /model sonnet'));
1523
+ console.log();
1524
+ return { success: true };
1525
+ }
1436
1526
  function normalizePermissionMode(raw) {
1437
1527
  const value = raw.trim().toLowerCase();
1438
1528
  if (!value)
@@ -1733,6 +1823,7 @@ async function handleChat(ctx, input) {
1733
1823
  costTracker: snapshot.costTracker,
1734
1824
  permissionMode: snapshot.permissionMode,
1735
1825
  toolConfirmation: ctx.config.toolConfirmation,
1826
+ toolAllowlist: (0, tool_allowlist_1.resolveProjectToolAllowlist)(ctx.cwd).evaluator,
1736
1827
  toolContext: {
1737
1828
  cwd: ctx.cwd,
1738
1829
  config: {
@@ -3184,7 +3275,7 @@ const COMMANDS = [
3184
3275
  {
3185
3276
  name: 'model',
3186
3277
  description: 'Show or change the current model',
3187
- argumentHint: '[model|list|help]',
3278
+ argumentHint: '[model|info]',
3188
3279
  category: 'model',
3189
3280
  priority: 10,
3190
3281
  type: 'builtin',
@@ -3192,9 +3283,20 @@ const COMMANDS = [
3192
3283
  risk: 'state-write',
3193
3284
  execute: (ctx, args) => handleModel(ctx, args),
3194
3285
  },
3286
+ {
3287
+ name: 'models',
3288
+ description: 'Switch the current model interactively',
3289
+ argumentHint: '',
3290
+ category: 'model',
3291
+ priority: 10,
3292
+ type: 'builtin',
3293
+ execution: 'builtin',
3294
+ risk: 'state-write',
3295
+ execute: (ctx, args) => handleModels(ctx, args),
3296
+ },
3195
3297
  {
3196
3298
  name: 'mode',
3197
- aliases: ['permissions', 'perm'],
3299
+ aliases: ['perm'],
3198
3300
  description: 'Show or change tool permission mode',
3199
3301
  argumentHint: '[default|accept-edits|plan|auto|next]',
3200
3302
  category: 'model',
@@ -3263,6 +3365,18 @@ const COMMANDS = [
3263
3365
  rendererScope: ['tui'],
3264
3366
  execute: () => ({ success: true }),
3265
3367
  },
3368
+ {
3369
+ name: 'permissions',
3370
+ description: 'Set the tool confirmation policy (TUI). No argument opens a picker; with an argument applies immediately.',
3371
+ argumentHint: '[allow|ask|deny]',
3372
+ category: 'system',
3373
+ priority: 33,
3374
+ type: 'builtin',
3375
+ execution: 'renderer-local',
3376
+ risk: 'state-write',
3377
+ rendererScope: ['tui'],
3378
+ execute: () => ({ success: true }),
3379
+ },
3266
3380
  {
3267
3381
  name: 'redraw',
3268
3382
  description: 'Redraw the TUI-owned live region',