Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6106e5b
feat(permissions): say what a sub-agent is blocked on, instead of not…
y49 Jul 29, 2026
38c9557
fix(daemon): escape the NUL separator so bootstrap.ts stays greppable
y49 Jul 29, 2026
1d7ff6e
feat(mode): add the 'all' rung and one shared posture module
y49 Jul 29, 2026
903d95c
fix: remove NUL byte from test file and expand guard to both src and …
y49 Jul 29, 2026
3e3f679
feat(cli): tlive mode accepts 'all'; status and setup follow the shar…
y49 Jul 29, 2026
9e3f77c
feat(mode)!: 'all' replaces approvals.holdSubagents, and the daemon s…
y49 Jul 29, 2026
051320e
feat(approvals): let a held request be handed back to the terminal on…
y49 Jul 29, 2026
e7ce8c3
feat(im): /mode sets the posture from the phone
y49 Jul 29, 2026
9a1f72d
fix(mode): document currentMode's missing-file path as intentional, p…
y49 Jul 29, 2026
c9e82e0
test(im): strengthen /mode tests so the current-rung marker can't be …
y49 Jul 29, 2026
f397f85
feat(cards): one-tap posture switch on sub-agent cards
y49 Jul 29, 2026
4071c69
docs(mode): document the four-rung ladder and the sub-agent card buttons
y49 Jul 29, 2026
77bac6e
test(telegram): include /mode in the expected bot command menu
y49 Jul 29, 2026
62e0c8d
fix(mode): re-read the posture live for permission_prompt gating
y49 Jul 29, 2026
c389686
test(mode): lock the live-posture fix, handback's wire mapping, and /…
y49 Jul 29, 2026
a4333d0
docs(mode): finish the four-rung sweep and note the removed config key
y49 Jul 29, 2026
d7f3cc6
fix(mode): a passed-through sub-agent dialog now fires the desktop to…
y49 Jul 29, 2026
84f909d
fix(mode): extend the desktop-clear condition to every site, not just…
y49 Jul 29, 2026
6025558
fix(daemon): register a session before hook.notify renders its label tag
y49 Jul 29, 2026
7cdba4e
fix(daemon): register a session before onPending/onPassthrough render…
y49 Jul 29, 2026
271b08b
fix(daemon): retirement edit keeps the base sub-agent notice body, no…
y49 Jul 29, 2026
256a9b0
fix(im): render the sub-agent waiting notice with *italic*, not _italic_
y49 Jul 29, 2026
bdf0ffe
docs(daemon): say what the session-registration guard actually does
y49 Jul 30, 2026
431beae
fix(mode): warn on any downgrade from all, only when a sub-agent is a…
y49 Jul 30, 2026
f57e2fe
fix(im): mark a held sub-agent's approval card as a sub-agent card
y49 Jul 30, 2026
a0ebf7e
fix(daemon): say 'each card' not 'their card' for multiple held sub-a…
y49 Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions KERNEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
> `tests/contract/` 锁定。改动任一接口 = breaking change = bump major。
>
> **默认姿态 `notify`**:只监看 / 通知,shim 把每个 `PermissionRequest` 短路成
> `{}`,tlive 物理上不 hold 任何审批。下文"审批"一节描述的 gating 只在
> **`mode: full`**(远程审批 opt-in)下发生;`mode: off` 则每个 hook 都 no-op。
> mode 语义见 `README.md`,由 `normalizer.ts` 的 `effectiveMode`
> 单点决定(notify 默认)。
> `{}`,tlive 物理上不 hold 任何审批。下文"审批"一节描述的 gating
> **`mode: full`**(远程审批 opt-in)及 **`mode: all`**(连子代理也 hold 住)
> 下发生;`mode: off` 则每个 hook 都 no-op。mode 语义见 `README.md`,由
> `normalizer.ts` 的 `effectiveMode` 单点决定(notify 默认)。
>
> v1.0 的 SDK-driver 冻结面(`RuntimeAdapter` / MCP 三工具等)已在 v2 移除——
> tlive 不再驱动/拥有会话。
Expand Down Expand Up @@ -98,11 +98,12 @@ continueDecisionOut(reply: string | null): object // reply → {decision:'bloc
Claude 的原始 hook JSON 在这里归一。加一个新 AI runtime = 把它的 hook 事件
映射进这套归一模型(hook 式集成)或走 companion 式集成(见下),kernel 不变。

**审批(Claude Code)**(下述 gating 仅 `mode: full`;默认 `notify` 下 shim
把 `PermissionRequest` 短路成 `{}`,以下一律不发生)。**带 `agent_id` 的
子代理请求默认透传**(→`{}`,交 CC 原生;被 hold 的子代理没有并行本地框可
兜底)——opt-in `approvals.holdSubagents:true` 才 hold 住等远程答;`safe`/
`trust` 的自动放行在此闸之前,不受影响。gating 走 `PermissionRequest` hook
**审批(Claude Code)**(下述主会话 gating 在 `mode: full`/`all` 下发生;
默认 `notify` 下 shim 把 `PermissionRequest` 短路成 `{}`,以下一律不发生)。
**带 `agent_id` 的子代理请求默认透传**(→`{}`,交 CC 原生;被 hold 的子代理
没有并行本地框可兜底)——只有 `mode: all` 才 hold 住等远程答(第四档,取代
已删除的 `approvals.holdSubagents`);`safe`/`trust` 的自动放行在此闸之前,
不受影响。gating 走 `PermissionRequest` hook
——它与本地权限对话**并行**(先答先得),窗口默认顶格(`approvals.windowSec`,默认 86200s
≈24h,clamp 60~86200,**两家共用**;hooks.json `timeout: 86400`,shim
IPC=窗+100s,daemon clamp 24h。超时 ≠ 拒绝——本地框永不超时仍在等,短窗口
Expand Down Expand Up @@ -261,7 +262,7 @@ File: `src/kernel/contracts/cli-surface.ts`

```
核心 8: setup, start, stop, status, logs, run, url, hook
加法命令: mode (off|notify|full 姿态,持久化到 config)
加法命令: mode (off|notify|full|all 姿态,持久化到 config)
mute, trust, safe, desktop (on|off 运行时开关,与 IM 命令同一 setter)
```

Expand Down
81 changes: 45 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,22 +77,30 @@ continue" line makes the distinction moot for what you actually do.

## What's in the box

- **Posture — `notify` (default) / `full` / `off`.** One coarse switch that
sits above every fine toggle. **`notify`** (default) watches and notifies
but the shim can never hold or block an approval — every prompt stays 100%
native (your local terminal dialog, or CC's own auto-deny when headless);
when a prompt is waiting at the terminal you still get told (desktop toast,
read-only dashboard card, graced IM text — pointers back to the terminal,
never a decision).
**`full`** turns on remote approval: tlive holds each tool call so you can
answer it from IM / desktop / dashboard (everything the *Approvals* bullet
below describes). **`off`** makes every hook a no-op (kill switch — no
gating, notifications, monitoring, or daemon autostart). Flip it live with
`tlive mode off|notify|full`; the shim re-reads config on the next hook, so
no restart or new session is needed, and `tlive status` shows the effective
mode. Remote approval is opt-in by design — a freshly-installed tool must
never be able to silently hang a workflow.
- **Approvals** *(remote approval — `mode: full`)* — a tool call that needs
- **Posture — `off` / `notify` (default) / `full` / `all`.** One coarse
switch that sits above every fine toggle, in escalation order (how much
tlive intercepts). **`off`** makes every hook a no-op (kill switch — no
gating, notifications, monitoring, or daemon autostart). **`notify`**
(default) watches and notifies but the shim can never hold or block an
approval — every prompt stays 100% native (your local terminal dialog, or
CC's own auto-deny when headless); when a prompt is waiting at the terminal
you still get told (desktop toast, read-only dashboard card, graced IM
text — pointers back to the terminal, never a decision). **`full`** turns
on remote approval for the main session: tlive holds each tool call so you
can answer it from IM / desktop / dashboard (everything the *Approvals*
bullet below describes), in parallel with the terminal dialog — first
answer wins. Sub-agent prompts still pass through to the terminal.
**`all`** holds sub-agent approvals too — the trade is that **a held
sub-agent has no terminal dialog until the window ends**, because Claude
Code awaits the hook before deciding whether to build one; use it only when
nobody is at the keyboard, and `tlive mode full` goes back. Flip postures
live with `tlive mode off|notify|full|all` (or `/mode` from IM — a bare
`/mode` replies with a card listing the ladder and marking the current
rung); the shim re-reads config on the next hook, so no restart or new
session is needed, and `tlive status` shows the effective mode. Remote
approval is opt-in by design — a freshly-installed tool must never be able
to silently hang a workflow.
- **Approvals** *(remote approval — `mode: full` or `all`)* — a tool call that needs
approval is held so you can answer it from IM buttons or the web card:
- **Parallel, first-answer-wins** — on Claude Code the `PermissionRequest`
hook fires alongside the local permission dialog; both are live, and
Expand All @@ -112,12 +120,14 @@ continue" line makes the distinction moot for what you actually do.
- **Power toggles** — **"Always allow \<tool\>"** grants a per-tool pass
(in-memory, cleared on restart; on Claude Code it answers the native dialog
for you remotely); `/trust on|off` pauses approvals entirely.
- **Sub-agents pass through by default** — tlive returns `{}` and lets CC
handle a backgrounded/async sub-agent natively, so its terminal dialog still
appears exactly as it would without tlive. Holding one instead would remove
that dialog (CC resolves an async agent's hook *before* it builds the box),
leaving remote as the only answer path — that is what
`approvals.holdSubagents: true` opts into.
- **Sub-agents pass through by default** — on `full`, tlive returns `{}` and
lets CC handle a backgrounded/async sub-agent natively, so its terminal
dialog still appears exactly as it would without tlive. **`tlive mode
all`** holds sub-agent approvals too, but the trade is real: CC resolves
an async agent's hook *before* it decides whether to build a dialog, so a
held sub-agent has **no** terminal box until the window ends — remote
becomes the only answer path. Worth it only when nobody is at the
keyboard; `tlive mode full` goes back.
- **Answer `AskUserQuestion` from IM or the dashboard (Claude Code only)** —
CC fires a `PermissionRequest` for its own question tool; tlive relays it as
a single-select or multi-select card (checkboxes, a live `Submit (N)` count,
Expand Down Expand Up @@ -289,7 +299,7 @@ tlive status health, effective mode, web URLs + QR, config paths
tlive logs [-f] tail the daemon log
tlive run <cmd> … wrap a process: local terminal + web terminal
tlive url print the dashboard URL + QR (when a full-screen app hid the run banner)
tlive mode off|notify|full set posture (see "What's in the box"); takes effect on the next hook
tlive mode off|notify|full|all set posture (see "What's in the box"); takes effect on the next hook
tlive hook <event> hook shim (called by Claude Code, not by you;
Codex has no hooks — see the app-server companion)
```
Expand All @@ -300,9 +310,10 @@ frozen surface (locked by `tests/contract/`); `mode` and the runtime toggles

IM commands: `/mute on|off` (silence IM notifications), `/trust on|off` (pause
approvals — auto-allow everything), `/safe on|off` (auto-allow routine ops),
`/help`. Tapping a bare command from the client's command menu replies with
on/off buttons instead of an error. Quote-reply any session message to type
into that session.
`/mode off|notify|full|all` (set posture; a bare `/mode` replies with the
ladder), `/help`. Tapping a bare command from the client's command menu
replies with on/off buttons instead of an error. Quote-reply any session
message to type into that session.

## Config (`~/.tlive/config.json`)

Expand All @@ -311,8 +322,10 @@ into that session.

```jsonc
{
// posture: "off" | "notify" (default) | "full" (remote approval on).
// Also set live with `tlive mode …`; unset/unknown falls back to notify.
// posture: "off" | "notify" (default) | "full" | "all" (remote approval;
// "all" also holds sub-agent prompts — see "What's in the box"). Also set
// live with `tlive mode …` or `/mode` from IM; unset/unknown falls back to
// notify.
"mode": "notify",
"adapters": {
"telegram": { "token": "…", "chatIdAllowList": ["123"] },
Expand All @@ -327,6 +340,10 @@ into that session.
"autoStart": true // default true; false disables session-start lazy-start
},
"approvals": {
// `holdSubagents` was removed — `tlive mode all` replaces it (upgrading
// from a config with `"holdSubagents": true`: that key is now dead and
// silently ignored, so switch to `tlive mode all` to keep holding
// sub-agent approvals).
// remote-approval window in seconds, shared by both vendors. The remote
// channel runs parallel to the local prompt, so a long window costs
// nothing — timing out never approves or denies anything, it only
Expand All @@ -353,14 +370,6 @@ into that session.
// /safe on|off. Never crosses the danger floor — only /trust on auto-allows
// dangerous ops.
// "autoApprove": "readonly",
// hold a backgrounded/async sub-agent's approval for a remote answer too.
// Default false, and changing it COSTS YOU THE TERMINAL: for an async agent
// CC waits for the hook's decision *before* building the dialog, so a held
// sub-agent request has no local box to fall back on — IM/dashboard becomes
// the only way to answer it, and an unanswered one waits out the whole
// window. Sub-agents have run in the background by default since Claude Code
// 2.1.198, so this applies to most of them. Only relevant in mode: full.
"holdSubagents": false,
// what a HELD approval does when its window times out with nobody
// answering: "defer" (default) → pass-through {} (CC-native fallback);
// "deny" → deny with a "timed out" message so the turn can end and the
Expand Down
60 changes: 33 additions & 27 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,23 @@ IM 消息带 `label · ` 前缀(会话目录名),但不再用图标区分包装/

## 功能一览

- **姿态 —— `notify`(默认)/ `full` / `off`。** 一个坐在所有细旋钮之上的
粗开关。**`notify`**(默认)只监看 + 通知,但 shim 物理上无法 hold 或
阻塞任何审批——每个提示都保持 100% 原生(你本地终端的对话框,或无头时
CC 自己的 auto-deny);提示在终端等你时仍会提醒(桌面通知、dashboard
只读卡、grace 后的 IM 文本——只是指回终端的路标,绝不代答)。
**`full`** 开启远程审批:tlive hold 住每个工具调用,
让你在 IM / 桌面 / dashboard 上作答(即下方*审批*那条描述的一切)。
**`off`** 让每个 hook 都成 no-op(kill switch——不 gating、不通知、不监看、
不懒启动 daemon)。用 `tlive mode off|notify|full` 实时切换;shim 每个 hook
都重读 config,无需重启也无需新会话,`tlive status` 会显示当前生效的 mode。
- **姿态 —— `off` / `notify`(默认)/ `full` / `all`。** 一个坐在所有细旋钮
之上的粗开关,按"tlive 拦多少"升序排列。**`off`** 让每个 hook 都成
no-op(kill switch——不 gating、不通知、不监看、不懒启动 daemon)。
**`notify`**(默认)只监看 + 通知,但 shim 物理上无法 hold 或阻塞任何
审批——每个提示都保持 100% 原生(你本地终端的对话框,或无头时 CC 自己的
auto-deny);提示在终端等你时仍会提醒(桌面通知、dashboard 只读卡、grace
后的 IM 文本——只是指回终端的路标,绝不代答)。**`full`** 为主会话开启
远程审批:tlive hold 住每个工具调用,让你在 IM / 桌面 / dashboard 上作答
(即下方*审批*那条描述的一切),与本地终端对话框并行竞速——先答先得;
子代理的提示仍照常透传给终端。**`all`** 连子代理审批也一并 hold 住——
代价是**被 hold 的子代理在窗口结束前没有终端框**,因为 Claude Code 会先
等 hook 决定要不要建框——只在没人守着键盘时才划算,`tlive mode full`
可以切回去。用 `tlive mode off|notify|full|all` 实时切换姿态(或在 IM 里
发 `/mode`——裸发也会回一张列出梯子并标出当前档的卡);shim 每个 hook 都
重读 config,无需重启也无需新会话,`tlive status` 会显示当前生效的 mode。
远程审批设计成 opt-in——刚装好的工具绝不该能悄悄挂起你的工作流。
- **审批** *(远程审批 —— `mode: full`)* —— 需要审批的工具调用会被 hold 住,
- **审批** *(远程审批 —— `mode: full` 或 `all`)* —— 需要审批的工具调用会被 hold 住,
让你从 IM 按钮或 web 卡作答:
- **并行、先答先得** —— Claude Code 上 `PermissionRequest` hook 与本地权限
对话并行;两边都活,键盘上答掉后远程卡几秒内自动收尾("已在终端处理")。
Expand All @@ -94,10 +99,12 @@ IM 消息带 `label · ` 前缀(会话目录名),但不再用图标区分包装/
expandable 折叠——用较新 Telegram 客户端渲染最佳。
- **高危开关** —— **"总是允许 \<工具\>"** 按工具放行(内存态、重启清零;
Claude Code 上远程替你点掉原生对话);`/trust on|off` 整体暂停审批。
- **子代理默认透传** —— tlive 返回 `{}` 交给 CC 原生处理,所以它的终端框和
没装 tlive 时完全一样地弹出。反过来 hold 住它就会抹掉那个框(对异步 agent,
CC 会在建框之前先解析 hook),只剩远程一条答复路径 ——
`approvals.holdSubagents: true` 就是选择这个代价。
- **子代理默认透传** —— 在 `full` 下,tlive 返回 `{}` 交给 CC 原生处理,
所以它的终端框和没装 tlive 时完全一样地弹出。**`tlive mode all`** 连
子代理审批也一并 hold 住,但代价是真实的:对异步 agent,CC 会在决定
要不要建框之前先解析 hook,所以被 hold 的子代理在窗口结束前**没有**
终端框——远程成了唯一答复路径。只有没人守着键盘时才划算,
`tlive mode full` 可以切回去。
- **远程回答 `AskUserQuestion`(仅 Claude Code)** —— CC 为自己的提问工具
fire `PermissionRequest`;tlive 把它转成单选或多选卡(复选框、实时
`Submit (N)` 计数、`Skip`)而非 Allow/Deny,IM 和 dashboard 会话卡都能答。
Expand Down Expand Up @@ -238,7 +245,7 @@ tlive status 健康态、当前生效 mode、web 地址 + 二维码、
tlive logs [-f] 看 daemon 日志
tlive run <cmd> … 包装进程:本地终端 + web 终端
tlive url 打印 dashboard 地址 + 二维码(全屏应用盖住 run banner 时用)
tlive mode off|notify|full 设置姿态(见"功能一览");下一个 hook 即生效
tlive mode off|notify|full|all 设置姿态(见"功能一览");下一个 hook 即生效
tlive hook <event> hook shim(Claude Code 调用,不是给你用的;
Codex 没有 hook——见 app-server companion)
```
Expand All @@ -248,9 +255,10 @@ tlive hook <event> hook shim(Claude Code 调用,不是给你用的;
(`on|off`)、`desktop`(`on|off`)是加法命令。

IM 命令:`/mute on|off`(静音 IM 通知)、`/trust on|off`(暂停审批——全部
自动放行)、`/safe on|off`(自动放行日常操作)、`/help`。在客户端命令菜单里
点一下裸命令,会回一组 on/off 按钮而不是报错。引用任意会话消息回复 =
打字进那个会话。
自动放行)、`/safe on|off`(自动放行日常操作)、`/mode off|notify|full|all`
(设置姿态;裸发 `/mode` 会回梯子卡)、`/help`。在客户端命令菜单里点一下裸
命令,会回一组 on/off 按钮而不是报错。引用任意会话消息回复 = 打字进那个
会话。

## 配置(`~/.tlive/config.json`)

Expand All @@ -259,8 +267,9 @@ IM 命令:`/mute on|off`(静音 IM 通知)、`/trust on|off`(暂停审批——

```jsonc
{
// 姿态:"off" | "notify"(默认)| "full"(开远程审批)。
// 也可用 `tlive mode …` 实时设置;未设 / 未知值都回落 notify。
// 姿态:"off" | "notify"(默认)| "full" | "all"(远程审批,"all" 连子代理
// 提示也一并 hold 住——见"功能一览")。也可用 `tlive mode …` 或 IM 里的
// `/mode` 实时设置;未设 / 未知值都回落 notify。
"mode": "notify",
"adapters": {
"telegram": { "token": "…", "chatIdAllowList": ["123"] },
Expand All @@ -275,6 +284,9 @@ IM 命令:`/mute on|off`(静音 IM 通知)、`/trust on|off`(暂停审批——
"autoStart": true // 默认 true;设 false 关闭 session-start 懒启动
},
"approvals": {
// `holdSubagents` 已删除 —— 由 `tlive mode all` 取代(如果你的配置里还有
// `"holdSubagents": true`:这个键现在是死的,会被静默忽略,想继续 hold
// 子代理审批请改用 `tlive mode all`)。
// 远程审批窗口(秒),两家共用。远程通道与本地提示并行,窗口开长
// 也不费事——超时不批也不拒,只是把你逼回键盘而已。
"windowSec": 86200, // 默认约 24 小时(同时是上限;最小 60)
Expand All @@ -294,12 +306,6 @@ IM 命令:`/mute on|off`(静音 IM 通知)、`/trust on|off`(暂停审批——
// 用于自主/agent 驱动场景减少卡量。可用 /safe on|off 实时切换。
// 绝不越过危险地板——只有 /trust on 才会自动放行危险操作。
// "autoApprove": "readonly",
// 是否连后台/异步子代理的审批也 hold 住等远程作答。默认 false,
// **改成 true 会让你失去终端**:对异步 agent,CC 会在建框之前先等 hook 的
// 决定,所以被 hold 的子代理请求没有本地框可兜底 —— IM/dashboard 成为唯一
// 答复路径,没人应答就会耗掉整个窗口。自 Claude Code 2.1.198 起子代理默认
// 后台运行,所以这适用于绝大多数子代理。仅在 mode: full 下有意义。
"holdSubagents": false,
// 一个被 hold 的审批在窗口超时、无人应答时怎么办:
// "defer"(默认)→ 透传 {}(回落 CC 原生);
// "deny" → 带一句"已超时"的拒绝,好让 turn 结束、续跑卡改道 agent。
Expand Down
Loading
Loading