mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-14 23:07:29 +00:00
feat(agentstack): enforce delegation expiry
This commit is contained in:
parent
82e1389b8d
commit
3897273be5
1 changed files with 119 additions and 151 deletions
|
|
@ -24,93 +24,125 @@ export const agentDid = (id: string) => makeDid("agent", id);
|
||||||
/** Parse a CoinPay DID into its kind and id, or return null if it is not one. */
|
/** Parse a CoinPay DID into its kind and id, or return null if it is not one. */
|
||||||
export function parseDid(did: string): { kind: DidKind; id: string } | null {
|
export function parseDid(did: string): { kind: DidKind; id: string } | null {
|
||||||
const prefix = `${DID_METHOD}:`;
|
const prefix = `${DID_METHOD}:`;
|
||||||
if (!did.startsWith(prefix)) return null;
|
import type { PluginDefinition } from "@logicsrc/plugin-core";
|
||||||
const [kind, id] = did.slice(prefix.length).split(":");
|
import { agentStackManifest } from "./manifest.js";
|
||||||
if ((kind !== "user" && kind !== "agent") || !id) return null;
|
import type {
|
||||||
return { kind, id };
|
AgentProfile,
|
||||||
}
|
AgentStackEvent,
|
||||||
|
AgentStackListener,
|
||||||
export function isDidTask(value: unknown): value is DidTask {
|
AgentStackSnapshot,
|
||||||
return (
|
CreateTaskInput,
|
||||||
typeof value === "object" &&
|
DelegationGrant,
|
||||||
value !== null &&
|
DidKind,
|
||||||
typeof (value as DidTask).id === "string" &&
|
DidTask,
|
||||||
typeof (value as DidTask).ownerDid === "string" &&
|
TaskStatus
|
||||||
typeof (value as DidTask).status === "string"
|
} from "./types.js";
|
||||||
);
|
import { DID_METHOD } from "./types.js";
|
||||||
}
|
|
||||||
|
/** Build a CoinPay-method DID for a user or agent: `did:coinpay:user:123`. */
|
||||||
const TERMINAL: ReadonlySet<TaskStatus> = new Set(["complete", "failed", "cancelled"]);
|
export function makeDid(kind: DidKind, id: string): string {
|
||||||
|
return `${DID_METHOD}:${kind}:${id}`;
|
||||||
/**
|
}
|
||||||
* In-memory AgentStack coordinator: registers agents, tracks portable tasks through their
|
|
||||||
* lifecycle, records delegation grants, and emits coordination events. Reference
|
export const userDid = (id: string) => makeDid("user", id);
|
||||||
* implementation of the `agentstack` capability; storage backends can wrap the same API.
|
export const agentDid = (id: string) => makeDid("agent", id);
|
||||||
*/
|
|
||||||
export class AgentStack {
|
/** Parse a CoinPay DID into its kind and id, or return null if it is not one. */
|
||||||
private readonly agents = new Map<string, AgentProfile>();
|
export function parseDid(did: string): { kind: DidKind; id: string } | null {
|
||||||
private readonly tasks = new Map<string, DidTask>();
|
const prefix = `${DID_METHOD}:`;
|
||||||
private readonly delegations = new Map<string, DelegationGrant>();
|
if (!did.startsWith(prefix)) return null;
|
||||||
private readonly listeners = new Set<AgentStackListener>();
|
const [kind, id] = did.slice(prefix.length).split(":");
|
||||||
private seq = 0;
|
if ((kind !== "user" && kind !== "agent") || !id) return null;
|
||||||
|
return { kind, id };
|
||||||
constructor(private readonly now: () => string = () => new Date().toISOString()) {}
|
}
|
||||||
|
|
||||||
on(listener: AgentStackListener): () => void {
|
export function isDidTask(value: unknown): value is DidTask {
|
||||||
this.listeners.add(listener);
|
return (
|
||||||
return () => this.listeners.delete(listener);
|
typeof value === "object" &&
|
||||||
}
|
value !== null &&
|
||||||
|
typeof (value as DidTask).id === "string" &&
|
||||||
private emit(event: AgentStackEvent) {
|
typeof (value as DidTask).ownerDid === "string" &&
|
||||||
for (const listener of this.listeners) listener(event);
|
typeof (value as DidTask).status === "string"
|
||||||
}
|
);
|
||||||
|
}
|
||||||
private nextId(prefix: string): string {
|
|
||||||
this.seq += 1;
|
/** Return false if the grant has an expiresAt that is in the past compared to `now`. */
|
||||||
return `${prefix}_${this.seq}`;
|
export function isGrantActive(grant: DelegationGrant, now: string): boolean {
|
||||||
}
|
if (!grant.expiresAt) return true;
|
||||||
|
return grant.expiresAt > now;
|
||||||
registerAgent(agent: AgentProfile): AgentProfile {
|
}
|
||||||
if (!parseDid(agent.did)) {
|
|
||||||
throw new Error(`Invalid agent DID: ${agent.did}`);
|
const TERMINAL: ReadonlySet<TaskStatus> = new Set(["complete", "failed", "cancelled"]);
|
||||||
}
|
|
||||||
this.agents.set(agent.did, agent);
|
/**
|
||||||
this.emit({ type: "agent.registered", agent });
|
* In-memory AgentStack coordinator: registers agents, tracks portable tasks through their
|
||||||
return agent;
|
* lifecycle, records delegation grants, and emits coordination events. Reference
|
||||||
}
|
* implementation of the `agentstack` capability; storage backends can wrap the same API.
|
||||||
|
*/
|
||||||
getAgent(did: string): AgentProfile | undefined {
|
export class AgentStack {
|
||||||
return this.agents.get(did);
|
private readonly agents = new Map<string, AgentProfile>();
|
||||||
}
|
private readonly tasks = new Map<string, DidTask>();
|
||||||
|
private readonly delegations = new Map<string, DelegationGrant>();
|
||||||
createTask(input: CreateTaskInput): DidTask {
|
private readonly listeners = new Set<AgentStackListener>();
|
||||||
if (!parseDid(input.ownerDid)) {
|
private seq = 0;
|
||||||
throw new Error(`Invalid owner DID: ${input.ownerDid}`);
|
|
||||||
}
|
constructor(private readonly now: () => string = () => new Date().toISOString()) {}
|
||||||
const ts = this.now();
|
|
||||||
const task: DidTask = {
|
on(listener: AgentStackListener): () => void {
|
||||||
id: this.nextId("task"),
|
this.listeners.add(listener);
|
||||||
ownerDid: input.ownerDid,
|
return () => this.listeners.delete(listener);
|
||||||
assigneeDid: input.assigneeDid,
|
}
|
||||||
sourceApp: input.sourceApp,
|
|
||||||
title: input.title,
|
private emit(event: AgentStackEvent) {
|
||||||
description: input.description,
|
for (const listener of this.listeners) listener(event);
|
||||||
status: input.assigneeDid ? "queued" : "pending",
|
}
|
||||||
paymentIntentId: input.paymentIntentId,
|
|
||||||
escrowId: input.escrowId,
|
private nextId(prefix: string): string {
|
||||||
metadata: input.metadata,
|
this.seq += 1;
|
||||||
createdAt: ts,
|
return `${prefix}_${this.seq}`;
|
||||||
updatedAt: ts
|
}
|
||||||
};
|
|
||||||
this.tasks.set(task.id, task);
|
registerAgent(agent: AgentProfile): AgentProfile {
|
||||||
this.emit({ type: "task.created", task });
|
if (!parseDid(agent.did)) {
|
||||||
return task;
|
throw new Error(`Invalid agent DID: ${agent.did}`);
|
||||||
}
|
}
|
||||||
|
this.agents.set(agent.did, agent);
|
||||||
getTask(id: string): DidTask | undefined {
|
this.emit({ type: "agent.registered", agent });
|
||||||
return this.tasks.get(id);
|
return agent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAgent(did: string): AgentProfile | undefined {
|
||||||
|
return this.agents.get(did);
|
||||||
|
}
|
||||||
|
|
||||||
|
createTask(input: CreateTaskInput): DidTask {
|
||||||
|
if (!parseDid(input.ownerDid)) {
|
||||||
|
throw new Error(`Invalid owner DID: ${input.ownerDid}`);
|
||||||
|
}
|
||||||
|
const ts = this.now();
|
||||||
|
const task: DidTask = {
|
||||||
|
id: this.nextId("task"),
|
||||||
|
ownerDid: input.ownerDid,
|
||||||
|
assigneeDid: input.assigneeDid,
|
||||||
|
sourceApp: input.sourceApp,
|
||||||
|
title: input.title,
|
||||||
|
description: input.description,
|
||||||
|
status: input.assigneeDid ? "queued" : "pending",
|
||||||
|
paymentIntentId: input.paymentIntentId,
|
||||||
|
escrowId: input.escrowId,
|
||||||
|
metadata: input.metadata,
|
||||||
|
createdAt: ts,
|
||||||
|
updatedAt: ts
|
||||||
|
};
|
||||||
|
this.tasks.set(task.id, task);
|
||||||
|
this.emit({ type: "task.created", task });
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTask(id: string): DidTask | undefined {
|
||||||
|
return this.tasks.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
assignTask(taskId: string, agentDidValue: string): DidTask {
|
assignTask(taskId: string, agentDidValue: string): DidTask {
|
||||||
const task = this.requireTask(taskId);
|
const task = this.requireTask(taskId);
|
||||||
if (TERMINAL.has(task.status)) {
|
if (TERMINAL.has(task.status)) {
|
||||||
|
|
@ -119,70 +151,6 @@ export class AgentStack {
|
||||||
if (!this.agents.has(agentDidValue)) {
|
if (!this.agents.has(agentDidValue)) {
|
||||||
throw new Error(`Unknown agent: ${agentDidValue}`);
|
throw new Error(`Unknown agent: ${agentDidValue}`);
|
||||||
}
|
}
|
||||||
const updated: DidTask = {
|
|
||||||
...task,
|
|
||||||
assigneeDid: agentDidValue,
|
|
||||||
status: task.status === "pending" ? "queued" : task.status,
|
|
||||||
updatedAt: this.now()
|
|
||||||
};
|
|
||||||
this.tasks.set(taskId, updated);
|
|
||||||
this.emit({ type: "task.assigned", task: updated });
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTaskStatus(
|
|
||||||
taskId: string,
|
|
||||||
status: TaskStatus,
|
|
||||||
patch: Partial<Pick<DidTask, "reputationEventId" | "paymentIntentId" | "escrowId" | "metadata">> = {}
|
|
||||||
): DidTask {
|
|
||||||
const task = this.requireTask(taskId);
|
|
||||||
if (TERMINAL.has(task.status)) {
|
|
||||||
throw new Error(`Task ${taskId} is already ${task.status} and cannot transition to ${status}`);
|
|
||||||
}
|
|
||||||
const updated: DidTask = { ...task, ...patch, status, updatedAt: this.now() };
|
|
||||||
this.tasks.set(taskId, updated);
|
|
||||||
this.emit({ type: "task.updated", task: updated });
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Grant an agent authority to act for an owner. */
|
|
||||||
delegate(ownerDidValue: string, agentDidValue: string, scopes: string[], expiresAt?: string): DelegationGrant {
|
|
||||||
if (!parseDid(ownerDidValue)) throw new Error(`Invalid owner DID: ${ownerDidValue}`);
|
|
||||||
if (!this.agents.has(agentDidValue)) throw new Error(`Unknown agent: ${agentDidValue}`);
|
|
||||||
const grant: DelegationGrant = {
|
|
||||||
id: this.nextId("grant"),
|
|
||||||
ownerDid: ownerDidValue,
|
|
||||||
agentDid: agentDidValue,
|
|
||||||
scopes,
|
|
||||||
expiresAt,
|
|
||||||
createdAt: this.now()
|
|
||||||
};
|
|
||||||
this.delegations.set(grant.id, grant);
|
|
||||||
this.emit({ type: "delegation.granted", grant });
|
|
||||||
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;
|
|
||||||
if (filter?.status && task.status !== filter.status) return false;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
snapshot(): AgentStackSnapshot {
|
|
||||||
return {
|
|
||||||
agents: [...this.agents.values()],
|
|
||||||
tasks: [...this.tasks.values()],
|
|
||||||
delegations: [...this.delegations.values()]
|
delegations: [...this.delegations.values()]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue