From 141435fe952555e2d6de92fd0f19ca09cda16043 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Mon, 1 Jun 2026 12:14:09 -0700 Subject: [PATCH] feat(fleet): bind advertised capabilities to the enrolled token scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The claim path already constrained a factory to its enrolled scope, but the heartbeat trusted self-reported capabilities — so (with enforcement on) a factory could advertise e.g. engine:codex it was never granted, polluting the engine picker (GET /fleet/factories) and routing/explain decisions even though a codex job still couldn't be claimed by it. Heartbeat now intersects the factory's self-reported capabilities with the token scope when enforcement is ON: it may report FEWER (an engine temporarily unavailable) but never MORE than enrolled. Enforcement OFF is unchanged (self-reported caps pass through verbatim). Covered by new route tests. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/modules/fleet/enrollment.test.ts | 34 +++++++++++++++++++ .../src/modules/fleet/routes.ts | 10 +++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/services/platform-service/src/modules/fleet/enrollment.test.ts b/services/platform-service/src/modules/fleet/enrollment.test.ts index 141cb6fb..0d13a7aa 100644 --- a/services/platform-service/src/modules/fleet/enrollment.test.ts +++ b/services/platform-service/src/modules/fleet/enrollment.test.ts @@ -267,6 +267,40 @@ describe('fleet enrollment + scoped tokens', () => { expect(JSON.parse(ok.body).claimed).toBe(true); }); + it('enforcement ON: heartbeat advertises only enrolled caps (out-of-scope cap dropped)', async () => { + const app = await buildApp(); + const enrolled = await enrollment.enrollFactory({ + productId: PID, + factoryId: 'fac_1', + capabilities: ['os:mac', 'engine:devin'], + }); + process.env.FLEET_REQUIRE_FACTORY_TOKEN = '1'; + + const hb = await app.inject({ + method: 'POST', + url: '/api/fleet/factories/heartbeat', + headers: { 'x-factory-token': enrolled.token }, + // claims engine:codex it was NOT enrolled for, and omits a granted cap + payload: { factoryId: 'fac_1', capabilities: ['engine:devin', 'engine:codex'] }, + }); + expect(hb.statusCode).toBe(200); + const caps = JSON.parse(hb.body).factory.capabilities as string[]; + expect(caps).toContain('engine:devin'); // enrolled + reported → kept + expect(caps).not.toContain('engine:codex'); // not enrolled → dropped + expect(caps).not.toContain('os:mac'); // enrolled but not reported → not invented + }); + + it('enforcement OFF: heartbeat advertises self-reported caps verbatim', async () => { + const app = await buildApp(); + const hb = await app.inject({ + method: 'POST', + url: '/api/fleet/factories/heartbeat', + payload: { factoryId: 'fac_1', capabilities: ['engine:codex'] }, + }); + expect(hb.statusCode).toBe(200); + expect(JSON.parse(hb.body).factory.capabilities).toEqual(['engine:codex']); + }); + it('enforcement ON: revoked token → 401 on heartbeat', async () => { const app = await buildApp(); const enrolled = await enrollment.enrollFactory({ diff --git a/services/platform-service/src/modules/fleet/routes.ts b/services/platform-service/src/modules/fleet/routes.ts index 16778c0d..527af349 100644 --- a/services/platform-service/src/modules/fleet/routes.ts +++ b/services/platform-service/src/modules/fleet/routes.ts @@ -319,11 +319,19 @@ export async function fleetRoutes(app: FastifyInstance) { factoryId: parsed.data.factoryId, }); const pid = scope?.productId ?? pidCandidate; + // §12 trust boundary: when enforcement is ON, a factory may advertise only the + // capabilities it was ENROLLED with — intersect its self-reported caps with the + // token scope (it can report fewer, e.g. an engine temporarily unavailable, but + // never MORE). This keeps a factory from advertising e.g. engine:codex it wasn't + // granted, which would otherwise pollute the engine picker and route decisions. + const advertisedCaps = scope + ? (parsed.data.capabilities ?? []).filter(c => scope.capabilities.includes(c)) + : parsed.data.capabilities; await coordinator.heartbeat({ productId: pid, factoryId: parsed.data.factoryId, descriptor: parsed.data.descriptor, - capabilities: parsed.data.capabilities, + capabilities: advertisedCaps, health: parsed.data.health, load: parsed.data.load, seatLimit: parsed.data.seatLimit,