mirror of
https://github.com/saltbo/agent-kanban.git
synced 2026-09-19 09:01:47 +08:00
96fdd70d2c
* feat(api)!: redesign task resource operations
Publish the complete resource-oriented OpenAPI contract, enforce scope-based authorization at the HTTP boundary, and replace lifecycle command routes with Task PATCH and nested Claim/Event resources.
Add server-side Task CAS, migration coverage, updated Agent Skills, and browser/API regression tests.
BREAKING CHANGE: remove the legacy task lifecycle endpoints and move bounded waits to /tasks/{taskId}/events.
* refactor(tasks)!: make claims create-only
Remove Task Claim retrieval and deletion routes, the release scope, live deletion logic, and release workflow guidance.
Keep historical release records readable for migration and audit compatibility.
BREAKING CHANGE: remove GET and DELETE /tasks/{taskId}/claims/{claimId} and the task:release scope.
* test(ci): align actor and projection contracts
Cover machine and service Task Action writes and update Agent detail fixtures to the canonical assignedTo query and paged lowerCamelCase response.
* fix(api): preserve merge patch fields and consume browser pagination
* test(ui): include terminal pagination in agent task fixture
---------
Co-authored-by: jarvis <jarvis@agents.realmroot.dev>
69 lines
2.9 KiB
TypeScript
69 lines
2.9 KiB
TypeScript
import type { Env } from "@server/env";
|
|
import { v2Problem } from "@server/http/middleware/v2Contract";
|
|
import type { Context } from "hono";
|
|
|
|
export type TaskContext = Context<{ Bindings: Env }>;
|
|
|
|
export function requestActorId(c: TaskContext): string {
|
|
const principal = c.get("principal");
|
|
return principal.actorId ?? principal.subjectId;
|
|
}
|
|
|
|
export function verifiedAgentRuntimeSession(c: TaskContext): { runtime: string; sessionId: string } | null {
|
|
const principal = c.get("principal");
|
|
return principal.runtime && principal.runtimeSessionId ? { runtime: principal.runtime, sessionId: principal.runtimeSessionId } : null;
|
|
}
|
|
|
|
export function taskWorkflowActor(c: TaskContext): { type: "agent" | "human" | "machine" | "service"; id: string } {
|
|
const principal = c.get("principal");
|
|
if (principal.type === "agent" && principal.actorId) return { type: "agent", id: principal.actorId };
|
|
return { type: principal.type, id: principal.subjectId };
|
|
}
|
|
|
|
export function agentRuntimeSessionRequired(c: TaskContext): Response {
|
|
return v2Problem(c, 409, "runtime-session-required", "Runtime Session required", "Task Claim requires a verified runtime Session binding");
|
|
}
|
|
|
|
export function taskNotFound(c: TaskContext, detail: string): Response {
|
|
return v2Problem(c, 404, "task-not-found", "Task not found", detail);
|
|
}
|
|
|
|
export function mediaType(c: TaskContext): string | undefined {
|
|
return c.req.header("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
}
|
|
|
|
export function readStrongVersionPrecondition(c: TaskContext, resourceName: string): string | Response {
|
|
const value = c.req.header("if-match");
|
|
if (!value) {
|
|
return v2Problem(c, 428, "precondition-required", "Precondition required", `If-Match must contain the current ${resourceName} ETag`);
|
|
}
|
|
const match = /^"([^"\\]{1,200})"$/.exec(value);
|
|
if (!match) {
|
|
return v2Problem(c, 400, "invalid-precondition", "Invalid precondition", `If-Match must contain one strong ${resourceName} ETag`);
|
|
}
|
|
return match[1]!;
|
|
}
|
|
|
|
export async function rejectNonEmptyRepresentation(c: TaskContext, type: string, title: string, detail: string): Promise<Response | null> {
|
|
if (c.req.raw.body === null) return null;
|
|
const contentType = mediaType(c);
|
|
if (!contentType) {
|
|
return (await c.req.text()).length === 0
|
|
? null
|
|
: v2Problem(c, 415, "unsupported-media-type", "Unsupported media type", "Content-Type must be application/json");
|
|
}
|
|
if (contentType !== "application/json") {
|
|
return v2Problem(c, 415, "unsupported-media-type", "Unsupported media type", "Content-Type must be application/json");
|
|
}
|
|
let body: unknown;
|
|
try {
|
|
body = await c.req.json();
|
|
} catch {
|
|
return v2Problem(c, 400, "invalid-json", "Invalid JSON", "The request body must be valid JSON");
|
|
}
|
|
if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).length > 0) {
|
|
return v2Problem(c, 422, type, title, detail);
|
|
}
|
|
return null;
|
|
}
|