From b4083844ceee9d2a80bbd44cbd72e3faa99f78be Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 19:04:01 +0000 Subject: [PATCH] feat(media): wave 2 integration Composition root shared by API and worker, media job handlers on every job host, HTTP byte routes, consent re-check before bundling, and auto-fix limited to changes that keep protection. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01NLSCTnhge4umMnemGXWr1w --- apps/api/src/app.ts | 10 ++++++---- apps/worker/src/index.ts | 6 +++--- packages/adapters/src/index.ts | 6 +++++- packages/adapters/src/media-publish.ts | 3 +++ packages/adapters/src/media-render.ts | 5 ++++- packages/adapters/src/media-services.ts | 4 +++- 6 files changed, 24 insertions(+), 10 deletions(-) diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 047c6a7..131eef4 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -25,10 +25,10 @@ import { createCloudAgentConnection, createConnectorStack, createJobReconciler, - createMessagingContextLoader, - createMessagingTeamChatSender, createMediaJobHandlers, createMediaServices, + createMessagingContextLoader, + createMessagingTeamChatSender, createProfileConsolidationScheduler, createRunExecutor, createRunSandbox, @@ -94,6 +94,7 @@ import { cors } from "hono/cors"; import type { AppEnv } from "./env.js"; import { loadEnv } from "./env.js"; import { mountLocalSettings } from "./local-settings.js"; +import { mountMediaHttpRoutes } from "./media-http.js"; import { createMessagingInboundHandler, teamChatSenderCanWakeMessageRoutines, @@ -112,7 +113,6 @@ import { TEAM_CHAT_STARTUP_SHUTDOWN_MS, } from "./team-chat-startup.js"; import { mountTradingBotStatus } from "./trading-bot-proxy.js"; -import { mountMediaHttpRoutes } from "./media-http.js"; import { mountVoiceHttpRoutes } from "./voice.js"; import { mountWebhookHttpRoutes } from "./webhook.js"; @@ -455,7 +455,9 @@ export async function createApp( await graphileWorkerHost.start(jobHandlers); } if (mediaJobs) { - void mediaJobs.resume().catch((error) => getLogger().error("media ingest resume failed", error)); + void mediaJobs + .resume() + .catch((error) => getLogger().error("media ingest resume failed", error)); } const reconciler = inMemoryJobs ? createJobReconciler({ diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index 9524a58..e0ef77a 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -10,10 +10,10 @@ import { createCloudAgentConnection, createConnectorStack, createJobReconciler, - createMessagingContextLoader, - createPostgresReconciliationLeadership, createMediaJobHandlers, createMediaServices, + createMessagingContextLoader, + createPostgresReconciliationLeadership, createProfileConsolidationScheduler, createRunExecutor, createRunSandbox, @@ -31,9 +31,9 @@ import { isPipedreamEnabled, LocalAgentHomeStore, LocalArtifactStore, - mediaConfigFromEnv, McpConnector, McpOAuthBroker, + mediaConfigFromEnv, messagingEnvFromProcess, messagingPlatformsFromEnv, PiAgentRuntime, diff --git a/packages/adapters/src/index.ts b/packages/adapters/src/index.ts index bf5df64..6f7dd24 100644 --- a/packages/adapters/src/index.ts +++ b/packages/adapters/src/index.ts @@ -77,6 +77,7 @@ export * from "./mcp-oauth.js"; export * from "./mcp-server-tool.js"; export * from "./mcp-transport.js"; export * from "./media-brand.js"; +export * from "./media-consent.js"; export * from "./media-context.js"; export * from "./media-emulator.js"; export * from "./media-emulator-clips.js"; @@ -85,6 +86,7 @@ export * from "./media-factory.js"; export * from "./media-ffmpeg-renderer.js"; export * from "./media-folder-source.js"; export * from "./media-folder-source-probe.js"; +export * from "./media-gate-autofix.js"; export * from "./media-ingest.js"; export { defaultMediaJobLimits, MediaJobLimiter, type MediaJobLimits } from "./media-jobs.js"; export * from "./media-marketing.js"; @@ -103,9 +105,10 @@ export { MotionTemplateError, ResvgMotionGraphicsRenderer, } from "./media-motion-renderer.js"; +export * from "./media-privacy.js"; +export { PrivacyUnavailableError } from "./media-privacy-io.js"; export * from "./media-publish.js"; export * from "./media-publisher-bundle.js"; -export * from "./media-services.js"; export { createMediaRenderHandler, createMediaRenderService, @@ -113,6 +116,7 @@ export { pickEncoder, softwareEncoder, } from "./media-render.js"; +export * from "./media-services.js"; export * from "./media-sidecar.js"; export * from "./media-zip.js"; export * from "./memory-provider-factory.js"; diff --git a/packages/adapters/src/media-publish.ts b/packages/adapters/src/media-publish.ts index 008a75d..1a01a92 100644 --- a/packages/adapters/src/media-publish.ts +++ b/packages/adapters/src/media-publish.ts @@ -8,6 +8,7 @@ import { InvalidMediaInputError, MediaStateConflictError } from "@rakazo/db"; import { getLogger } from "@rakazo/logging"; import { buildTrackingUrl, normalizeHashtags, uploadChecklist } from "@rakazo/media"; import { resolveInside } from "@rakazo/media/node"; +import { assertExportConsent } from "./media-consent.js"; import type { MediaActor, MediaMarketingService, MediaServiceDeps } from "./media-context.js"; import type { MediaToolPaths } from "./media-folder-source-probe.js"; import { runMediaTool } from "./media-folder-source-probe.js"; @@ -249,6 +250,8 @@ export async function buildBundle( const profile = profileSet.profiles.get(pub.profileId)?.profile; if (!profile) throw new MediaStateConflictError(`Profile ${pub.profileId} is not available`); const edl = plan.edl; + // Consent is re-read here: a withdrawal after the gate passed still stops the export. + await assertExportConsent(deps, actor, { edl, profile }); const outputs = storedRenderOutputs(row.outputs); const video = outputs.find((output) => output.kind === "video"); if (!video) throw new MediaStateConflictError("Render has no video output"); diff --git a/packages/adapters/src/media-render.ts b/packages/adapters/src/media-render.ts index 259577f..3612162 100644 --- a/packages/adapters/src/media-render.ts +++ b/packages/adapters/src/media-render.ts @@ -29,6 +29,7 @@ import type { MediaServiceDeps, } from "./media-context.js"; import { MediaToolMissingError } from "./media-folder-source-probe.js"; +import { nonCompliantChange } from "./media-gate-autofix.js"; /* * Render service (API side) and render job (worker side). @@ -573,10 +574,12 @@ async function runRender(input: RunInput): Promise { return; } - const fix = + const planned = round < MAX_AUTO_FIX_ROUNDS ? planAutoFix(edl, { ...report, autoFixRound: round }, alternatives) : null; + // An auto-fix may only add protection or cut content, never weaken a redaction. + const fix = planned && nonCompliantChange(edl, planned.edl) === null ? planned : null; if (!fix) { await rm(renderDir, { recursive: true, force: true }); const blocked = await update({ status: "blocked", progress: 1, gateReport: report }); diff --git a/packages/adapters/src/media-services.ts b/packages/adapters/src/media-services.ts index 9eaea81..9a3aec6 100644 --- a/packages/adapters/src/media-services.ts +++ b/packages/adapters/src/media-services.ts @@ -72,7 +72,9 @@ export function createMediaServices(input: { ]); const set = mergeProfiles(builtin.profiles, own.profiles); set.errors = [...builtin.errors, ...own.errors]; - for (const error of set.errors) getLogger().warn("media profile ignored", error); + for (const error of set.errors) { + getLogger().warn("media profile ignored", { file: error.file, reason: error.message }); + } cached = { at: Date.now(), set }; return set; };