agentteams.cli — CLI Package Decomposition¶
Source:
agentteams/cli/
This page documents the module layout, not the command-line surface. For flags, option combinations and their interactions, see CLI Reference — restating them here would duplicate rather than reference (CH-14).
Why the package exists¶
build_team.py was a single module holding argument parsing, the generate pipeline,
artifact writers and the security gate. CH-07 caps a module at 1000 lines, so it was
carved into this package. build_team.py re-exports what it moved, so
build_team.main, build_team._write_memory_index and friends resolve unchanged — the
carve was behaviour-preserving by construction, and the re-exports are what let the
existing test suite pin that.
The LENGTH_ALLOWLIST in tests/test_code_hygiene.py is now empty — every module in
this package is under the 1000-line CH-07 ceiling on its own merits. app.py came down from
1174 to 465 when the pipeline moved out, and generate.py from 979 to 974 when the
standalone modes moved to standalone_modes.py.
Line counts are deliberately not restated per module here: they change on every carve, and a
doc that hard-codes them goes stale the way this paragraph did. Run
wc -l agentteams/cli/*.py for current figures.
Module map¶
| Module | Owns |
|---|---|
app.py |
Entry point. Dispatches on parsed arguments to the right runner; holds no pipeline logic itself. |
parser.py |
Argument parser definition — every flag lives here. |
parser_validate.py |
Option-combination validation, carved from parser.py. Rejects mutually exclusive pairs (e.g. two bridge modes) before any work begins. |
generate.py |
The generate / update / check pipeline: analyse → render → merge → emit → attest. |
render_pipeline.py |
Template rendering and content-merge helpers used by the pipeline. |
artifacts.py |
Writers for the generator-owned artifacts: delivery receipt, eval suite, model routing, memory index, code index. Also owns the memory index's source-scope rules. |
commands.py |
Sub-command runners for --convert, --interop-* and --bridge-*. |
security_gate.py |
The destructive-action gate: requires a recorded PASS decision, or an explicit waiver, before a destructive operation proceeds. |
decision_log.py |
Authenticates rows in the security-decisions log (HMAC signing/verification, calls decision_chain for the chain check); carved from security_gate.py at its own CH-07 ceiling. |
action_ids.py |
How a decision row's action_reviewed id is matched: action_matches (strict, decides which rows can clear an action) and halt_names_action (a HALT also blocks every action named as a token run in its id). Integrity-pinned. |
decision_chain.py |
The security-decisions log's hash chain in one place: verify_chain (including the pre-1.0 legacy prefix), chain_tip for every writer, and describe_log_chain for --verify-integrity. Integrity-pinned. |
decision_log_migrate.py |
--migrate-decision-log: appends the missing signing columns to a team's decisions-log header (never prev_digest), padding short legacy rows with empty cells (signed payloads unchanged), and lists the unsigned authorizing rows that signing revokes. A separate module because decision_log.py is integrity-pinned. See below. |
adopt_only.py |
--adopt-only: adopts a named subset of orphan agents by rewriting only the orchestrator's adopted routing rows (and, gated, its agents: list). tools_grant reports each slug's tools: line, wider than read-only unless every tool is. |
operator_signing.py |
The operator Ed25519 signing path for --sign-decision and --sign-grant (and the --issue-grant spec helpers): key read, payload construction, the pre-sign display, the pre-sign refusals, signing, verify-before-append and the append. Integrity-pinned (integrity.ENFORCEMENT_MODULES); the runners in commands.py / grant_commands.py are one-line delegators. See below. |
schema_cache.py |
Shared JSON-Schema validation plus a content-hash cache, so re-validating unchanged bytes is free. |
goose_switch.py |
Glue for --goose-source / --goose-model / --goose-show. |
backup_switch.py |
Glue for --stale-check / --stale-remediate / --prune-backups / --backup-mirror. |
fleet_switch.py |
Glue for --fleet / --fleet-frameworks / --fleet-report. |
package_switch.py |
Glue + dispatch for --package-team / --package-source-framework. |
sync_switch.py |
Glue + dispatch for --sync-init / --sync / --sync-since / --pin. |
recipe_check.py |
Standalone structural validator for Goose recipe YAML. |
standalone_modes.py |
The "do one thing and exit" modes carved out of generate.py: restore-backup, scan-security, check-budget, template pinning, and the retrieval utilities. They were never part of the generate pipeline. |
output_target.py |
Resolves and validates the output directory, including the foreign-output refusal behind --allow-foreign-output. |
post_emit_checks.py |
Checks that run after the write phase and cannot change its outcome. |
code_index_artifacts.py |
Writers for the code & API index cache (references/code-index/, gitignored). |
json_mode.py |
--json output shaping. |
exit_codes.py |
The named exit-code constants, so a status is set in one place and read everywhere. |
Two behaviours worth knowing when reading this package¶
The memory index's scope lives in artifacts.py, not memory_index.py.
_memory_index_sources decides what to index and _memory_index_root decides what
relative paths are relative to; memory_index.py only builds an index over whatever
it is handed. _SCRATCH_DIR_NAMES / _is_durable_source exclude backup and cache
directories from every recursive scan — without that filter the index reached 2120
documents, 1488 of them backup snapshots, in a 51 MB committed artifact.
Reporting never changes an outcome. generate.py calls
update_report.report_run after the write phase and only when
--dry-run was not passed. A failure to write the report must not turn a successful
update into a failed one.
The pinned operator signing path¶
--sign-decision and --sign-grant are the only code paths that read the operator's Ed25519
private key (the file named by AGENTTEAMS_DECISION_ED25519_KEYFILE). Everything between that
env read and the appended row lives in operator_signing.py, which is listed in
integrity.ENFORCEMENT_MODULES, so an in-place edit to it shows up in
references/enforcement-integrity.json, --verify-integrity and red-team phase 6. The
commands.py and grant_commands.py runners only choose the spec path and the output/team dir.
What the pin does not cover. It closes in-place edits and unnoticed drift. It does not defend
against unpinned code in the same process: the dispatch chain (app.py, parser.py,
agentteams/__init__.py, .pth files) can patch Path.read_text or this module's primitives,
or call signed_ledger directly. Since #9 the display is a gate: a y/N confirm on a terminal, or
an exact --confirm-review-sha256 match without one, before the key is read. Sign from a trusted
install (SECURITY.md, "Signing from a trusted install"), and run --verify-integrity first.
The pre-sign self-check (presign_integrity_check, run before the key is read). Both minters
verify the signing closure (SIGNING_CLOSURE: this module, signed_ledger.py, decision_log.py,
grants.py, effect_classifier.py, governance_targets.py, management_directives.py,
atomicio.py, signer_location.py, _sandbox_emit.py, integrity.py) against the integrity
manifest. They refuse on drift in that closure, on a missing manifest the repository should
carry, or on an unreadable manifest, and the key is never read. They warn (and still sign) on
drift in other pinned modules, and on a closure that differs from git HEAD or from the nearest
local tag. A local tag is unverified: anyone who can write the repository can create one, so
an agent that can commit and tag silences both comparisons. Git runs with repo config neutralised
and never through a content filter (ls-tree object ids against Python-computed blob ids, and
cat-file blob, because git diff/show would run a planted .gitattributes clean filter as the
operator). Since #10 a package, prefix or interpreter inside the current directory, a git work
tree or a sandbox allowWrite root is refused (signer_location.py) unless
--allow-checkout-signing. There is no environment override. The check lives in the module it
protects, so deleting it is itself recorded by the manifest: a speed bump with a trail, not a
boundary. An agent that edits the closure AND regenerates the manifest passes it, which is what
the HEAD/tag warnings are for.
sign_decision(output_dir, spec_path, *, confirm_sha256=None, allow_checkout=False)¶
Source:
agentteams/cli/operator_signing.py
Mints and appends one Ed25519-signed constraint-relaxing decision row under the team dir
output_dir. Returns 0, or 1 on any error (fail-closed). The steps run in this order (#9/#10):
- team-dir refusal;
- spec load and shape check, then the display-spoofing refusal; the chain link (
prev_digest) and, when the log has the column,text_digest(bindingconditionsandevidence) are computed, never taken from the spec; - eligibility and effect classification, then the PR-E grant-purpose refusal;
- preflight: env, integrity, install location, key-file existence. With
allow_checkout, an overridden location refusal is recorded in the signedconditions_verified; - the display, with the review digest;
- the confirm gate: a y/N prompt on a terminal, or an exact
confirm_sha256match; - the key read, then the signature over the same in-memory row;
- verify-before-append, then the append.
sign_grant(spec_path, resolve_dirs, *, confirm_sha256=None, allow_checkout=False)¶
Source:
agentteams/cli/operator_signing.py
Mints and appends one Ed25519-signed capability grant. Returns 0, or 1 on any error. resolve_dirs is a
zero-argument callable returning (ledger_root, team_dir); it runs unpinned adapter code, so it is always
called before the key read. The error precedence is (amended by #9):
- spec;
key_id;- display-spoofing refusal;
- preflight;
- directories and
max_uses; - an install-location re-check against the holder project;
- the display (every field, grant id and timestamp minted once, review digest);
- confirm;
- the key read, then the signature.
sign_decision_team_refusal(output_dir)¶
Source:
agentteams/cli/operator_signing.py
F-2: the refusal sentence when output_dir holds neither references/agent-privilege.json nor
references/build-log.json, else None.
load_grant_spec(path, flag) / grant_spec_kwargs(spec) / report_issued(record, ledger_root, team_dir)¶
Source:
agentteams/cli/operator_signing.py
The grant spec helpers shared with the HMAC --issue-grant runner: read and shape-check a spec
against GRANT_SPEC_REQUIRED, map it to minter keyword arguments (fresh grant_id and
timestamp), and print the post-append summary.
Decision-log migration¶
Source:
agentteams/cli/decision_log_migrate.py
decision_log.append_signed_decision_row refuses a log whose header lacks a signing column, so a team
generated before signing could not record a signed decision. This module upgrades the header and nothing
else. It imports decision_log and never changes it (that module is pinned). Flag semantics:
CLI Reference.
missing_signing_columns(columns) -> list[str]¶
The SIGNED_DECISION_COLUMNS entries a header lacks, in that order, never prev_digest
(EXCLUDED_COLUMNS): chaining is opt-in on that column, and the legacy rows were never chained.
plan_migration(log_path) -> MigrationPlan¶
Works out the migration and writes nothing. The new bytes are the old header line, the appended columns, the
old line ending, and every data line with its bytes kept as a prefix and empty cells appended to rows narrower
than the new header (plan.padded_lines). They are re-read as the gate reads them, must still classify
(_security_decision_schema_kind) with every added cell empty, and must leave every row's signed payload
unchanged. A header-current log with short rows yields a pad-only plan. plan.revoked lists the live unsigned
authorizing rows (unconsumed PASS / CONDITIONAL PASS, and every HALT-RETRACTED) by first line and action,
when the migration adds signature. Raises DecisionLogMigrationError for a missing or symlinked log, a
malformed or unknown header, malformed data rows, a row with a value past the old header (no laundering), or a
row too short to be a legacy row (never padded).
apply_migration(plan, team_dir, *, framework="") -> Path¶
Backs the log up (backup.backup_output_dir, reason migrate-decision-log), refuses if the log changed since
it was planned, then replaces it atomically with the file's own mode. Returns the backup directory.
run_migrate_decision_log(args) -> int¶
The CLI runner (re-exported by commands.py as _run_migrate_decision_log, dispatched by app.py after
--self resolves the team dir). resolve_team_dir(args) resolves the agents dir as the generate pipeline does.
Returns 0 for a migration, a no-op or a dry run, and 1 for a refusal or a real run without --yes.
Decision text binding¶
Source:
agentteams/cli/decision_text.py
Binds a signed decision row's free-text conditions and evidence into its signature. text_digest is the last
entry of decision_log._OPTIONAL_SIGNED_AXES, so it is signed and chained only when non-empty and every older
signature still verifies. The module is integrity-pinned and in the signing closure.
text_digest_for(row) -> str¶
sha256: plus the hex digest of the length-prefixed encoding f"{len(c)}:{c}{len(e)}:{e}" over the stripped texts, with lengths in UTF-8 bytes. A length prefix
keeps the boundary unambiguous: a plain \x1f join let ("A", "B\x1fC") and ("A\x1fB", "C") collide. A text
holding \x1f is refused (RuntimeError). A legacy row has no evidence; it reads as empty.
bind_text_digest(row) -> dict¶
Sets row["text_digest"] from text_digest_for(row) and returns the row. Bind, then sign: every minting path
calls it before computing the signature. decision_log.sign_decision_row does so itself when the row has a
text_digest key; the gate verifies with the non-binding _hmac_decision_row.
assert_text_bound(row, *, action)¶
Raises RuntimeError ("failed signature verification") when the row's non-empty text_digest no longer matches
its text. decision_log calls it right after a row's HMAC or Ed25519 signature verifies, so an edited row stops
clearing. A row without text_digest passes. text_is_bound(row) and text_digest_mismatch(row) are the
underlying predicates.
describe_text_binding(team_dir) -> list[str]¶
The --verify-integrity report lines: signed rows that bind their text, those that don't, a missing column, and
any mismatched row. Empty when the log is absent, unreadable or has no signed row. Advisory only.
Related pages¶
- CLI Reference — flags and option semantics
update_report— theupdate.report.mdrecordmemory_index— index construction and path storageoutput_plan— which files a manifest produces