You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Services refuse to boot without their required config, and cluster-agent routes split one per file (#2082)
Every service now fails fast at startup when a required environment
variable or database parameter is missing, instead of booting
half-configured. `requiredEnv` and new enforce helpers in `libs/shared`
do the checking, and `pg-pool` refuses a connection with missing
parameters. Request body limits and the event drain timeout now come
from shared constants, so the Floor, lore-api, the event-router and
cluster-agent can no longer disagree about them.
cluster-agent's 235-line `routes/cluster.ts` is split into one file per
endpoint under `routes/cluster/` (agents, jobs, pods, pod listing,
per-task tokens, restart), with the shared dependencies in
`cluster-route-deps.ts`. The heartbeat loop and registrant were tidied
along the way.
The branch also adds the `collaborative-feature-planning` spec.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: adrs/ADR-024-ubiquitous-language-execution-model.md
+7-7Lines changed: 7 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -114,10 +114,10 @@ and lose the update; no `resourceVersion` ever crosses the wire.
114
114
describes the read surface, which callers do still reach over HTTP.)*
115
115
([validated by reports created:false for code 409, so a redelivered claim is idempotent](apps/cluster-agent/src/outbound/kube-agent-api.test.ts#L26), [`kube-agent-api.test.ts:19`](apps/cluster-agent/src/outbound/kube-agent-api.test.ts#L19), [`kube-agent-api.test.ts:33`](apps/cluster-agent/src/outbound/kube-agent-api.test.ts#L33), [`kube-agent-api.test.ts:39`](apps/cluster-agent/src/outbound/kube-agent-api.test.ts#L39), [`kube-agent-api.test.ts:47`](apps/cluster-agent/src/outbound/kube-agent-api.test.ts#L47))
116
116
- A missing CR is an ordinary answer — `found:false` at 200, not a 404 that
117
-
would be indistinguishable from the route itself being absent. ([validated by answers 200 with found:false for a missing CR, not 404](apps/cluster-agent/src/transport/routes/cluster.test.ts#L73), [`cluster.test.ts:131`](apps/cluster-agent/src/transport/routes/cluster.test.ts#L131))
117
+
would be indistinguishable from the route itself being absent. ([validated by answers 200 with found:false for a missing CR, not 404](apps/cluster-agent/src/transport/routes/cluster/cluster-routes.test.ts#L73), [`cluster-routes.test.ts:131`](apps/cluster-agent/src/transport/routes/cluster/cluster-routes.test.ts#L131))
118
118
- The list serves ONE apiserver page per call and the caller drives `continue`.
119
119
A one-shot list is not a convenience: 180 accumulated CRs at ~1.4MB of status
120
-
each blew Node's heap and crash-looped the Floor on 2026-07-24. ([validated by passes the caller's continue token straight through, one page per call](apps/cluster-agent/src/transport/routes/cluster.test.ts#L84), [`cluster.test.ts:97`](apps/cluster-agent/src/transport/routes/cluster.test.ts#L97), [`cluster.test.ts:228`](apps/cluster-agent/src/transport/routes/cluster.test.ts#L228), [`cluster.test.ts:218`](apps/cluster-agent/src/transport/routes/cluster.test.ts#L218))
120
+
each blew Node's heap and crash-looped the Floor on 2026-07-24. ([validated by passes the caller's continue token straight through, one page per call](apps/cluster-agent/src/transport/routes/cluster/cluster-routes.test.ts#L84), [`cluster-routes.test.ts:97`](apps/cluster-agent/src/transport/routes/cluster/cluster-routes.test.ts#L97), [`cluster-routes.test.ts:228`](apps/cluster-agent/src/transport/routes/cluster/cluster-routes.test.ts#L228), [`cluster-routes.test.ts:218`](apps/cluster-agent/src/transport/routes/cluster/cluster-routes.test.ts#L218))
121
121
- The paging the route requires is walked by the CLIENT, not pushed onto every
122
122
caller: `listByLabel` follows `continue` to the end and returns the whole
123
123
match. A truncated list is worse than a failed one — it answers, and the
@@ -148,7 +148,7 @@ and lose the update; no `resourceVersion` ever crosses the wire.
148
148
-`DELETE /api/cluster/per-task-tokens/{taskId}` reclaims a terminal task's
149
149
Secret key and catalog clones — the one per-task-token operation that stays
150
150
a route, since a settled task's cleanup runs from the Floor, not the cluster
151
-
that provisioned. ([validated by reclaims a task's token and catalog clones](apps/cluster-agent/src/transport/routes/cluster.test.ts#L207))
151
+
that provisioned. ([validated by reclaims a task's token and catalog clones](apps/cluster-agent/src/transport/routes/cluster/cluster-routes.test.ts#L207))
152
152
- One call also means one OUTCOME: a provision whose recipe pair fails to land
153
153
takes back everything it had already provisioned — the Secret key AND any
154
154
catalog object that landed before the failure — before it throws. `cleanup`
@@ -165,9 +165,9 @@ and lose the update; no `resourceVersion` ever crosses the wire.
165
165
from being amputated by a merge split across the network.
166
166
([validated by writes the station before the agent definition that points at it](apps/cluster-agent/src/outbound/paired-writes.test.ts#L27), [`paired-writes.test.ts:35`](apps/cluster-agent/src/outbound/paired-writes.test.ts#L35), [`paired-writes.test.ts:54`](apps/cluster-agent/src/outbound/paired-writes.test.ts#L54))
167
167
- The log tail is clamped by the AGENT, because the Floor's clamp no longer
168
-
protects this process's heap. ([validated by clamps the tail server-side rather than trusting the caller](apps/cluster-agent/src/transport/routes/cluster.test.ts#L121), [`cluster.test.ts:187`](apps/cluster-agent/src/transport/routes/cluster.test.ts#L187), [`cluster.test.ts:197`](apps/cluster-agent/src/transport/routes/cluster.test.ts#L197), [`cluster.test.ts:177`](apps/cluster-agent/src/transport/routes/cluster.test.ts#L177))
168
+
protects this process's heap. ([validated by clamps the tail server-side rather than trusting the caller](apps/cluster-agent/src/transport/routes/cluster/cluster-routes.test.ts#L121), [`cluster-routes.test.ts:187`](apps/cluster-agent/src/transport/routes/cluster/cluster-routes.test.ts#L187), [`cluster-routes.test.ts:197`](apps/cluster-agent/src/transport/routes/cluster/cluster-routes.test.ts#L197), [`cluster-routes.test.ts:177`](apps/cluster-agent/src/transport/routes/cluster/cluster-routes.test.ts#L177))
169
169
- Every route requires the same bearer token every other service-to-service
170
-
call presents. ([validated by refuses every route without a bearer token](apps/cluster-agent/src/transport/routes/cluster.test.ts#L143), [validated by refuses to restart without a bearer token](apps/cluster-agent/src/transport/routes/cluster.test.ts#L266))
170
+
call presents. ([validated by refuses every route without a bearer token](apps/cluster-agent/src/transport/routes/cluster/cluster-routes.test.ts#L143), [validated by refuses to restart without a bearer token](apps/cluster-agent/src/transport/routes/cluster/cluster-routes.test.ts#L266))
171
171
- A CR the controller has not stamped yet reads as Pending rather than absent —
172
172
the distinction a watcher acts on. ([validated by a CR the controller has not stamped yet maps to Pending, not absence](libs/shared/src/outbound/cluster/agent-node-status.test.ts#L6), [`agent-node-status.test.ts:12`](libs/shared/src/outbound/cluster/agent-node-status.test.ts#L12))
173
173
- An empty minted token is refused where the cause is legible, rather than
@@ -189,7 +189,7 @@ scoped settling a run to the one cluster this Floor can reach; it now settles
189
189
from the event's own report, which carries the full status. The distinction
190
190
above still governs the READ surface — the reconcile pass, the reaper's status
191
191
probe, the pod-log reads — where a caller genuinely has to ask.*
192
-
([validated by answers 200 with found:false for a missing CR, not 404](apps/cluster-agent/src/transport/routes/cluster.test.ts#L73), [`k8s-errors.test.ts:28`](apps/cluster-agent/src/lib/k8s-errors.test.ts#L31), [`k8s-errors.test.ts:44`](apps/cluster-agent/src/lib/k8s-errors.test.ts#L47), [`k8s-errors.test.ts:56`](apps/cluster-agent/src/lib/k8s-errors.test.ts#L59), [`kubernetes.test.ts:21`](apps/floor/src/events/handlers/kubernetes.test.ts#L21), [`kubernetes.test.ts:49`](apps/floor/src/events/handlers/kubernetes.test.ts#L49), [`kubernetes.test.ts:55`](apps/floor/src/events/handlers/kubernetes.test.ts#L55))
192
+
([validated by answers 200 with found:false for a missing CR, not 404](apps/cluster-agent/src/transport/routes/cluster/cluster-routes.test.ts#L73), [`k8s-errors.test.ts:28`](apps/cluster-agent/src/lib/k8s-errors.test.ts#L31), [`k8s-errors.test.ts:44`](apps/cluster-agent/src/lib/k8s-errors.test.ts#L47), [`k8s-errors.test.ts:56`](apps/cluster-agent/src/lib/k8s-errors.test.ts#L59), [`kubernetes.test.ts:21`](apps/floor/src/events/handlers/kubernetes.test.ts#L21), [`kubernetes.test.ts:49`](apps/floor/src/events/handlers/kubernetes.test.ts#L49), [`kubernetes.test.ts:55`](apps/floor/src/events/handlers/kubernetes.test.ts#L55))
193
193
194
194
The reconcile pass keeps paging, and its seam narrowed with the cut: it now
195
195
depends on one page-fetch method rather than a slice of a Kubernetes client, so
@@ -200,7 +200,7 @@ The Role this service carries also closes two gaps the Floor had been silently
200
200
living with: it never held `delete` on `agents` or `agents/status`, yet issued
201
201
both at sites that swallowed the failure — which is why the CR prune could
202
202
never actually shrink the pile it was written to shrink.
203
-
([validated by deletes a CR — the verb the Floor's RBAC never granted](apps/cluster-agent/src/transport/routes/cluster.test.ts#L108))
203
+
([validated by deletes a CR — the verb the Floor's RBAC never granted](apps/cluster-agent/src/transport/routes/cluster/cluster-routes.test.ts#L108))
|`GITHUB_APP_ID` / `GITHUB_APP_PRIVATE_KEY` / `GITHUB_APP_INSTALLATION_ID`| The GitHub App triple used to mint per-task installation tokens |
86
-
|`LORE_CLUSTER_AGENT_IDENTITY_SECRET` / `_NAMESPACE` / `_KEY`|Where the registered `{id, token}` persists; local runs fall back to `LORE_CLUSTER_AGENT_IDENTITY_FILE`|
86
+
|`LORE_CLUSTER_AGENT_IDENTITY_SECRET` / `_NAMESPACE` / `_KEY`|Required. The Kubernetes Secret the registered `{id, token}` persists in (`_KEY` defaults to `identity.json`); `npm start` points it at minikube|
87
87
88
88
The first three have no defaults and no off switch: the process exits naming
/** The cluster surface this agent exposes: what the Kubernetes clients in `outbound` provide and what the `transport` routes call — a contract owned by neither side. */
/** What registering takes: the triple to register with, where to persist the identity, and how to hand the minted token to the run pods. Named because both `establishIdentity` and the single-flight re-registration take exactly this. */
// What both side loops need: where to talk, who this cluster is, and whether the process is still up. One shape because they are started together and stopped together.
/** Registers, and hands back the identity as a GETTER rather than a value: a 401 rotates it mid-run, and every loop must read the current one rather than the one it captured at startup. */
@@ -100,7 +105,7 @@ async function establishIdentity(
100
105
101
106
// The first registration, with the line that says this cluster is now claiming. Announced here rather than by the caller because the id only exists once registration has succeeded.
/** Detached on purpose: `startClaimLoop` returns a handle immediately so a caller can stop the agent before it has finished registering. The catch is unreachable by design — register and claim never throw — but a defect here must surface as a log rather than an unhandled rejection that kills the process. */
90
+
/** Detached so a caller can stop the agent before it registers; register and claim never throw, so the catch only turns a defect into a log instead of a process-killing unhandled rejection. */
"[cluster-agent] claim loop crashed — this agent will not register or claim until restarted:",
@@ -138,14 +137,8 @@ function publishCredential(
138
137
};
139
138
}
140
139
141
-
/** In a cluster the identity persists through the Kubernetes Secret API — the chart mounts the container read-only, so a file write would EROFS and strand the identity. File store only for local runs. */
142
-
asyncfunctionbuildIdentityStore(
143
-
config: IdentityStoreConfig,
144
-
): Promise<IdentityStore>{
145
-
if(config.kind==="file"){
146
-
returnnewFileIdentityStore(config.path);
147
-
}
148
-
140
+
/** Through the Kubernetes Secret API, never a file: the chart mounts the container read-only, so a file write would EROFS and strand the identity. */
0 commit comments