Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
a10215d
feat(fleet): workspace-level Fleet view — approvals inbox + bot roster
haowei2000 Jul 14, 2026
f1587d0
feat(fleet): P2 — live status, rail approval badge
haowei2000 Jul 14, 2026
547549e
docs: record Fleet P3 + bot-to-bot dispatch design
haowei2000 Jul 14, 2026
bc11f48
feat(dispatch): bot-to-bot dispatch under the grant matrix (D1)
haowei2000 Jul 14, 2026
88c8ece
docs: resource-context design — attachable Cheers resources as agent …
haowei2000 Jul 14, 2026
e7886b0
docs: resource-context — relationship to workbench pin
haowei2000 Jul 14, 2026
8c4c00b
feat(context): F0 — context_bundle schema + delivery
haowei2000 Jul 14, 2026
948da95
feat(context): F1 — human context picker + chips
haowei2000 Jul 14, 2026
9430af8
feat(context): F1 slice 2 — in-panel attach (Viewboard + Workbench file)
haowei2000 Jul 14, 2026
36e232d
feat(context): F2 — bot auto-handoff (gateway-assembled bundle)
haowei2000 Jul 14, 2026
367e4b4
feat(context): F2 handoff card — make the auto-handoff visible in chat
haowei2000 Jul 14, 2026
af28eca
perf(build): Dockerfile dependency-cache layer [UNVERIFIED]
haowei2000 Jul 14, 2026
f42beae
fix(build): touch real sources so the dep-cache layer rebuilds the bi…
haowei2000 Jul 15, 2026
2ef36ec
test(context): add context_bundle field to flows integration params
haowei2000 Jul 15, 2026
96fdd01
fix(context): render context_bundle into the connector prompt
haowei2000 Jul 15, 2026
6e73238
fix(dispatch): reject dispatch capability on the human event-access API
haowei2000 Jul 15, 2026
55bf971
fix(fleet): require active workspace membership to serve Fleet
haowei2000 Jul 15, 2026
cf0d6f7
Merge pull request #198 from haowei2000/feat/resource-context
haowei2000 Jul 15, 2026
fcc7f2b
Merge pull request #197 from haowei2000/feat/bot-dispatch
haowei2000 Jul 15, 2026
4e62996
Merge pull request #196 from haowei2000/feat/fleet-view
haowei2000 Jul 15, 2026
c650bd1
feat(context): F4 — bot manual pick (post_message context bundle)
haowei2000 Jul 15, 2026
51cf8dc
Merge pull request #199 from haowei2000/fix/dockerfile-dep-cache
haowei2000 Jul 15, 2026
e7157bc
Merge pull request #200 from haowei2000/feat/resource-context-bot-pick
haowei2000 Jul 15, 2026
a2f849f
Merge remote-tracking branch 'origin/develop' into feat/resource-context
haowei2000 Jul 15, 2026
fadefce
Merge pull request #201 from haowei2000/feat/resource-context
haowei2000 Jul 15, 2026
345766b
feat(context): F3 — suggested context (reply-target ghost chips)
haowei2000 Jul 15, 2026
ec33ecc
Merge remote-tracking branch 'origin/develop' into feat/resource-cont…
haowei2000 Jul 15, 2026
f72b269
Merge pull request #202 from haowei2000/feat/resource-context-f3-sugg…
haowei2000 Jul 15, 2026
1ac9805
feat(context): F3 follow-on signals — filename + plan suggestions
haowei2000 Jul 15, 2026
fbd87d8
feat(context): Workbench passage picking — attach a file line range
haowei2000 Jul 15, 2026
9b7e62b
feat(context): Remote workspace picker — snapshot + locator
haowei2000 Jul 15, 2026
d1ee372
fix(context): harden the bundle pipeline — no permission bypass
haowei2000 Jul 15, 2026
1d59f6a
style: rustfmt the authz prompt test (cargo fmt --check)
haowei2000 Jul 15, 2026
2a59fac
feat(context): unified XML <context> prompt envelope (Phase 1)
haowei2000 Jul 15, 2026
3155086
style: rustfmt the XML envelope tests (cargo fmt --check)
haowei2000 Jul 15, 2026
b006b8f
feat(context): one shared per-target bundle finalizer (Phase 2)
haowei2000 Jul 15, 2026
227e864
Merge remote-tracking branch 'origin/feat/context-xml-envelope' into …
haowei2000 Jul 15, 2026
757a5be
feat(context): bot-subject workspace_read grant + resolver (P3-1)
haowei2000 Jul 15, 2026
7c40548
Merge pull request #206 from haowei2000/feat/workspace-as-reference
haowei2000 Jul 15, 2026
4959921
Merge pull request #205 from haowei2000/feat/context-finalizer
haowei2000 Jul 15, 2026
dc90cbe
Merge pull request #204 from haowei2000/feat/context-xml-envelope
haowei2000 Jul 15, 2026
83e5490
Merge pull request #203 from haowei2000/fix/context-bundle-authz
haowei2000 Jul 15, 2026
66f2b6d
Merge pull request #207 from haowei2000/feat/resource-context-f3-sugg…
haowei2000 Jul 15, 2026
53d78ab
feat(context): P3-2 — workspace.read broker + MCP read_workspace tool
haowei2000 Jul 15, 2026
c35d067
feat(context): P3-3 — remote-workspace pick is a reference, deprecate…
haowei2000 Jul 15, 2026
72cf3a0
Merge pull request #208 from haowei2000/feat/workspace-broker
haowei2000 Jul 15, 2026
6301eb9
docs(context): sync RESOURCE_CONTEXT.md with the P3 reference model
haowei2000 Jul 15, 2026
75eea27
feat(bots): owner-facing bot-to-bot grant management (dispatch + work…
haowei2000 Jul 15, 2026
636db89
Merge pull request #210 from haowei2000/feat/bot-grants-management
haowei2000 Jul 16, 2026
87d4abb
Merge pull request #209 from haowei2000/docs/p3-workspace-read-sync
haowei2000 Jul 16, 2026
45cdb78
feat(bots): friendly permission labels by default, technical names on…
haowei2000 Jul 16, 2026
8f79920
Merge pull request #211 from haowei2000/feat/grant-friendly-labels
haowei2000 Jul 16, 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
23 changes: 23 additions & 0 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,29 @@ Three phases:

Roadmap filter for new items: **does this make multi-agent governance stronger?**

### Fleet view — follow-ups

The Fleet view (workspace mission control: approvals inbox + bot roster) shipped
its live P2 (`bot_processing` chips, rail approval badge). Recorded next:

- **Bot-to-bot dispatch under the grant matrix** (in progress) — an agent may
`@` another agent to hand off a subtask, but every such dispatch passes a new
`dispatch` capability in the `user ▸ group ▸ role ▸ *` matrix (deny wins,
owner-only default) and is audited. This is the governance edge no bridge can
match — see [docs/design/BOT_DISPATCH.md](design/BOT_DISPATCH.md).
- **Fleet P3** (recorded, not started) — in-channel mini fleet strip (a compact
per-channel roster in the work lane) and approvals-inbox filters (by bot / by
operation kind / by channel).

### Resource context — attachable Cheers resources as agent context

The Cheers-native `@context`: any participant attaches Cheers's own resources
(plan, board, file, message/thread, decisions) as structured context to an agent
invocation — two producers (human manual pick, bot automatic handoff) over one
foundation, with consumer-governed reads. Design:
[docs/design/RESOURCE_CONTEXT.md](design/RESOURCE_CONTEXT.md). Phases:
F0 foundation → F1 human picker → F2 bot handoff → F3 suggested context.

## Near-Term Plans

### UI
Expand Down
20 changes: 20 additions & 0 deletions docs/ROADMAP.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,26 @@

新条目的路线图过滤问题:**这让多 agent 治理更强吗?**

### Fleet 视图 —— 后续

Fleet 视图(工作区任务控制中心:审批收件箱 + bot 名册)已交付实时 P2
(`bot_processing` 芯片、rail 待批角标)。记录如下:

- **Bot 间 dispatch 过授权矩阵**(进行中)—— 一个 agent 可以 `@` 另一个 agent 交接
子任务,但每一次这样的 dispatch 都要过 `user ▸ group ▸ role ▸ *` 矩阵里新增的
`dispatch` 能力位(deny 优先,默认仅 owner),并留审计。这是任何桥接派都做不到的
治理优势 —— 见 [docs/design/BOT_DISPATCH.md](design/BOT_DISPATCH.md)。
- **Fleet P3**(已记录,未开始)—— 频道内 mini 舰队条(work lane 里紧凑的按频道
名册)+ 审批收件箱筛选(按 bot / 按操作类型 / 按频道)。

### 资源上下文 —— 把 Cheers 资源当成 agent 的可附加上下文

Cheers 原生的 `@context`:任何参与者都能把 Cheers 自己的资源(plan、看板、文件、
消息/线程、决定)作为结构化上下文附加到一次 agent 调用上 —— 两个生产者(人-手动
拾取、bot-自动 handoff)共用一套底座,读取由消费方权限决定。设计见
[docs/design/RESOURCE_CONTEXT.md](design/RESOURCE_CONTEXT.md)。阶段:
F0 底座 → F1 人工 picker → F2 bot handoff → F3 建议式上下文。

## 近期计划(资源占用考虑/安全性问题/关键断点->行动建议->工作量)

### UI
Expand Down
201 changes: 201 additions & 0 deletions docs/design/BOT_DISPATCH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# Bot-to-bot dispatch under the grant matrix — design

> Status: 📝 **Design accepted, not yet implemented** (2026-07)
> Roadmap: multi-agent governance track, the "moat" feature
> ([ROADMAP.md](../ROADMAP.md) → Strategic Direction).

## The one-sentence pitch

An agent can `@` another agent to hand off a subtask — but **every such
dispatch passes the same `user ▸ group ▸ role ▸ *` grant matrix humans do, under
a first-class `Dispatch` capability, and every decision (allow *and* deny) is
audited.** Bridges (OpenAB et al.) have bot-to-bot messaging; none can say
"agent A may command agent B, here's who allowed it, here's the trail." That is
the governance edge, and it exists only because Cheers owns the identity model.

## What already exists (and why this is hardening, not greenfield)

Bot@bot triggering already works and is already gated — see
[DECENTRALIZED_MESH.md §8](../arch/DECENTRALIZED_MESH.md) for the cascade spec.
The current gate lives in
[`domain/chains.rs:98-120`](../../server/src/domain/chains.rs) inside
`trigger_bot_replies`:

```rust
// chains.rs — today
let may_prompt = acp_policy::allows(
db, &bot_id.to_string(), // rules loaded for the TARGET bot
&channel_id.to_string(),
&author_bot_id.to_string(), // triggering bot shoved into the user_id slot
BOT_INITIATOR_ROLE, // role = "bot"
"session/prompt", Capability::Initiate,
).await.unwrap_or(true); // fail-OPEN
```

Three cascade guards already bound loops and are **out of scope** here — we build
on them, not replace them:

- depth cap `MAX_BOT_REPLY_DEPTH = 5` (`chains.rs:52`),
- per-channel dispatch rate limiter (30/60s, `ratelimit.rs:169`),
- chain cancel gate `task_chains::is_active` (`chains.rs:58`).

The problem is not that bot@bot is ungoverned — it's that the governance is
**invisible, unauditable, and fail-open**:

1. **No first-class bot subject.** To deny bot A → bot B today, an owner overloads
`subject_kind='role', subject_id='bot'` (all bots) or the `user` tier with a
bot UUID. These rows are storable but invisible to `effective_matrix` /
`MATRIX_ROLES` (`bot_event_policy.rs:363`) and the owner UI. You cannot *see*
or *author* "who may command whom".
2. **Denials aren't audited.** A denied dispatch only `tracing::info!`-logs
(`chains.rs:112-119`); no DB row. The core promise — a permanent trail of
every dispatch decision — is currently false.
3. **Fail-open on error.** `unwrap_or(true)` + `task_chains::is_active` fail-open:
a DB hiccup lets a denied dispatch through. A governance gate that opens on
error is worthless.

This design fixes exactly those three, plus surfaces the result.

## Decisions (settled)

- **A first-class `Dispatch` capability** — a 4th column in the matrix, distinct
from human `Initiate·prompt`. Rationale: "a human may prompt bot B" and "bot A
may dispatch bot B" are genuinely different authorizations an owner wants to
control independently (let humans prompt freely, restrict which *bots* can
chain in — or vice versa). Reusing `Initiate` would fuse them.
- **Default allow (backward compatible).** Bots may dispatch each other by
default; an owner writes an explicit `deny` to restrict. No seed rules, no
breakage of existing cascades. We ship *auditable + surfaced restriction* first;
tightening the default to owner-only is a separate, later decision.

## Model

### The `Dispatch` capability

Add `Capability::Dispatch` alongside `Initiate | See | Respond`
(`bot_event_policy.rs:73`). It gates exactly one thing: **one bot causing another
bot's turn to start.** Its event class is the existing `prompt` class, but
evaluated on the **`Dispatch` capability** rather than `Initiate`, so
`events_for(Dispatch)` returns `{prompt}` (`bot_event_policy.rs:41`).

The gate call in `chains.rs` becomes:

```rust
let may_dispatch = acp_policy::allows_bot_subject(
db,
/* target */ bot_id,
channel_id,
/* initiator*/ author_bot_id, // a BOT subject, not a user
"session/prompt",
Capability::Dispatch,
).await; // fail-CLOSED on error (see below)
```

### Bot as a first-class grant subject

Add `subject_kind = 'bot'` to `bot_event_access`
([migration 0029](../../server/migrations/0029_bot_event_access.sql) CHECK,
widened again in 0032). A dispatch rule reads:

| target bot | subject_kind | subject_id | capability | access |
|---|---|---|---|---|
| `B` | `bot` | `A` (a bot_id) | `dispatch` | `deny` — "A may not command B" |
| `B` | `bot` | `*` | `dispatch` | `deny` — "no bot may command B" |
| `B` | `bot` | `A` | `dispatch` | `allow` — carve A back in |

`subject_kind='bot'` slots into `resolve_access` precedence
(`bot_event_policy.rs:146`) at **user-tier specificity** (a specific bot is as
specific as a specific user), with `subject_id='*'` as its catch-all:

```
(chan, bot:A) ▸ (chan, bot:*) ▸ (bot-wide, bot:A) ▸ (bot-wide, bot:*) ▸ default(allow)
```

This *replaces* the `role='bot'` / `user=<botid>` overloading hack — those rows
migrate to `subject_kind='bot'` (one-shot data migration; the hack was
undocumented so blast radius is small).

### Default resolution

`default_access_for(Dispatch, …)` returns **allow** (decision above). `prompt` is
**not** added to `OWNER_DEFAULT_INITIATE` for the `Dispatch` capability. So:
absent any rule → allow; explicit `deny` rule → deny.

### Fail-closed on error (the one behavioral flip)

Unlike the fail-open human INITIATE gate, the dispatch gate is **fail-closed on
evaluation error**: if `load_rules` errors, deny the dispatch *and write a deny
audit row* (reason `policy_unavailable`). "No rule found" still means allow;
"couldn't evaluate the rules" means deny. A moat feature cannot silently open
when its own policy store is unreachable. (This diverges from `chains.rs:111`
`unwrap_or(true)` — intentional and documented.)

## Audit — the actual product

Every dispatch **decision** writes one append-only row: allowed and denied alike.
This is what makes the feature real; the gate without the trail is just a filter.

Reuse the generic ACP substrate
[`acp_event_log`](../../server/migrations/0031_acp_event_log.sql) — it already
keys on `(bot_id, channel_id, session_id, name, home, payload)` and every ACP
event flows through it. A dispatch decision is a `name='dispatch'`, `home='cheers'`
row whose `payload` carries:

```jsonc
{
"initiator_bot_id": "A", "target_bot_id": "B",
"channel_id": "…", "chain_id": "…", "depth": 2,
"decision": "allow" | "deny",
"reason": "default_allow" | "rule" | "policy_unavailable",
"matched_rule_id": "…" // when decision came from a stored rule
}
```

Surfacing (later phases, not the gateway change):

- **Viewboard → Audit tab** gains dispatch rows next to approval rows (the panel
already renders `approval_audit`; add an `acp_event_log` dispatch source).
- **Fleet** can show a per-bot "dispatched by / dispatches to" edge list — the raw
material for a future "who commands whom" graph.

## What this touches (implementation surface)

Grounded in the [permission-matrix map](#) from exploration:

| Area | Files |
|---|---|
| Capability enum + mappings | `domain/bot_event_policy.rs`: `Capability` (`:73`), `as_str`/`parse` (`:81`/`:89`), `events_for` match (`:44`), `default_access_for` (`:122`), `effective_matrix` cap loop (`:389`) |
| Bot subject | `bot_event_policy.rs` `resolve_access` (`:146`) + `matched_groups` neighbourhood; **migration**: relax `chk_bea_capability` (add `dispatch`) and the `subject_kind` CHECK (add `bot`) in a new `00xx_dispatch_capability.sql` |
| The gate | `domain/chains.rs:98-120` → new `acp_policy::allows_bot_subject` (fail-closed) |
| Audit | `domain/acp_event_log` writer (new `record_dispatch_decision`), called from the gate on both branches |
| Matrix REST + UI | `api/bot_permission.rs:311+` (event-access endpoints) gain a `dispatch` column and `bot`-subject rows; `frontend/.../BotPermissionGrantsSection.tsx` renders the 4th column and a bot-subject picker |
| Data migration | one-shot: existing `role='bot'` / `user=<botid>` INITIATE·prompt rows → `subject_kind='bot'`, capability `dispatch` |

## Phasing

| Phase | Scope | Verifiable outcome |
|---|---|---|
| **D1 — gateway** | `Dispatch` capability, `subject_kind='bot'`, fail-closed gate in `chains.rs`, audit row on every decision, data migration | A `deny` rule blocks A→B and writes a deny audit row; default still allows; `cargo test` |
| **D2 — surface** | matrix UI 4th column + bot-subject picker; Viewboard Audit shows dispatch rows | Owner authors "A may not command B" in the UI; the block + its audit row are visible |
| **D3 — Fleet graph** | per-bot dispatch edges in Fleet ("dispatches to / dispatched by") | Fleet shows the command graph of the fleet |

## Explicitly out of scope

- Changing the depth cap / rate limiter / chain-cancel (existing loop guards stand).
- Tightening the default to owner-only (a later, separate decision — this ships
default-allow).
- Reconciling the doc/code divergences noted in
[DECENTRALIZED_MESH.md §8](../arch/DECENTRALIZED_MESH.md) (no-depth-cap wording,
`bot_runs.chain_id` vs `messages.chain_id`) — tracked separately.
- Approval-card-on-dispatch (a bot dispatch raising a human approval): a natural
future compose with [ACP_APPROVAL_FLOW.md](../arch/ACP_APPROVAL_FLOW.md), not D1.

## Open questions

1. **Owner-of-bot as subject?** Should a dispatch rule be able to target "any bot
owned by user U" (`subject_kind='bot_owner'`)? Deferred — `bot` + `*` covers
the near-term stories.
2. **Chain attribution on proactive paths.** `chain_for_proactive_send`
(`stream.rs:688`) infers the chain by "latest partial placeholder", which can
misattribute concurrent same-bot turns. The dispatch audit inherits that
imperfection; acceptable for D1, worth a note in the row.
Loading
Loading