Skip to content

Commit bb010cd

Browse files
committed
fix(opencode): allow aft_outline + aft_zoom for all hidden subagents
Adds the read-only AFT navigation tools to historian, dreamer, and sidekick allow-lists. These are exactly the right shape for hidden subagents that need to inspect code structure without dragging in whole files: aft_outline returns a symbol-level outline, aft_zoom returns the source of a single named symbol. Per-agent rationale: - historian / historian-editor / compressor: can now verify a referenced symbol or skim file structure when writing accurate compartment summaries, without falling back to whole-file reads. - dreamer: the key-files identification prompt at features/magic-context/key-files/identify-key-files.ts:257-258 already tells the model to use aft_outline / aft_zoom. Without them in the allow-list those calls would have been denied, which would silently regress the key-files identification task. - sidekick: can pull lightweight symbol-scoped context when the user's prompt references a specific file or symbol. Sidekick's read still stays denied — it should pull symbol-scoped views via aft_zoom, not arbitrary file contents. Allow-lists now: HISTORIAN_ALLOWED_TOOLS = read, aft_outline, aft_zoom DREAMER_ALLOWED_TOOLS = read, grep, glob, bash, aft_outline, aft_zoom, ctx_memory, ctx_search, ctx_note SIDEKICK_ALLOWED_TOOLS = ctx_search, ctx_memory, aft_outline, aft_zoom Tests updated and expanded: 1479 pass / 0 fail (+3 new aft assertions across the three agents). Permission integration shape tests now verify the exact final ruleset for each agent.
1 parent a480af1 commit bb010cd

2 files changed

Lines changed: 87 additions & 26 deletions

File tree

packages/plugin/src/agents/permissions.test.ts

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,18 @@ describe("buildAllowOnlyPermission", () => {
5050
});
5151

5252
describe("HISTORIAN_ALLOWED_TOOLS", () => {
53-
it("includes only `read` (for state-file offload)", () => {
54-
// Historian's only tool need is reading the offloaded existing-state
55-
// XML the runner writes to a temp file. Nothing else.
56-
expect(HISTORIAN_ALLOWED_TOOLS).toEqual(["read"]);
53+
it("includes `read` (for state-file offload)", () => {
54+
// Historian's primary tool need is reading the offloaded
55+
// existing-state XML the runner writes to a temp file.
56+
expect(HISTORIAN_ALLOWED_TOOLS).toContain("read");
57+
});
58+
59+
it("includes `aft_outline` and `aft_zoom` for token-efficient repo navigation", () => {
60+
// Read-only AFT navigation tools let historian/compressor verify
61+
// a symbol or skim file structure when writing accurate
62+
// compartment summaries without dragging in whole files.
63+
expect(HISTORIAN_ALLOWED_TOOLS).toContain("aft_outline");
64+
expect(HISTORIAN_ALLOWED_TOOLS).toContain("aft_zoom");
5765
});
5866

5967
it("does NOT include `task` (the bug we're fixing — preventing subagent fanout)", () => {
@@ -65,6 +73,13 @@ describe("HISTORIAN_ALLOWED_TOOLS", () => {
6573
expect(HISTORIAN_ALLOWED_TOOLS).not.toContain(dangerous);
6674
}
6775
});
76+
77+
it("does NOT include `grep` or `glob` (historian summarizes, not explores)", () => {
78+
// Historian's job is summarizing the input it was given.
79+
// Repo-wide exploration belongs to dreamer / primary agents.
80+
expect(HISTORIAN_ALLOWED_TOOLS).not.toContain("grep");
81+
expect(HISTORIAN_ALLOWED_TOOLS).not.toContain("glob");
82+
});
6883
});
6984

7085
describe("DREAMER_ALLOWED_TOOLS", () => {
@@ -111,13 +126,25 @@ describe("DREAMER_ALLOWED_TOOLS", () => {
111126
});
112127

113128
describe("SIDEKICK_ALLOWED_TOOLS", () => {
114-
it("includes only ctx_search + ctx_memory (read-only memory retrieval)", () => {
129+
it("includes ctx_search + ctx_memory for memory retrieval", () => {
115130
// Sidekick is the /ctx-aug memory retriever. It augments the user
116-
// prompt with relevant memories — no edits, no subagents, no bash.
117-
expect(SIDEKICK_ALLOWED_TOOLS).toEqual(["ctx_search", "ctx_memory"]);
131+
// prompt with relevant memories.
132+
expect(SIDEKICK_ALLOWED_TOOLS).toContain("ctx_search");
133+
expect(SIDEKICK_ALLOWED_TOOLS).toContain("ctx_memory");
134+
});
135+
136+
it("includes `aft_outline` and `aft_zoom` for lightweight structural context", () => {
137+
// Sidekick can pull file outline / symbol body when the user's
138+
// prompt references a specific file or symbol, without dragging
139+
// in whole files.
140+
expect(SIDEKICK_ALLOWED_TOOLS).toContain("aft_outline");
141+
expect(SIDEKICK_ALLOWED_TOOLS).toContain("aft_zoom");
118142
});
119143

120-
it("does NOT include `read` (sidekick doesn't touch the filesystem)", () => {
144+
it("does NOT include `read` (use aft_outline/aft_zoom for navigation instead)", () => {
145+
// Sidekick should pull symbol-scoped views, not arbitrary file
146+
// contents. If it needs full source it can use aft_zoom on a
147+
// specific symbol.
121148
expect(SIDEKICK_ALLOWED_TOOLS).not.toContain("read");
122149
});
123150

@@ -129,34 +156,40 @@ describe("SIDEKICK_ALLOWED_TOOLS", () => {
129156
});
130157

131158
describe("integration: full hidden-agent permission shape", () => {
132-
it("historian permission object: `*` denied, only `read` allowed", () => {
159+
it("historian permission object: `*` denied + read + aft_outline + aft_zoom allowed", () => {
133160
const perm = buildAllowOnlyPermission(HISTORIAN_ALLOWED_TOOLS);
134161
expect(perm).toEqual({
135162
"*": "deny",
136163
read: "allow",
164+
aft_outline: "allow",
165+
aft_zoom: "allow",
137166
});
138167
});
139168

140-
it("dreamer permission object: `*` denied + read + grep + glob + bash + ctx_* allowed", () => {
169+
it("dreamer permission object: `*` denied + repo-exploration + ctx_* + aft_* allowed", () => {
141170
const perm = buildAllowOnlyPermission(DREAMER_ALLOWED_TOOLS);
142171
expect(perm).toEqual({
143172
"*": "deny",
144173
read: "allow",
145174
grep: "allow",
146175
glob: "allow",
147176
bash: "allow",
177+
aft_outline: "allow",
178+
aft_zoom: "allow",
148179
ctx_memory: "allow",
149180
ctx_search: "allow",
150181
ctx_note: "allow",
151182
});
152183
});
153184

154-
it("sidekick permission object: `*` denied + ctx_search + ctx_memory allowed", () => {
185+
it("sidekick permission object: `*` denied + ctx_* + aft_* allowed", () => {
155186
const perm = buildAllowOnlyPermission(SIDEKICK_ALLOWED_TOOLS);
156187
expect(perm).toEqual({
157188
"*": "deny",
158189
ctx_search: "allow",
159190
ctx_memory: "allow",
191+
aft_outline: "allow",
192+
aft_zoom: "allow",
160193
});
161194
});
162195
});

packages/plugin/src/agents/permissions.ts

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,16 @@
3939
*
4040
* # What each agent needs
4141
*
42-
* - **historian / historian-editor / compressor**: just `read`. The
43-
* runner offloads large existing-state XML to a temp file under
44-
* `<project>/.opencode/magic-context/historian/` and the prompt
45-
* instructs the model to read that file. The model's only output
46-
* channel is its text response (the `<output>...</output>` XML).
42+
* - **historian / historian-editor / compressor**: `read` plus the
43+
* read-only AFT navigation tools `aft_outline` and `aft_zoom`.
44+
* The runner offloads large existing-state XML to a temp file
45+
* under `<project>/.opencode/magic-context/historian/` and the
46+
* prompt instructs the model to read that file. AFT navigation
47+
* is allowed so historian can verify a symbol or file structure
48+
* when writing accurate compartment summaries.
4749
*
48-
* - **dreamer**: `read`, `grep`, `glob`, `bash`, plus the Magic
50+
* - **dreamer**: `read`, `grep`, `glob`, `bash`, the read-only AFT
51+
* navigation tools `aft_outline` and `aft_zoom`, plus the Magic
4952
* Context MCP tools `ctx_memory`, `ctx_search`, `ctx_note`.
5053
* Dreamer task prompts in
5154
* `features/magic-context/dreamer/task-prompts.ts` explicitly tell
@@ -57,9 +60,12 @@
5760
* `websearch` remain denied — dreamer must not spawn subagents
5861
* or commit changes.
5962
*
60-
* - **sidekick**: `ctx_search` and `ctx_memory` (read-only ops).
61-
* Sidekick's job is augmenting user prompts via memory retrieval
62-
* — see `features/magic-context/sidekick/agent.ts`.
63+
* - **sidekick**: `ctx_search`, `ctx_memory`, plus the read-only AFT
64+
* navigation tools `aft_outline` and `aft_zoom`. Sidekick's job
65+
* is augmenting user prompts via memory retrieval — see
66+
* `features/magic-context/sidekick/agent.ts`. AFT navigation lets
67+
* it pull symbol-scoped structural context for prompts that
68+
* reference a specific file or symbol.
6369
*/
6470

6571
/**
@@ -86,11 +92,20 @@ export function buildAllowOnlyPermission(
8692

8793
/**
8894
* Tools the historian + historian-editor + compressor agents need.
95+
*
8996
* Historian runners offload large `<existing_state>` XML to disk and
90-
* tell the model to `read` it before emitting the summary XML. Nothing
91-
* else is needed — no bash, no edits, no other subagents.
97+
* tell the model to `read` it before emitting the summary XML. The
98+
* core need is `read`; we also allow the read-only AFT navigation
99+
* tools `aft_outline` and `aft_zoom` so that if a historian/compressor
100+
* ever needs to verify a symbol or skim a file's structure to write
101+
* an accurate compartment summary, it can do so token-efficiently
102+
* instead of pulling whole files via `read`.
103+
*
104+
* Still denied: bash, edit, write, task, grep/glob, webfetch/
105+
* websearch. Historian's job is summarizing the input it was given,
106+
* not exploring the repo.
92107
*/
93-
export const HISTORIAN_ALLOWED_TOOLS = ["read"] as const;
108+
export const HISTORIAN_ALLOWED_TOOLS = ["read", "aft_outline", "aft_zoom"] as const;
94109

95110
/**
96111
* Tools the dreamer agent needs. This is the broadest hidden-agent
@@ -125,6 +140,8 @@ export const DREAMER_ALLOWED_TOOLS = [
125140
"grep",
126141
"glob",
127142
"bash",
143+
"aft_outline",
144+
"aft_zoom",
128145
"ctx_memory",
129146
"ctx_search",
130147
"ctx_note",
@@ -134,7 +151,18 @@ export const DREAMER_ALLOWED_TOOLS = [
134151
* Tools the sidekick agent needs. Sidekick is a read-only memory
135152
* retriever for `/ctx-aug` — it queries the project's memory store
136153
* via `ctx_search` and (rarely) reads specific memories with
137-
* `ctx_memory(action="list")`. It must NOT spawn subagents, edit
138-
* files, or run bash.
154+
* `ctx_memory(action="list")`.
155+
*
156+
* Also allow `aft_outline` and `aft_zoom` so sidekick can pull
157+
* lightweight structural context about a file or symbol when the
158+
* user's prompt references it directly — token-efficient navigation
159+
* without dragging in whole files.
160+
*
161+
* Still denied: spawning subagents, edits, bash, web fetches.
139162
*/
140-
export const SIDEKICK_ALLOWED_TOOLS = ["ctx_search", "ctx_memory"] as const;
163+
export const SIDEKICK_ALLOWED_TOOLS = [
164+
"ctx_search",
165+
"ctx_memory",
166+
"aft_outline",
167+
"aft_zoom",
168+
] as const;

0 commit comments

Comments
 (0)