[OPEND-3114] feat(od-next): open registered agents and CLI BYOK (#8702)

* feat(od-next): open registered runtimes and CLI BYOK

Use the effective runtime registry for OD Next admission and remove retired
capability qualification. Keep native observation and existing resume/cold
recovery, with the evidence graph owned by observability contracts.

Add run-scoped BYOK connection flags with caller-selected environment key input
and reuse the existing provider bridge. Accept equivalent frozen Skill JSON
and Base64 representations while preserving content and ownership boundaries.

Validated with focused contracts, runtime, public Run and CLI regressions;
eight real native agents and a real BYOK request/production pair; guard and
workspace typecheck. Refs #8701.

* fix(web): describe BYOK project file tools

Explain that API-provider project runs use OpenCode file tools and require
OpenCode to be available. Update the existing notice in all 19 locales and
its component/browser regressions.

Validated with red/green notice checks, real settings and preview follow-ups,
guard and workspace typecheck. Refs #8701.

* fix(od-next): preserve hidden prototype form feedback

Keep HTML hidden semantics within the shared layout layer without overriding
authored product CSS or hidden-until-found behavior. This fixes name errors
remaining visible after a successful registration.

Validated with a red/green Chromium regression, three resource-boundary
checks, a real Claude follow-up with desktop/mobile form interaction, guard
and workspace typecheck. Refs #8701.

* test(od-next): make Run fixtures select their execution path

Explicitly select ordinary Runs for retry persistence, plain stdout replay
and the compatibility single-run telemetry case. Keep task hierarchy and
terminal fallback telemetry cases active, and assert the selected path.

Restore owner write permission to the leftover-agent fixture's frozen
directories only during teardown so recursive deletion works on POSIX.
Skip symlinks and preserve cleanup errors and every original behavior check.

Reproduced all four CI failures before the fix. Focused daemon and E2E
checks passed (12 tests), along with guard, workspace typecheck and focused
LSP diagnostics. Refs #8701.

* fix(daemon): avoid passing Pi resource directories as prompt files

Pi reads existing --append-system-prompt paths as files. Stop passing skills, design systems and task input directories there; their resource paths already travel in the composed RPC prompt.

Validated with 104 focused runtime/RPC tests, a real Pi 1.1.0 offline startup red/green probe, workspace typecheck and guard.

* fix(web): coalesce streamed calls in generation progress

Fold in-flight snapshots and terminal inputs through the existing tool-event reducer before rendering the Design Files progress feed. Keep the latest input, stable call identity and the limit of twelve distinct calls.

Reproduced the duplicate React key warning before the fix. Runtime and progress-feed regressions now pass alongside existing in-flight tool-row tests.

* fix(web): preserve forms and browser storage in prototype previews

Permit client-side form submission in opaque interactive previews. Route Web Storage artifacts through the existing isolated powered preview, including daemon hints for large files, and retain guards when isolation is unavailable.

Verified URL and srcDoc form interaction, persisted values after reload, and host isolation with focused Chromium regressions. Focused viewer/runtime and daemon tests, workspace typecheck and guard passed.

* test(web): keep srcDoc fixtures on the opaque preview path

Use an existing non-storage sandbox-shim signal in transport lifecycle fixtures now that Web Storage artifacts select the isolated preview origin. Preserve every reload, activation, recovery and manual-save assertion.

Validated all 338 cases in the three affected FileViewer test files, workspace guard and typecheck, and focused diagnostics.

* fix(byok): accept keyless IPv6 loopback providers

Recognize the bracketed IPv6 hostname returned by WHATWG URL parsing in both settings validation and the OpenCode BYOK runtime. Keep remote hosts key-required.

Validated with focused daemon and Web tests, settings browser checks, workspace typecheck and guard.

* fix(daemon): restore Pi sessions and preserve completed RPC turns

Load prior Pi history with switch_session and capture the authoritative sessionFile through nonblocking get_state. Treat a host-requested SIGTERM after a clean completed turn as shutdown, including Pi builds that report exit 143, while preserving real errors and cancellation.

Validated with focused protocol and public Run lifecycle tests, a real Pi generation and native resumed edit, workspace typecheck and guard.

* fix(preview): isolate project storage and preserve hidden controls

Give powered previews stable per-project loopback origins and reserve those hosts for matching powered GETs before API or static exceptions. Advertise the capability so newer Web clients retain the opaque fallback with older daemons.

Reuse the active powered transport in Present. Share a low-specificity hidden-control guard across URL and srcDoc previews without overriding explicit author rules or until-found; preserve doctype mode when synthesizing a missing head.

Validated with actual two-project storage, Workers, reload, Present and API isolation browser witnesses, form/hidden regressions, focused Web and daemon checks, workspace typecheck and guard.

* test(e2e): align public canaries with best-effort OD Next

Read the remaining recipe identity marker without the retired serialized package hash. Make the fake OpenCode help probe finite. Exercise the current public question-form action, completed question turns/new answer tasks, and no-artifact normal endings while retaining cancellation, artifact and deck assertions.

Validated with six fake-runtime cases and focused active/off public browser canaries with explicit rollout flags; no production completion policy or timeout was relaxed.

* fix(od-next): guide focus restoration after list updates

Prefer native modal lifecycle and restore focus to a current control after list rerenders, falling back to the primary action when a filtered item disappears. Real OpenCode and Pi prototypes exposed detached-trigger focus loss.

Keep this as best-effort build guidance, without output contracts or automatic repair rounds. Validated the actual artifacts through normal public edit tasks and browser interaction checks; profile resource tests passed.

* fix(od-next): retain focus through inline task updates

Extend the existing live-control guidance to inline list actions. Real Copilot and Qoder boards also lost keyboard focus when status updates rebuilt their cards; hidden filtered items need a visible fallback.

* fix: align Discovery integration tests and presentation storage

* test(daemon): tolerate transient memory origin cleanup races

---------

Co-authored-by: zhouzheng <zhouzheng@refly.ai>
This commit is contained in:
Cheems
2026-10-22 05:14:59 +00:00
committed by GitHub
co-authored by zhouzheng
parent 4d4f53fdf4
commit 75693e95f4
94 changed files with 2508 additions and 3436 deletions
@@ -35,6 +35,8 @@ export type PiRpcSessionOptions = {
/** Handle returned by `attachPiRpcSession` for querying run state and requesting abort. */
export type PiRpcSession = {
hasFatalError(): boolean;
completedSuccessfully(): boolean;
completionShutdownRequested(): boolean;
abort(): void;
getLastSessionPath(): string | null;
};
@@ -159,7 +161,7 @@ export function resolveSessionPathChangedSince(
* Attaches the daemon's run lifecycle to an already-spawned `pi --mode rpc` child process.
*
* Responsibilities:
* - Sends a `new_session` RPC command (with `parentSession`) before the prompt when
* - Loads `parentSession` with `switch_session` before the prompt when
* resuming a prior conversation, waiting for acknowledgement before the prompt is sent.
* This preserves conversation history across edit rounds; if the parent session is
* rejected, the run is failed immediately rather than continuing without prior context.
@@ -169,7 +171,7 @@ export function resolveSessionPathChangedSince(
* - Streams pi's stdout through `createJsonLineStream`, delegating each parsed object to
* `mapPiRpcEvent` for typed SSE event dispatch, and auto-resolving extension-UI requests
* via `replyExtensionUi` so pi does not block waiting for interactive dialogs.
* - On `agent_end`, captures the changed `.pi/sessions/` file path for conversational
* - Queries the native `sessionFile`; on `agent_end`, captures it for conversational
* resume, closes stdin, and schedules a SIGTERM fallback after
* `PI_GRACEFUL_SHUTDOWN_MS` ms (default 5000).
*
@@ -203,6 +205,10 @@ export function attachPiRpcSession({
let fatal = false;
const sentFirstToken = { value: false };
let capturedSessionPath: string | null = null;
let reportedSessionPath: string | null = null;
let promptCompleted = false;
let agentErrorObserved = false;
let completionShutdownRequested = false;
let nextRpcId = 1;
let stdinOpen = true;
@@ -219,6 +225,7 @@ export function attachPiRpcSession({
// it loaded the parent session.
let parentSessionRpcId: number | null = null;
let promptRpcId: number | null = null;
let sessionStateRpcId: number | null = null;
const fail = (message: string, code?: string): void => {
if (finished) return;
@@ -236,7 +243,7 @@ export function attachPiRpcSession({
model: typeof model === 'string' && model ? model : null,
});
// ---- Outbound: send new_session (if parentSession provided) then prompt via RPC ----
// ---- Outbound: load an existing session, then prompt via RPC ----
stdin.on('error', (err: unknown) => {
if (errorCode(err) !== 'EPIPE') {
fail(`stdin: ${errorMessage(err)}`);
@@ -246,9 +253,7 @@ export function attachPiRpcSession({
stdinOpen = false;
});
// If a prior session file path is provided, send new_session with
// parentSession so pi loads the prior conversation history into the
// new session, enabling conversational continuity across edit rounds.
// Images accompany the prompt after any existing session has been loaded.
// Build the images array for pi's prompt command. pi's RPC protocol
// accepts `images` as an array of {type, data, mimeType} objects where
// `data` is base64-encoded file contents. The daemon's safeImages guard
@@ -310,14 +315,13 @@ export function attachPiRpcSession({
});
};
// If a prior session file path is provided, send new_session with
// parentSession so pi loads the prior conversation history into the
// new session, enabling conversational continuity across edit rounds.
// `new_session.parentSession` records ancestry but starts with no history.
// `switch_session` restores the native conversation used by a trimmed prompt.
// Do not send the prompt until pi acknowledges this RPC: resumed prompts
// intentionally contain only the latest user turn, so continuing after a
// failed parent load would silently drop prior conversation context.
if (parentSession) {
parentSessionRpcId = sendCommand(stdin, 'new_session', { parentSession });
parentSessionRpcId = sendCommand(stdin, 'switch_session', { sessionPath: parentSession });
} else {
sendPromptCommand();
}
@@ -325,6 +329,17 @@ export function attachPiRpcSession({
// ---- Inbound: parse stdout events ----
const parser = createJsonLineStream((raw: unknown) => {
if (!isRecord(raw)) return;
// Metadata belongs to this process/RPC id, even when its reply follows
// agent_end in the same stdout batch. It never delays or fails generation.
if (raw.type === 'response' && sessionStateRpcId !== null && raw.id === sessionStateRpcId) {
sessionStateRpcId = null;
const sessionFile = getRecord(raw.data)?.sessionFile;
if (raw.success === true && typeof sessionFile === 'string' && sessionFile.trim()) {
reportedSessionPath = sessionFile;
if (promptCompleted) capturedSessionPath = sessionFile;
}
return;
}
// Once finished (agent_end or abort), stop processing — the run is
// over, so no more agent events should be emitted. We still drain
// stdout via parser.feed() so the pipe doesn't break; we just skip
@@ -340,7 +355,12 @@ export function attachPiRpcSession({
// RPC responses (prompt accepted, set_model ack, etc.) — not
// agent events. Log the prompt acceptance, ignore the rest.
if (raw.type === 'response') {
if (raw.id === parentSessionRpcId) {
if (parentSessionRpcId !== null && raw.id === parentSessionRpcId) {
parentSessionRpcId = null;
if (getRecord(raw.data)?.cancelled === true) {
fail('Parent session loading was canceled by a Pi extension.', 'PI_PARENT_SESSION_FAILED');
return;
}
if (raw.success === false) {
fail(
`parent session rejected: ${String(raw.error ?? 'unknown')}`,
@@ -351,21 +371,35 @@ export function attachPiRpcSession({
sendPromptCommand();
return;
}
if (raw.id === promptRpcId && raw.success === false) {
if (promptRpcId !== null && raw.id === promptRpcId && raw.success === false) {
fail(`prompt rejected: ${String(raw.error ?? 'unknown')}`);
} else if (promptRpcId !== null && raw.id === promptRpcId && raw.success === true) {
promptRpcId = null;
try {
sessionStateRpcId = sendCommand(stdin, 'get_state');
} catch {
// Older runtimes and closed pipes may not answer a metadata query.
}
}
return;
}
// Agent events: delegate to the pure mapper.
const result = mapPiRpcEvent(raw, send, { runStartedAt, sentFirstToken });
const result = mapPiRpcEvent(raw, (channel, data) => {
if (channel === 'error' || (channel === 'agent' && data.type === 'error')) {
agentErrorObserved = true;
}
send(channel, data);
}, { runStartedAt, sentFirstToken });
if (result === 'agent_end') {
finished = true;
// Capture only the session file changed by this run. If another pi
// process wrote to the shared session directory concurrently, the
// resolver returns null instead of risking cross-conversation resume.
capturedSessionPath = resolveSessionPathChangedSince(cwd, sessionFilesBeforePrompt);
promptCompleted = true;
// Pi owns its session location (normally under its external agent home).
// Legacy fixtures/runtimes without get_state may still write the old
// project-local directory; only one changed file is safe to associate.
capturedSessionPath = reportedSessionPath
?? resolveSessionPathChangedSince(cwd, sessionFilesBeforePrompt);
// pi's RPC process stays alive after agent_end (designed for
// multi-prompt sessions). The daemon's /api/chat is single-shot,
// so close stdin and let the process exit naturally, or kill it
@@ -379,7 +413,9 @@ export function attachPiRpcSession({
// for resource-constrained machines where the event loop drains slowly.
const shutdownMs = Number(process.env.PI_GRACEFUL_SHUTDOWN_MS) || 5000;
setTimeout(() => {
if (!child.killed) child.kill('SIGTERM');
if (!child.killed && child.exitCode == null && child.signalCode == null) {
completionShutdownRequested = child.kill('SIGTERM');
}
}, shutdownMs);
}
});
@@ -398,6 +434,16 @@ export function attachPiRpcSession({
hasFatalError() {
return fatal;
},
completedSuccessfully() {
// The fallback SIGTERM ends the process, not an already-completed turn.
// An abort before agent_end, RPC rejection, or native error stays unclean.
return promptCompleted && !fatal && !agentErrorObserved;
},
completionShutdownRequested() {
// Some Pi builds trap this signal and report code 143 instead. Keep the
// actual host signal separate from a naturally failing process exit.
return completionShutdownRequested;
},
getLastSessionPath() {
return capturedSessionPath;
},
+59 -16
View File
@@ -16,6 +16,9 @@ import { runLiveArtifactsToolCli } from './tools-live-artifacts-cli.js';
import { runDeliverableSyntaxToolCli } from './tools-deliverable-syntax-cli.js';
import { splitResearchSubcommand } from './research/cli-args.js';
import { resolveDaemonUrl } from './daemon-url.js';
import { byokProviderFromCliOptions } from './runtimes/byok-cli.js';
import { BYOK_OPENCODE_AGENT_ID } from './runtimes/byok-opencode.js';
import { redactSecrets } from './connectionTest.js';
import { SidecarFactory } from '@open-design/sidecar';
import { APP_KEYS, SIDECAR_MESSAGES } from '@open-design/sidecar-proto';
import { EXPORT_FORMATS, EXPORT_IMAGE_FORMATS, mediaFailureNextStep, parseApiFailureDetail } from '@open-design/contracts';
@@ -261,6 +264,11 @@ const PROJECT_RESOURCE_STRING_FLAGS = new Set([
'workspace-member',
]);
const PROJECT_BOOLEAN_FLAGS = new Set(['help', 'h', 'json', 'follow', 'thumbnail']);
const RUN_START_STRING_FLAGS = new Set([
...PROJECT_RESOURCE_STRING_FLAGS,
'byok-protocol', 'byok-base-url', 'byok-api-version', 'byok-key-env',
]);
const RUN_START_BOOLEAN_FLAGS = new Set([...PROJECT_BOOLEAN_FLAGS, 'byok-keyless']);
const WORKSPACE_STRING_FLAGS = new Set([
'model',
'daemon-url', 'workspace', 'view', 'visibility', 'owner', 'project',
@@ -3034,10 +3042,10 @@ otherwise a minimal \`od mcp --daemon-url <url>\` command is used.`);
// code + a JSON envelope on stderr. Code agents read these to decide
// whether the failure is recoverable (re-grant capabilities, prompt
// the user, retry with --grant-caps, etc.).
function exitWithStructuredError({ code, message, data }) {
function exitWithStructuredError({ code, message, data }, byokApiKey) {
const exit = RECOVERABLE_EXIT_CODES[code] ?? 1;
const envelope = { error: { code, message, data: data ?? {} } };
process.stderr.write(JSON.stringify(envelope) + '\n');
process.stderr.write(serializeRunCliJson(envelope, byokApiKey) + '\n');
process.exit(exit);
}
@@ -8370,7 +8378,9 @@ async function runRun(args) {
[--prompt-file <path|->] [--task-execution <id>]
[--client-request-id <id>]
[--skill <id>[,<id>]] [--plugin <id>] [--inputs <json>] [--grant-caps a,b]
[--agent claude|codex|opencode] [--model <id>] [--service-tier <id>]
[--agent <id>] [--model <id>] [--service-tier <id>]
[--byok-protocol <protocol>] [--byok-base-url <url>]
[--byok-api-version <version>] [--byok-key-env <env>] [--byok-keyless]
[--workspace <id> --workspace-member <id>] [--follow] [--json]
od run redesign [--path <folder>] [--message "<text>" | --prompt-file <path|->]
[--agent claude] [--model <id>] [--service-tier <id>] [--follow] [--json]
@@ -8389,6 +8399,16 @@ async function runRun(args) {
od run result-package <runId> [--json] Inspect run outputs and workspace
provenance without applying them.
BYOK start options:
BYOK requires --model <id>; Azure also requires --byok-base-url.
--byok-protocol <protocol> anthropic|openai|azure|google|ollama|senseaudio|aihubmix.
Selects byok-opencode unless --agent is explicit.
--byok-base-url <url> Override the protocol's default service URL.
--byok-api-version <value> Optional provider API version.
--byok-key-env <env> Read the API key from this environment variable.
--byok-keyless Explicitly use a provider without an API key.
Use --model <id> to select the BYOK model.
Common options:
--daemon-url <url> OpenDesign daemon HTTP base.
--workspace <id> Explicit Workspace id for a bound project.
@@ -8399,8 +8419,8 @@ Common options:
const sub = args[0];
const rest = args.slice(1);
const flags = parseFlags(rest, {
string: PROJECT_RESOURCE_STRING_FLAGS,
boolean: PROJECT_BOOLEAN_FLAGS,
string: sub === 'start' ? RUN_START_STRING_FLAGS : PROJECT_RESOURCE_STRING_FLAGS,
boolean: sub === 'start' ? RUN_START_BOOLEAN_FLAGS : PROJECT_BOOLEAN_FLAGS,
});
const base = (await projectDaemonUrl(flags)).replace(/\/$/, '');
const workspaceHeaders = workspaceHeadersFromExplicitFlags(flags) ?? {};
@@ -8644,7 +8664,21 @@ Common options:
console.error('--project <projectId> is required');
process.exit(2);
}
let byokProvider;
try {
byokProvider = byokProviderFromCliOptions({
protocol: flags['byok-protocol'],
baseUrl: flags['byok-base-url'],
apiVersion: flags['byok-api-version'],
keyEnv: flags['byok-key-env'],
keyless: flags['byok-keyless'] === true,
}, process.env);
} catch (err) {
console.error(err.message);
process.exit(2);
}
const body = { projectId: flags.project };
if (byokProvider) body.byokProvider = byokProvider;
if (flags.conversation) body.conversationId = flags.conversation;
const message = await readRunMessageFromFlags(flags);
if (message) body.message = message;
@@ -8659,6 +8693,7 @@ Common options:
}
if (flags['design-system']) body.designSystemId = flags['design-system'];
if (flags.agent) body.agentId = flags.agent;
else if (byokProvider) body.agentId = BYOK_OPENCODE_AGENT_ID;
if (flags.model) body.model = flags.model;
if (flags['service-tier']) body.serviceTier = flags['service-tier'];
if (flags.inputs) {
@@ -8682,26 +8717,26 @@ Common options:
if (!resp.ok) {
if (resp.status === 409 && data?.error?.code === 'capabilities-required') {
return exitWithStructuredError({
code: 'capabilities-required',
code: data.error.code,
message: data.error.message,
data: data.error.data,
});
}, byokProvider?.apiKey);
}
if (resp.status === 422 && data?.error?.code === 'missing-input') {
return exitWithStructuredError({
code: 'missing-input',
code: data.error.code,
message: data.error.message,
data: data.error.data,
});
}, byokProvider?.apiKey);
}
console.error(`POST /api/runs failed: ${resp.status} ${JSON.stringify(data)}`);
console.error(`POST /api/runs failed: ${resp.status} ${serializeRunCliJson(data, byokProvider?.apiKey)}`);
process.exit(1);
}
if (flags.json && !flags.follow) {
return process.stdout.write(JSON.stringify(data, null, 2) + '\n');
return process.stdout.write(serializeRunCliJson(data, byokProvider?.apiKey, 2) + '\n');
}
console.log(`[run] started ${data.runId}`);
if (flags.follow) await streamRunEvents(base, data.runId, workspaceHeaders);
console.log(redactSecrets(`[run] started ${data.runId}`, [byokProvider?.apiKey]));
if (flags.follow) await streamRunEvents(base, data.runId, workspaceHeaders, byokProvider?.apiKey);
return;
}
default:
@@ -8710,15 +8745,23 @@ Common options:
}
}
function serializeRunCliJson(value, byokApiKey, indentation) {
// Only redact printed values; Run ids and task-chain decisions keep raw data.
const secrets = byokApiKey ? [byokApiKey, JSON.stringify(byokApiKey).slice(1, -1)] : [];
return JSON.stringify(value, (_key, entry) => (
typeof entry === 'string' && byokApiKey ? redactSecrets(entry, secrets) : entry
), indentation);
}
// Stream the SSE events at /api/runs/:id/events as ND-JSON on stdout.
// Each line is one event: { event, data } so a code agent can parse it
// without needing an SSE library.
async function streamRunEvents(base, initialRunId, workspaceHeaders = {}) {
async function streamRunEvents(base, initialRunId, workspaceHeaders = {}, byokApiKey) {
let runId = initialRunId;
const visited = new Set();
while (true) {
if (visited.has(runId)) {
console.error(`run watch failed: cyclic strategy task chain at ${runId}`);
console.error(redactSecrets(`run watch failed: cyclic strategy task chain at ${runId}`, [byokApiKey]));
process.exit(1);
}
visited.add(runId);
@@ -8750,7 +8793,7 @@ async function streamRunEvents(base, initialRunId, workspaceHeaders = {}) {
const dataRaw = dataLines.join('\n');
let parsed;
try { parsed = JSON.parse(dataRaw); } catch { parsed = dataRaw; }
process.stdout.write(JSON.stringify({ event, data: parsed }) + '\n');
process.stdout.write(serializeRunCliJson({ event, data: parsed }, byokApiKey) + '\n');
if (event !== 'end') continue;
const task = parsed?.strategyTask;
if (task && task.terminal !== true) {
@@ -0,0 +1,78 @@
import { createHash } from 'node:crypto';
import { parseHostHeader } from '../origin-validation.js';
export function projectPoweredPreviewHostname(projectId: string): string {
return `od-${createHash('sha256').update(projectId).digest('hex').slice(0, 32)}.localhost`;
}
interface PoweredPreviewOriginRequest {
method: string;
/** Full pathname, including the /api prefix. */
path: string;
headers: {
host?: unknown;
origin?: unknown;
referer?: unknown;
'sec-fetch-site'?: unknown;
'sec-fetch-dest'?: unknown;
};
}
function header(value: unknown): string {
const first = Array.isArray(value) ? value[0] : value;
return typeof first === 'string' ? first : '';
}
function headerUrl(value: unknown): URL | null {
try {
return new URL(header(value));
} catch {
return null;
}
}
function isProjectPreviewHost(hostname: string | undefined): boolean {
return Boolean(hostname?.startsWith('od-') && hostname.endsWith('.localhost'));
}
/**
* Project preview hosts are reserved for GETs of that project's powered
* files. Check both the target and browser source before any API exception;
* a preview cannot regain daemon access by targeting the normal loopback host.
* The hostname hash names an origin, rather than granting project authority.
*/
export function projectPoweredPreviewOriginDecision(
request: PoweredPreviewOriginRequest,
daemonPort: number | null | undefined,
): 'allow' | 'reject' | null {
const target = parseHostHeader(request.headers.host);
const origin = headerUrl(request.headers.origin);
const referer = headerUrl(request.headers.referer);
const targetIsPreview = isProjectPreviewHost(target?.hostname);
const sourceIsPreview = isProjectPreviewHost(origin?.hostname)
|| isProjectPreviewHost(referer?.hostname);
if (!targetIsPreview) return sourceIsPreview ? 'reject' : null;
if (!daemonPort || target?.port !== String(daemonPort) || request.method !== 'GET') {
return 'reject';
}
const match = /^\/api\/projects\/([^/]+)\/powered\/.+$/u.exec(request.path);
if (!match) return 'reject';
let projectId: string;
try {
projectId = decodeURIComponent(match[1]!);
} catch {
return 'reject';
}
const expectedHost = projectPoweredPreviewHostname(projectId);
if (target.hostname !== expectedHost) return 'reject';
const expectedOrigin = new URL(`http://${expectedHost}:${daemonPort}`).origin;
const originHeader = header(request.headers.origin);
if (originHeader && originHeader !== expectedOrigin) return 'reject';
if (sourceIsPreview && referer && referer.origin !== expectedOrigin) return 'reject';
const site = header(request.headers['sec-fetch-site']);
const destination = header(request.headers['sec-fetch-dest']);
if (site && site !== 'same-origin' && destination !== 'document' && destination !== 'iframe') {
return 'reject';
}
return 'allow';
}
@@ -47,7 +47,6 @@ import {
import { normalizeTelemetryAppVersion } from '../app-version.js';
import { buildStructuredMainRunObservationV1 } from './main-run-observation.js';
import { getDetectedRuntimeVersions } from '../runtimes/detection.js';
import { OD_NEXT_RUNTIME_PATH_DESCRIPTORS } from '../runtimes/od-next-capability-gate.js';
import {
adaptMainRunToolObservationsV1,
adaptRuntimeChildObservationsV1,
@@ -69,6 +68,15 @@ import {
export type TaskObservationRolloutMode = 'off' | 'observe' | 'send';
// Existing adapter labels describe observation provenance only. Runtimes
// without a known label still execute and export their available observations.
const runtimeObservationAdapterVersions: ReadonlyMap<string, string> = new Map([
['codex', 'od-codex-json-events/v1'],
['claude', 'od-claude-stream-json/v1'],
['opencode', 'od-opencode-json-events/v1'],
['amr', 'od-vela-opencode-acp/v1'],
]);
export interface TaskObservationRolloutConfig {
requestedMode: 'auto' | 'off' | 'observe' | 'send' | 'invalid';
mode: TaskObservationRolloutMode;
@@ -507,9 +515,9 @@ async function taskAggregate(
events: run.events,
});
const detectedVersions = getDetectedRuntimeVersions(run.agentId);
const runtimeAdapterVersion = OD_NEXT_RUNTIME_PATH_DESCRIPTORS.find(
(descriptor) => descriptor.agentId === run.agentId,
)?.runtimeAdapterVersion;
const runtimeAdapterVersion = run.agentId
? runtimeObservationAdapterVersions.get(run.agentId)
: undefined;
if (run.promptTelemetry && !run.promptTelemetry.odNextExactSend) {
throw new Error(
'Mapped OD Next Run is missing mandatory exact-send Prompt evidence.',
+3
View File
@@ -28,6 +28,7 @@ import {
} from '@open-design/contracts/runtime/preview-build-focus';
import {
buildPreviewFocusGuard,
buildPreviewVisibilityGuard,
buildPreviewRedirectGuard,
buildPreviewSandboxShim,
PREVIEW_URL_GUARD_MAX_HTML_BYTES,
@@ -1873,6 +1874,7 @@ function applyUrlPreviewBridgesToHtml(
}
let html = Buffer.isBuffer(transformed) ? transformed.toString('utf8') : transformed;
html = injectAfterHeadOpen(html, 'data-od-preview-hidden-guard', buildPreviewVisibilityGuard());
// Sanitize the <title> so Cmd+P -> "Save as PDF" produces a Teams-safe
// filename. URL-load iframes cannot rely on the host rewriting the document
// title after load, and powered previews are intentionally cross-origin.
@@ -6254,6 +6256,7 @@ export function registerProjectFileRoutes(app: Express, ctx: RegisterProjectFile
}
function htmlHasPoweredPreviewSignal(source: string): boolean {
if (/\b(?:local|session)Storage\b/.test(source)) return true;
if (/\bSharedArrayBuffer\b/.test(source)) return true;
if (/\bnew\s+(?:Worker|SharedWorker)\s*\(/.test(source)) return true;
if (/\bimportScripts\s*\(/.test(source)) return true;
+2 -71
View File
@@ -58,14 +58,9 @@ import {
} from '../db.js';
import { readVelaLoginStatus } from '../integrations/vela.js';
import {
ensureDetectedRuntimeCapabilities,
ensureDetectedRuntimeVersions,
getDetectedRuntimeVersions,
} from '../runtimes/detection.js';
import {
odNextAdvertisedCapabilityGap,
resolveBundledOdNextRuntimeCapability,
} from '../runtimes/od-next-capability-gate.js';
import { mintRunDoneKey } from '../runtimes/run-done-key.js';
import {
deriveLangfuseDeliveryState,
@@ -1517,9 +1512,6 @@ export function registerRunRoutes(app: Express, ctx: RegisterRunRoutesDeps) {
}
let resolvedSnapshot: SuccessfulRunSnapshotResolution | null = null;
let strategyRolloutDecision: OdNextRolloutDecision | null = null;
let rolloutCapabilitySnapshot: ReturnType<
typeof resolveBundledOdNextRuntimeCapability
>['snapshot'] = null;
let resolveAutomaticOrdinaryFallback: (() => ResolveSnapshotResult) | null = null;
let automaticOrdinaryFallbackPluginId: string | null = null;
let automaticSnapshotPreparationError: Error | null = null;
@@ -1697,8 +1689,6 @@ export function registerRunRoutes(app: Express, ctx: RegisterRunRoutesDeps) {
}
const rolloutPlugin = rolloutResolved?.ok ? rolloutResolved.record : null;
let rolloutVersions: Awaited<ReturnType<typeof ensureDetectedRuntimeVersions>> | null = null;
let rolloutCapability: ReturnType<typeof resolveBundledOdNextRuntimeCapability> | null = null;
let advertisedCapabilityGap: string[] = [];
if (routeApplicability === 'eligible' && rolloutPlugin) {
try {
if (effectiveAgentId) {
@@ -1706,54 +1696,13 @@ export function registerRunRoutes(app: Express, ctx: RegisterRunRoutesDeps) {
(rolloutAppConfig as { agentCliEnv?: AgentCliEnv }).agentCliEnv,
effectiveAgentId,
);
// Both probes read the same resolved launch path. The `--version`
// read establishes invocability; the `--help` read establishes
// which optional flags this installed build advertises. OD Next
// needs both, because the fixture registry below only proves what
// the runtime *path* can do, not what the user's build exposes.
const [versions, advertised] = await Promise.all([
ensureDetectedRuntimeVersions(effectiveAgentId, agentCliEnv),
ensureDetectedRuntimeCapabilities(effectiveAgentId, agentCliEnv),
]);
rolloutVersions = versions;
advertisedCapabilityGap = odNextAdvertisedCapabilityGap({
agentId: effectiveAgentId,
advertised,
});
rolloutVersions = await ensureDetectedRuntimeVersions(effectiveAgentId, agentCliEnv);
}
rolloutCapability = effectiveAgentId
? resolveBundledOdNextRuntimeCapability({
agentId: effectiveAgentId,
...(rolloutVersions?.agentCliVersion
? { agentCliVersion: rolloutVersions.agentCliVersion }
: {}),
...(rolloutVersions?.runtimeCompanionName
? { runtimeCompanionName: rolloutVersions.runtimeCompanionName }
: {}),
...(rolloutVersions?.runtimeCompanionVersion
? { runtimeCompanionVersion: rolloutVersions.runtimeCompanionVersion }
: {}),
})
: null;
} catch (error) {
automaticAdmissionPreparationFailed = true;
console.warn('[od-next-rollout] automatic capability preparation failed; using ordinary default', error);
console.warn('[od-next-rollout] runtime detection failed; using ordinary default', error);
}
}
const nativeSubagents = rolloutCapability?.snapshot?.nativeSubagents;
const runtimeCapabilityVerified = Boolean(
(rolloutVersions as ({ invocable?: boolean } | null))?.invocable === true
&& rolloutCapability?.reason === 'capability_resolved'
&& rolloutCapability.snapshot?.nativeSessionContinuation.support === 'verified'
&& nativeSubagents?.support === 'verified'
&& (nativeSubagents.evidenceLevel === 'L2' || nativeSubagents.evidenceLevel === 'L3')
&& advertisedCapabilityGap.length === 0
);
// An installed CLI that does not advertise what OD Next will demand at
// launch must lose admission here, not fail the user's Run at spawn.
const advertisedCapabilityReason = advertisedCapabilityGap.length > 0
? 'advertised_capability_missing'
: null;
strategyRolloutDecision = evaluateOdNextRollout({
policy: rolloutPolicy,
assignmentIdentity: `${requestBody.projectId}:${snapshotConversationId ?? ''}`,
@@ -1761,10 +1710,6 @@ export function registerRunRoutes(app: Express, ctx: RegisterRunRoutesDeps) {
agentId: effectiveAgentId,
agentVersion: rolloutVersions?.agentCliVersion ?? null,
sourceKind: rolloutPlugin?.sourceKind ?? null,
runtimeCapabilityVerified,
runtimeCapabilityReason: advertisedCapabilityReason
?? rolloutCapability?.reason
?? 'runtime_out_of_scope',
routeApplicability,
});
if (
@@ -1776,9 +1721,6 @@ export function registerRunRoutes(app: Express, ctx: RegisterRunRoutesDeps) {
'od_next_rollout_prestart_preparation_failed',
);
}
if (strategyRolloutDecision.effectiveMode === 'active') {
rolloutCapabilitySnapshot = rolloutCapability?.snapshot ?? null;
}
console.info('[od-next-rollout]', {
decisionClass: strategyRolloutDecision.decisionClass,
requestedMode: strategyRolloutDecision.requestedMode,
@@ -1789,9 +1731,6 @@ export function registerRunRoutes(app: Express, ctx: RegisterRunRoutesDeps) {
sourceKind: rolloutPlugin?.sourceKind ?? null,
assignmentClass: strategyRolloutDecision.eligible ? 'included' : 'not_included',
primaryReasonCode: strategyRolloutDecision.primaryReasonCode,
...(advertisedCapabilityGap.length > 0
? { advertisedCapabilityGap }
: {}),
});
if (!explicitExecutablePlugin) {
const projectRow = rolloutProject;
@@ -1957,9 +1896,6 @@ export function registerRunRoutes(app: Express, ctx: RegisterRunRoutesDeps) {
// unbound project.
workspaceScope: preparedWorkspaceScope,
...(strategyRolloutDecision ? { strategyRolloutDecision } : {}),
...(strategyRolloutDecision?.effectiveMode === 'active' && rolloutCapabilitySnapshot
? { runtimeCapabilitySnapshot: rolloutCapabilitySnapshot }
: {}),
};
if (resolvedSnapshot?.ok) {
meta.appliedPluginSnapshotId = resolvedSnapshot.snapshotId;
@@ -2386,7 +2322,6 @@ export function registerRunRoutes(app: Express, ctx: RegisterRunRoutesDeps) {
strategyRolloutDecision,
'od_next_rollout_prestart_preparation_failed',
);
rolloutCapabilitySnapshot = null;
meta.strategyRolloutDecision = strategyRolloutDecision;
delete meta.runtimeCapabilitySnapshot;
delete meta.odNextTaskInputSnapshot;
@@ -2648,10 +2583,6 @@ export function registerRunRoutes(app: Express, ctx: RegisterRunRoutesDeps) {
intake: {
inputRefs: [{ id: 'request', accessible: true }],
selectedAgentAvailable: true,
nativeContinuation: strategyRolloutDecision.syntheticCanary
? 'verified'
: rolloutCapabilitySnapshot?.nativeSessionContinuation.support
?? 'unknown',
taskProfileAvailable: true,
dependencies: [],
},
+49
View File
@@ -0,0 +1,49 @@
import type { ByokChatProviderConfig } from '@open-design/contracts';
export interface ByokCliOptions {
protocol?: string;
baseUrl?: string;
apiVersion?: string;
keyEnv?: string;
keyless?: boolean;
}
export function byokProviderFromCliOptions(
options: ByokCliOptions,
env: NodeJS.ProcessEnv,
): ByokChatProviderConfig | undefined {
if (
options.protocol === undefined
&& options.baseUrl === undefined
&& options.apiVersion === undefined
&& options.keyEnv === undefined
&& !options.keyless
) return undefined;
const protocol = options.protocol;
switch (protocol) {
case 'anthropic':
case 'openai':
case 'azure':
case 'google':
case 'ollama':
case 'senseaudio':
case 'aihubmix':
break;
default:
throw new Error('--byok-protocol must be anthropic, openai, azure, google, ollama, senseaudio or aihubmix');
}
const apiKey = options.keyEnv === undefined ? '' : (env[options.keyEnv] ?? '').trim();
if (options.keyEnv !== undefined && !apiKey) {
throw new Error('--byok-key-env must name a non-empty environment variable containing the API key');
}
return {
protocol,
apiKey,
...(options.baseUrl !== undefined ? { baseUrl: options.baseUrl } : {}),
...(options.apiVersion !== undefined ? { apiVersion: options.apiVersion } : {}),
...(options.keyless ? { requiresApiKey: false } : {}),
};
}
+1 -1
View File
@@ -138,7 +138,7 @@ function isLocalOllamaBaseUrl(value: string): boolean {
try {
const parsed = new URL(value);
const hostname = parsed.hostname.toLowerCase();
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
} catch {
return false;
}
+1 -6
View File
@@ -89,12 +89,7 @@ export const claudeAgentDef = {
if (caps.thinkingDisplay) {
args.push('--thinking-display', 'summarized');
}
if (runtimeContext.observeNativeChildBehavior === true) {
if (!caps.forwardSubagentText) {
throw new TypeError(
'Claude native Child behavior observation requires advertised --forward-subagent-text support.',
);
}
if (runtimeContext.observeNativeChildBehavior === true && caps.forwardSubagentText) {
args.push('--forward-subagent-text');
}
if (options.model && options.model !== 'default') {
+4 -19
View File
@@ -1,4 +1,3 @@
import path from 'node:path';
import { DEFAULT_MODEL_OPTION, execAgentFile, parsePiModels } from './shared.js';
import type { RuntimeAgentDef } from '../types.js';
@@ -57,7 +56,7 @@ export const piAgentDef = {
buildArgs: (
_prompt,
_imagePaths,
extraAllowedDirs = [],
_extraAllowedDirs = [],
options = {},
runtimeContext = {},
) => {
@@ -70,23 +69,9 @@ export const piAgentDef = {
if (options.reasoning && options.reasoning !== 'default') {
args.push('--thinking', options.reasoning);
}
// pi supports --append-system-prompt for cwd and extra context.
// For now we rely on the composed prompt containing the cwd hint
// (same pattern as other agents) rather than using system-prompt flags.
//
// extraAllowedDirs carries skill seed and design-system directories
// that live outside the project cwd. pi doesn't have an --add-dir
// sandbox flag (it uses OS cwd), so we use --append-system-prompt to
// hint that these directories exist. The agent can then use its Read
// tool to access files inside them. Without this, pi runs inside the
// project cwd and has no way to discover or reach skill/design-system
// assets that live elsewhere.
const dirs = (extraAllowedDirs || []).filter(
(d) => typeof d === 'string' && path.isAbsolute(d),
);
for (const d of dirs) {
args.push('--append-system-prompt', d);
}
// Pi has no directory-authorization flag. Resource paths are supplied
// in the composed RPC prompt; --append-system-prompt expects text or a
// file, so passing resource directories there causes EISDIR warnings.
return args;
},
// Prompt is sent via RPC `prompt` command on stdin, not as a CLI arg.
@@ -1,726 +0,0 @@
import { createHash } from 'node:crypto';
import {
OD_NEXT_RUNTIME_CAPABILITY_SNAPSHOT_V1_SCHEMA,
OD_NEXT_RUNTIME_CAPABILITY_EVIDENCE_V1_SCHEMA,
OD_NEXT_RUNTIME_FIXTURE_MANIFEST_V1_SCHEMA,
OdNextRuntimeCapabilitySnapshotV1Schema,
RuntimeCapabilityFixtureManifestV1Schema,
RuntimeCapabilityRegistryEntryV1Schema,
type CapabilitySupportV2,
type OdNextRuntimeCapabilitySnapshotV1,
type RuntimeCapabilityFixtureManifestV1,
type RuntimeCapabilityRegistryEntryV1,
type RuntimeCapabilitySnapshotSourceV1,
type RuntimeObservationEvidenceLevelV1,
} from '@open-design/contracts';
import type { RuntimeCapabilityMap } from './types.js';
export interface OdNextRuntimePathDescriptor {
runtimePath: string;
agentId: string;
runtimeAdapterVersion: string;
requiredRuntimeCompanionName?: string;
}
/**
* Initial rollout descriptors only. Capability support remains registry-driven:
* describing a path here does not make its adapter/schema contract eligible.
*/
export const OD_NEXT_RUNTIME_PATH_DESCRIPTORS = [
{
runtimePath: 'codex',
agentId: 'codex',
runtimeAdapterVersion: 'od-codex-json-events/v1',
},
{
runtimePath: 'claude-code',
agentId: 'claude',
runtimeAdapterVersion: 'od-claude-stream-json/v1',
},
{
runtimePath: 'native-opencode',
agentId: 'opencode',
runtimeAdapterVersion: 'od-opencode-json-events/v1',
},
{
runtimePath: 'vela-opencode',
agentId: 'amr',
runtimeAdapterVersion: 'od-vela-opencode-acp/v1',
requiredRuntimeCompanionName: 'opencode',
},
] as const satisfies readonly OdNextRuntimePathDescriptor[];
const ALL_REQUIRED_CASES = [
{ id: 'main_run', expectedMinimumEvidence: 'L0' },
{ id: 'tool', expectedMinimumEvidence: 'L1' },
{ id: 'child_success', expectedMinimumEvidence: 'L2' },
{ id: 'child_failure_parent_recovers', expectedMinimumEvidence: 'L2' },
{ id: 'cancel', expectedMinimumEvidence: 'L0' },
{ id: 'timeout', expectedMinimumEvidence: 'L0' },
{ id: 'resume', expectedMinimumEvidence: 'L0' },
] as const;
/**
* Exact Codex 0.147.0 tuple replayed by Open Design against the installed CLI
* and the matching rust-v0.147.0 source. The failure case uses a local
* Responses endpoint that closes only the already-started Child stream, so it
* exercises Codex's native task_complete.error and parent recovery path
* without sending telemetry or inference data to an online service.
*/
export const CODEX_0_147_0_BEST_EFFORT_MANIFEST =
RuntimeCapabilityFixtureManifestV1Schema.parse({
schema: OD_NEXT_RUNTIME_FIXTURE_MANIFEST_V1_SCHEMA,
fixtureVersion: 'codex-0.147.0-seven-path/v1',
runtimePath: 'codex',
agentId: 'codex',
agentCliVersion: 'codex-cli 0.147.0',
runtimeAdapterVersion: 'od-codex-json-events/v1',
provenance: {
kind: 'sanitized_real',
recordingDigest:
'sha256:729d0e58e80e7b8b81eb90ad286471a26c3df80411f0b0f7092c19d157b50cc6',
anonymizationVersion: 'od-runtime-evidence/v1',
evidenceReview: 'open_design_best_effort',
},
containsSensitiveContent: false,
cases: ALL_REQUIRED_CASES,
});
/**
* Open Design-owned best-effort replay for the exact native OpenCode tuple.
* The source recording is reduced to structural facts in the checked-in seed;
* no upstream or runtime-owner endorsement is implied.
*/
export const OPENCODE_1_18_18_BEST_EFFORT_MANIFEST =
RuntimeCapabilityFixtureManifestV1Schema.parse({
schema: OD_NEXT_RUNTIME_FIXTURE_MANIFEST_V1_SCHEMA,
fixtureVersion: 'opencode-1.18.18-seven-path/v1',
runtimePath: 'native-opencode',
agentId: 'opencode',
agentCliVersion: '1.18.18',
runtimeAdapterVersion: 'od-opencode-json-events/v1',
provenance: {
kind: 'sanitized_real',
recordingDigest:
'sha256:b1224716a340401879cfb2f366d1252e9f837ce24050d9b0dfa4430f89492fc5',
anonymizationVersion: 'od-runtime-evidence/v1',
evidenceReview: 'open_design_best_effort',
},
containsSensitiveContent: false,
cases: ALL_REQUIRED_CASES,
});
/**
* Open Design-owned replay of the exact Claude 2.1.233 Agent stream-json
* protocol. The checked-in seed contains structural identities only; no
* Prompt body, output, path, credential, or upstream endorsement is retained.
*/
export const CLAUDE_2_1_233_BEST_EFFORT_MANIFEST =
RuntimeCapabilityFixtureManifestV1Schema.parse({
schema: OD_NEXT_RUNTIME_FIXTURE_MANIFEST_V1_SCHEMA,
fixtureVersion: 'claude-2.1.233-seven-path/v1',
runtimePath: 'claude-code',
agentId: 'claude',
agentCliVersion: '2.1.233 (Claude Code)',
runtimeAdapterVersion: 'od-claude-stream-json/v1',
provenance: {
kind: 'sanitized_real',
recordingDigest:
'sha256:5681a9a211562e119efc470b6846a4be8b3ee822f4e15f8967c19871e69c9b8b',
anonymizationVersion: 'od-runtime-evidence/v1',
evidenceReview: 'open_design_best_effort',
},
containsSensitiveContent: false,
cases: ALL_REQUIRED_CASES,
});
/**
* Provider-backed local replay for the Vela candidate paired with native
* OpenCode 1.18.18. All seven paths pass.
*
* Vela drives the same native OpenCode runtime this registry already admits
* under `native-opencode`; its Child mechanism is that runtime's, reached over
* the ACP extension rather than the CLI stream. Holding the tuple out of the
* registry therefore did not withhold an unproven capability — it refused
* complex execution to an agent whose evidence paths all pass, on the separate
* grounds that the build is not yet immutable. Admit it, and re-pin
* `agentCliVersion` here once Vela publishes a build that reports a stable
* producer version — this field records which CLI the fixture was captured
* against, and nothing gates on it.
*
* Child evidence is reached by ACP capability negotiation, not by sniffing that
* version: a Vela without the `vela.opencode.child_agent_lifecycle` producer
* simply never advertises it, so a complex task blocks on honestly missing
* evidence, and one that does advertise it works the moment it is installed.
* That is why admitting the tuple cannot make an older Vela claim Children it
* never reported, and why no version pin has to be walked back when the
* producer ships.
*/
export const VELA_OPENCODE_LOCAL_BEST_EFFORT_MANIFEST =
RuntimeCapabilityFixtureManifestV1Schema.parse({
schema: OD_NEXT_RUNTIME_FIXTURE_MANIFEST_V1_SCHEMA,
fixtureVersion: 'vela-opencode-0.0.1-local-opencode-1.18.18-seven-path/v1',
runtimePath: 'vela-opencode',
agentId: 'amr',
agentCliVersion: '0.0.1-od-next-local',
runtimeAdapterVersion: 'od-vela-opencode-acp/v1',
runtimeCompanionName: 'opencode',
runtimeCompanionVersion: '1.18.18',
provenance: {
kind: 'sanitized_real',
recordingDigest:
'sha256:6fe49f1e0946b2220052b2239494786879c03c972b5be12dee30a7973872f6aa',
anonymizationVersion: 'od-runtime-evidence/v1',
evidenceReview: 'open_design_best_effort',
},
containsSensitiveContent: false,
cases: ALL_REQUIRED_CASES,
});
export const OD_NEXT_RUNTIME_CAPABILITY_FIXTURE_MANIFESTS:
readonly RuntimeCapabilityFixtureManifestV1[] = [
CODEX_0_147_0_BEST_EFFORT_MANIFEST,
CLAUDE_2_1_233_BEST_EFFORT_MANIFEST,
OPENCODE_1_18_18_BEST_EFFORT_MANIFEST,
VELA_OPENCODE_LOCAL_BEST_EFFORT_MANIFEST,
];
export const OD_NEXT_RUNTIME_CAPABILITY_REGISTRY:
readonly RuntimeCapabilityRegistryEntryV1[] = [
CODEX_0_147_0_BEST_EFFORT_MANIFEST,
CLAUDE_2_1_233_BEST_EFFORT_MANIFEST,
OPENCODE_1_18_18_BEST_EFFORT_MANIFEST,
VELA_OPENCODE_LOCAL_BEST_EFFORT_MANIFEST,
].map((manifest) => RuntimeCapabilityRegistryEntryV1Schema.parse({
runtimePath: manifest.runtimePath,
agentId: manifest.agentId,
recordedAgentCliVersion: manifest.agentCliVersion!,
runtimeAdapterVersion: manifest.runtimeAdapterVersion,
...(manifest.runtimeCompanionName
? { recordedRuntimeCompanionName: manifest.runtimeCompanionName }
: {}),
...(manifest.runtimeCompanionVersion
? { recordedRuntimeCompanionVersion: manifest.runtimeCompanionVersion }
: {}),
fixtureVersion: manifest.fixtureVersion,
fixtureHash: hashRuntimeCapabilityFixtureManifestV1(manifest),
evidence: {
schema: OD_NEXT_RUNTIME_CAPABILITY_EVIDENCE_V1_SCHEMA,
source: 'fixture_replay',
nativeSessionContinuation: { support: 'verified', evidenceLevel: 'L0' },
nativeSubagents: { support: 'verified', evidenceLevel: 'L2' },
caseResults: ALL_REQUIRED_CASES.map(({ id }) => ({ id, outcome: 'passed' })),
},
}));
export type OdNextCapabilityResolutionReason =
| 'runtime_out_of_scope'
| 'runtime_version_denied'
| 'fixture_manifest_missing'
| 'fixture_manifest_invalid'
| 'fixture_tuple_mismatch'
| 'x1_runtime_fixture_missing'
| 'synthetic_fixture_not_accepted'
| 'capability_tuple_unverified'
| 'fixture_hash_mismatch'
| 'capability_resolved';
export interface ResolveOdNextRuntimeCapabilityInput {
agentId: string;
agentCliVersion?: string | null;
runtimeCompanionName?: string | null;
runtimeCompanionVersion?: string | null;
fixtureVersion: string;
fixtureManifest?: unknown;
registry?: readonly RuntimeCapabilityRegistryEntryV1[];
incompatibleVersions?: readonly {
agentId: string;
agentCliVersion: string;
}[];
capturedAt?: number;
}
/**
* Confirmed adapter-breaking releases only. Keep this list narrow and empty
* unless a concrete incompatibility has been reproduced; unknown versions are
* forward-compatible by default.
*/
const OD_NEXT_RUNTIME_INCOMPATIBLE_VERSIONS: readonly {
agentId: string;
agentCliVersion: string;
}[] = [];
export interface OdNextRuntimeCapabilityResolution {
includedInInitialRollout: boolean;
tupleMatched: boolean;
reason: OdNextCapabilityResolutionReason;
snapshot: OdNextRuntimeCapabilitySnapshotV1 | null;
}
function canonicalize(value: unknown): unknown {
if (value === null || typeof value !== 'object') return value;
if (Array.isArray(value)) return value.map(canonicalize);
return Object.fromEntries(
Object.entries(value as Record<string, unknown>)
.filter(([, nested]) => nested !== undefined)
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
.map(([key, nested]) => [key, canonicalize(nested)]),
);
}
function sha256Canonical(value: unknown): string {
return `sha256:${createHash('sha256')
.update(JSON.stringify(canonicalize(value)), 'utf8')
.digest('hex')}`;
}
/** Hash only the replay contract and recording identity, not review timestamps. */
export function hashRuntimeCapabilityFixtureManifestV1(
input: unknown,
): string {
const manifest = RuntimeCapabilityFixtureManifestV1Schema.parse(input);
return sha256Canonical({
schema: manifest.schema,
fixtureVersion: manifest.fixtureVersion,
runtimePath: manifest.runtimePath,
agentId: manifest.agentId,
agentCliVersion: manifest.agentCliVersion ?? null,
runtimeAdapterVersion: manifest.runtimeAdapterVersion,
runtimeCompanionName: manifest.runtimeCompanionName ?? null,
runtimeCompanionVersion: manifest.runtimeCompanionVersion ?? null,
provenance: manifest.provenance.kind === 'sanitized_real'
? {
kind: manifest.provenance.kind,
recordingDigest: manifest.provenance.recordingDigest,
anonymizationVersion: manifest.provenance.anonymizationVersion,
}
: { kind: manifest.provenance.kind },
containsSensitiveContent: manifest.containsSensitiveContent,
cases: [...manifest.cases].sort(({ id: left }, { id: right }) => (
left < right ? -1 : left > right ? 1 : 0
)),
});
}
function snapshotHashInput(
snapshot: Omit<OdNextRuntimeCapabilitySnapshotV1, 'snapshotHash'>,
): unknown {
return {
schema: snapshot.schema,
runtimePath: snapshot.runtimePath,
agentId: snapshot.agentId,
agentCliVersion: snapshot.agentCliVersion ?? null,
runtimeAdapterVersion: snapshot.runtimeAdapterVersion,
runtimeCompanionName: snapshot.runtimeCompanionName ?? null,
runtimeCompanionVersion: snapshot.runtimeCompanionVersion ?? null,
recordedAgentCliVersion: snapshot.recordedAgentCliVersion ?? null,
recordedRuntimeCompanionName: snapshot.recordedRuntimeCompanionName ?? null,
recordedRuntimeCompanionVersion: snapshot.recordedRuntimeCompanionVersion ?? null,
fixtureVersion: snapshot.fixtureVersion,
fixtureHash: snapshot.fixtureHash ?? null,
nativeSessionContinuation: snapshot.nativeSessionContinuation,
nativeSubagents: snapshot.nativeSubagents,
};
}
/** capturedAt is excluded; actual diagnostics and recorded provenance remain auditable. */
export function hashOdNextRuntimeCapabilitySnapshotV1(
snapshot: Omit<OdNextRuntimeCapabilitySnapshotV1, 'snapshotHash'>,
): string {
return sha256Canonical(snapshotHashInput(snapshot));
}
function descriptorForAgent(agentId: string): OdNextRuntimePathDescriptor | null {
return OD_NEXT_RUNTIME_PATH_DESCRIPTORS.find((descriptor) => (
descriptor.agentId === agentId
)) ?? null;
}
function buildSnapshot(input: {
descriptor: OdNextRuntimePathDescriptor;
agentCliVersion?: string | undefined;
runtimeCompanionName?: string | undefined;
runtimeCompanionVersion?: string | undefined;
recordedAgentCliVersion?: string | undefined;
recordedRuntimeCompanionName?: string | undefined;
recordedRuntimeCompanionVersion?: string | undefined;
fixtureVersion: string;
fixtureHash?: string | undefined;
continuationSupport: CapabilitySupportV2;
continuationEvidenceLevel: RuntimeObservationEvidenceLevelV1;
subagentSupport: CapabilitySupportV2;
subagentEvidenceLevel: RuntimeObservationEvidenceLevelV1;
source: RuntimeCapabilitySnapshotSourceV1;
capturedAt: number;
}): OdNextRuntimeCapabilitySnapshotV1 {
const withoutHash: Omit<OdNextRuntimeCapabilitySnapshotV1, 'snapshotHash'> = {
schema: OD_NEXT_RUNTIME_CAPABILITY_SNAPSHOT_V1_SCHEMA,
runtimePath: input.descriptor.runtimePath,
agentId: input.descriptor.agentId,
...(input.agentCliVersion ? { agentCliVersion: input.agentCliVersion } : {}),
runtimeAdapterVersion: input.descriptor.runtimeAdapterVersion,
...(input.runtimeCompanionName
? { runtimeCompanionName: input.runtimeCompanionName }
: {}),
...(input.runtimeCompanionVersion
? { runtimeCompanionVersion: input.runtimeCompanionVersion }
: {}),
...(input.recordedAgentCliVersion
? { recordedAgentCliVersion: input.recordedAgentCliVersion }
: {}),
...(input.recordedRuntimeCompanionName
? { recordedRuntimeCompanionName: input.recordedRuntimeCompanionName }
: {}),
...(input.recordedRuntimeCompanionVersion
? { recordedRuntimeCompanionVersion: input.recordedRuntimeCompanionVersion }
: {}),
fixtureVersion: input.fixtureVersion,
...(input.fixtureHash ? { fixtureHash: input.fixtureHash } : {}),
nativeSessionContinuation: {
support: input.continuationSupport,
evidenceLevel: input.continuationEvidenceLevel,
source: input.source,
},
nativeSubagents: {
support: input.subagentSupport,
evidenceLevel: input.subagentEvidenceLevel,
source: input.source,
},
capturedAt: input.capturedAt,
};
return OdNextRuntimeCapabilitySnapshotV1Schema.parse({
...withoutHash,
snapshotHash: hashOdNextRuntimeCapabilitySnapshotV1(withoutHash),
});
}
function unknownResolution(input: {
descriptor: OdNextRuntimePathDescriptor;
agentCliVersion?: string | undefined;
runtimeCompanionName?: string | undefined;
runtimeCompanionVersion?: string | undefined;
fixtureVersion: string;
fixtureHash?: string | undefined;
capturedAt: number;
reason: Exclude<OdNextCapabilityResolutionReason, 'runtime_out_of_scope' | 'capability_resolved'>;
tupleMatched?: boolean;
source?: RuntimeCapabilitySnapshotSourceV1 | undefined;
continuationEvidenceLevel?: RuntimeObservationEvidenceLevelV1 | undefined;
subagentEvidenceLevel?: RuntimeObservationEvidenceLevelV1 | undefined;
}): OdNextRuntimeCapabilityResolution {
return {
includedInInitialRollout: true,
tupleMatched: input.tupleMatched ?? false,
reason: input.reason,
snapshot: buildSnapshot({
descriptor: input.descriptor,
agentCliVersion: input.agentCliVersion,
runtimeCompanionName: input.runtimeCompanionName,
runtimeCompanionVersion: input.runtimeCompanionVersion,
fixtureVersion: input.fixtureVersion,
fixtureHash: input.fixtureHash,
continuationSupport: 'unknown',
continuationEvidenceLevel: input.continuationEvidenceLevel ?? 'L0',
subagentSupport: 'unknown',
subagentEvidenceLevel: input.subagentEvidenceLevel ?? 'L0',
source: input.source ?? 'unverified',
capturedAt: input.capturedAt,
}),
};
}
function manifestMatchesCapabilityContract(
manifest: RuntimeCapabilityFixtureManifestV1,
input: {
descriptor: OdNextRuntimePathDescriptor;
fixtureVersion: string;
},
): boolean {
return manifest.runtimePath === input.descriptor.runtimePath &&
manifest.agentId === input.descriptor.agentId &&
manifest.runtimeAdapterVersion === input.descriptor.runtimeAdapterVersion &&
manifest.fixtureVersion === input.fixtureVersion &&
(manifest.runtimeCompanionName ?? undefined) ===
input.descriptor.requiredRuntimeCompanionName;
}
function entryMatchesCapabilityContract(
entry: RuntimeCapabilityRegistryEntryV1,
input: {
descriptor: OdNextRuntimePathDescriptor;
fixtureVersion: string;
manifest: RuntimeCapabilityFixtureManifestV1;
},
): boolean {
return entry.runtimePath === input.descriptor.runtimePath &&
entry.agentId === input.descriptor.agentId &&
entry.runtimeAdapterVersion === input.descriptor.runtimeAdapterVersion &&
entry.fixtureVersion === input.fixtureVersion &&
entry.recordedAgentCliVersion === input.manifest.agentCliVersion &&
(entry.recordedRuntimeCompanionName ?? undefined) ===
(input.manifest.runtimeCompanionName ?? undefined) &&
(entry.recordedRuntimeCompanionVersion ?? undefined) ===
(input.manifest.runtimeCompanionVersion ?? undefined);
}
function hasCompleteOdNextCapability(
evidence: RuntimeCapabilityRegistryEntryV1['evidence'],
): boolean {
return evidence.nativeSessionContinuation.support === 'verified' &&
evidence.nativeSubagents.support === 'verified' &&
(
evidence.nativeSubagents.evidenceLevel === 'L2' ||
evidence.nativeSubagents.evidenceLevel === 'L3'
);
}
export function resolveOdNextRuntimeCapability(
input: ResolveOdNextRuntimeCapabilityInput,
): OdNextRuntimeCapabilityResolution {
const descriptor = descriptorForAgent(input.agentId);
if (!descriptor) {
return {
includedInInitialRollout: false,
tupleMatched: false,
reason: 'runtime_out_of_scope',
snapshot: null,
};
}
const capturedAt = input.capturedAt ?? Date.now();
const agentCliVersion = input.agentCliVersion?.trim() || undefined;
const runtimeCompanionName = input.runtimeCompanionName?.trim() || undefined;
const runtimeCompanionVersion = input.runtimeCompanionVersion?.trim() || undefined;
const base = {
descriptor,
agentCliVersion,
runtimeCompanionName,
runtimeCompanionVersion,
fixtureVersion: input.fixtureVersion,
capturedAt,
};
if ((input.incompatibleVersions ?? OD_NEXT_RUNTIME_INCOMPATIBLE_VERSIONS).some(
(entry) => entry.agentId === descriptor.agentId
&& entry.agentCliVersion === agentCliVersion,
)) {
return unknownResolution({ ...base, reason: 'runtime_version_denied' });
}
if (input.fixtureManifest === undefined) {
return unknownResolution({ ...base, reason: 'fixture_manifest_missing' });
}
const parsedManifest = RuntimeCapabilityFixtureManifestV1Schema.safeParse(
input.fixtureManifest,
);
if (!parsedManifest.success) {
return unknownResolution({ ...base, reason: 'fixture_manifest_invalid' });
}
const manifest = parsedManifest.data;
const fixtureHash = hashRuntimeCapabilityFixtureManifestV1(manifest);
if (manifest.provenance.kind === 'contract_only') {
return unknownResolution({
...base,
fixtureHash,
reason: 'x1_runtime_fixture_missing',
});
}
const contractInput = {
descriptor,
fixtureVersion: input.fixtureVersion,
};
if (!manifestMatchesCapabilityContract(manifest, contractInput)) {
return unknownResolution({ ...base, fixtureHash, reason: 'fixture_tuple_mismatch' });
}
const registry = (input.registry ?? OD_NEXT_RUNTIME_CAPABILITY_REGISTRY)
.flatMap((candidate) => {
const parsed = RuntimeCapabilityRegistryEntryV1Schema.safeParse(candidate);
return parsed.success ? [parsed.data] : [];
});
const entry = registry.find((candidate) => entryMatchesCapabilityContract(candidate, {
...contractInput,
manifest,
}));
if (!entry) {
return unknownResolution({
...base,
fixtureHash,
tupleMatched: true,
reason: manifest.provenance.kind === 'test_synthetic'
? 'synthetic_fixture_not_accepted'
: 'capability_tuple_unverified',
source: manifest.provenance.kind === 'test_synthetic'
? 'test_synthetic'
: 'unverified',
});
}
if (entry.fixtureHash !== fixtureHash) {
return unknownResolution({
...base,
fixtureHash,
tupleMatched: true,
reason: 'fixture_hash_mismatch',
});
}
if (entry.evidence.source !== 'fixture_replay') {
return unknownResolution({
...base,
fixtureHash,
tupleMatched: true,
reason: entry.evidence.source === 'test_synthetic'
? 'synthetic_fixture_not_accepted'
: 'capability_tuple_unverified',
source: entry.evidence.source === 'test_synthetic'
? 'test_synthetic'
: 'unverified',
continuationEvidenceLevel: entry.evidence.nativeSessionContinuation.evidenceLevel,
subagentEvidenceLevel: entry.evidence.nativeSubagents.evidenceLevel,
});
}
if (
manifest.provenance.kind === 'test_synthetic'
) {
return unknownResolution({
...base,
fixtureHash,
tupleMatched: true,
reason: 'synthetic_fixture_not_accepted',
source: 'test_synthetic',
continuationEvidenceLevel: entry.evidence.nativeSessionContinuation.evidenceLevel,
subagentEvidenceLevel: entry.evidence.nativeSubagents.evidenceLevel,
});
}
if (!hasCompleteOdNextCapability(entry.evidence)) {
return unknownResolution({
...base,
fixtureHash,
tupleMatched: true,
reason: 'capability_tuple_unverified',
continuationEvidenceLevel: entry.evidence.nativeSessionContinuation.evidenceLevel,
subagentEvidenceLevel: entry.evidence.nativeSubagents.evidenceLevel,
});
}
const snapshotSource: RuntimeCapabilitySnapshotSourceV1 =
'sanitized_fixture_replay';
return {
includedInInitialRollout: true,
tupleMatched: true,
reason: 'capability_resolved',
snapshot: buildSnapshot({
...base,
fixtureHash,
recordedAgentCliVersion: entry.recordedAgentCliVersion,
recordedRuntimeCompanionName: entry.recordedRuntimeCompanionName,
recordedRuntimeCompanionVersion: entry.recordedRuntimeCompanionVersion,
continuationSupport: entry.evidence.nativeSessionContinuation.support,
continuationEvidenceLevel:
entry.evidence.nativeSessionContinuation.evidenceLevel,
subagentSupport: entry.evidence.nativeSubagents.support,
subagentEvidenceLevel: entry.evidence.nativeSubagents.evidenceLevel,
source: snapshotSource,
}),
};
}
export function resolveBundledOdNextRuntimeCapability(input: {
agentId: string;
agentCliVersion?: string | null;
runtimeCompanionName?: string | null;
runtimeCompanionVersion?: string | null;
capturedAt?: number;
}): OdNextRuntimeCapabilityResolution {
const descriptor = descriptorForAgent(input.agentId);
const fixture = OD_NEXT_RUNTIME_CAPABILITY_FIXTURE_MANIFESTS.find((candidate) => (
descriptor !== null
&& candidate.agentId === descriptor.agentId
&& candidate.runtimePath === descriptor.runtimePath
&& candidate.runtimeAdapterVersion === descriptor.runtimeAdapterVersion
&& (candidate.runtimeCompanionName ?? undefined) ===
descriptor.requiredRuntimeCompanionName
));
return resolveOdNextRuntimeCapability({
...input,
fixtureVersion: fixture?.fixtureVersion ?? 'od-next-runtime-contract/v1',
...(fixture ? { fixtureManifest: fixture } : {}),
registry: OD_NEXT_RUNTIME_CAPABILITY_REGISTRY,
});
}
/**
* Advertised `--help` capability keys an admitted OD Next runtime must expose
* on the *installed* CLI, keyed by agent id.
*
* The fixture registry above proves what a runtime *path* is capable of; it
* says nothing about the build the user actually has on disk. `buildArgs`
* refuses to launch an admitted OD Next Run whose CLI does not advertise these
* flags, so admission must establish the same facts. Otherwise the daemon
* admits a task it cannot start and the user's Run dies with
* AGENT_EXECUTION_FAILED instead of quietly taking the ordinary route.
*
* `forwardSubagentText` backs native Child behaviour observation, which every
* admitted Claude strategy Run enables. Custom --agents definitions belonged
* to the retired Build Package protocol and are no longer required.
*/
export const OD_NEXT_REQUIRED_ADVERTISED_CAPABILITIES: Readonly<
Record<string, readonly string[]>
> = {
claude: ['forwardSubagentText'],
};
/**
* Names the advertised capability keys OD Next requires but the installed CLI
* did not advertise. Empty means the installed build satisfies the contract;
* a null capability map means the probe could not read `--help` at all, which
* is treated as "not advertised" so admission stays fail-closed.
*/
export function odNextAdvertisedCapabilityGap(input: {
agentId: string;
advertised: RuntimeCapabilityMap | null;
}): string[] {
const required = OD_NEXT_REQUIRED_ADVERTISED_CAPABILITIES[input.agentId];
if (!required || required.length === 0) return [];
const advertised = input.advertised ?? {};
return required.filter((key) => advertised[key] !== true);
}
export type OdNextExecutionMode = 'simple' | 'complex';
export type OdNextExecutionEligibilityReason =
| 'eligible'
| 'native_continuation_not_verified'
| 'native_subagents_not_verified'
| 'structured_child_lifecycle_not_verified';
/**
* Pure policy boundary used after a runtime has passed initial-path selection.
* Fixture provenance is enforced by the resolver; this function only applies
* the complete OD Next capability rules to the resolved snapshot. Both modes
* rely on native child lifecycle even when the current task is classified as
* simple, because the strategy may escalate after planning.
*/
export function evaluateOdNextExecutionEligibility(
capabilities: {
nativeSessionContinuation: { support: CapabilitySupportV2 };
nativeSubagents: {
support: CapabilitySupportV2;
evidenceLevel: RuntimeObservationEvidenceLevelV1;
};
},
_executionMode: OdNextExecutionMode,
): { eligible: boolean; reason: OdNextExecutionEligibilityReason } {
if (capabilities.nativeSessionContinuation.support !== 'verified') {
return { eligible: false, reason: 'native_continuation_not_verified' };
}
if (capabilities.nativeSubagents.support !== 'verified') {
return { eligible: false, reason: 'native_subagents_not_verified' };
}
if (
capabilities.nativeSubagents.evidenceLevel !== 'L2' &&
capabilities.nativeSubagents.evidenceLevel !== 'L3'
) {
return { eligible: false, reason: 'structured_child_lifecycle_not_verified' };
}
return { eligible: true, reason: 'eligible' };
}
@@ -227,10 +227,10 @@ export const OD_NEXT_EXACT_INPUT_MAP_V1 = [
{
id: 'runtime_facts',
classification: 'initial_bundle',
source: 'renderOdNextRuntimeFactsV2()',
source: 'empty runtime facts slot',
owner: 'bundle context serializer',
textTarget: 'context/runtime_facts',
note: 'Runtime-owned planning facts the Agent copies verbatim; per-task, so excluded from the cache-stable head.',
note: 'The existing Bundle slot stays empty; runtime capability proof is no longer sent to the model.',
},
{
id: 'runtime_tool_prompt',
@@ -559,15 +559,6 @@ export const OD_NEXT_SEMANTIC_REQUEST_FACT_MAP_V1 = [
textTarget: 'task_metadata/task_type',
note: 'Selects the verified core recipe and is serialized as the canonical Task 04 task type.',
},
{
id: 'strategy_runtime_capability_facts',
classification: 'initial_bundle',
producer: 'daemon_system_prompt',
source: 'odNextStrategyRecipe.planningFacts and capabilitySnapshotHash',
owner: 'Task 02 core recipe; Task 04 canonical task configuration',
textTarget: 'context/runtime_facts',
note: 'Runtime facts are per-task, so they are emitted as Bundle context instead of inside the cross-task-constant head; task_metadata carries only the stable allowlisted execution selection.',
},
{
id: 'request_execution_configuration',
classification: 'out_of_band',
+31 -32
View File
@@ -33,7 +33,6 @@ import {
composeOdNextStrategyCorePromptV2,
OD_NEXT_BUNDLE_ECHO_GUARD_V2,
odNextStrategyRecipeIdentityV2,
renderOdNextRuntimeFactsV2,
composeOdNextStrategyStableRequestContextV2,
executionProfileFromStreamFormat,
PLUGIN_SHARE_ACTION_PLUGIN_IDS,
@@ -1196,6 +1195,7 @@ import {
isZeroConfigClipperLibraryRequest,
parseHostHeader,
} from './origin-validation.js';
import { projectPoweredPreviewOriginDecision } from './http/powered-preview-origin.js';
import { registerLibraryRoutes } from './routes/library.js';
import {
libraryExtensionAllowedOrigins,
@@ -3460,10 +3460,23 @@ export async function startServer({
/^\/projects\/[^/]+\/(?:raw|preview)\/|^\/codex-pets\/[^/]+\/spritesheet$|^\/asset-cache$/;
const _POWERED_PREVIEW_SAFE_RE = /^\/projects\/[^/]+\/powered\/.+$/u;
// Reject cross-origin requests to API endpoints.
// Health/version remain open for monitoring probes.
// Non-browser clients (no Origin header) are always allowed.
// Reserve project preview hosts before API exceptions and static serving.
app.use((req, res, next) => {
const projectPreviewDecision = projectPoweredPreviewOriginDecision(req, resolvedPort);
if (projectPreviewDecision === 'reject') {
return res.status(403).json({
error: 'Powered preview origin cannot access this API route',
});
}
res.locals.projectPoweredPreview = projectPreviewDecision === 'allow';
next();
});
// Reject cross-origin requests to API endpoints. Normal non-browser
// clients (no Origin header) retain their existing access.
app.use('/api', (req, res, next) => {
if (res.locals.projectPoweredPreview === true) return next();
// Live artifact previews have stricter local-daemon validation and
// loopback CORS handling on the route itself. Let that middleware produce
// the structured error shape and preflight headers for preview embeds.
@@ -8697,9 +8710,9 @@ export async function startServer({
// Powered-preview isolation info. Reports the daemon's own directly-reachable
// http origin so the web host can render WebGL/Worker/WASM/SharedArrayBuffer
// artifacts in a cross-origin-isolated iframe (see the /powered route and
// apps/web/src/runtime/powered-preview.ts). The web host always swaps this
// loopback hostname before loading powered files; the /api origin middleware
// then treats that swapped browser origin as preview-only.
// apps/web/src/runtime/powered-preview.ts). The web host derives a separate
// loopback hostname per project; the /api origin middleware reserves each
// hostname for that project's powered GETs.
app.get('/api/preview/isolation', (_req, res) => {
const reportHost = reportHostForPoweredPreview();
const baseOrigin = resolvedPort ? `http://${reportHost}:${resolvedPort}` : null;
@@ -8707,6 +8720,7 @@ export async function startServer({
/** @type {import('@open-design/contracts').ProjectPreviewIsolationResponse} */
const body = {
supported: Boolean(baseOrigin),
projectOrigins: true,
baseOrigin,
pathPrefix: 'powered',
};
@@ -10334,9 +10348,6 @@ export async function startServer({
platformHintSignal,
devicePlatformSignal = null,
frozenSkillPackage,
odNextSyntheticCanary = false,
odNextAutomaticAdmission = false,
runtimeCapabilitySnapshot,
}) => {
const project =
typeof projectId === 'string' && projectId
@@ -11003,23 +11014,13 @@ export async function startServer({
}
// Strategy identity, package/profile drift checks, atom loading, runtime
// capability facts, and local-only synthetic policy are owned by the OD
// Next prompt service. This composition root only supplies detected runtime
// versions and process configuration.
// profile selection are owned by the OD Next prompt service.
const odNextStrategyRecipe = await resolveOdNextPromptRecipeForRun({
db,
bundledPluginsDir: BUNDLED_PLUGINS_DIR,
appliedPluginSnapshotId,
agentId,
streamFormat,
atomPromptsEnabled: bundledAtomPromptsEnabled,
syntheticCanary: odNextSyntheticCanary,
automaticAdmission: odNextAutomaticAdmission,
runtimeCapabilitySnapshot,
getRuntimeVersions: () => ensureDetectedRuntimeVersions(
agentId,
agentCliEnvForAgent(appConfigForPrompt?.agentCliEnv, agentId),
),
});
// Hoisted verbatim out of the composeSystemPrompt() call so the exact same
@@ -11210,7 +11211,6 @@ export async function startServer({
assertOdNextSemanticRequestFactProducerCoverage('daemon_system_prompt', {
strategy_task_skill: odNextStrategyRecipe.taskSkill,
strategy_task_type: odNextStrategyRecipe.taskType,
strategy_runtime_capability_facts: odNextStrategyRecipe.planningFacts,
project_and_design_context: {
metadata,
template,
@@ -11232,18 +11232,14 @@ export async function startServer({
? composeOdNextStrategyCorePromptV2(odNextStrategyRecipe)
: composeSystemPrompt(systemPromptInputs);
// OD Next sends a canonical XML tree, not this Markdown string. The tree's
// cache-stable head, its per-task identity, and the runtime-owned planning
// facts are three separate products of the same verified recipe, so they are
// composed here together and placed in different bundle slots downstream.
// cache-stable head and per-task identity are composed here together and
// placed in their existing bundle slots downstream.
const odNextBundleHead = odNextStrategyRecipe
? composeOdNextStrategyBundleHeadV2(odNextStrategyRecipe)
: null;
const odNextRecipeIdentity = odNextStrategyRecipe
? odNextStrategyRecipeIdentityV2(odNextStrategyRecipe)
: null;
const odNextRuntimeFacts = odNextStrategyRecipe
? renderOdNextRuntimeFactsV2(odNextStrategyRecipe, odNextStableRequestContext ?? {})
: '';
// The chat handler also needs to know where the active skill lives
// on disk so it can stage a per-project copy of its side files
// before spawning the agent. Returning that here avoids a second
@@ -11254,7 +11250,7 @@ export async function startServer({
prompt,
odNextBundleHead,
odNextRecipeIdentity,
odNextRuntimeFacts,
odNextRuntimeFacts: '',
odNextStableContextPrompt: odNextStableRequestContext
? composeOdNextStrategyStableRequestContextV2(
odNextStableRequestContext,
@@ -16888,8 +16884,8 @@ export async function startServer({
));
return finishWithRetryDecision('failed', 0, signal);
}
// ACP agents that don't shut down on stdin.end() (e.g. Devin for
// Terminal) are forced to exit via SIGTERM from attachAcpSession after
// Native agents that don't shut down on stdin.end() (e.g. Devin or Pi)
// are forced to exit via SIGTERM after
// a clean prompt completion. Without an override, the chat run would
// be marked `failed` because `code === 0` fails (code is null on a
// signal exit). `completedSuccessfully()` reports whether the ACP
@@ -16910,7 +16906,10 @@ export async function startServer({
code,
signal,
acpCleanCompletion,
acpCompletionShutdownRequested,
acpCompletionShutdownRequested: acpCompletionShutdownRequested || (
typeof acpSession?.completionShutdownRequested === 'function' &&
acpSession.completionShutdownRequested()
),
artifactQuietShutdownRequested,
turnCompletedCleanly: !!run.turnCompletedCleanly,
artifactProducedThisRun:
@@ -348,7 +348,7 @@ export function getFrozenSkillPackage(
throw new InvalidFrozenSkillPackageError('Frozen Skill package JSON is malformed.');
}
const verified = verifyFrozenSkillPackage(parsed);
if (verified.identity !== row.identity || canonicalJson(verified) !== row.payloadJson) {
if (verified.identity !== row.identity) {
throw new InvalidFrozenSkillPackageError('Frozen Skill package identity is tampered.');
}
return verified;
@@ -933,9 +933,6 @@ function verifyFile(value: unknown): FrozenSkillFileV1 {
throw new InvalidFrozenSkillPackageError('Frozen Skill file shape is invalid.');
}
const bytes = Buffer.from(file.bytesBase64, 'base64');
if (bytes.toString('base64') !== file.bytesBase64) {
throw new InvalidFrozenSkillPackageError(`Frozen Skill file ${file.path} is not canonical base64.`);
}
if (bytes.byteLength !== file.byteLength || digestBytes(bytes) !== file.digest) {
throw new InvalidFrozenSkillPackageError(`Frozen Skill file ${file.path} is tampered.`);
}
@@ -2,7 +2,6 @@ import path from 'node:path';
import {
OD_NEXT_BUNDLE_ECHO_GUARD_V2,
OdNextRuntimeCapabilitySnapshotV1Schema,
detectOdNextDevicePlatformFromText,
executionProfileFromStreamFormat,
renderChatTurnHostProtocolInstructions,
@@ -42,12 +41,6 @@ import {
import { renderRunContextPrompt } from '../../runtimes/chat-run-context.js';
import type { RunWorkspaceScope } from '../../runtimes/project-amr-trace-env.js';
import type { RuntimeAgentDef } from '../../runtimes/types.js';
import type { DetectedRuntimeVersions } from '../../runtimes/detection.js';
import {
evaluateOdNextExecutionEligibility,
hashOdNextRuntimeCapabilitySnapshotV1,
resolveBundledOdNextRuntimeCapability,
} from '../../runtimes/od-next-capability-gate.js';
import {
renderFrozenSkillRosterContext,
resolveFrozenSkillBundleBodies,
@@ -57,7 +50,6 @@ import {
loadOdNextTaskInputSnapshot,
type OdNextTaskInputSnapshotDescriptor,
} from './task-input-snapshot.js';
import { daemonOwnedOdNextPlanningCatalog } from './resolver.js';
type SqliteDb = Parameters<typeof getProject>[0];
type IntentSignals = ReturnType<typeof latchConversationIntentSignals>;
@@ -98,7 +90,6 @@ export interface OdNextInitialPromptMeta {
syntheticCanary?: boolean;
effectiveMode?: 'off' | 'observe' | 'active';
} | null;
runtimeCapabilitySnapshot?: unknown;
}
interface DaemonSystemPromptResult {
@@ -156,18 +147,13 @@ function runScopedMcpServers(toolBundle: unknown): McpServerConfig[] {
return Array.isArray(servers) ? servers as McpServerConfig[] : [];
}
/** Resolve and enrich the exact strategy recipe with daemon-owned runtime facts. */
/** Resolve the installed strategy recipe using its existing execution profile. */
export async function resolveOdNextPromptRecipeForRun(input: {
db: SqliteDb;
bundledPluginsDir: string;
appliedPluginSnapshotId: unknown;
agentId: string;
streamFormat: string;
atomPromptsEnabled: boolean;
syntheticCanary: boolean;
automaticAdmission: boolean;
runtimeCapabilitySnapshot?: unknown;
getRuntimeVersions: () => Promise<DetectedRuntimeVersions | null>;
}): Promise<OdNextStrategyRequestRecipeV2 | null> {
const snapshotId = stringValue(input.appliedPluginSnapshotId);
if (!snapshotId) return null;
@@ -186,74 +172,7 @@ export async function resolveOdNextPromptRecipeForRun(input: {
});
if (!resolvedRecipe) return null;
if (input.syntheticCanary && process.env.NODE_ENV === 'production') {
throw new Error('OD Next synthetic runtime planning facts are local-only.');
}
let capabilitySnapshot;
if (input.runtimeCapabilitySnapshot !== undefined) {
const parsed = OdNextRuntimeCapabilitySnapshotV1Schema.safeParse(
input.runtimeCapabilitySnapshot,
);
if (!parsed.success) {
throw new Error('OD Next runtime planning facts unavailable: frozen_snapshot_invalid');
}
if (parsed.data.agentId !== input.agentId) {
throw new Error('OD Next runtime planning facts unavailable: frozen_snapshot_runtime_mismatch');
}
const { snapshotHash, ...withoutHash } = parsed.data;
if (hashOdNextRuntimeCapabilitySnapshotV1(withoutHash) !== snapshotHash) {
throw new Error('OD Next runtime planning facts unavailable: frozen_snapshot_hash_mismatch');
}
capabilitySnapshot = parsed.data;
} else {
if (input.automaticAdmission) {
throw new Error('OD Next runtime planning facts unavailable: frozen_snapshot_missing');
}
// Compatibility fallback for explicitly selected/test strategy paths. The
// automatic active path must consume the snapshot frozen at admission.
const versions = await input.getRuntimeVersions();
if (!versions?.invocable) {
throw new Error('OD Next runtime planning facts unavailable: runtime_not_invocable');
}
const capability = resolveBundledOdNextRuntimeCapability({
agentId: input.agentId,
...(versions.agentCliVersion
? { agentCliVersion: versions.agentCliVersion }
: {}),
...(versions.runtimeCompanionName
? { runtimeCompanionName: versions.runtimeCompanionName }
: {}),
...(versions.runtimeCompanionVersion
? { runtimeCompanionVersion: versions.runtimeCompanionVersion }
: {}),
});
if (
!capability.snapshot
|| (capability.reason !== 'capability_resolved' && !input.syntheticCanary)
) {
throw new Error(
`OD Next runtime planning facts unavailable: ${capability.reason}`,
);
}
capabilitySnapshot = capability.snapshot;
}
const eligibility = evaluateOdNextExecutionEligibility(capabilitySnapshot, 'complex');
if (!eligibility.eligible && !input.syntheticCanary) {
throw new Error(
`OD Next runtime planning facts unavailable: ${eligibility.reason}`,
);
}
const catalog = daemonOwnedOdNextPlanningCatalog(resolvedRecipe.taskType);
return {
...resolvedRecipe,
planningFacts: {
capabilitySnapshotHash: capabilitySnapshot.snapshotHash.replace(/^sha256:/, ''),
inputRefs: ['request'],
productionRoutes: catalog.productionRoutes,
outputKinds: catalog.outputKinds,
nativeChildLifecycleVerified: eligibility.eligible,
},
};
return resolvedRecipe;
}
/**
@@ -347,11 +266,6 @@ export function createOdNextInitialPromptBundleService(
devicePlatformSignal: intentSignals.devicePlatform,
workspaceScope: meta.workspaceScope,
frozenSkillPackage,
odNextSyntheticCanary:
meta.strategyRolloutDecision?.syntheticCanary === true,
odNextAutomaticAdmission:
meta.strategyRolloutDecision?.effectiveMode === 'active',
runtimeCapabilitySnapshot: meta.runtimeCapabilitySnapshot,
});
const loadedTaskInputs = loadOdNextTaskInputSnapshot(
taskInputSnapshot,
@@ -51,7 +51,6 @@ export function composeResumedRequest(task: Pick<StrategyTaskExecutionRecord, 'p
? 'Reassess deliverables for this request using the existing discovery catalog. For a new deliverable type, use native file reading to load its main Skill before planning or editing. Reuse complete same-version bodies already in context; do not reload unrelated types. HTML is a format, not a reason to select prototype. A prior chat-only turn does not satisfy this prerequisite. Keep replies in the user\'s language; omit internal routing and tool commands unless requested.'
: '',
gate,
contextDelta('Runtime capabilities', runtimeCapabilities(context.runtimeFacts), runtimeCapabilities(prior.context.runtimeFacts)),
contextDelta('Runtime tool environment', context.runtimeToolEnvironment, prior.context.runtimeToolEnvironment),
contextDelta('Workspace input references', context.requestInputFacts, prior.context.requestInputFacts),
contextDelta('Selected run context', context.runContext, prior.context.runContext),
@@ -69,15 +68,3 @@ function contextDelta(label: string, current: string | undefined, previous: stri
? `${label} updated; this replaces the previous value:\n${current}`
: `${label}: cleared. The previous value no longer applies to this turn.`;
}
/** Strip audit-only snapshot identifiers while retaining the actual capability facts. */
function runtimeCapabilities(text: string | undefined): string {
if (!text?.trim()) return '';
const start = text.indexOf('{');
if (start < 0) return text;
try {
const facts = JSON.parse(text.slice(start)) as Record<string, unknown>;
const { appliedSnapshot: _snapshot, capabilitySnapshotHash: _hash, ...capabilities } = facts;
return JSON.stringify(capabilities);
} catch { return text; }
}
@@ -1,42 +1,12 @@
import { DELIVERABLE_SKILLS, DELIVERABLE_PRODUCTION_ROUTES } from '@open-design/contracts';
type Availability = { id: string; available: boolean };
export interface OdNextIntakePreflightInput {
inputRefs: ReadonlyArray<{ id: string; accessible: boolean }>;
selectedAgentAvailable: boolean;
nativeContinuation: 'verified' | 'advertised' | 'unknown' | 'unsupported';
taskProfileAvailable: boolean;
dependencies: ReadonlyArray<Availability>;
}
const DAEMON_OWNED_PRODUCTION_ROUTES = {
discovery: new Set(DELIVERABLE_PRODUCTION_ROUTES),
prototype: new Set(['html', 'prototype-html']),
ppt: new Set(['ppt-html', 'html', 'deck-html']),
marketing: new Set(['marketing-html', 'html', 'image-html']),
hyperframes: new Set(['hyperframes-html', 'html']),
'motion-design': new Set(['hyperframes-html', 'html']),
} as const;
const DAEMON_OWNED_OUTPUT_KINDS = {
discovery: new Set(DELIVERABLE_SKILLS.flatMap(skill => skill.routes.map(route => route.kind))),
prototype: new Set(['prototype', 'html', 'source']),
ppt: new Set(['presentation', 'ppt', 'deck', 'html', 'source']),
marketing: new Set(['image', 'marketing', 'html', 'source']),
hyperframes: new Set(['video', 'hyperframes', 'html', 'source', 'rendered-video']),
'motion-design': new Set(['video', 'html', 'source', 'rendered-video']),
} as const;
export function daemonOwnedOdNextPlanningCatalog(
taskType: keyof typeof DAEMON_OWNED_PRODUCTION_ROUTES,
): { productionRoutes: string[]; outputKinds: string[] } {
return {
productionRoutes: [...DAEMON_OWNED_PRODUCTION_ROUTES[taskType]],
outputKinds: [...DAEMON_OWNED_OUTPUT_KINDS[taskType]],
};
}
export interface OdNextPreflightResult {
status: 'passed' | 'blocked';
reasonCodes: string[];
@@ -59,9 +29,6 @@ export function runIntakePreflight(input: OdNextIntakePreflightInput): OdNextPre
.map((value) => `od_next_preflight_input_unavailable:${value.id}`)
.sort(),
...(!input.selectedAgentAvailable ? ['od_next_preflight_agent_unavailable'] : []),
...(input.nativeContinuation !== 'verified'
? ['od_next_preflight_native_continuation_unverified']
: []),
...(!input.taskProfileAvailable ? ['od_next_preflight_task_profile_unavailable'] : []),
...availabilityReasonCodes(
input.dependencies,
+2 -11
View File
@@ -1,4 +1,5 @@
import { createHash } from 'node:crypto';
import { AGENT_DEFS } from '../../runtimes/registry.js';
import type {
OdNextRolloutDecision,
@@ -44,7 +45,6 @@ const DEFAULT_TASK_TYPES: readonly OdNextRolloutTaskType[] = [
'discovery',
'motion-design',
];
const DEFAULT_AGENTS = ['codex', 'claude', 'opencode', 'amr'] as const;
function bool(value: string | undefined, fallback: boolean): boolean {
if (value === undefined) return fallback;
@@ -92,7 +92,7 @@ export function readOdNextRolloutPolicy(
eligibleTaskTypes: taskTypes.length > 0 ? taskTypes : DEFAULT_TASK_TYPES,
eligibleAgents: list(env.OD_NEXT_STRATEGY_AGENTS).length > 0
? list(env.OD_NEXT_STRATEGY_AGENTS)
: DEFAULT_AGENTS,
: AGENT_DEFS.map((agent) => agent.id),
productionActiveApproved: bool(env.OD_NEXT_STRATEGY_PRODUCTION_ACTIVE_APPROVED, true),
localSyntheticCanary: bool(env.OD_NEXT_STRATEGY_LOCAL_SYNTHETIC_CANARY, false),
};
@@ -123,8 +123,6 @@ export function evaluateOdNextRollout(input: {
agentId: string | null;
agentVersion: string | null;
sourceKind: string | null;
runtimeCapabilityVerified?: boolean;
runtimeCapabilityReason?: string | null;
routeApplicability?: 'eligible' | 'explicit_user' | 'not_applicable';
}): OdNextRolloutDecision {
const { policy } = input;
@@ -159,13 +157,6 @@ export function evaluateOdNextRollout(input: {
const syntheticCanary = Boolean(
policy.localSyntheticCanary && process.env.NODE_ENV !== 'production',
);
// agentVersion is retained as diagnostic rollout evidence only. Runtime
// invocability is established by preflight and capability admission is
// keyed by runtime path + agent id + adapter/schema, not a version pin.
if (evaluateEligibility && !input.runtimeCapabilityVerified && !syntheticCanary) {
reasons.push('od_next_rollout_x1_capability_fixture_unverified');
if (input.runtimeCapabilityReason) reasons.push(`od_next_rollout_capability_${input.runtimeCapabilityReason}`);
}
if (evaluateEligibility && !policy.productionActiveApproved && !syntheticCanary) {
reasons.push('od_next_rollout_x2_active_unapproved');
}