Repository navigation
feat: a payload leaves the spool only when the backend has it (ADR 0075) - #114
Merged
Merged
Conversation
Adds the transmit path. One setting — `backend.baseURL`, no default — and a
`POST {base}/v1/kubernetes/{kind}` per payload, the body the spool file
verbatim. Nothing is read out of a response but its status (ADR 0001).
A file is deleted only after a 2xx, and only if it is still the file that was
sent: a superseding writer replaces it by rename mid-flight, and deleting by
name would drop a payload that was never offered. 429/5xx/network defer and back
off with jitter; 400 and 413 are held, counted and never retried, so the bytes
that prove a schema disagreement stay readable until the spool's age bound
removes them; 401/403 halt shipping for the life of the process.
One goroutine, one request in flight — which is how ADR 0027 §2's "at most one
request per natural key" is met, since the spool holds one file per key. Run
returns only on cancellation, halted or not: a lifecycle task that returns would
stop collection, and collection does not depend on shipping.
`collection_coverage` gains a `shipping` block (delivered, deferred, unreadable,
halted, and rejections by reason) and `backend_configured` in its config shape.
It does not reuse `intake_rejections`, which carries the same three words about
the opposite direction — what this agent's receiver refuses from a node
(ADR 0067).
The 4 MiB size ceiling is a placeholder; the ADR records what it waits for.
go test -race ./... ok
golangci-lint run 0 issues
buf lint && buf breaking --against origin/main ok
make chart-lint 1 chart linted, 0 failed
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeZECJXTDHL6Nts95eKMJA
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeZECJXTDHL6Nts95eKMJA
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the transmit path. A backend exists and receives payloads; this agent had
none at all, so nothing had ever travelled between the two. That gap is the
point of the change — two systems built toward one document diverge in the
places the document does not cover, and only a running agent talking to a
running backend finds them.
What it does
One setting,
backend.baseURL, with no default: absent means ship nothing,which is exactly today's behaviour.
POST {base}/v1/kubernetes/{kind}, onepayload per request, the body the spool file verbatim,
{kind}read out of thepayload's own bytes and checked against the registry (ADR 0022).
Nothing is read out of a response but its status — the body is drained so the
connection can be reused and discarded unparsed. ADR 0001 forbids the backend to
send anything the agent acts on; this is where that stops being a promise about
the backend and becomes a property of the agent.
A file is deleted only after a 2xx, and only if it is still the file that was
sent. A superseding writer replaces a file by rename while its predecessor is
in flight, so deleting by name alone would drop a newer payload that was never
offered, and nothing downstream could notice.
malformed, never offered again.too_large, never offered again.unauthorized. Shipping halts for the life of the process.An unrecognized status is deliberately transient: a base URL with a typo answers
404 to every payload, and treating that as permanent would empty the spool
against a configuration mistake.
The two decisions the ADR was asked to record
A permanently rejected payload is held, not dropped. Both options lose it —
the spool is bounded and nothing re-reads it — so the choice is between losing
it now and losing it in a day. The day buys the bytes: a 400 means the agent and
the backend disagree about a schema, and finding exactly that is what this stage
is for.
backend-requirements.md§5 already permitted either. The mark is keyedby file identity, so the next pass writing under the same name clears it.
The 4 MiB ceiling is a placeholder, chosen to sit under limits proxies
commonly impose rather than to describe anything either side measured. It is not
a chunking threshold and must not become one (ADR 0027 §4). What it waits for is
a stated protocol limit, which belongs in the registry as a property of a kind
beside cadence — ADR 0073 already built the place. Until then the
too_largecounter is what makes the constant's wrongness visible.
Ordering, and ADR 0027 §2
One goroutine, one request in flight. The spool holds one file per natural key,
so a shipper that never has two requests open never has two versions of a key in
flight. That closes the obligation ADR 0027 recorded against a shipper that did
not exist, and
backend-requirements.md§4 has stated since then.Collection does not depend on shipping
Runreturns only on cancellation, halted or not: incmd/agenta lifecycletask that returns cancels the agent, so a backend refusing this agent's identity
would otherwise stop collection. An unreachable, slow or hostile backend changes
nothing about what the agent collects or writes, and the tests hold that line.
Counters
collection_coveragegains ashippingblock —delivered,deferred,unreadable,halted, andrejectedcarrying the three reasons — plusbackend_configuredin its configuration shape (a switch, never the address;tested).
It does not reuse
intake_rejections. That block carries the same threewords about the opposite direction: what this agent's own receiver refused from
a node inside the cluster (ADR 0067, wired through
internal/nodeintakeandalready exposed as
node_reports_rejected_total). Merging them would leaveneither answerable.
/metricsgainsshipments_total{outcome},shipments_rejected_total{reason}and
shipping_halted— the last because a halted agent ships nothing, so thecoverage payload carrying the same fact never arrives. No new label names.
Deliberately not here
No TLS, certificates, enrollment, renewal or CA pinning, and no invented
identity — no cluster id in a payload, no header naming the sender. Identity
is a property of the connection and arrives with mTLS (ADR 0005, ADR 0008); a
field standing in for it now would have to be torn out of the schema, the
handlers and the stored keys. The backend attributes everything to one
configured source and knows it. Nothing is stubbed.
The chart value and
README/security.mdsay plainly that this stage is plainHTTP for a backend the operator controls.
Still open
ADR 0027 §4's other deferral —
container_restartsandpod_disruptionsarebyte-identical whether a window is final or sampled part way through. It was
deferred because there was no ingest to ask; there is one now, and the deadline
was always "before a backend stores these kinds". It is a payload change and a
conversation, not a transport change. Recorded in ADR 0075's Not addressed
here.
Evidence
Run against a clean
git archive HEADextraction, not the working tree:e2e has not been run yet; the chart change is cluster-facing, so it should run
against kind before merge.
🤖 Generated with Claude Code
https://claude.ai/code/session_01KeZECJXTDHL6Nts95eKMJA