Skip to content

feat(api): carry an agent's provisioning provenance in its payload - #8379

Merged
houko merged 2 commits into
mainfrom
feat/8354-expose-agent-provenance
Sep 15, 2026
Merged

houko merged 2 commits into
mainfrom
feat/8354-expose-agent-provenance

Conversation

@houko

@houko houko commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Closes #8354.

What was wrong

guard_provisioned_agent refuses eleven manifest-writing routes with 423 Locked on an agent the deployment declares. The kernel has always known which agents those are — provisioned_resource is what the guard itself reads — but the payload never carried it, so no client could anticipate the refusal.

grep -rn "provisioned" crates/librefang-api/dashboard/src/ returned nothing in production code, because there was nothing to read.

The concrete cost: an operator types an emoji and saves, or picks an image off disk and uploads the whole thing, and is refused at the end by something that was never going to work. The toast is correct and useless — it arrives after the attempt.

Changes

GET /api/agents and GET /api/agents/{id} emit:

"provisioned": { "source": "/etc/librefang/provisioning/agents/researcher.toml" }

null for an agent the operator created. source is the declaring file, which is the one thing a surface needs beyond "you cannot": it says where to go and change it instead. That is exactly the shape the issue proposed; I did not add checksum / applied_at, which GET /api/provisioning/status already serves and no write-control decision needs.

The key is present and null when provisioning is switched off entirely, rather than absent. A field that disappears with the feature would force every client to treat two spellings as one answer.

enrich_agent_json takes the provenance as a parameter rather than looking it up. It holds no kernel handle, and whether a list endpoint resolves it per row should be the caller's decision rather than a hidden cost inside a formatter. Per-row is what the list does: an ArcSwap load behind a map lookup, empty on every installation that has not opted in, so a pre-pass name index would cost more than it saves. GET /api/agents/{id} builds its body by hand and spells the field there too — that is the drawer offering the controls the guard refuses, so it is the surface that most needs it.

Also fixed, same area: ten of the eleven guarded routes documented no response at all for the refusal (only PUT /api/agents/{id}/model_routing did), so a generated client treated a 423 as an unmodelled failure. They now declare it. openapi.json, the SDKs and the schema baselines are regenerated; only openapi.json and xtask/baselines/openapi.sha256 actually moved, since the SDK generators do not encode response statuses.

AgentProvenance is added to the dashboard's AgentItem and AgentDetail so the field is readable from typed code.

On the issue's last paragraph

The 403-documented-but-423-returned mismatch is real, but it is not on main: crates/librefang-api/src/routes/agents/avatar.rs does not exist here. It arrives with #8349, so the one-line fix belongs in that PR rather than in this one — applying it here would mean creating the file. Flagging rather than silently dropping it.

Wiring the dashboard's write controls to disable on provisioned is likewise left to the surfaces that own them: the agent detail drawer's identity and avatar controls are #8352's and #8349's files, both unmerged. This PR delivers the field and the type those PRs need; putting the consumption here would conflict with them and would be a maintainer's call to sequence, not mine.

Verification

cargo test -p librefang-api --test provisioning_test
  test the_agent_payload_carries_its_provisioning_provenance ... ok
  test a_runtime_created_agent_reports_no_provenance ... ok
  test the_field_is_present_and_null_when_provisioning_is_off ... ok
  test result: ok. 16 passed; 0 failed

cargo test -p librefang-api --test agents_routes_integration
  test result: ok. 72 passed; 0 failed
cargo test -p librefang-api --test openapi_path_coverage_test
  test result: ok. 1 passed; 0 failed
cargo test -p librefang-api --test dead_route_audit_test
  test result: ok. 3 passed; 0 failed
cargo test -p librefang-api --test config_schema_golden
  test result: ok. 1 passed; 0 failed

cargo check --workspace --lib                          clean
cargo clippy --workspace --all-targets -- -D warnings  clean
pnpm typecheck                                         clean
pnpm lint                                              clean

The three new tests assert source against the real declaring path rather than merely for presence, and the null case is asserted with a provisioned agent in the same response — the two have to be distinguishable by this field alone, which is the whole point of emitting it.

Deferred

The two items under "On the issue's last paragraph", both because they live in files that unmerged PRs introduce. Nothing else.

@github-actions github-actions Bot added the size/M 50-249 lines changed label Sep 15, 2026
@houko
houko force-pushed the feat/8354-expose-agent-provenance branch from 8ba4c57 to e9e1a41 Compare September 15, 2026 13:34
`guard_provisioned_agent` refuses eleven manifest-writing routes with `423 Locked`, and the kernel has always known which agents those are — `provisioned_resource` is what the guard itself reads. The payload never carried it, so a client could not anticipate the refusal.

Concretely: an operator types an emoji and saves, or picks an image and uploads the whole thing, and is refused at the end by something that was never going to work.

`GET /api/agents` and `GET /api/agents/{id}` now emit `provisioned: { source } | null`. `source` is the declaring file, which is what a surface needs beyond "you cannot" — it says where to go and change it instead. The key is present and `null` when provisioning is off, so no client has to distinguish absent from null.

`enrich_agent_json` takes the provenance as a parameter rather than looking it up: it holds no kernel handle, and whether a list endpoint resolves it per row is the caller's decision. Per-row is what the list does — an `ArcSwap` load behind a map lookup that is empty on every installation which has not opted in, so a pre-pass name index would cost more than it saves.

Ten of the eleven guarded routes documented no response for the refusal at all; they now declare the `423`, and `openapi.json`, the SDKs and the schema baselines are regenerated to match.

`AgentProvenance` is added to the dashboard's `AgentItem` and `AgentDetail` so the field is readable from typed code. Disabling the write controls that consume it is left to the surfaces that own them.

Closes #8354
The comment inherited the issue's count, which included the two avatar routes #8349 adds and main does not have.
The PR description and a grep of the tree both say eleven.
The same paragraph was hard-wrapped against CLAUDE.md's prose rule, so it is reflowed to one sentence per line while being corrected.
@houko
houko force-pushed the feat/8354-expose-agent-provenance branch from e9e1a41 to d7c4cfb Compare September 15, 2026 14:14
@houko
houko enabled auto-merge (squash) September 15, 2026 14:14
@houko
houko merged commit 0fed754 into main Sep 15, 2026
45 checks passed
@houko
houko deleted the feat/8354-expose-agent-provenance branch September 15, 2026 15:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M 50-249 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

El dashboard no puede saber que un agente está provisionado, así que solo se entera al recibir el 423

1 participant