feat(agentstack): add delegation authority checks (#87)

This commit is contained in:
Qyra-One 2026-07-01 04:14:40 -04:00 committed by GitHub
parent c495041adb
commit 0bca203527
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 92 additions and 27 deletions

View file

@ -120,15 +120,38 @@ describe("AgentStack coordinator", () => {
it("records delegation grants and emits events", () => {
const stack = new AgentStack();
const listener = vi.fn();
stack.on(listener);
stack.registerAgent(agent);
const grant = stack.delegate(owner, agent.did, ["tasks:create"]);
expect(grant.ownerDid).toBe(owner);
expect(grant.agentDid).toBe(agent.did);
expect(listener).toHaveBeenCalledWith(expect.objectContaining({ type: "agent.registered" }));
expect(listener).toHaveBeenCalledWith(expect.objectContaining({ type: "delegation.granted" }));
});
stack.on(listener);
stack.registerAgent(agent);
const grant = stack.delegate(owner, agent.did, ["tasks:create"]);
expect(grant.ownerDid).toBe(owner);
expect(grant.agentDid).toBe(agent.did);
expect(listener).toHaveBeenCalledWith(expect.objectContaining({ type: "agent.registered" }));
expect(listener).toHaveBeenCalledWith(expect.objectContaining({ type: "delegation.granted" }));
});
it("checks active delegation authority by owner, agent, scope, and expiry", () => {
const stack = new AgentStack(() => "2026-01-01T00:00:00.000Z");
stack.registerAgent(agent);
stack.delegate(owner, agent.did, ["tasks:create"], "2026-01-02T00:00:00.000Z");
expect(stack.hasDelegation(owner, agent.did, "tasks:create")).toBe(true);
expect(stack.hasDelegation(owner, agent.did, "tasks:update")).toBe(false);
expect(stack.hasDelegation(owner, agent.did, "tasks:create", "2026-01-03T00:00:00.000Z")).toBe(false);
expect(stack.listDelegations({ ownerDid: owner, agentDid: agent.did, scope: "tasks:create" })).toHaveLength(1);
expect(stack.listDelegations({ activeAt: "2026-01-03T00:00:00.000Z" })).toHaveLength(0);
});
it("allows wildcard delegation scopes and ignores revoked grants", () => {
const stack = new AgentStack();
stack.registerAgent(agent);
const grant = stack.delegate(owner, agent.did, ["*"]);
expect(stack.hasDelegation(owner, agent.did, "tasks:update")).toBe(true);
stack.revokeDelegation(grant.id);
expect(stack.hasDelegation(owner, agent.did, "tasks:update")).toBe(false);
});
it("rejects unknown agents and invalid DIDs", () => {
const stack = new AgentStack();

View file

@ -178,18 +178,46 @@ export class AgentStack {
return grant;
}
revokeDelegation(grantId: string): DelegationGrant {
const grant = this.delegations.get(grantId);
if (!grant) throw new Error(`Unknown delegation grant: ${grantId}`);
this.delegations.delete(grantId);
this.emit({ type: "delegation.revoked", grant });
return grant;
}
listTasks(filter?: { ownerDid?: string; assigneeDid?: string; status?: TaskStatus }): DidTask[] {
return [...this.tasks.values()].filter((task) => {
if (filter?.ownerDid && task.ownerDid !== filter.ownerDid) return false;
if (filter?.assigneeDid && task.assigneeDid !== filter.assigneeDid) return false;
revokeDelegation(grantId: string): DelegationGrant {
const grant = this.delegations.get(grantId);
if (!grant) throw new Error(`Unknown delegation grant: ${grantId}`);
this.delegations.delete(grantId);
this.emit({ type: "delegation.revoked", grant });
return grant;
}
listDelegations(filter?: {
ownerDid?: string;
agentDid?: string;
scope?: string;
activeAt?: string;
}): DelegationGrant[] {
return [...this.delegations.values()].filter((grant) => {
if (filter?.ownerDid && grant.ownerDid !== filter.ownerDid) return false;
if (filter?.agentDid && grant.agentDid !== filter.agentDid) return false;
if (filter?.scope && !this.grantAllowsScope(grant, filter.scope)) return false;
if (filter?.activeAt && !this.grantIsActive(grant, filter.activeAt)) return false;
return true;
});
}
hasDelegation(ownerDidValue: string, agentDidValue: string, scope: string, at: string = this.now()): boolean {
if (!parseDid(ownerDidValue)) throw new Error(`Invalid owner DID: ${ownerDidValue}`);
if (!this.agents.has(agentDidValue)) throw new Error(`Unknown agent: ${agentDidValue}`);
return (
this.listDelegations({
ownerDid: ownerDidValue,
agentDid: agentDidValue,
scope,
activeAt: at
}).length > 0
);
}
listTasks(filter?: { ownerDid?: string; assigneeDid?: string; status?: TaskStatus }): DidTask[] {
return [...this.tasks.values()].filter((task) => {
if (filter?.ownerDid && task.ownerDid !== filter.ownerDid) return false;
if (filter?.assigneeDid && task.assigneeDid !== filter.assigneeDid) return false;
if (filter?.status && task.status !== filter.status) return false;
return true;
});
@ -205,10 +233,18 @@ export class AgentStack {
private requireTask(id: string): DidTask {
const task = this.tasks.get(id);
if (!task) throw new Error(`Unknown task: ${id}`);
return task;
}
}
if (!task) throw new Error(`Unknown task: ${id}`);
return task;
}
private grantAllowsScope(grant: DelegationGrant, scope: string): boolean {
return grant.scopes.includes(scope) || grant.scopes.includes("*");
}
private grantIsActive(grant: DelegationGrant, at: string): boolean {
return !grant.expiresAt || grant.expiresAt > at;
}
}
/** LogicSRC plugin definition exposing AgentStack as a coordination plugin. */
export const agentStackPlugin: PluginDefinition = {