fix(hub): retain local execution grants

This commit is contained in:
Mohamed Boudra
2026-07-13 19:07:07 +02:00
parent 6472d9dcbd
commit fc8ed24840
3 changed files with 34 additions and 2 deletions
@@ -116,6 +116,29 @@ describe("Hub relationship", () => {
});
});
test("Hub enrollment cannot widen the daemon's locally granted scopes", async () => {
relationship = await HubRelationshipHarness.start();
relationship.returnEnrollmentScopes(["hub.execution.*", "*"]);
await relationship.beginConnect().result;
relationship.connectLatestSocket();
const responses = relationship.sendHubRequestOnLatest({
type: "daemon.get_status.request",
requestId: "scope-escalation",
});
expect(relationship.relationshipFile()?.relationship.scopes).toEqual(["hub.execution.*"]);
expect(responses).toContainEqual({
type: "rpc_error",
payload: {
requestId: "scope-escalation",
requestType: "daemon.get_status.request",
error: "Session is not authorized for daemon.get_status.request",
code: "access_denied",
},
});
});
test("a lost enrollment response reuses the exact ceremony", async () => {
relationship = await HubRelationshipHarness.start();
relationship.holdEnrollment();
@@ -343,7 +343,7 @@ export class HubRelationshipController implements HubRelationshipManagement {
const active: ActiveRecord = {
version: 1,
state: "active",
relationship: { ...pending.relationship, scopes: enrollment.scopes },
relationship: pending.relationship,
credential: pending.credential,
transport: { kind: "direct_websocket", webSocketUrl: enrollment.webSocketUrl },
};
@@ -318,6 +318,7 @@ class InMemoryHubRelationships implements HubRelationshipRemote {
private enrollmentObserved = deferred<void>();
private socketObserved = deferred<void>();
private enrollmentRejection: 401 | 403 | null = null;
private enrollmentScopes = ["hub.execution.*"];
private revokeFailures = 0;
private readonly relationships = new Set<string>();
readonly enrollmentSnapshots: RelationshipInvocationSnapshot[] = [];
@@ -333,6 +334,10 @@ class InMemoryHubRelationships implements HubRelationshipRemote {
this.enrollmentRejection = statusCode;
}
returnEnrollmentScopes(scopes: string[]): void {
this.enrollmentScopes = scopes.slice();
}
async enroll(input: HubEnrollment): Promise<HubEnrollmentResult> {
this.enrollmentSnapshots.push(this.captureRelationship());
this.enrollments.push({ ...input, scopes: input.scopes.slice() });
@@ -415,7 +420,7 @@ class InMemoryHubRelationships implements HubRelationshipRemote {
private enrollmentResult(input: HubEnrollment): HubEnrollmentResult {
return {
daemonId: input.daemonId,
scopes: ["hub.execution.*"],
scopes: this.enrollmentScopes.slice(),
webSocketUrl: SOCKET_URL,
};
}
@@ -579,6 +584,10 @@ export class HubRelationshipHarness {
this.remote.completeEnrollment();
}
returnEnrollmentScopes(scopes: string[]): void {
this.remote.returnEnrollmentScopes(scopes);
}
loseEnrollmentResponse(): void {
this.remote.loseEnrollmentResponse();
}