---
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"
token_count: "not exposed by the harness"
---

# 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.
