diff --git a/AGENTS.md b/AGENTS.md index 8ed0295ff..2e5484f52 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ Grok Bot is the single entry point for humans; it schedules OpenCode (implements 4. **PR** to `main`. Imperative English title, `Fixes #N` in the body, and what you ran and did **not** run at the tip SHA. A user-visible change adds one fragment `changelog.d/..md` ([changelog.d/README.md](changelog.d/README.md)); never edit `CHANGELOG.md` in a feature PR. Squash is the only merge method; the PR title and body become the commit message, so keep the body short. 5. **CI.** The required check is `tests-ok`. It is green only when `test`, `plugins (tree)`, `plugins (social)` and `package` all pass. Never merge around a red check. 6. **Review and merge.** Review the **tip SHA** with [`review-pr`](.claude/skills/review-pr/SKILL.md) (PASS / FAIL / INC); a tip move voids the PASS. Once the tip has its PASS and the maintainer has delegated the merge, enable auto-merge with `gh pr merge --auto --squash --match-head-commit `. Without that delegation, the maintainer merges. [`fix-issue`](.claude/skills/fix-issue/SKILL.md) walks all of this, with the environment gotchas. -7. **Stop** and hand over to the maintainer on a security-sensitive change, an open decision such as #16, or when you cannot get a green tip. +7. **Stop** and hand over to the maintainer on a security-sensitive change, an unresolved maintainer decision, or when you cannot get a green tip. When a class of bug repeats, date a lesson in LESSONS.md (`Seen N×`); after **≥3 days** promote the durable rule into `docs/code-standards.md` / CONTRIBUTING. @@ -55,8 +55,9 @@ The Bot team: - Every Bot is a DSH Session and runs through the normal DSH agent loop, tools and model routes. - The team works together: Bots message each other, share group chats and can see what the other Bots are doing. Each Bot still runs on its own and does not disturb the others. -- Bots have clear, different roles. A Main Bot has the highest rights: it creates, changes and removes other Bots. Other Bots do not get these rights. There can be more than one Main Bot. -- The multi-human authority contract (human owner ↔ designated Chief ↔ specialists) is documented in [docs/subsystems/team.md](docs/subsystems/team.md) as **planned, not shipped**: team mode stays disabled until its authorization gates pass, and the `create_child_bot` decision (#16) is still open—do not assume either option in code, schema or tests. +- Bots have clear, different roles. A Main Bot has the highest rights: it changes and removes other Bots, and can create them. Other Bots do not get those management rights. There can be more than one Main Bot. +- **Tree child creation (#16, Option B).** A tree Bot (not only the Main/root) may call `create_child_bot` to create a child directly under itself. The tool takes no parent argument; `maxChildren` and depth limits stay in force. The new child is usable only by its creator's owner; sharing it with another person still requires an independent per-Bot grant (default deny). There is no publish mechanism. See [docs/subsystems/tree.md](docs/subsystems/tree.md). +- The multi-human authority contract (human owner ↔ designated Chief ↔ specialists) is documented in [docs/subsystems/team.md](docs/subsystems/team.md) as **planned, not shipped**: team mode stays disabled until its authorization gates pass. Issue #16 is resolved (Option B); do not re-open Main-only child creation. - Users customize each Bot (name, role label, instructions, model, character). - Behavior must be reliable: a Bot acts on evidence, stays inside its goal, and its results can be checked. diff --git a/README.md b/README.md index 1c2b49cc3..16b7c12b8 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,10 @@ Turns your Bots into one recursive tree that splits a request into parts, sends ![Tree plugin: Chief splits a Hangzhou trip across Travel, Food, and Budget](docs/images/demo-tree.gif) - **One root.** The tree's root is the Main Bot (Chief); no new bot is created for it. A separate `Root` bot is created only if the team has Bots but no Main Bot. Existing local Bots join under the root as its children, up to `maxChildren`. -- **Children on demand.** A tree Bot calls `create_child_bot(name, role_description)` to make a child directly under *itself*; the tool takes no parent argument. Calls are handled one at a time per parent, so parallel calls never go over the limits or create two siblings with the same name. The rules: +- **Children on demand.** Any tree Bot (not only the Main/root) calls `create_child_bot(name, role_description)` to make a child directly under *itself*; the tool takes no parent argument. Calls are handled one at a time per parent, so parallel calls never go over the limits or create two siblings with the same name. The rules: - names are 1–40 characters, and duplicate sibling names are rejected (case and spaces ignored); - - depth is at most 5 (the root counts as 1), with at most 8 children per Bot (set in `limits.json` or the plugin config). + - depth is at most 5 (the root counts as 1), with at most 8 children per Bot (set in `limits.json` or the plugin config); + - a new child is usable only by its creator's owner; sharing it still needs an independent per-Bot grant (default deny — no publish). - **Level-by-level delegation.** Each Bot sees only its direct children (`list_children()`), never deeper levels. - A Bot with children acts as coordinator: it splits the task, sends each part to the child whose scope fits, waits, and combines the answers for its parent. - A Bot with no children does the work itself. diff --git a/README.zh.md b/README.zh.md index 189aaca4c..42f564e3e 100644 --- a/README.zh.md +++ b/README.zh.md @@ -30,9 +30,10 @@ FlyBot 是在固定版本的 [DS Bot](https://github.com/Fei2411/DeepSeek-Bot) ![树插件:Chief 把杭州行程拆给 Travel、Food、Budget](docs/images/demo-tree.gif) - **一个根。** 树的根是主 Bot(Chief),不会再新建一个根。只有团队里有 Bot、却没有主 Bot 时,才会单独建一个 `Root`。已有的本地 Bot 会挂到根下面作子节点,数量不超过 `maxChildren`。 -- **按需生子。** 树中的 Bot 调用 `create_child_bot(name, role_description)`,在**自己**下面直接建子 Bot;工具没有 parent 参数。同一父节点的调用串行处理,避免超限或重名。规则: +- **按需生子。** 树中的任意 Bot(不只是主 Bot / 根)调用 `create_child_bot(name, role_description)`,在**自己**下面直接建子 Bot;工具没有 parent 参数。同一父节点的调用串行处理,避免超限或重名。规则: - 名称 1–40 个字符;同级重名会被拒绝(忽略大小写和空格); - - 深度最多 5(根算第 1 层),每个 Bot 最多 8 个子节点(见 `limits.json` 或插件配置)。 + - 深度最多 5(根算第 1 层),每个 Bot 最多 8 个子节点(见 `limits.json` 或插件配置); + - 新建的子 Bot 默认只给创建者的主人用;分享给别人仍需对该 Bot 单独授权(默认拒绝,没有发布机制)。 - **按层委托。** 每个 Bot 只能看到直接子节点(`list_children()`),看不到更深。 - 有子节点的 Bot 当协调者:拆任务,把每块发给职责匹配的子节点,等待,再汇总给父节点。 - 没有子节点的 Bot 自己干活。 diff --git a/changelog.d/16-tree-create-child-bots.changed.md b/changelog.d/16-tree-create-child-bots.changed.md new file mode 100644 index 000000000..8a481c445 --- /dev/null +++ b/changelog.d/16-tree-create-child-bots.changed.md @@ -0,0 +1 @@ +**Tree Bots may create children under themselves.** Issue [#16](https://github.com/ThinkFlowLab/FlyBot/issues/16) is Option B: any tree Bot (not only Main/root) can call `create_child_bot` for a child directly under it. `maxChildren` and depth limits stay. A new child is usable only by its creator's owner; sharing it with another person still needs an independent per-Bot grant (default deny). There is no publish path. diff --git a/docs/architecture.md b/docs/architecture.md index f4b219880..9548b04aa 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -26,7 +26,7 @@ Each subsystem has a detail page under [docs/subsystems/](subsystems/README.md) ## Team authority (planned, not shipped) -Multi-human ownership is **planned, not shipped**. The proposed contract — human owner ↔ designated Chief ↔ specialists, owner-scoped management, Chief-only cross-owner traffic, approval authority and the routing matrix — lives in [docs/subsystems/team.md](subsystems/team.md). Schema (#47), member sign-in (#48), owner-scoped access (#49) and typed room membership (#50) are foundations on that page; **team mode stays disabled** until that page's authorization gates pass, and the child-creation decision (#16) remains an open maintainer decision recorded there. +Multi-human ownership is **planned, not shipped**. The proposed contract — human owner ↔ designated Chief ↔ specialists, owner-scoped management, Chief-only cross-owner traffic, approval authority and the routing matrix — lives in [docs/subsystems/team.md](subsystems/team.md). Schema (#47), member sign-in (#48), owner-scoped access (#49) and typed room membership (#50) are foundations on that page; **team mode stays disabled** until that page's authorization gates pass. Issue #16 is resolved: tree Bots may create children under themselves (Option B; see [tree](subsystems/tree.md)). ## Integrating diff --git a/docs/code-standards.md b/docs/code-standards.md index 131adcf59..ff3953490 100644 --- a/docs/code-standards.md +++ b/docs/code-standards.md @@ -46,8 +46,7 @@ When you report status on a change: ## Team authority contract (planned) - The human ↔ Chief ↔ specialist ownership contract lives in [docs/subsystems/team.md](subsystems/team.md). It is a **planned** contract: multi-human is not shipped, and **team mode stays disabled** until that page's authorization gates pass. -- Issue #16 (who may call `create_child_bot`) is **open**; both options are documented there. Do not implement, review or merge work that assumes either answer, and do not encode one in a test. -- Main/Chief creation rights stay exactly as [AGENTS.md](../AGENTS.md) states them until #16 is decided. +- Issue #16 is **resolved (Option B)**: a tree Bot (not only Main/root) may call `create_child_bot` under itself, within `maxChildren` and depth limits. A newly created child is not auto-granted; sharing still needs an independent per-Bot grant. See [AGENTS.md](../AGENTS.md) and [docs/subsystems/tree.md](subsystems/tree.md). Do not re-open Main-only child creation. ## Tiny loops diff --git a/docs/subsystems/team.md b/docs/subsystems/team.md index 69acb1705..16c5bf6fa 100644 --- a/docs/subsystems/team.md +++ b/docs/subsystems/team.md @@ -19,10 +19,10 @@ Shipped behavior stays what [core/README.md](../../core/README.md) (roles: You / ## Does not own -- Tree shape, root adoption, `maxChildren`/depth limits → [tree](tree.md). +- Tree shape, root adoption, `maxChildren`/depth limits, and shipped `create_child_bot` (#16 Option B) → [tree](tree.md). - Core runtime, shipped single-human roles and tools → [ds-bot-core](ds-bot-core.md) (`core/README.md`). - Plugin runtime surface → [plugin-runtime](plugin-runtime.md). -- The open `create_child_bot` decision itself → issue #16 (this page only records it as open). +- Per-Bot grants / default deny (#90) → core reach + [tree](tree.md); this page does not invent a publish path. ## Key concepts @@ -44,7 +44,7 @@ Shipped by the core store (`core/src/host/store.js`, owned by [ds-bot-core](ds-b - The saved team (`state.json`) is **version 2**. `members` holds records with an immutable `id`, `createdAt`, and `designatedChiefId`; every Bot carries an `ownerId` binding. - Loading a legacy **version-1** team runs one migration: it creates the single initial owner (`owner-1`), binds every existing Bot to it, sets the designated Chief from the **first Main Bot** (the shipped Main role — no name matching, no new Main rules), and writes version 2. Bot ids, Session parts and private histories are preserved. The pre-migration file is backed up once as `state.json.v1` before the first version-2 write; if a backup already exists it counts only when it is a readable file holding a parseable version-1 state — a directory, an empty or truncated file, or anything else there fails the load closed instead of migrating. - Malformed ownership **fails closed**: a Bot owner that names no member, duplicate or mismatched member ids, or a designated Chief that is not a Bot of its member makes the file read-only — it is never repaired by guessing. An older binary already refuses a newer version read-only, so it cannot overwrite the schema. -- This is **not team mode**: the gates below are unchanged and unsatisfied. The schema resolves nothing about `create_child_bot` (#16 stays open) and defines no flag/config/env for team mode. The #48 member-auth foundation (invitations, sessions, whoami) is a sign-in surface only — it does not enable team mode or cross-owner routing. +- This is **not team mode**: the gates below are unchanged and unsatisfied except #16 (resolved Option B on the tree plugin). The schema does not encode child-creation policy and defines no flag/config/env for team mode. The #48 member-auth foundation (invitations, sessions, whoami) is a sign-in surface only — it does not enable team mode or cross-owner routing. ## Member auth foundation (landed, #48) @@ -63,8 +63,8 @@ Central authorization in `core/src/host/authz.js`, installed after member-auth: - When enforced, every Bot / room / session an endpoint touches must belong to the signed-in actor (`bot/forbidden` otherwise). `body.actorId` still grants nothing. Session targets resolve through the store's owner maps (`botOf` for a Bot id; `selfOf` / `groupOwners` / `pastOwners` for a current chat, group Session or earlier part); unresolvable ids deny. `approval-answer` is judged by the approval's own Bot or room, resolved by approval id — caller-supplied session fields grant nothing. - Shared resources that cannot yet be isolated per owner (secrets, connectors, folder picks, browser bridge, update-install, diagnostics, set-prefs, capability library writes, `add-agent` — the DSH Agent is one per deployment with no owner) are **explicitly blocked** with `bot/blocked-shared` while more than one member exists — not hidden only in the UI. `flybot/*` plugin endpoints follow the same split: plugin reads reach any signed-in actor, plugin writes are shared-blocked. - `create-bot` stamps `ownerId` from the actor; `view()` and `live()` narrow to the actor — per-session entries (`questions`, `exchangeTurns`) keep the actor's own, deployment-wide surfaces (agents, secrets, connectors, memory reviews) stay visible while a single member exists and hide with more than one, the same rule as the shared writes. `create-room` refuses a request with neither a member Bot nor any participant (a room with nothing in it would belong to nobody); view() also keeps the shared rooms the actor belongs to and the Bots in them (#50). -- Watch item: `set-main` lets an owner promote their own Bot to a Main Bot. Main rights inside the Bot team are a shipped product concept, but how they compose with multi-human authority is exactly what #16 owns — revisit there before team mode turns on. -- **Not done here:** cross-owner task execution and filesystem sandbox redesign (reach is #51, below). Team mode stays disabled; #16 stays open. +- Watch item: `set-main` lets an owner promote their own Bot to a Main Bot. Main rights inside the Bot team are a shipped product concept; how they compose with multi-human authority is still a team-mode question (not #16 — that issue decided tree child creation). Revisit before team mode turns on. +- **Not done here:** cross-owner task execution and filesystem sandbox redesign (reach is #51, below). Team mode stays disabled. ## Room membership foundation (landed, #50) @@ -136,7 +136,7 @@ Shipped room privacy (a group never reads a Bot's private chat) is unchanged; th 5. **No forwarding to owner approval.** No route may turn another human's request into owner approval by forwarding it. An approval is created only by the owner whose authority is spent, for the exact action; receipt, relay, or a Chief's word never becomes approval. 6. **Approval authority stays with the human.** A Chief may ask its own owner, present options, and act on an approval it actually received from that owner. It cannot mint an approval, replay another owner's approval, or treat silence as consent. 7. **Team mode is disabled** until every gate below passes. -8. **#16 is open.** Neither option may be assumed by schema or implementation work (see [Open decisions](#open-decisions-16-unresolved)). +8. **#16 is resolved (Option B).** Tree Bots may create children under themselves within `maxChildren` and depth limits. A new child is usable only by its creator's owner; sharing still needs an independent per-Bot grant (default deny). The shipped rule lives in [tree](tree.md), not here. 9. **Contract honesty.** Nothing on this page describes shipped behavior; shipped behavior is [core/README.md](../../core/README.md) and [plugins/tree/README.md](../../plugins/tree/README.md). ## Routing matrix @@ -173,18 +173,13 @@ Planned rules for team mode. Default is **deny**: a route is allowed only if a r | Action touching a second owner's scope | Each affected owner, for their own scope only | Chief → Chief may negotiate; neither Chief can approve for the other's owner. | | Anything forwarded from another human | Nobody, by forwarding alone | Reject the forwarded "approval"; it is a request, not consent. | -## Open decisions (#16, unresolved) +## Resolved decision (#16, Option B) -Issue **#16** (who may call `create_child_bot`) is an **OPEN maintainer decision** since the PR #2 review. Both options, recorded without preference: +Issue **#16** (who may call `create_child_bot`) is **resolved**. The owner chose **Option B**: -| Option | Statement | -| --- | --- | -| **A** | Restrict `create_child_bot` to the root/Main/Chief Bots (or require owner approval for each creation). | -| **B** | Amend AGENTS.md so specialists / tree Bots may create children under themselves within owner limits (`maxChildren`, depth limits). | +Tree Bots (not only Main/root/Chief) may call `create_child_bot` to create a child directly under themselves. `maxChildren` and depth limits stay. A newly created child is usable only by its creator's owner; sharing with another person still requires an independent per-Bot grant (default deny). There is no publish mechanism. -- Status: **open** — this contract picks neither side. -- Schema and team-mode work (#47+) **must not assume either answer**, and no test may encode one. -- The gate below requires #16 to be *resolved* before team mode can be enabled; resolving it is a maintainer act outside this page. +Shipped enforcement and tests live in [tree](tree.md) (`plugins/tree/`). This contract does not re-open Main-only creation. Remaining team-mode gates below are unchanged. ## Team mode gate @@ -193,7 +188,7 @@ Issue **#16** (who may call `create_child_bot`) is an **OPEN maintainer decision 1. **Owner identity enforced server-side** on every management call (create/update/delete Bot, group administration, memory writes) — not prompt-level, not client-side only. 2. **Room/Bot scoping enforced server-side**: a request from outside an owner's scope fails closed, for reads and writes alike. 3. **Approval binding**: an approval names the owner whose authority is spent and the exact action; forwarded or replayed approvals never satisfy it (invariant 5). -4. **#16 resolved** by a maintainer, with a test asserting who may create Bots. +4. **#16 resolved** (Option B): tree Bots may create children under themselves; `plugins/tree/test` asserts who may create, that limits still bind, and that a new child is not auto-granted. This item is satisfied; the others are not. 5. **Chief lifecycle fails closed**: a disabled, missing or replaced Chief leaves no path for specialists to reach cross-owner traffic or approvals (next section). Until every gate passes, team mode is off and the shipped single-human model is the only supported one. @@ -214,7 +209,7 @@ Planned rules (fail closed by default): - Treating team administration (shared groups, team memory) as private owner authority, or vice versa. - Letting a specialist "just relay" across owners — relaying is routing, and it is forbidden. - Treating a forwarded request or forwarded "approval" as owner approval. -- Assuming #16 Option A or Option B has been decided, in schema drafts, code or tests. +- Treating #16 as still open, or restricting `create_child_bot` to Main/root after Option B. - Adding a team-mode flag, config key or env var before the gates pass. - Rewriting tree ownership here instead of in [tree](tree.md). diff --git a/docs/subsystems/tree.md b/docs/subsystems/tree.md index f9c71900b..3287f7ee1 100644 --- a/docs/subsystems/tree.md +++ b/docs/subsystems/tree.md @@ -34,13 +34,19 @@ Everything in `plugins/tree/` (entry `plugins/tree/index.js` and any helpers). 2. Uses documented plugin APIs only (hooks/slots). 3. Reach (#90): tree never routes an outside sender through the root. A remote sender (friend's Bot) reaching a tree Bot is denied if `flybot.bot.hasAnyGrant` is false (new and adopted Bots, and the Chief, start with no grant) and is not the tree's call if it is granted, so a granted Bot is reached directly, at any depth. Granted Bots on both sides are not the tree's call across trees or to non-tree Bots (the core's owner policy decides); inside one tree the parent/child rule is unchanged, and `stopped` / `report` notices are unchanged. On a core without `hasAnyGrant` the remote-sender rule gives no opinion. +### Child creation (#16, Option B) + +- Every tree member (not only the Main/root) may call `create_child_bot`. The parent is always the calling Bot; the tool has no parent argument. +- `maxChildren` and depth limits stay. Parallel creates are serialized per parent. +- A newly created child starts with no person-grant (`bot.hasAnyGrant` false, even when the parent is granted). Sharing with another person still needs an independent per-Bot grant (default deny). Tree never copies grants and never publishes. + ### Optional Tree section toggle - The Bot list itself uses the core `.bt-tree-toggle` (see [core-client](core-client.md)). When `sidebarSection: true` brings this plugin's separate Tree section back, `.flybot-tree-toggle` matches that control: 28×28 hit target, 16px inner `.flybot-tree-caret`, hover, focus ring, and rotation of the glyph only. Leaf spacers (`.flybot-tree-dot`) keep the same 28px width. `aria-expanded` and Expand/Collapse `{name}` labels stay on the button. ## Tests -Covered by the plugin's own tests under `plugins/tree/` / core test suite as wired. +Covered by the plugin's own tests under `plugins/tree/` / core test suite as wired. `#16 Option B` in `test/tree.test.js` (and the matching host test) asserts a non-Main tree Bot can create a child under itself, that `maxChildren`/depth still refuse, and that the child is not auto-granted. ## Common mistakes diff --git a/plugins/tree/README.md b/plugins/tree/README.md index 783ec351d..832419aa9 100644 --- a/plugins/tree/README.md +++ b/plugins/tree/README.md @@ -14,7 +14,7 @@ Recursive bot tree plugin for FlyBot (built on ds-bot / DeepSeek Harness). - **Deleting a bot:** its children re-attach to its parent and every moved child (and its subtree) gets a fresh prompt/trace. `maxChildren` is not enforced on reparenting (no child is dropped); an overflow is logged as a warning and `create_child_bot` refuses new children there until it is back under the limit. - **`create_child_bot` is serialized per parent:** parallel calls never exceed `maxChildren` or create duplicate-name siblings. - Delegation is level by level: coordinators delegate to direct children whose scope fits and aggregate upward; a bot with no children does the work itself. A one-line trace like `Chief → Work → Dev` is added only to answers a tree bot sends to its parent tree bot; replies to the user or to a remote (friend's, Bot Social) bot never carry it, and the root (Chief) never adds one. -- Tools (visible only to tree bots): `list_children()` and `create_child_bot(name, role_description)`. The parent is always the calling bot. Limits: depth 5 (root = 1), 8 children per node; duplicate sibling names (case/space-insensitive) are rejected; names are 1–40 characters (ds-bot's roster limit). Every new child gets the same recursive system prompt template (`src/prompt.js`). +- Tools (visible to every tree bot, not only Main/root — #16 Option B): `list_children()` and `create_child_bot(name, role_description)`. The parent is always the calling bot. Limits: depth 5 (root = 1), 8 children per node; duplicate sibling names (case/space-insensitive) are rejected; names are 1–40 characters (ds-bot's roster limit). Every new child gets the same recursive system prompt template (`src/prompt.js`). A new child starts with no person-grant even if its parent is granted; sharing it still needs an independent Invite to this Bot (default deny). - Frontend (`client.js`, bundle id `@flybot/tree`, injects `flybot`): the tree is drawn by ds-bot's own Bot list, so this plugin's separate "Tree" sidebar section is **off by default** (`sidebarSection: true` in the client config brings it back). It always exposes the synchronous helper `ctx.flybot.tree.forest(bots)` (also `globalThis.__flybot__.tree.forest(bots)`), see **Tree data** below. It also registers `flybot.sidebar.structure` (`{ parentOf(botId) }`, parents from `forestOf` over `ctx.flybot.bots()`, refreshed on field changes and every 10 s); ds-bot's sidebar uses it to draw its Bot list as one expandable tree with Chief on top. Retired and non-tree Bots have no parent there, so ds-bot shows them directly under Chief. The entry also has `menuItems({ bot })`, which ds-bot adds to each Bot's right-click menu: two items, `{ id: 'copy-invite', label: 'Copy invite link' }` and `{ id: 'copy-invite-text', label: 'Copy invite with instructions' }` (each with `visible`, `run`). `visible` is `social.canInvite(bot.id) === true` (false without the social plugin); `run` calls `social.createInvite(botId)` (or `flybot.call('social.createInvite', { botId })`). **Copy invite link** copies only the bare URL (`link`), **Copy invite with instructions** the full message `text`; a missing field is an error, never swapped for the other. The clipboard write starts inside the click via `ClipboardItem` with the text as a promise, so it keeps the click's permission while the invite is created; `writeText` then a textarea are the fallbacks. They return "Invite link for copied" or "Invite for copied, with instructions", which ds-bot shows as a toast; a failure throws and shows as the error toast. Right-clicking never opens the Bot's chat. ### Config diff --git a/plugins/tree/src/tools.js b/plugins/tree/src/tools.js index 28996e961..9a2f8ccaa 100644 --- a/plugins/tree/src/tools.js +++ b/plugins/tree/src/tools.js @@ -1,6 +1,7 @@ import { MAX_NAME_LENGTH } from './limits.js' // = ds-bot roster's 40-character name truncation // Model tools. The caller comes from the tool context ({ botId }), never from arguments, -// so a bot can only create children under itself. Visible only to tree members. +// so a bot can only create children under itself. Visible to every tree member (#16 Option B: +// not Main/root-only). maxChildren and depth stay in createChild; a new child is not granted. // tools.register keeps only type/description/enum/items from JSON Schema (minLength/maxLength // are dropped), so every execute() validates its own arguments with validateArgs(). @@ -55,7 +56,7 @@ export function registerTreeTools(fb, tree) { }), fb.registerTool({ name: 'create_child_bot', - description: 'Create a NEW child bot directly under yourself, then delegate to it. Only when none of your children fits; never a duplicate.', + description: 'Create a NEW child bot directly under yourself, then delegate to it. Any tree bot may call this (not only the root). Only when none of your children fits; never a duplicate.', parameters: CREATE_CHILD_SCHEMA, visibleTo: inTree, async execute(args, { botId }) { diff --git a/plugins/tree/src/tree.js b/plugins/tree/src/tree.js index 856775309..b0636c66d 100644 --- a/plugins/tree/src/tree.js +++ b/plugins/tree/src/tree.js @@ -154,7 +154,8 @@ export function createTree(fb, { limits, rootName = 'Root', rootBotId, adoptExis const reason = checkCreate({ parentName: parent.name, parentDepth: depthOf(nodes, parentId), siblings: childrenOf(nodes, parentId), name: clean, limits }) if (reason) throw new Error(reason) const prompt = systemPrompt({ name: clean, role, trace: `${traceOf(nodes, parentId)} → ${clean}`, children: [] }) - const id = await fb.createBot({ name: clean, systemPrompt: withTreeBlock('', prompt), fields: { parent: parentId, treeRole: role } }) + // createdBy is the parent Bot. Do not copy its grants: a child starts owner-private (#16). + const id = await fb.createBot({ name: clean, systemPrompt: withTreeBlock('', prompt), fields: { parent: parentId, treeRole: role }, createdBy: parentId }) refreshPrompt(parentId) // the parent now lists the new child return { id, name: clean, depth: depthOf(nodes, parentId) + 1 } } diff --git a/plugins/tree/test/host.test.js b/plugins/tree/test/host.test.js index f5466736e..04d5c62d7 100644 --- a/plugins/tree/test/host.test.js +++ b/plugins/tree/test/host.test.js @@ -87,6 +87,27 @@ test('tree plugin on the real host: Chief adopted as root, create_child_bot, sco } }) +test('#16 Option B on the real host: a non-Main tree Bot creates a child that stays ungranted', async () => { + const h = await start() + try { + const fork = h.ctx.plugin(tree, {}) + await waitFor(() => h.fb.scope('flybot-tree').fields.get('b', 'parent') === 'a') + assert.equal(h.fb.bot.get('a').isMain, true) + assert.equal(h.fb.bot.get('b').isMain, false) + await h.fb.grants.set('b', [{ grantee: '@bob.home' }]) + const created = JSON.parse(await h.tool('create_child_bot', { name: 'Work', role_description: 'work' }, 'b')).created + assert.ok(created.id) + assert.equal(h.fb.scope('flybot-tree').fields.get(created.id, 'parent'), 'b') + assert.equal(h.fb.bot.get(created.id).isMain, false) + assert.equal(h.fb.bot.get(created.id).granted, false) + assert.equal(h.fb.bot.hasAnyGrant(created.id), false) + assert.deepEqual(h.fb.grants.of(created.id), []) + fork.dispose() + } finally { + await h.stop() + } +}) + test('#90 on the real host: a friend reaches a granted tree Bot directly, never an ungranted one, and nothing goes through the root', async () => { const h = await start() try { diff --git a/plugins/tree/test/tree.test.js b/plugins/tree/test/tree.test.js index 0eb118626..30685b2bb 100644 --- a/plugins/tree/test/tree.test.js +++ b/plugins/tree/test/tree.test.js @@ -74,6 +74,35 @@ test('tools: configurable limits and hidden from non-tree bots', async () => { await assert.rejects(m.call(o, 'list_children'), /not visible/) }) +test('#16 Option B: a non-Main tree Bot may create a child under itself; limits still bind; the child is not auto-granted', async () => { + const m = await setup({ maxDepth: 3, maxChildren: 2 }) + assert.equal(m.flybot.bot.get(m.root).isMain, true) + const specialist = await make(m, m.root, 'Work') + assert.equal(m.flybot.bot.get(specialist).isMain, false) + // A grant on the parent (or the Chief) must not follow the new child. + m.setGrants(m.root, [{ grantee: '@alice.home' }]) + m.setGrants(specialist, [{ grantee: '@alice.home' }]) + const child = await make(m, specialist, 'Dev') + assert.ok(child) + assert.equal(f(m, child, 'parent'), specialist) + assert.equal(m.flybot.bot.get(child).isMain, false) + assert.equal(m.flybot.bot.hasAnyGrant(child), false) + assert.deepEqual(m.flybot.grants.of(child), []) + const toward = to => m.middlewares[0].handle({ from: { kind: 'remote', handle: '@alice.home' }, to, target: { remote: false, id: to } }) + assert.equal(toward(child)?.action, 'deny') + assert.match(toward(child)?.reason, /has not been granted/) + // Sharing still takes an independent grant on that Bot. + m.setGrants(child, [{ grantee: '@alice.home' }]) + assert.equal(m.flybot.bot.hasAnyGrant(child), true) + m.setGrants(child, []) + assert.equal(m.flybot.bot.hasAnyGrant(child), false) + assert.ok(await make(m, specialist, 'QA')) + assert.match((await m.call(specialist, 'create_child_bot', { name: 'Extra', role_description: 'x' })).error, /maxChildren=2/) + assert.match((await m.call(child, 'create_child_bot', { name: 'TooDeep', role_description: 'x' })).error, /maxDepth=3/) + const o = await outsider(m) + await assert.rejects(m.call(o, 'create_child_bot', { name: 'X', role_description: 'x' }), /not visible/) +}) + test('fix 1: fields are written under fields["flybot-tree"] via flybot.scope, never shared', async () => { const m = await setup() assert.ok(m.scopes.includes('flybot-tree'))