mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-13 14:37:26 +00:00
feat(agentstack): add delegation authority checks (#87)
This commit is contained in:
parent
c495041adb
commit
0bca203527
4 changed files with 92 additions and 27 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue