diff --git a/docs/superpowers/specs/2026-04-17-agent-config-endpoint-flat.md b/docs/superpowers/specs/2026-04-17-agent-config-endpoint-flat.md new file mode 100644 index 0000000..3354c1f --- /dev/null +++ b/docs/superpowers/specs/2026-04-17-agent-config-endpoint-flat.md @@ -0,0 +1,89 @@ +# Agent Config Endpoint — Flat, JWT-Resolved + +**Date:** 2026-04-17 +**Status:** Approved +**Scope:** `cameleer-core`, `cameleer-common` +**Supersedes:** The agent-side URL from `2026-04-16-env-scoped-config-url-agent-design.md` only. The env-scoped route family (`/api/v1/environments/{envSlug}/...`) remains for user-facing endpoints on the server; it simply is not how the agent fetches its own config. + +## Problem + +The 2026-04-16 landing moved the agent's config fetch to `GET /api/v1/environments/{envId}/apps/{app}/config`. In practice that route is 403 for the agent because the server's only `hasRole("AGENT")` config-read endpoint is flat and JWT-scoped (`SecurityConfig.java:127`). The env-scoped route is reserved for user-facing calls. + +## Design + +### Endpoint + +``` +GET {baseUrl}/api/v1/agents/config +``` + +Zero URL parameters. The server resolves `(application, environment)` from the agent's JWT subject → registry entry (heartbeat-authoritative), with the JWT env claim as fallback. + +### `ServerConnection.fetchApplicationConfig` + +Signature changes from `(String application)` to `()` — the parameter was only used to build the URL and is now redundant. Callers drop the argument. + +New behavior: + +``` +path = "/api/v1/agents/config" +``` + +401/403 refresh-retry path (existing) targets the same URL. + +Post-response checks are unchanged from the 2026-04-16 design: + +1. HTTP status / 401+403 refresh path (existing). +2. Deserialize envelope → `ApplicationConfig`. +3. Merge `mergedSensitiveKeys` into `ApplicationConfig.sensitiveKeys` (existing). +4. Version check: if `config.getVersion() == 0`, return `null` (no config stored). +5. Env validation (version > 0 only): throw if `config.getEnvironment() == null` or does not equal `registeredEnvironmentId`. + +The response envelope keeps `environment` populated when `version > 0`, so strict validation remains a cheap cross-check that the server's JWT→env resolution agrees with what the agent registered as. + +### Fields and state + +- `registeredEnvironmentId` is still populated by `register()` and still used for response validation and the heartbeat body. No change. +- `ConfigCacheManager` keeps its per-application key. No change. +- Heartbeat body keeps `environmentId`. No change. + +### PROTOCOL.md + +- Section 3 endpoint table: replace the `GET /api/v1/environments/{environmentId}/apps/{applicationId}/config` row with `GET /api/v1/agents/config` — "Fetch per-agent config (server resolves application and environment from the agent's JWT)". +- "Config Endpoint Response Envelope" heading: update to the new URL. +- The paragraph on strict env validation and the `version == 0` bypass stays as-is — the response envelope and validation behavior are unchanged. +- SSE reconnection bullet that references the config URL: update to the flat URL. + +## Error handling + +Unchanged from 2026-04-16. Table applies verbatim: + +| Case | Behavior | +|---|---| +| 401 / 403 | Refresh token, retry once. | +| Non-200 after refresh | `RuntimeException("Config fetch failed: HTTP nnn")`. | +| 200 + `version == 0` | Return `null`. Env not validated. | +| 200 + `version > 0` + `environment == null` | Throw. | +| 200 + `version > 0` + `environment != registeredEnvironmentId` | Throw. | +| 200 + `version > 0` + env matches | Return config. | + +## Testing + +`cameleer-core/src/test/java/com/cameleer/core/connection/ServerConnectionTest.java`: + +- **URL shape** — `ArgumentCaptor` asserts the request URI path is `/api/v1/agents/config` (no env/app segments). +- **Strict mismatch rejection** — retained; response body still carries `environment`, and the agent still rejects a mismatch. +- **Strict null rejection** — retained. +- **Happy path env match** — retained. +- All `fetchApplicationConfig(...)` call sites in tests drop the argument. + +## Non-goals + +- No changes to `/api/v1/data/*` (unchanged, JWT-authoritative). +- No changes to `/api/v1/agents/{instanceId}/*` (register, heartbeat, deregister, refresh, ack, SSE). +- No removal of `registeredEnvironmentId` field or heartbeat env field. +- No protocol-version bump. + +## Rollout + +Hard cut on main. Server's agent-config route is already live (prior env-scoped URL is 403), so this fixes the agent immediately on deploy.