Skip to content

Commit 71b1fe7

Browse files
committed
Remove unreachable AgentProfile.fleetTier
No loader ever populated it from any config format, so profile-sourced orchestrators always resolved to leaf. Fail-closed denial of fleet verbs is unchanged; CL-6942/CL-6944 can add a real opt-in when a caller needs one.
1 parent 6c05612 commit 71b1fe7

6 files changed

Lines changed: 16 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ matching `## [X.Y.Z]` section (plus install instructions). Do not maintain
1111
parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
1212
`## [Unreleased]` to `## [X.Y.Z] - YYYY-MM-DD`, then run the release script.
1313

14+
## [Unreleased]
15+
16+
### Agent
17+
18+
- Removed `AgentProfile.fleetTier`: no loader ever populated it from any config
19+
format, so it was declared but unreachable. Fail-closed behavior is
20+
unchanged — a profile-sourced orchestrator is still denied `task`/
21+
`search_agents` with no supported opt-in.
22+
1423
## [0.2.108] - 2026-08-24
1524

1625
### Agent

docs/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ Every director package carries a required `tier: SubagentTier` field (`src/agent
239239

240240
Enforcement is runtime code at the existing tool-mount point, not prompt wording — this is the fix for four prior mechanisms (`writePaths`, `report.requiredSections`, a `--config` comment, the thrash matcher) that were documented-as-enforced while enforcing nothing:
241241

242-
- **Mount-time gate — live today, and fails closed.** `task-tool.ts` resolves the caller's tier at dispatch time — a closed director's `DirectorPackage.tier`, or an explicit non-leaf `AgentProfile.fleetTier` opt-in for a profile-sourced orchestrator — and forwards it as `RunSubAgentParams.orchestratorTier`. `runSubAgent` (`src/subagent/run.ts`) then calls `assertTierMayMountFleetVerb(tier, toolName)` (`src/subagent/authority.ts`) before installing `task` / `search_agents`, treating a **missing** `orchestratorTier` as `"leaf"` — deny, not skip. This is the case that matters most: a project-local or plugin `AgentProfile` with `orchestrator: true` is outside the closed director set and is **not** trusted with fleet verbs just because `orchestrator: true` is set — it must also declare `fleetTier: "nested-orchestrator"`, or the mount throws `FleetAuthorityError`. (`AgentProfile.fleetTier` is deliberately not named `tier` — that name is already used, ad hoc, by some profiles for an unrelated model-speed selector; reusing it would have silently broken schema validation for those profiles, which is exactly what happened during review and was caught by the full test suite, not by inspection.) `FLEET_VERBS` in `authority.ts` also names the not-yet-implemented verbs (`spawn_agent`, `wait_agents`, `list_agents`, `send_input`, `interrupt_agent`, `close_agent`, `resume_agent`, `read_agent_trace`, `followup_task`) so their future mount sites inherit the same gate.
242+
- **Mount-time gate — live today, and fails closed.** `task-tool.ts` resolves the caller's tier at dispatch time — a closed director's `DirectorPackage.tier` — and forwards it as `RunSubAgentParams.orchestratorTier`. `runSubAgent` (`src/subagent/run.ts`) then calls `assertTierMayMountFleetVerb(tier, toolName)` (`src/subagent/authority.ts`) before installing `task` / `search_agents`, treating a **missing** `orchestratorTier` as `"leaf"` — deny, not skip. This is the case that matters most: a project-local or plugin `AgentProfile` with `orchestrator: true` is outside the closed director set and is **not** trusted with fleet verbs just because `orchestrator: true` is set — there is no profile-level opt-in today, so the mount always throws `FleetAuthorityError` for a profile-sourced orchestrator (CL-6942/CL-6944 can add one when a real caller needs it). `FLEET_VERBS` in `authority.ts` also names the not-yet-implemented verbs (`spawn_agent`, `wait_agents`, `list_agents`, `send_input`, `interrupt_agent`, `close_agent`, `resume_agent`, `read_agent_trace`, `followup_task`) so their future mount sites inherit the same gate.
243243
- **Subtree authority — a seam, not yet wired.** `assertCanTargetAgent(actor, targetId, nodes)` (`src/subagent/authority.ts`) implements the "root owns its tree; a child manages only its own descendants" rule (Tier 1 may target anyone, Tier 2 may target only its own descendants over the same `{id, parentSessionId}` shape `SubAgentSessionStore` already tracks, Tier 3 always fails closed) — but **it has no production call site yet**. No verb today lets one live agent address another (`task` only spawns), so this rule is exercised only by `authority.test.ts` and is not enforced at runtime in this PR. It exists so CL-6942 (split spawn from wait) and CL-6944 (`send_input` steering) — the first verbs that make an agent addressable by another — can call it from day one instead of each inventing its own check. Treat it as unenforced until one of those wires a call site.
244244
- `task()` is unaffected and remains the only spawn verb until the new verbs land beside it (deprecated-not-deleted per the CL-6940 epic). Its argument schema and wire contract are unchanged; the tier check only gates which packages may have it mounted at all.
245245

src/agent/profile-types.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,6 @@ export interface AgentProfile {
6868
// coordinators (e.g. a planning agent that fans work out to specialists);
6969
// leaf-task agents should leave this unset.
7070
orchestrator?: boolean;
71-
// Fleet authority tier (CL-6941) for a profile-sourced orchestrator. Only
72-
// meaningful alongside orchestrator: true. Named `fleetTier`, not `tier` —
73-
// `tier` is already an established profile field for model speed selection
74-
// ("fast" | "standard" | "clever", resolved via task(tier=...)); reusing
75-
// the name silently broke schema validation for profiles that set it. A
76-
// profile is outside the closed director set, so it is NOT trusted with
77-
// fleet verbs by default even when orchestrator: true is set — this must
78-
// be declared explicitly as "nested-orchestrator" to opt in.
79-
// Runtime-enforced at the tool-mount point (src/subagent/run.ts /
80-
// src/subagent/authority.ts): an orchestrator=true profile with no
81-
// fleetTier (or fleetTier: "leaf") is denied task/search_agents, fail-closed.
82-
fleetTier?: "orchestrator" | "nested-orchestrator" | "leaf";
8371
// Optional inference-turn budget when this profile is dispatched via task(agent=...).
8472
// Floor-sanitized (≥1) at dispatch time; task(maxTurns) overrides when set.
8573
maxTurns?: number;

src/agent/profiles.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ const AgentProfileSchema = type({
5151
"systemPromptRole?": "string",
5252
"systemPromptPath?": "string",
5353
"orchestrator?": "boolean",
54-
"fleetTier?": "'orchestrator' | 'nested-orchestrator' | 'leaf'",
5554
"maxTurns?": "number",
5655
});
5756

src/subagent/task-tool.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -486,14 +486,9 @@ export function createTaskTool(deps: TaskToolDeps): AgentTool {
486486
if (profile.orchestrator === true && deps.allowOrchestrator !== false) {
487487
orchestrator = true;
488488
// Fail closed (CL-6941): a profile is outside the closed director
489-
// set, so orchestrator: true alone does not grant a tier. Only an
490-
// explicit non-leaf profile.fleetTier opts in; anything else
491-
// (absent, or "leaf") leaves orchestratorTier undefined, which
489+
// set, so orchestrator: true alone does not grant a tier. No
490+
// profile field opts in; orchestratorTier stays undefined, which
492491
// runSubAgent treats as "leaf" and denies task/search_agents.
493-
orchestratorTier =
494-
profile.fleetTier !== undefined && profile.fleetTier !== "leaf"
495-
? profile.fleetTier
496-
: undefined;
497492
}
498493
// Per-agent pinned inference (provider/model/effort), if declared.
499494
// Resolution uses policy (mode: pin / agentModelFallback: none) so a

tests/unit/subagent.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,5 +972,9 @@ describe("createTaskTool profile resolution", () => {
972972
await callHandler(tool, { description: "task", prompt: "do it", agent: "karen" });
973973

974974
expect(received?.orchestrator).toBe(true);
975+
// Fail-closed (CL-6941): no profile field opts a profile-sourced
976+
// orchestrator into fleet verbs, so the tier stays unresolved and
977+
// runSubAgent treats it as "leaf" — denied task/search_agents.
978+
expect(received?.orchestratorTier).toBeUndefined();
975979
});
976980
});

0 commit comments

Comments
 (0)