fix(aos): fail closed when acompose is handed integrated capability flags (#810) #1194
No reviewers
Labels
No labels
burndown-2026-06
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/agentic-os!1194
Loading…
Reference in a new issue
No description provided.
Delete branch "aos/810-acompose-fail-closed"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #810.
Reproduced first
It accepted every flag and went on to fetch seventeen repositories. Ward routing lives only in the root action, and the root flags stay readable after subcommand selection, so nothing objected. The operator gets a standalone container under a command that reads as Ward-brokered.
After
The check runs first in
runAcompose, ahead ofvalidateLegacyDensityand the role and command checks, because failing closed means before materialization rather than reporting after it. A test asserts that ordering directly, so a later refactor cannot let argument validation mask the refusal.What is deliberately not refused
--composedis a documented no-op on every path ("compatibility flag; AOS always materializes agent-compose role context"), so refusing it would break the standalone form to no effect. The issue's list names--warded,--guarded,--agent, and not this one.acompose-checkinresolves its layout from--agent, so the refusal must not reach it. A test holds that boundary.Coverage
Seven tests in a new
aos-cli/acompose_capability_flags_test.go, covering the acceptance criteria:IsSetreads through the lineage in both orders.--composedalone falls through.acompose-checkinstill honors--agent.The error is asserted to name the offending flags and to show the canonical root shape, since an error that does not point somewhere gets retried in the same shape.
Docs and help
docs/aos-cli.mdstates the root-action versus legacy-subcommand boundary in the Routing section, and the subcommand's ownUsagestring now carries it, which is where the mistake is made.No
docs/FEATURES.mdentry: this is validation hardening, which that rule names as not earning one.623 python tests pass, the Go suite passes,
pre-commit run --all-filespasses.Verified against a binary built from your branch, not against the tests. Every claim in the PR body holds.
go build ./aos-clifrom2b8...(your head), then the exact command from #810:Exit 1, and nothing ran before it. No reclaim, no residency resolution, no fleet pass. That is the acceptance criterion #810 actually cares about ("fail closed before Docker or Ward starts"), and putting the check ahead of
validateLegacyDensityis what buys it. The test that pins the ordering is the right thing to have written, because that property is invisible in the diff a year from now.Both deliberate non-refusals hold too, which is the half that would have been easy to get wrong:
cmd.IsSetseeing root-level flags from a subcommand action was the thing I most expected to be wrong, since a false return there would have made the whole guard a silent no-op. It works.I also confirmed
--composedis genuinely a documented no-op rather than a fourth dropped capability, so leaving it out of the refusal list is right rather than an omission.One follow-up, explicitly not a blocker
The same silent acceptance exists on every other subcommand:
Materially this does not matter today. Nobody believes
aos version --wardeddid something Ward-brokered, andconvergeandrepositoriesare not launches, so there is no wrong execution to be fooled about.acomposewas the one where silent acceptance produced a different launch under a command that read as integrated, and you prioritized it correctly.What is worth a follow-up is the shape rather than the instances: the refusal lives in
runAcomposerather than in the flag definitions, so a subcommand added later inherits the hole by default and someone has to remember. If the root capability flags were rejected for any subcommand that does not consume them, this would be structural instead of a list of three strings. That is a bigger change than #810 asked for and I would not fold it in here.Nothing blocking. Good fix.