feat(agentstack): add delegation authority checks

This commit is contained in:
Qyra-One 2026-06-27 23:59:06 -04:00
parent edf32139ae
commit 61d42e4aaf
4 changed files with 92 additions and 27 deletions

View file

@ -74,7 +74,8 @@ Backed by `src/index.ts`, `src/types.ts`, `src/manifest.ts`:
- **Status lifecycle**`pending → queued → running → blocked → complete | failed | cancelled`.
- **`AgentStack` coordinator** (in-memory `Map` storage, injectable clock):
`registerAgent`, `getAgent`, `createTask`, `getTask`, `assignTask`, `updateTaskStatus`,
`delegate`, `revokeDelegation`, `listTasks`, `snapshot`, and an `on(listener)` event subscription.
`delegate`, `revokeDelegation`, `listDelegations`, `hasDelegation`, `listTasks`, `snapshot`,
and an `on(listener)` event subscription.
- **Events**`agent.registered`, `task.created`, `task.assigned`, `task.updated`,
`delegation.granted`, `delegation.revoked`.
- **`agentStackPlugin`** — a LogicSRC `PluginDefinition`, plus `agentStackManifest` declaring

View file

@ -15,7 +15,8 @@ Shared AppKit OpenSpec (`profullstack-web/openspec/specs/agentstack`).
(`pending → queued → running → blocked → complete | failed | cancelled`) that can bind to
payment, escrow, and reputation events.
- **`AgentStack`** — an in-memory coordinator that registers agents, tracks tasks, records
delegation grants, and emits coordination events. Storage backends can wrap the same API.
delegation grants, checks active scoped authority, and emits coordination events. Storage
backends can wrap the same API.
- **`agentStackPlugin`** — a LogicSRC `PluginDefinition` (validated against the plugin
manifest schema) exposing AgentStack as a coordination plugin with routes, events,
permissions, and a TUI panel.
@ -39,6 +40,10 @@ const task = stack.createTask({ ownerDid: owner, sourceApp: "sh1pt.com", title:
stack.assignTask(task.id, agentDid("abc"));
stack.updateTaskStatus(task.id, "running");
stack.updateTaskStatus(task.id, "complete", { reputationEventId: "rep_1" });
const grant = stack.delegate(owner, agentDid("abc"), ["tasks:create"]);
stack.hasDelegation(owner, agentDid("abc"), "tasks:create"); // true while the grant is active
stack.revokeDelegation(grant.id);
```
## Cross-app identity

View file

@ -130,6 +130,29 @@ describe("AgentStack coordinator", () => {
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();
expect(() => stack.createTask({ ownerDid: "nope", sourceApp: "x", title: "t" })).toThrow();

View file

@ -186,6 +186,34 @@ export class AgentStack {
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;
@ -208,6 +236,14 @@ export class AgentStack {
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. */