Files
saltbo__agent-kanban/server/http/tasks/workflowSupport.ts
realmroot[bot] 96fdd70d2c feat(api)!: redesign task resource operations (#265)
* 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>
2026-09-05 02:40:42 +00:00

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;
}