mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-06 14:02:36 +01:00
Add company-scoped Muse native control, OAuth, wake receiver and recovery. Recheck current authority after waits and before effects or receipt replay. Preserve task-read scope, assignment expiry, independent holds, nonblocking reassignment and truthful cleanup provenance. Retain source trust on progress comments and verify current Muse V8 authority through real signed-JWT HTTP requests. Preserve the upstream trimmed ordinary authentication lookup. Refs #16448. Preserve captured canonical queue recovery and concurrent environment preparation while retaining the scoped Muse V8 exclusion. Normal migration 0354 and existing Muse SQL remain unchanged. Fresh current-head Greptile review is 5/5 with the project-write finding addressed. Required CI and independent technical review gate the merge; maintainer override is reserved for formal approval rules. Recheck current Muse project-write authority after all receipt/task/run/admission waits, before creation or replay. Real PostgreSQL regressions reproduce four late effects/replays on the old route; the repaired three-file treatment passes 354 cases with no skips, and the scoped server typecheck passes. Ordinary/Dot behavior is preserved. Co-Authored-By: Paperclip <noreply@paperclip.ing>
53 lines
2.9 KiB
TypeScript
53 lines
2.9 KiB
TypeScript
import { Router, type Request } from "express";
|
|
import { z } from "zod";
|
|
import type { Db } from "@paperclipai/db";
|
|
import { assertBoard, assertCompanyAccess } from "./authz.js";
|
|
import { forbidden, notFound } from "../errors.js";
|
|
import { accessService } from "../services/access.js";
|
|
import { agentService } from "../services/agents.js";
|
|
import { museRunnerBroker } from "../services/muse-runner-broker.js";
|
|
|
|
const qualificationIdentity = z.object({ bindingId: z.uuid(), qualificationId: z.uuid() }).strict();
|
|
const startQualification = qualificationIdentity.extend({ generation: z.number().int().positive(), expectedRevision: z.number().int().positive() }).strict();
|
|
|
|
/** Operator-only bounded experiment. The broker owns the durable deadline and fencing. */
|
|
export function museQualificationRoutes(db: Db) {
|
|
const router = Router();
|
|
const broker = museRunnerBroker(db);
|
|
const path = "/companies/:companyId/agents/:agentId/muse-binding/qualification";
|
|
async function operator(req: Request) {
|
|
const companyId = z.uuid().parse(req.params.companyId), agentId = z.uuid().parse(req.params.agentId);
|
|
assertBoard(req); assertCompanyAccess(req, companyId);
|
|
if (!req.actor.userId || !["session", "cloud_tenant", "board_key"].includes(req.actor.source ?? "")) {
|
|
throw forbidden("Sign in as an operator to manage a Muse qualification.");
|
|
}
|
|
const agent = await agentService(db).getById(agentId);
|
|
if (!agent || agent.companyId !== companyId) throw notFound("Agent not found");
|
|
const decision = await accessService(db).decide({ actor: req.actor, action: "agent_config:update",
|
|
resource: { type: "agent", companyId, agentId }, scope: { requiresChangeGrant: true } });
|
|
if (!decision.allowed) throw forbidden(decision.explanation);
|
|
return { companyId, agentId, operatorId: req.actor.userId };
|
|
}
|
|
router.post(path, async (req, res) => {
|
|
const scope = await operator(req);
|
|
const body = startQualification.parse(req.body ?? {});
|
|
// The caller cannot extend the deadline on retries. The broker reuses the
|
|
// existing qualification ID and its original deadline atomically.
|
|
res.set("Cache-Control", "no-store").status(201).json(await broker.beginQualification({
|
|
...scope, ...body, expiresAt: new Date(Date.now() + 24 * 60 * 60_000),
|
|
}));
|
|
});
|
|
router.get(path, async (req, res) => {
|
|
const { companyId, agentId } = await operator(req);
|
|
const { bindingId, qualificationId } = qualificationIdentity.parse(req.query);
|
|
res.set("Cache-Control", "no-store").json(await broker.readQualificationEvidence(companyId, agentId, bindingId, qualificationId));
|
|
});
|
|
router.delete(path, async (req, res) => {
|
|
const scope = await operator(req);
|
|
const body = qualificationIdentity.parse(req.body ?? {});
|
|
await broker.endQualification({ ...scope, ...body });
|
|
res.set("Cache-Control", "no-store").status(204).end();
|
|
});
|
|
return router;
|
|
}
|