Add the gated worker subagent kind

worker holds the write tools and routes every write and command through the parent's approval gate: same permission rules, same guard, same prompt, flagged as coming from a subagent. Without an approval channel, as in headless runs, the kind is not offered at all rather than silently downgraded to read-only.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Muhammad Zakir Ramadhan
2026-09-03 16:42:20 +07:00
co-authored by Sisyphus
parent 83f4399e64
commit 41546154fe
4 changed files with 404 additions and 30 deletions
+21 -1
View File
@@ -18,7 +18,7 @@ import * as registry from './registry';
import { Session } from './session';
import { loadSkills } from './skills';
import * as store from './store';
import { createTaskTool } from './subagent';
import { createTaskTool, type SubagentApproval } from './subagent';
import { VERSION, versionLine } from './version';
import { createAskBridge } from './ui/Ask';
import { App, createApprovalBridge, createNoticeBus, createSubagentBus, type AppHooks } from './ui/App';
@@ -245,6 +245,20 @@ async function persist(messages: ModelMessage[]): Promise<void> {
}
const bridge = createApprovalBridge();
/**
* Late-bound so the task tool can be built before the Session that owns the gate.
*
* `createTaskTool` decides whether to offer the `worker` kind from whether an
* approval channel exists, so the callback has to be present at construction even
* though the Session it delegates to does not exist yet.
*/
let approveSubagent: SubagentApproval | undefined;
const subagentGate: SubagentApproval = (req) => {
if (!approveSubagent) throw new Error('the subagent approval channel is not wired yet');
return approveSubagent(req);
};
const session = new Session({
model: languageModel ?? unconfiguredModel,
askApproval: bridge.ask,
@@ -268,6 +282,10 @@ const session = new Session({
task: createTaskTool({
model: languageModel ?? unconfiguredModel,
...(headless ? {} : { report: subagents.emit }),
// A worker's writes go through the parent's rules and the parent's
// prompt. Headless has nobody to answer, so `worker` is withheld there
// rather than silently running unattended writes.
...(headless ? {} : { approve: subagentGate }),
}),
}),
},
@@ -280,6 +298,8 @@ const session = new Session({
},
});
approveSubagent = session.approveForSubagent();
async function shutdown(code: number): Promise<never> {
clearTimeout(saveTimer);
if (session.messages.length > 0) await persist(session.messages);