Connect personal Muse through secure native execution and Vault OAuth (#16448)

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>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-10-17 22:44:36 -05:00
1 parent 63fbb714e6
commit e6dcc6b095
72 files changed
+9775 -1021

No files matched your search

+25 -1
View File
@@ -1,6 +1,6 @@
import { createHash, timingSafeEqual } from "node:crypto";
import type { Request, RequestHandler } from "express";
import { and, eq, isNull } from "drizzle-orm";
import { and, eq, isNull, sql } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import {
activityLog,
@@ -429,6 +429,29 @@ export function actorMiddleware(db: Db, opts: ActorMiddlewareOptions): RequestHa
identityRun.activeIdentityContextId = captured.context?.id ?? null;
identityRun.responsibleUserId = captured.context?.responsibleUserId ?? null;
}
if (claims.authorizing_user_id) {
// This signed projection exists only for internal native tools. Recheck
// the admitted run and current immutable binding on every API call.
try {
const { parseNativeExecutionInput } = await import("../vendor/paperclip-runner/index.js");
const { museRunnerBroker } = await import("../services/muse-runner-broker.js");
const [nativeRun] = await db.select({ status: heartbeatRuns.status, runtimeMode: heartbeatRuns.runtimeMode,
nativeExecutionInput: sql<unknown>`${heartbeatRuns.runnerProfileJson}->'nativeExecutionInput'`,
}).from(heartbeatRuns).where(and(
eq(heartbeatRuns.id, claims.run_id), eq(heartbeatRuns.companyId, claims.company_id), eq(heartbeatRuns.agentId, claims.sub),
));
const execution = parseNativeExecutionInput(nativeRun?.nativeExecutionInput);
if (nativeRun?.status !== "running" || nativeRun.runtimeMode !== "native" || execution.schema !== "paperclip.native-execution-input.v8"
|| execution.binding.runId !== claims.run_id || execution.binding.agentId !== claims.sub || execution.binding.companyId !== claims.company_id
|| await museRunnerBroker(db).authorizingUserIdForBinding(execution.provider.binding) !== claims.authorizing_user_id) {
throw new Error("Muse internal tool authority mismatch");
}
await museRunnerBroker(db).assertRunAuthority(execution);
} catch {
_res.status(403).json({ error: "Muse native tool authority is unavailable" });
return;
}
}
const onBehalfOfUserId = identityRun?.activeIdentityContextId
? identityRun.responsibleUserId
: claims.responsible_user_id !== undefined
@@ -451,6 +474,7 @@ export function actorMiddleware(db: Db, opts: ActorMiddlewareOptions): RequestHa
keyScope: normalizeAgentApiKeyScope(claims.key_scope),
runId: claims.run_id,
onBehalfOfUserId,
...(claims.authorizing_user_id ? { authorizingUserId: claims.authorizing_user_id } : {}),
identityContextId: identityRun?.activeIdentityContextId ?? null,
onBehalfOfMemberships,
source: "agent_jwt",
+13 -1
View File
@@ -1,6 +1,7 @@
const SILENCED_SUCCESS_METHODS = new Set(["GET", "HEAD"]);
const SILENCED_SUCCESS_API_PATHS = [
/^\/api\/muse\/v1\/signal$/i,
/^\/api\/health(?:\/|$)/,
/^\/api\/companies\/[^/]+\/activity(?:\/|$)/,
/^\/api\/companies\/[^/]+\/dashboard(?:\/|$)/,
@@ -53,6 +54,17 @@ export function isPrivateAgentCommentaryHttpRequest(url: string | undefined): bo
return /^\/api\/companies\/[^/]+\/agent-commentary(?:\/|$)/i.test(pathname);
}
/** Muse OAuth and worker payloads stay private even on malformed/rejected paths. */
export function isPrivateMuseHttpRequest(url:string|undefined):boolean {
if(!url)return false;
let pathname=normalizePath(url).replace(/^https?:\/\/[^/]*/i, "");
for(let depth=0;depth<4;depth++) {
if(/^\/api\/muse(?:\/|$)/i.test(pathname) || /^\/api\/companies\/[^/]+\/(?:muse-invitations|agents\/[^/]+\/muse-binding)(?:\/|$)/i.test(pathname))return true;
try {const decoded=decodeURIComponent(pathname);if(decoded===pathname)break;pathname=decoded;} catch {break;}
}
return false;
}
/** Provider payloads are private even when a method/signature is rejected. */
export function isPrivateWebhookHttpRequest(
method: string | undefined,
@@ -87,7 +99,7 @@ export function isSecretSensitiveHttpRequest(
method: string | undefined,
url: string | undefined,
): boolean {
if (isPrivateWebhookHttpRequest(method, url)) return true;
if (isPrivateWebhookHttpRequest(method, url) || isPrivateMuseHttpRequest(url)) return true;
if (url && normalizePath(url).startsWith("/mcp/files/")) return true;
if (isPrivateAgentCommentaryHttpRequest(url)) return true;
// Avatar uploads and dedicated Dot MCP calls may contain private image bytes.
+6 -2
View File
@@ -6,6 +6,7 @@ import {
isPrivateWebhookHttpRequest,
isSecretSensitiveHttpRequest,
isPrivateAgentCommentaryHttpRequest,
isPrivateMuseHttpRequest,
shouldSilenceHttpSuccessLog,
} from "./http-log-policy.js";
import {
@@ -74,6 +75,7 @@ function requestLogUrl(req: {
originalUrl?: unknown;
url?: unknown;
}) {
if(isPrivateMuseHttpRequest(requestClassificationUrl(req)))return "/api/muse/:protocol";
return isPrivateWebhook(req)
? privateWebhookLogUrl(requestClassificationUrl(req))
: stripSecretBearingUrlParts(typeof req.url === "string" ? req.url : "");
@@ -84,6 +86,7 @@ export function createHttpLogger(baseLogger: Logger) {
logger: baseLogger,
serializers: {
req(req: Record<string, unknown> & { url?: unknown }) {
if(isPrivateMuseHttpRequest(typeof req.url === "string" ? req.url : undefined))return {id:req.id,method:req.method,url:"/api/muse/:protocol"};
if (isPrivateAgentCommentaryHttpRequest(typeof req.url === "string" ? req.url : undefined)) {
return { id: req.id, method: req.method, url: stripSecretBearingUrlParts(req.url as string) };
}
@@ -123,7 +126,7 @@ export function createHttpLogger(baseLogger: Logger) {
) {
// A provider error may also be reflected in response headers. Keep the
// same content-free contract on both sides of a webhook request.
return res.raw?.req && (isPrivateWebhook(res.raw.req) || isPrivateAgentCommentaryHttpRequest(requestClassificationUrl(res.raw.req)))
return res.raw?.req && (isPrivateWebhook(res.raw.req) || isPrivateMuseHttpRequest(requestClassificationUrl(res.raw.req)) || isPrivateAgentCommentaryHttpRequest(requestClassificationUrl(res.raw.req)))
? { statusCode: res.statusCode }
: res;
},
@@ -154,6 +157,7 @@ export function createHttpLogger(baseLogger: Logger) {
return `${req.method} ${stripSecretBearingUrlParts(req.url ?? "")} ${res.statusCode} — ${errMsg}`;
},
customErrorObject(req, _res, _err, value) {
if (isPrivateMuseHttpRequest(requestClassificationUrl(req)))return {...value,err:{type:"Error",message:"External agent request failed"}};
if (isPrivateAgentCommentaryHttpRequest(requestClassificationUrl(req))) {
return { ...value, err: { type: "Error", message: "Feedback request failed" } };
}
@@ -172,7 +176,7 @@ export function createHttpLogger(baseLogger: Logger) {
if (/^\/mcp\/(?:oauth|paperclip)(?:\/|$)/.test(requestClassificationUrl(req) ?? "")) {
return { reqBody: "[REDACTED]", ...(ctx ? { errorContext: { name: "Error" } } : {}) };
}
if (isPrivateWebhook(req) || isPrivateAgentCommentaryHttpRequest(requestClassificationUrl(req))) {
if (isPrivateWebhook(req) || isPrivateMuseHttpRequest(requestClassificationUrl(req)) || isPrivateAgentCommentaryHttpRequest(requestClassificationUrl(req))) {
// Omit, rather than recursively redact, the entire provider payload.
// This applies equally before/after parsing and with/without context.
return {