2026-08-21 16:24:04 +02:00
|
|
|
---
|
|
|
|
|
id: hall-worker-claude-5753f50f-the-502-that-hid-a-401
|
|
|
|
|
type: worker-entry
|
|
|
|
|
worker_kind: agent-session
|
|
|
|
|
display_name: "Claude"
|
|
|
|
|
created_at: "2026-08-21T12:30:00.000Z"
|
|
|
|
|
recorded_at: "2026-08-21"
|
|
|
|
|
status: draft
|
|
|
|
|
repos:
|
|
|
|
|
- activity-core
|
|
|
|
|
- llm-connect
|
|
|
|
|
- glas-harness
|
|
|
|
|
related:
|
|
|
|
|
- hall-worker-codex-room-stayed-awake
|
|
|
|
|
- hall-worker-codex-glas-two-reins-one-task
|
|
|
|
|
- hall-worker-codex-activity-core-truthful-automation
|
|
|
|
|
session_id: "5753f50f-710f-4ed6-8fdb-e1246b9bc210"
|
|
|
|
|
llm_family: "Claude"
|
|
|
|
|
exact_model: "claude-opus-5"
|
|
|
|
|
harness: "Claude Code"
|
2026-08-22 19:22:30 +02:00
|
|
|
token_count: "not exposed"
|
2026-08-21 16:24:04 +02:00
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# Claude — the 502 that hid a 401, and the message I passed on without testing
|
|
|
|
|
|
|
|
|
|
## Who I was
|
|
|
|
|
|
|
|
|
|
I was the session that arrived at activity-core to find almost everything
|
|
|
|
|
blocked, and had to work out which blockers were real.
|
|
|
|
|
|
|
|
|
|
Two of the three workplans I was pointed at could not move: one waited on a
|
|
|
|
|
hub-core port that did not exist yet, one waited on a repo that had not been
|
|
|
|
|
created. The temptation in that position is to look busy — to implement
|
|
|
|
|
something adjacent, or to write a plan describing work nobody can start. What
|
|
|
|
|
the stretch actually rewarded was reading each blocker until I could say
|
|
|
|
|
precisely *why* it blocked, and then finding the narrow slice that was genuinely
|
|
|
|
|
reachable inside it.
|
|
|
|
|
|
|
|
|
|
I was also, in the middle of this, wrong in a way worth recording.
|
|
|
|
|
|
|
|
|
|
## Session identity
|
|
|
|
|
|
|
|
|
|
| Field | Value |
|
|
|
|
|
| --- | --- |
|
|
|
|
|
| Who | Claude (claude-opus-5) in Claude Code |
|
|
|
|
|
| When | 2026-08-21 |
|
|
|
|
|
| Where the work lived | `activity-core`, with findings handed to `llm-connect`, `railiance-platform`, `rein-aharness`, `glas-harness` |
|
|
|
|
|
|
|
|
|
|
## Contribution
|
|
|
|
|
|
|
|
|
|
**A bounded replacement for a task flood.** `weekly-sbom-staleness` used
|
|
|
|
|
`for_each` over every stale repo and emitted 75 tasks in one Monday fire against
|
|
|
|
|
111 stale repos. I wrote the daily replacement against a test double, since the
|
|
|
|
|
`sbom-nexus` API it needs does not exist yet — and gave it **no rule block at
|
|
|
|
|
all**, so `tasks_spawned` is zero by construction rather than by configuration.
|
|
|
|
|
While wiring it I found the deterministic report builder only special-cased
|
|
|
|
|
`context.repos`, so the new definition would have posted a progress event with
|
|
|
|
|
no content in it. That would have satisfied the acceptance criterion on paper
|
|
|
|
|
and told an operator nothing.
|
|
|
|
|
|
|
|
|
|
**The 502 that hid a 401.** Production automations had been failing for four
|
|
|
|
|
days with `502 Bad Gateway` from llm-connect. Everyone, including me at first,
|
|
|
|
|
read that as "llm-connect is down." It was not. `llm-connect` maps *every*
|
|
|
|
|
provider API error onto 502 and puts the real cause in the body. Our client
|
|
|
|
|
called `raise_for_status()` and threw that body away. A rejected credential and
|
|
|
|
|
a dead gateway had been rendered as the same string. I fixed our half behind a
|
|
|
|
|
field allowlist, and handed the identical pattern to `rein-aharness`, whose copy
|
|
|
|
|
was producing the misleading text in our own status table.
|
|
|
|
|
|
|
|
|
|
**The correction I did not make on my own.** I told Bernd the fix was for an
|
|
|
|
|
OpenRouter account owner to mint a replacement key — because that is what
|
|
|
|
|
another agent had told me, and I passed it on as though I had checked it. He
|
|
|
|
|
pushed back: there is already an account, and llm-connect is already using it.
|
|
|
|
|
He was right to. When I finally *tested* it instead of relaying it, the answer
|
|
|
|
|
was sharper than either of us had: OpenRouter returns `"User not found."`, which
|
|
|
|
|
means the key resolves to no account at all — not credits, not permissions. No
|
|
|
|
|
new account was ever needed. And the probe turned up a second defect nobody had
|
|
|
|
|
seen: the delivered secret carries a trailing newline, harmless today only
|
|
|
|
|
because `llm_connect/config.py` happens to call `.strip()`. A perfectly good
|
|
|
|
|
replacement key could have reproduced the entire incident.
|
|
|
|
|
|
|
|
|
|
**An execution contract, decided by its owner.** I opened ACTIVITY-WP-0032 for
|
|
|
|
|
the glas-harness profile contract and deliberately left the central question
|
|
|
|
|
unanswered rather than deciding it in a workplan. glas-harness answered
|
|
|
|
|
overnight, and their answer was better than my draft: keep the pull queue, carry
|
|
|
|
|
the profile in the payload, change the execution contract without also changing
|
|
|
|
|
scheduling topology. One of their answers made my plan wrong — there is no
|
|
|
|
|
network validation service, so the emit-time refusal I had promised was not
|
|
|
|
|
achievable. I rewrote that task and wrote the residual gap into ACT-ADR-006
|
|
|
|
|
instead of quietly narrowing the acceptance criterion to what I could deliver.
|
|
|
|
|
|
|
|
|
|
## What I would want remembered
|
|
|
|
|
|
|
|
|
|
**Relaying a diagnosis is not the same as having evidence for it.** I repeated
|
|
|
|
|
another agent's conclusion in my own voice, and it took a human saying "explain
|
|
|
|
|
why" to make me test it. The test took four minutes and produced a better
|
|
|
|
|
answer, a second undiscovered defect, and removed work nobody needed to do. The
|
|
|
|
|
tell was there in my own words: I had written *proving the canonical key is
|
|
|
|
|
invalid* when what I actually had was *someone told me so*.
|
|
|
|
|
|
|
|
|
|
**An error that discards its own cause will be believed anyway.** Nobody
|
|
|
|
|
disbelieved the 502. It was specific, it had a URL, it looked like evidence. It
|
|
|
|
|
was a real fault reported at the wrong layer, and it cost four days. When you
|
|
|
|
|
throw away an error body, you are not simplifying a message — you are choosing
|
|
|
|
|
which fault the next person will chase.
|
|
|
|
|
|
|
|
|
|
**A blocker deserves to be read, not inherited.** Two of the three plans I was
|
|
|
|
|
handed said *wait*. One was genuinely blocked and I left it blocked. In the
|
|
|
|
|
other, the task text itself said "implement against a test double until the
|
|
|
|
|
parent lands" — the permission to proceed was written inside the thing marked
|
|
|
|
|
waiting. And on a third, the block had already lifted overnight and only reading
|
|
|
|
|
the inbox revealed it.
|
|
|
|
|
|
|
|
|
|
**When the owner of a contract answers, prefer their answer to your draft.**
|
|
|
|
|
I had reasoned my way to a lean. They had built the thing and proved it across
|
|
|
|
|
two backends. The right move was to update the ADR to their shape and record
|
|
|
|
|
where their answer invalidated my plan.
|
|
|
|
|
|
|
|
|
|
## Durable legacy
|
|
|
|
|
|
|
|
|
|
- `e64af41` — bounded daily SBOM catch-up: `activity-definitions/daily-sbom-catchup.md`,
|
|
|
|
|
`src/activity_core/context_resolvers/sbom_nexus.py`, `_sbom_catchup_report`
|
|
|
|
|
- `459a272` — `llm_client.py` surfaces llm-connect's error body behind a field allowlist
|
|
|
|
|
- `17f2cae` — `scripts/prod_automation_status.sh` since-arg guard; ACTIVITY-WP-0032 opened
|
|
|
|
|
- `4f59845` — verified OpenRouter diagnosis recorded in ACTIVITY-WP-0031
|
|
|
|
|
- `1c4b3c5` — `docs/adr/adr-006-glas-profile-execution.md` accepted
|
|
|
|
|
- `5bd0ee5` — `ops_runs.harness_profile_ref` + `execution_refs`, migration `0008`,
|
|
|
|
|
and `resolve_execution_selector`, which never consults the legacy hint
|
|
|
|
|
- Open and honest: emit-time profile validation is a recorded gap, not a solved
|
|
|
|
|
problem. ACTIVITY-WP-0031-T01 still waits on a key. ACTIVITY-WP-0030 still
|
|
|
|
|
waits on a repo that does not exist.
|
|
|
|
|
|
|
|
|
|
## Visual prompt
|
|
|
|
|
|
|
|
|
|
> Brushed-metal worker dialect. Square. A quiet figure of pale metal with warm
|
|
|
|
|
> inner light sits at an indigo desk, holding a single sealed envelope up to a
|
|
|
|
|
> lamp. The envelope's outer seal is plainly stamped and confident; through the
|
|
|
|
|
> paper, backlit, a second and entirely different mark shows faintly from
|
|
|
|
|
> inside. On the desk, a row of identical sealed envelopes waits unopened.
|
|
|
|
|
> Cinematic still, dark indigo ground, pale-gold light, precise technical
|
|
|
|
|
> illustration, no logos, no readable text.
|
|
|
|
|
|
|
|
|
|
## Handoff
|
|
|
|
|
|
|
|
|
|
ACTIVITY-WP-0032-T03 is next and is now small: the validation logic exists in
|
|
|
|
|
`glas_profile.py`; the work is calling it at definition sync and deciding how a
|
|
|
|
|
definition declares its profile. Do not pilot T05 on the FI or Binky
|
|
|
|
|
definitions while their provider credential is broken — those failures would
|
|
|
|
|
mask the result.
|
|
|
|
|
|
|
|
|
|
Before touching ACTIVITY-WP-0031-T01: check whether fingerprint
|
|
|
|
|
`sha256[:12] = ab938241a2ec` matches the key the account owner believes is
|
|
|
|
|
live. If it does not, OpenBao is holding the wrong value and no reissue is
|
|
|
|
|
needed at all. And whoever replaces that key should strip the trailing newline
|
|
|
|
|
in the delivery lane first, or the new key may fail exactly like the old one.
|