Summary
GET /v1/agents serializes an agent whose stored status is 'active' as "status": "unknown". Every live agent in the workspace appears indeterminate. The filter parameter reads the real column, so the same request filtered and unfiltered disagrees with itself.
Reproduction
Against a live workspace of 864 agents on 2026-08-07:
GET /v1/agents?status=active -> 329 rows, every row serialized as "status": "unknown"
GET /v1/agents?status=offline -> 536 rows, serialized as "offline"
GET /v1/agents?status=online -> 0 rows
GET /v1/agents?status=unknown -> 0 rows
329 + 536 = 865, the full workspace at the time of the read.
The filter is applied to the stored column before serialization:
// packages/engine/src/engine/agent.ts:109
rows = await db
.select()
.from(agents)
.where(
and(eq(agents.workspaceId, workspaceId), eq(agents.status, status)),
);
So ?status=active matching 329 rows proves the column holds 'active', and ?status=unknown matching zero proves nothing holds 'unknown'. Yet those 329 rows serialize as "unknown".
Two agents confirmed in the ?status=active set were exchanging messages at the moment of the read, so this is not a staleness artifact.
Where it is not
Nothing in the engine writes 'unknown' to agents.status — the only writers are 'active' (registerAgent :59, touchLastSeen :294) and 'offline' (sweepStaleAgents :303). listAgents returns status: a.status unmapped (:129). No mapping was found in packages/sdk-typescript either.
The divergence is therefore introduced somewhere in the deployed build that this checkout does not reflect. Worth confirming which build is deployed as part of triage — the gap between this tree and the running engine is itself useful to know.
One plausible reading, offered as a hypothesis rather than a finding: the response may be reporting presence (derived, "unknown" when no live presence signal is subscribed) using the same field name as the stored registration/staleness status. The event types in packages/sdk-typescript/src/types.ts:561-567 describe a richer presence model (active / idle / waiting / blocked / offline) than the two values the column holds, which would be consistent with two different concepts sharing one name.
Why this is worth more than a cosmetic fix
The stored value is load-bearing for an identity decision. registerAgentViaNode guards its tokenHash overwrite with:
setWhere: or(
ne(agents.status, 'active'),
and(eq(agents.locationType, 'via_node'), or(...)),
),
An engineer reasoning about that guard who checks the field through the API will conclude that no agent is ever 'active', that the first disjunct is always true, and therefore that the guard is unconditional and the identity boundary on the fleet registration path is wide open. That conclusion is false — the column does hold 'active' and the guard works — but it is the conclusion the API hands you, and it was very nearly escalated as a live security incident on 2026-08-07 before the column was probed directly.
A field that reads one way to SQL and another way to every API consumer is a trap for exactly the people trying to verify a security property.
Suggested direction
Either is fine; the current state is not:
- serialize the stored value faithfully, or
- keep presence in the response but give it a different name, and expose the stored value under its own key
If the two-concepts reading is right, the naming is the whole fix.
Related
Context
Filed from an incident investigation on 2026-08-07. Khaliq owns the merge gate — no agent merges.
Summary
GET /v1/agentsserializes an agent whose storedstatusis'active'as"status": "unknown". Every live agent in the workspace appears indeterminate. The filter parameter reads the real column, so the same request filtered and unfiltered disagrees with itself.Reproduction
Against a live workspace of 864 agents on 2026-08-07:
329 + 536 = 865, the full workspace at the time of the read.
The filter is applied to the stored column before serialization:
So
?status=activematching 329 rows proves the column holds'active', and?status=unknownmatching zero proves nothing holds'unknown'. Yet those 329 rows serialize as"unknown".Two agents confirmed in the
?status=activeset were exchanging messages at the moment of the read, so this is not a staleness artifact.Where it is not
Nothing in the engine writes
'unknown'toagents.status— the only writers are'active'(registerAgent:59,touchLastSeen:294) and'offline'(sweepStaleAgents:303).listAgentsreturnsstatus: a.statusunmapped (:129). No mapping was found inpackages/sdk-typescripteither.The divergence is therefore introduced somewhere in the deployed build that this checkout does not reflect. Worth confirming which build is deployed as part of triage — the gap between this tree and the running engine is itself useful to know.
One plausible reading, offered as a hypothesis rather than a finding: the response may be reporting presence (derived,
"unknown"when no live presence signal is subscribed) using the same field name as the stored registration/staleness status. The event types inpackages/sdk-typescript/src/types.ts:561-567describe a richer presence model (active / idle / waiting / blocked / offline) than the two values the column holds, which would be consistent with two different concepts sharing one name.Why this is worth more than a cosmetic fix
The stored value is load-bearing for an identity decision.
registerAgentViaNodeguards itstokenHashoverwrite with:An engineer reasoning about that guard who checks the field through the API will conclude that no agent is ever
'active', that the first disjunct is always true, and therefore that the guard is unconditional and the identity boundary on the fleet registration path is wide open. That conclusion is false — the column does hold'active'and the guard works — but it is the conclusion the API hands you, and it was very nearly escalated as a live security incident on 2026-08-07 before the column was probed directly.A field that reads one way to SQL and another way to every API consumer is a trap for exactly the people trying to verify a security property.
Suggested direction
Either is fine; the current state is not:
If the two-concepts reading is right, the naming is the whole fix.
Related
Context
Filed from an incident investigation on 2026-08-07. Khaliq owns the merge gate — no agent merges.