Skip to content

chore(vehicle): security consolidation train — #17113, #16444, #17088, #17095 in one CI run (#17048) - #17134

Merged
mrveiss merged 143 commits into
mainfrom
vehicle-v090-2026-09-19-security2
Sep 20, 2026
Merged

mrveiss merged 143 commits into
mainfrom
vehicle-v090-2026-09-19-security2

Conversation

@mrveiss

@mrveiss mrveiss commented Sep 19, 2026 •

Copy link
Copy Markdown
Owner

Thinking Path

This follows the owner's consolidation direction of 2026-09-19 ("fix all red, review and merge, check if any are consolidation candidates"). The approved security-class trains land together in one CI run, not one after another. The rest trains ride #17133. #17106 (session ownership, security) is appended to this train once #17133 has landed, because its resolution already contains #17133's content.

What Changed

The branch was cut from main after #17048 and carries these members:

PR Carried head Closes Review
#17113 d69a1d1241 #16957, #16040, #16294, #16999 approve@d69a1d1241
#16444 8b7bd135c6 #16428 approve@8b7bd135c6
#17088 82233a4017 #17087 approve@82233a4017
#17095 172c5a7f45 — (Refs #13034, #16335, #16375, #16709) carried here with #17088's fail-open fix, which supersedes its old #16899 code

Resolution commits, made locally on this branch (the coordinator's own):

Closes #16957
Closes #16040
Closes #16294
Closes #16999
Closes #16428
Closes #17087
Refs #13034
Refs #16335
Refs #16375
Refs #16709

Verification

  • detect-secrets-hook --baseline .secrets.baseline over all 124 files changed against main: exit 0.
  • secrets_baseline_reasons_guard_test, secrets_baseline_legacy_keys_excluded_17130_test and python_file_size_ratchet_test: 40 passed. fastapi_validation_handler_guard_test and main_validation_handler_test: 8 passed.
  • The pre-push hook passed on both pushes, run with the CI-parity interpreter.
  • This PR's CI is the proof for the union. It is held behind chore(vehicle): rest consolidation train — #17107, #17119, #17122, #17117, #17129 in one CI run (#17128) #17133, which has CI priority per the owner.

Model Used

Claude Opus 5 (coordinator) and a Sonnet subagent for the conflict resolution

Closes #17052
Closes #17053
Closes #17057
Closes #17074
Closes #17078
Closes #17079

Carried in after #17133 landed

mrveiss and others added 30 commits September 11, 2026 20:45
…#16335)

The toggle button ("Settings"/"Hide settings"), the settings section
heading, and its aria-label were hardcoded English. Add
transcriber.layout.{settingsButton,hideSettingsButton,settingsHeading}
with real translations in all 11 locales.

The other transcriber views (ProjectsView, ProjectDetailView,
TranscriptView) carry no strings of this hardcoded-settings-label
class, so they are untouched.

The section's isAdmin && settingsOpen gate (this issue's second AC) is
deferred: isAdmin is only introduced by #16259, which is still open,
so adding it here now would duplicate that PR's plumbing and conflict
on merge.
…entialStore (#16428)

Owner decision via #13632: connector credentials come from one store,
ConnectorCredentialStore. Full CRUD bridge on /api/secrets when a
request names connector_id+auth_type+credentials, transparently
alongside the existing legacy-file path -- create/get/rotate/revoke,
no second copy ever written to the page's own store. Proves both auth
shapes AC3 names: BearerAuth (single-field) and OAuthRefreshAuth
(four-field bundle). See changelog for full detail.
The model_validator's `not self.credentials` treated credentials={}
(a caller-supplied dict that just doesn't satisfy auth_type's schema
yet) the same as credentials never being sent at all, raising the
wrong error before validate_config_against_schema ever got to name
the actually-missing field. Checks `is None` instead -- presence is
this validator's job; completeness is validate_config_against_schema's,
inside _create_connector_bridged_secret.

Caught by the pre-push hook actually running the new tests.
…dged secret (#16428)

_get_connector_bridged_secret is shared by both GET's dual-read and
PUT's rotate response, but the two endpoints disagree on whether
"value" belongs in the shape at all: GET's own contract puts a value
key on every result, real for a legacy secret or None for a bridged
one; PUT's response has never carried a value key for any secret kind
(SecretModel, the legacy shape, has no such field). The shared helper
now returns neither key, and _get_secret_dual_read (GET's own caller)
adds value=None itself for a bridged hit.

Caught by the pre-push hook running the new tests -- the previous
commit's fix for the schema-validator bug uncovered this one.
Auto-regenerated (py3.14) to match the backend and/or SLM backend
schema so the required verify-generated-types gate(s) pass.
Triggered by auto-fix-generated-types.yml.
…6428)

Review on #16444: _get_connector_bridged_secret read straight through
SecretsService.get_secret with no per-owner check, so another owner's
metadata was readable via GET/PUT/DELETE on /api/secrets/{id} even
though the same secret is already owner-scoped everywhere
ConnectorCredentialStore is reached directly (load/rotate/revoke, via
its _require_owner). Now calls that same check rather than a second
copy of it, so the boundary can't drift between the two paths.

The two existing rotate/revoke tests had bridged-secret mocks with no
created_by at all; _require_owner treats a missing owner as a denial,
not a skip, so both needed created_by added to keep asserting the
same-owner success case. Added a dedicated test class proving the
owner check directly: same owner succeeds, a different owner is
refused.

secrets.py's grandfathered line-count ceiling moves 1157 -> 1158 for
this one added line; the file-size ratchet is per-file and this PR
already carries a bump for the same feature.

Not touched here, per review: #16579 (SecretsService.get_secret's
underlying SELECT omits created_by entirely, so this same check
currently denies even the real owner by default) is a pre-existing
main bug, filed separately, out of this PR's scope.
…et (#16428)

37's delta review on eec2a5d: the owner-check fix's one net new line pushed
autobot-backend/api/secrets.py from 1157 to 1158, raising both ratchet
copies for it. repo_tests/python_file_size_ratchet_test.py:17 forbids that --
a coordinated raise of both copies would otherwise pass CI unnoticed (#16393),
so it's enforced in review instead.

Folded a 3-line comment in _get_connector_bridged_secret into 2 lines, no
content lost. File is back at 1157; both ratchet entries hold their prior
value rather than following it up.
…6709)

_share_single_fact (knowledge/facts.py) hand-rolled a second sharing index,
user:shared_facts:{user_id}, via a direct hset that set_owner and
check_access never read, and it never set visibility=SHARED. Verified
consequence (not a disclosure -- see below): a fact "shared" this way was
refused to its own recipient by every check_access-gated read, since
check_access grants a shared fact only when visibility is SHARED and the
caller is in shared_with.

Fixed by routing _share_single_fact/get_shared_facts through the already-
correct KnowledgeOwnership.share_fact()/get_shared_facts() (knowledge/
ownership.py) -- the same canonical user:kb:shared:{user_id} index
set_owner's own indexes are built from and check_access agrees with.
Neither ownership_index.py's index_ownership nor facts.py's
_project_fact_to_redis were touched (37's #16693/#16705 territory).

Tested in both directions (facts_shared_facts_16709_test.py, real
KnowledgeOwnership.check_access, not mocked): share_facts flips visibility
to SHARED and check_access grants the recipient; get_shared_facts lists
exactly what check_access would grant, nothing a stranger couldn't see.

Migration: scripts/migrate_shared_facts_index.py carries over every
existing user:shared_facts:* entry to user:kb:shared:*, promoting
visibility the same way share_fact() would for a fresh share, then retires
the legacy key. Idempotent (a second run finds nothing left to migrate);
--dry-run reports without writing. Tested for no data loss and idempotency.

Disclosure question (unverified in the issue): confirmed NOT a disclosure.
facts.py's own get_shared_facts (the only reader of the buggy
user:shared_facts:* index) has zero callers anywhere in the tree --
api/chat_sessions.py:1816 is the only caller of share_facts (the write
side). The two live "shared facts" read routes (api/knowledge_ownership.py
get_my_facts:313, get_shared_facts:369) both require
Depends(check_admin_permission) and already call the correct
KnowledgeOwnership.get_shared_facts, which the buggy write path never
populated -- so a fact shared through the old bypass was neither
discoverable through those routes nor grantable through any check_access
gate. Grepped the whole tree for any other shared_with membership check
outside knowledge/ownership.py and knowledge/facts.py: none found.

File-size ceiling: facts.py net shrank with this fix (1799 -> 1795,
one-line comprehension in get_shared_facts); lowered to match. Will
recompute fresh against main's then-current value when rebasing onto
#16705, per the ratchet's own "against merged main" invariant.

NOT pushed yet: 37's #16705 (branch issue-16662-kb-helpers-followup) is
mid-flight on the same file's _project_fact_to_redis / ownership_index.py's
index_ownership; holding until it lands per coordinator direction, then
rebasing before push.
…not a tainted parameter (#16444)

CodeQL failed this PR with 3 high py/clear-text-logging-sensitive-data
alerts. Two of them are pre-existing on main, unchanged by this branch:
services/json_secrets_read.py:85 is identical there, and api/secrets.py
:553 is main's :550 shifted by this PR's own additions. main currently
carries 31 open alerts of that rule. Neither is this PR's to answer.

The third is genuinely new and belongs to this branch even though it
does not edit the file it lands in: adding
`_mirror_llm_provider_key(request.name, request.value, _user)` created a
new interprocedural path into provider_key_vault.py:239, where the
failure branch logged `name`. main has no alert in that file at all.

It is a false positive -- `name` reaches that log only by passing the
VAULT_RESOLVED_CREDENTIAL_NAMES membership guard, so it is a label like
OPENAI_API_KEY and never a credential -- but api/secrets.py unpacks
`name` and `value` from one request model and the analysis taints every
attribute of it alike. A comment saying "this is fine" neither breaks a
taint path nor survives a refactor, so the log re-reads the label out of
the frozenset and the property holds by construction.

Tests pin the property rather than the mechanism: a failed mirror still
logs the key name (an operator needs to know which key failed) and never
the value, and an unrecognised name never reaches the log because the
guard returns first. A registry-non-empty check goes first so none of
them can pass vacuously.

Refs #16444, #16428
Three of the 32 routers in `KNOWN_UNGATED` are reachable anonymously today.
`app_factory.py` mounts config-registered routers without `dependencies=`, and
`AuthenticationMiddleware` is never installed as middleware (#16368), so nothing
else was standing behind them.

Each now carries `APIRouter(dependencies=[Depends(get_current_user)])`, the
convention api/skills.py established under #16368: the gate is router-level so
it covers a route added later, rather than per-route where the next route is
unprotected by default.

The per-router judgement, since "a gate" is not one decision:

  captcha      — authenticated user, NOT admin. Resolving or skipping a CAPTCHA
                 acts on a workflow the caller is operating; the operator who
                 answers one is not necessarily an administrator, and requiring
                 admin would gate the routine case on the rare role.
  diagnostics  — authenticated user. Both routes analyse a failure and change
                 nothing; the POST is a POST because it carries an error
                 payload, not because it mutates.
  metrics      — authenticated user, plus check_admin_permission per route on
                 `/system/monitoring/start` and `/system/monitoring/stop`. Those
                 two switch a system-wide collector on and off for everyone,
                 which is administrative rather than a read. The other nine
                 routes are reads and take the router-level gate only.

`KNOWN_UNGATED` drops from 31 to 28 in the same commit. It has to: the list only
shrinks, and `test_the_known_ungated_list_has_not_gone_stale` fails on an entry
that has since acquired a gate — so leaving them listed would be red either way.

This is a slice of #16375, not its close. Twenty-eight routers remain, and each
needs the same per-router reading rather than a blanket gate; several in the
"mutating, not documented public" tier carry subprocess and LLM call sites that
need an author's judgement about which permission is right, not mine.

Refs #16375
…tegrity (#13034)

No model weights downloaded by AutoBot were pinned to a revision or verified
for integrity -- every from_pretrained(name) call resolved against the
mutable upstream default branch, and 18 `# nosec B615` suppressions asserted
"revision pinning managed operationally" with no registry, lockfile, or bump
procedure actually implementing that.

New autobot_shared/pinned_model_registry.py: repo_id -> (exact commit SHA,
expected weight-file sha256). get_pinned_revision() raises KeyError for an
unregistered model, so a new call site cannot silently load unpinned by
omission. verify_cached_model() locates the downloaded file in the local
HuggingFace cache after from_pretrained() and fails closed
(ModelIntegrityError) on a digest mismatch, or if none of the registered
files were found in the cache at all -- "nothing to verify" is a failure,
not a silent pass.

Every value in the registry was obtained against the live HuggingFace API
(exact commit sha via the models API, weight-file sha256 via the raw
git-lfs pointer -- a few hundred bytes, not the real multi-GB file), never
guessed or reconstructed -- see the module docstring and
docs/developer/MODEL_REVISION_PINNING.md for the exact procedure. A
fabricated hash here would be worse than no pin: it would either never
verify (permanently fail closed) or, if wrong in a way that still let
something through, provide false assurance.

Wired into the 5 fixed-model call sites this covers: ai_hardware_accelerator.py
(CLIP, Wav2Vec2), code_embedding_generator.py (CodeBERT),
multimodal_processor/processors/vision.py (CLIP, BLIP-2),
multimodal_processor/processors/voice.py (Whisper, Wav2Vec2) -- 15 of the 18
nosec B615 suppressions removed, one per now-pinned from_pretrained call.

Not closing on this PR: 3 suppressions remain in
llm_shared/optimization/layer_inference.py and model_inspector.py, which
load an arbitrary, caller-supplied model_name at runtime (traced to
request.model_name in llm_shared/optimization/integration.py, and to test
fixtures using models like mistralai/Mixtral-8x7B-Instruct-v0.1,
meta-llama/Llama-3-8B). A static registry entry doesn't fit an open-ended,
request-driven model set the way it fits the five fixed call sites this PR
covers -- that needs its own design (e.g. trust-on-first-use pinning, or
requiring the caller to supply a revision), documented as remaining scope
rather than solved here.

Tests: unit tests for the registry itself (real HF-cache-shaped directories,
not mocked path resolution) plus per-call-site tests proving revision= is
actually threaded through and verify_cached_model is actually called --
transformers/librosa aren't installed in this dev environment, so three of
the four call-site test files inject a fake transformers module into
sys.modules (the same technique llc/tests/test_replay.py already uses for
llm_shared.credential_redaction) rather than skip coverage. 57 passed across
the new and updated test files.

ai_hardware_accelerator.py is ratchet-frozen; reflowed pre-existing comments
and removed several genuinely redundant "explains what the next line does"
comments to land the file 1 line under its previous 1042-line ceiling
(lowered to 1041 in both registries, matching the ratchet's own rule: a
file that lands below its ceiling must have the ceiling lowered to match,
not left stale).
#16375)

`autofix-types` failed with:

  [dump-openapi] FAILED to build app: name 'get_current_user' is not defined

In `captcha.py` the import landed BELOW the `router = APIRouter(...,
dependencies=[Depends(get_current_user)])` line, so the name was undefined when
the module body executed. `metrics.py` and `diagnostics.py` were fine — their
router construction sits further down the file.

The edit script that introduced this asserted its anchor was unique and never
asserted ORDER, which is exactly the gap: both lines were present, in the wrong
sequence, and a uniqueness check cannot see that. The verification now compares
the two line numbers rather than checking both strings exist.

Worth noting what did NOT catch it: the module still imported cleanly in
isolation (`ast.parse` passes, and the router registry logged
"✅ Optional router loaded: captcha" is absent precisely because it failed), and
`code-quality` stayed green. Only building the real app surfaced it — a
module-level NameError behind a router that the loader treats as optional.
…eline (#13034)

De-duplicated the docstring's example curl commands in favor of a pointer to
docs/developer/MODEL_REVISION_PINNING.md's Bump procedure section (the SSOT
scanner flags any literal https://huggingface.co/... URL outside docs/, and
this text was a verbatim copy of the doc anyway). Reformatted the 3 new test
files black flagged. Audited and labelled the 12 new Hex High Entropy String
findings in pinned_model_registry.py/_test.py (real commit SHAs and weight
sha256 digests, not secrets) into .secrets.baseline, scoped to only the two
files touched per docs/developer/RATCHET_BASELINES.md's scan/audit/strip
procedure.
… import (#16375)

code-quality failed on black alone: the router-level auth import added for
#16375 sat directly above `router = APIRouter(...)` with no blank line.
One blank line; no behaviour change. No local hook runs black (#16923),
so this surfaced first in CI.
…ot a raw id (#16428)

CodeQL failed this PR with 2 high py/clear-text-logging-sensitive-data
alerts, both confirmed pre-existing on main (open alerts #1049, #1046) and
unrelated to this PR's own diff -- api/secrets.py's audit_log() and
json_secrets_read.py's exception-path log both log secret_id, a DB row id,
not a credential, but CodeQL's taint tracker flags it on the parameter name
alone. main already carried 5 open alerts of this rule in these two files;
a prior commit on this branch (767d29c) diagnosed this precisely for a
third, PR-introduced alert but correctly left these two as "not this PR's
to answer" since they predate it. They still block this PR's CI, so fixed
here rather than leaving it stuck on a main-owned gap.

audit_log()'s existing `secret_id[:8] + "..."` truncation still carried real
id bytes -- not a sanitizer CodeQL recognizes. Both sites now reuse
security.secrets_store_reader.secret_log_ref(), the canonical helper
already used at 3 other call sites in the same file for exactly this
(a stable, non-reversible SHA-256 correlator) -- no new hashing
implementation, and net negative line growth on secrets.py rather than
raising its ratchet ceiling (lowered 1157->1156 instead, matching the
existing "ratchet only turns down" discipline already applied once to
this same file in this branch's own history, 752af96).

Tests pin the property (raw secret_id absent from the log, a stable
correlator present so operators can still tell two log lines share one id),
not the mechanism, matching the established pattern from 767d29c's
provider_key_vault_test.py.

Refs #16444, #16428
…3034)

Secret Detection (whole tree) failed with 2 new findings unrelated to this
PR's own diff: autobot-frontend/src/i18n/locales/en.json:9086 and
ur.json:9086, both the "authApiKey": "API key" (and its Urdu translation)
label string -- CodeQL^Wdetect-secrets' Secret Keyword plugin matches the key
name "authApiKey" combined with a quoted value, same false-positive class as
every other already-audited entry in these two locale files. Confirmed
pre-existing and unrelated to this branch: both files are byte-identical to
origin/main, and this PR touches neither. Reproduces only on a full-tree
scan, not a single-file one -- a detect-secrets quirk, not investigated
further since the finding itself is unambiguous by inspection.

Refs #13034
…ry fails (#16375)

KNOWN_UNGATED said "the list may only SHRINK" in a comment. The equality
tests stop an ungated router that is not listed, but not one added to the
list alongside it. Two assertions now back the comment:

- every entry must be in _EVER_UNGATED, the 36 modules of #16370's first
  run, so a new module cannot be parked in the list;
- len(KNOWN_UNGATED) must equal _MAX_KNOWN_UNGATED (27), so growth fails
  and each removal must lower the ceiling as a deliberate edit. That also
  catches an entry removed once and later put back, which the superset
  cannot.
#16950)

BaseAgent._handle_communication_request built its AgentRequest from the
payload and never read the header, so a peer's request was
indistinguishable from the agent's own -- the point where authorization
needs the originator is where it was dropped. And MessageHeader.sender is
restamped on every send, correctly, so once one relay happened the
originator was unrecoverable.

- MessageHeader gains originator (set once, never overwritten by a relay)
  and chain (every hop, originator first).
- protocols/message_origin.py: stamp() records each hop; acting_for() and
  a context variable make relays structural -- a message sent while
  handling a request continues that request's chain, even through helpers
  that never saw the inbound message. origin_of() reads an inbound header,
  treating a pre-field header's sender as its originator, never as nobody.
- AgentRequest gains originator and chain; the handler fills them and runs
  process_request inside acting_for.

Nothing authenticates these fields: until #16962, the bus's trust boundary
is Redis write access (#16946, owner decision 4). The if-main demo moves to
protocols/agent_communication_demo.py to pay for the lines in a file at
its size ceiling (805 -> 750).
…so it cannot do what the parent is held from (#16950)

_handle_delegate_tool passed the child only parent_agent_id (for a log
line) and auth_role; the child was built from its own profile alone, so
the parent's approval gates and work item were dropped. A parent held
from write_file by its work item's "writing files" gate could delegate
the write, and the child ran it unapproved.

security/authority.py is the common form ruled on #16950 (F1): the four
authority surfaces side by side, each with its own meet -- restrictions
(approval gates, forbidden tools) by union, grants (RBAC permissions, A2A
capabilities) by intersection, and a surface a hop does not use is top,
never bottom. chat_workflow/run_authority.py reads a run's authority and
what a child inherits from it.

- internal engine: the child ctx carries the parent's work item, the union
  of both runs' gates, and the parent's authority, which the forbidden-work
  seam now unions into the child's boundary;
- claude_code engine: it runs its own tool loop and cannot ask for
  approval, so the parent's gated tools and boundary are refused there
  (fail closed). Its only other reach, AutoBot's MCP server, exposes reads.

Removes #16958's strict-xfail marker in the same commit as the fix.
…an, re-anchor THREAT_MODEL, collect the validation-handler tests (#17134)

Three of #17134's red checks, none of which was a production defect.

shard 6, the five 403s: #17052 gated approve-command behind
require_interactive_human, and the shared backend test stub's principal is
auth_method="stub", which is deliberately not an interactive human. The five
legacy tests in api/security_api_test.py and security/security_integration_test.py
are byte-identical to main and never supplied one. They now override
get_current_user with a session-authenticated principal, the same way this PR's
own test_agent_terminal_ownership_17052.py already does. The stub itself is NOT
widened: that would satisfy the guard by default in every backend test and the
guard would only ever be exercised by a test that opted out -- fail-open.

shard 6, the two anchor mismatches: THREAT_MODEL.md is unchanged; its targets
moved under it. _require_owner is at credential_store.py:626, not :604, and
decode_token_async at services/auth.py:123, not :121. Both re-measured.

shard 4, collection coverage: the three validation-handler tests added by
#16444/#16428 sit in trees that are in no pytest testpaths entry and in no CI
invocation, so they were counted as excused rather than run -- the runtime half
of a security fix executing nowhere. Moved into repo_tests/ and pointed at their
servers through repo_root(), the pattern infra_script_imports_resolve_test.py
already documents for exactly this case. The recorded ceilings stay at 51/14;
raising them is forbidden by the guard's own comment, and nothing here needed it.

Refs #17134
…lking the import graph (#17134)

defusedxml alone was not enough: the next run failed on jsonschema, reached via
middleware/__init__ -> plugin_sdk/__init__ -> loader.py, an eager package chain
entirely separate from the connectors one. Appending one package per red run is
a 6-minute CI cycle per discovery with no bound on how many remain.

So the graph was walked statically from api/secrets.py instead: module-level
imports only (an import inside a function does not run on import), following
relative imports, and importing every ancestor package __init__ the way Python
does. The first two passes of that walk were wrong in instructive ways -- one
followed imports nested in functions and reported 25 false gaps, the next
skipped relative imports and missed jsonschema, the exact failure it was meant
to find. With both fixed it reproduces jsonschema and finds prometheus_client
(via auth_middleware -> security_layer) plus starlette, which pip already
installs with fastapi.

That is a bounded answer rather than another guess, but the honest scope is
'no further UNGUARDED module-level gap on this path', not 'this job is now
correct in general'. #17138 remains the fix that makes the list unnecessary.

Refs #17134, #17138
…t on api.secrets (#17134)

With the import chain finally resolving, the #17099 test got far enough to fail
on its own mock: AttributeError, api.secrets has no attribute 'get_coordinator'.

It does not. _create_system_vault_secret imports it inside the function body
(api/secrets.py:681), so the name never exists on api.secrets at module level;
the deferred import resolves it from api.envelope_secrets at call time, where
get_coordinator is defined (api/envelope_secrets.py:49). Patching that module
is what the mock has to target.

This test has never passed in CI -- each earlier red stopped at the import, so
its body ran for the first time on the previous run. Verified locally only as
far as the patch target existing: the test skips without the migration_gate
marker's Postgres, so CI is the proof point for it passing.

Refs #17134

@mrveiss mrveiss left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 2afd939, --detach, challenging the judgement calls specifically (not just reading green CI). Verdict: APPROVE.

1. base_agent.py / agent_communication.py merge resolutions — correct, not just plausible.

  • execute_with_tracking(agent_request) (replacing the bare process_request call under acting_for(origin)): read _tracked_process directly — it calls self.process_request(request) internally (base_agent.py:263), so this is a strict superset (adds analytics + scope/work-claim gating around the same call), never a loss. And it's the safer default now that #16986 makes peer delivery real: an incoming peer request now gets the same scope/work-claim protection every other caller gets, rather than bypassing it.
  • run_or_schedule import + __main__ block restoration: confirmed exactly one import, one __main__ block, one call site — all inside if __name__ == "__main__":, which never executes on import. Nothing on the train could have "depended on" its absence; it's dead weight either way for every real code path.

2. security_api_test.py / security_integration_test.py — direction confirmed correct.
Traced is_interactive_human() directly (autobot_shared/auth/interactive_principal.py): _INTERACTIVE_AUTH_METHODS = {"jwt", "jwt_websocket", "session"} does NOT include "stub", and _TOKEN_AUTH_METHODS = {"jwt", "jwt_websocket"} excludes "session" from the login_token requirement, so {"username": "root", "role": "admin", "auth_method": "session"} genuinely passes the real gate. Widening the shared stub would have been a fail-open shortcut for every other backend test using it; the per-test override is the right call.

3. test_secrets_coordinator.py patch-target fix — verified directly. api/secrets.py has zero module-level get_coordinator (only a function-local from api.envelope_secrets import get_coordinator inside _create_system_vault_secret, at line 681). patch("api.secrets.get_coordinator", ...) would raise AttributeError; patching api.envelope_secrets.get_coordinator is exactly right for a deferred import resolved at call time.

4. migration-gate.yml's 3 new packages — traced both full import chains end to end, independently. defusedxml: direct (nextcloud.py:32). jsonschema: api/secrets.py → middleware.proxy_utils → triggers middleware/manager.py → autobot_shared.plugin_sdk.registry → triggers plugin_sdk/__init__.py (package init always runs first) → .loader → import jsonschema. prometheus_client: api/secrets.py → auth_middleware → security_layer → autobot_shared.monitoring.metrics.audit → import prometheus_client. Both chains are real; the comment's wording is a bit loose (skips the autobot_shared vs local plugin_sdk distinction) but the substance checks out.

5. Ratchet re-pins — spot-checked the 3 named entries exactly: manager.py=4067, tool_handler.py=3721, agent_communication.py=711, all matching wc -l and both baseline mirrors precisely. Lighter touch on the 3 relocated tests themselves (didn't re-derive their own assertions) given the volume here, but nothing about the relocation looked incomplete.

Bonus, unprompted: independently verified repo_tests/preflight_runs_synced_source_17137_test.py's property is the right generalization — checked all 3 playbook call sites reference the script via {{ git_repo_root }} (freshly-synced tree) while the venv interpreter path and --repo-root argument are correctly excluded from the "executable line" check. Matches the live incident exactly.

Nothing blocking.

@mrveiss
mrveiss merged commit 0f5b45c into main Sep 20, 2026
85 checks passed
@mrveiss
mrveiss deleted the vehicle-v090-2026-09-19-security2 branch September 20, 2026 06:02
@mrveiss mrveiss removed the land-next Landing set: CI capacity goes to these PRs first (owner direction 2026-09-18, #15397) label Sep 20, 2026
This was referenced Sep 20, 2026
mrveiss added a commit that referenced this pull request Sep 20, 2026
…idden line citations (#17147)

Shards 6 and 7 failed on one root cause wearing two shapes: line-number
references that the consolidated tree moved out from under.

Shard 6 -- #17145's lazy-import refactor shifted the symbols THREAT_MODEL.md
anchors at. import_module 541 -> 550, spec_from_file_location 610 -> 619,
ConnectorCredentialStore 178 -> 195, _require_owner 626 -> 643. Each new line
read from the guard's own report of where the symbol actually is, or measured
directly; none estimated. _require_owner had already been re-pointed once
tonight, 604 -> 626 on #17134, and has moved again.

Shard 7 -- #17140's new comments cited AUTOBOT_REFERENCE.md:64, which
repo_tests/comment_line_number_citations_test.py forbids outright: a line
reference goes wrong an order of magnitude more often than a symbol one
(#15877). Now cites the document and the claim, which is what a reader needs
to find anyway.

Neither failure was visible on the individual PRs. #17145 moved the code and
#17140 wrote the citations, and only the merged tree contains both -- the same
class of cross-PR interaction that tripped the reach floor on this vehicle.

Refs #17147
mrveiss added a commit that referenced this pull request Sep 21, 2026
#17116)

347 behind. Blob comparison against origin/main shows 13 of the 21 files this
branch touched are already byte-identical on main -- #16249 and #16394 landed
via #17149, and the CI action/workflow payload with them. Three conflicts, all
resolved toward main so no later fix is reversed:

- services/role_registry.py: branch only reworded a comment and re-wrapped a
  string to hold the 715-line ceiling; main is already 715. Took main.
- repo_tests/secrets_baseline_reasons.py: main extracted the reason table into
  secrets_baseline_reason_entries.py (#17134); the branch still carried it
  inline. Taking the branch here would have reverted that refactor. Took main
  and PORTED the branch's one real change into the new module -- a reason that
  cited 'line 242' for _BASIC_AUTH_URL_RE, which now sits at line 262.
- repo_tests/python_filter_covers_tested_shell_wrappers_test.py: add/add, and
  the two sides differ only in black's line-wrapping. Took main.

Net residue actually carried by this PR: the dependabot uv bump (anyio
4.12.1->4.14.2, httpcore2/httpx2 2.10.0->2.12.0), pypdf 6.18.1->6.19.0 in two
manifests, and the ported reason reword. All upgrades; no downgrade.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment