feat(runner): harden Phase 7I demo deployment

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-09 16:15:17 +00:00
1 parent dcd6b23fa1
commit 0566a9f3ca
10 files changed
+1102 -13

No files matched your search

@@ -3,7 +3,6 @@ import * as React from "react";
import { Tabs, type TabDefinition } from "@paperclipai/paperclip-runner/react";
import {
phase7RunScenario,
Phase7ChatSession,
type Phase7ChatSessionArtifact,
type Phase7EvalSuiteLookup,
type Phase7ParityStatus,
@@ -23,7 +22,11 @@ import { buildFacets, ScenarioPicker, type Phase7PickerFacets } from "./componen
import { RunHeader } from "./components/run-header.js";
import { ScenarioTranscript } from "./components/scenario-transcript.js";
import { EmptyState, Mono } from "./components/primitives.js";
import { ChatSurface, type Phase7ChatState } from "./components/chat-surface.js";
import {
ChatSurface,
type OpenPhase7ChatSession,
type Phase7ChatState,
} from "./components/chat-surface.js";
import {
ActivityEmptyState,
ActivityStream,
@@ -57,7 +60,7 @@ export interface ExplorerAppProps {
/** Test seam: run scenarios synchronously through an injected runner. */
runScenario?: typeof phase7RunScenario;
/** Test seam: open chat sessions through an injected session factory. */
openSession?: typeof Phase7ChatSession.open;
openSession?: OpenPhase7ChatSession;
}
type RunState = "idle" | "pending" | "settled" | "failed";
@@ -555,7 +558,7 @@ function IntroCard({
</p>
<h1>{index.entries.length} scenarios across {index.groups.length} eval groups</h1>
<p>
Every scenario runs against the deterministic mock control plane in this page. The
Every scenario runs against the deterministic, package-local mock control plane. The
{chat ? " chat" : " explorer"} renders records the runtime produced: it holds no credential,
decides no policy, and owns no control-plane state.
</p>
@@ -4,6 +4,7 @@ import { Composer, Dialog, Button } from "@paperclipai/paperclip-runner/react";
import {
Phase7ChatSession,
type Phase7ChatSessionArtifact,
type Phase7ChatSessionOptions,
type Phase7ScenarioIndexEntry,
} from "@paperclip-runner-local/phase7";
@@ -14,10 +15,9 @@ import { EmptyState } from "./primitives.js";
/**
* Phase 7I chat surface.
*
* Owns one live `Phase7ChatSession` and nothing else: no protocol state, no
* policy decision, no credential. Prompts go to the package-local mock control
* plane in this page, and the records that come back are rendered as they
* arrived.
* Owns one live session client and nothing else: no protocol state, no policy
* decision, no credential. Prompts go to the package-local mock control plane,
* and the records that come back are rendered as they arrived.
*
* Session identity is deliberately explicit. `activeEntry` is the scenario the
* open session belongs to, which is not always the scenario the route names:
@@ -27,6 +27,19 @@ import { EmptyState } from "./primitives.js";
export type Phase7ChatState = "seeding" | "pending" | "streaming" | "settled" | "failed";
export interface Phase7ChatSessionClient {
artifact(): Phase7ChatSessionArtifact;
nextPrompt(): string | null;
send(prompt: string): Promise<Phase7ChatSessionArtifact>;
replay(): Promise<Phase7ChatSessionArtifact>;
close(): Promise<void>;
}
export type OpenPhase7ChatSession = (
entry: Phase7ScenarioIndexEntry,
options?: Phase7ChatSessionOptions,
) => Promise<Phase7ChatSessionClient>;
export interface ChatSurfaceProps {
entry: Phase7ScenarioIndexEntry;
/** `replay=fake` plays the whole script on load for screenshot routes. */
@@ -46,7 +59,7 @@ export interface ChatSurfaceProps {
onStateChange: (state: Phase7ChatState) => void;
/** Called when the board declines a scenario switch, to restore the route. */
onCancelScenarioSwitch: (entryId: string) => void;
openSession?: typeof Phase7ChatSession.open;
openSession?: OpenPhase7ChatSession;
headerRef?: React.Ref<HTMLElement>;
}
@@ -61,7 +74,7 @@ export function ChatSurface({
onArtifactChange,
onStateChange,
onCancelScenarioSwitch,
openSession = Phase7ChatSession.open,
openSession = (nextEntry, options) => Phase7ChatSession.open(nextEntry, options),
headerRef,
}: ChatSurfaceProps) {
const [artifact, setArtifact] = React.useState<Phase7ChatSessionArtifact | null>(null);
@@ -72,7 +85,7 @@ export function ChatSurface({
const [revealed, setRevealed] = React.useState(0);
const [mode, setMode] = React.useState<Phase7ChatMode>("scripted");
const [confirm, setConfirm] = React.useState<"none" | "reset" | "switch">("none");
const sessionRef = React.useRef<Phase7ChatSession | null>(null);
const sessionRef = React.useRef<Phase7ChatSessionClient | null>(null);
const composerRef = React.useRef<HTMLDivElement>(null);
const switching = entry.id !== activeEntry.id;
@@ -275,11 +288,15 @@ export function ChatSurface({
/>
<p className="pcr7-muted" data-testid="composer-scope-note">
{mode === "scripted"
? `Scripted mode — your prompt drives the next recorded turn against the in-page mock control plane. ${
? `Scripted mode — your prompt drives the next recorded turn against the isolated mock control plane. ${
artifact?.remainingScriptedTurns ?? 0
} scripted turn${(artifact?.remainingScriptedTurns ?? 0) === 1 ? "" : "s"} left.`
: "Codex mode drives the same mock control plane through the server-side relay; no provider credential reaches this page."}
</p>
<p className="pcr7-muted">
Synthetic demo only. Session state is memory-only and is cleared on reset, expiry, or restart;
do not enter confidential or regulated data.
</p>
</div>
<Dialog
@@ -12,6 +12,7 @@ import evalReport from "virtual:phase7-eval-report";
import manifestSource from "virtual:phase7-scenario-manifest";
import { ExplorerApp } from "./app.js";
import { Phase7RemoteChatSession } from "./remote-chat-session.js";
/**
* Package-local entry. The scenario index and every fixture are bundled from
@@ -29,5 +30,6 @@ createRoot(container).render(
index={index}
evalSuite={evalReport === null ? undefined : phase7EvalSuiteLookup(evalReport)}
codexAvailable={false}
openSession={Phase7RemoteChatSession.available() ? Phase7RemoteChatSession.open : undefined}
/>,
);
@@ -0,0 +1,107 @@
import type {
Phase7ChatSessionArtifact,
Phase7ChatSessionOptions,
Phase7ScenarioIndexEntry,
} from "@paperclip-runner-local/phase7";
import type { Phase7ChatSessionClient } from "./components/chat-surface.js";
type SessionResponse = {
sessionId: string;
artifact: Phase7ChatSessionArtifact;
nextPrompt: string | null;
};
function apiBaseFromDocument(): URL | null {
const configured = document
.querySelector<HTMLMetaElement>('meta[name="phase7i-api-base"]')
?.content.trim();
if (!configured) return null;
const url = new URL(configured.replace(/\/*$/, "/"), document.baseURI);
return url.origin === window.location.origin ? url : null;
}
async function parseResponse(response: Response): Promise<SessionResponse> {
if (!response.ok) throw new Error(`The Phase 7I demo returned HTTP ${response.status}.`);
const value: unknown = await response.json();
if (
typeof value !== "object" || value === null ||
typeof (value as { sessionId?: unknown }).sessionId !== "string" ||
typeof (value as { artifact?: unknown }).artifact !== "object"
) {
throw new Error("The Phase 7I demo returned an invalid session response.");
}
return value as SessionResponse;
}
export class Phase7RemoteChatSession implements Phase7ChatSessionClient {
private constructor(
private readonly apiBase: URL,
private sessionId: string,
private currentArtifact: Phase7ChatSessionArtifact,
private suggestedPrompt: string | null,
) {}
static available(): boolean {
return apiBaseFromDocument() !== null;
}
static async open(
entry: Phase7ScenarioIndexEntry,
_options: Phase7ChatSessionOptions = {},
): Promise<Phase7RemoteChatSession> {
const apiBase = apiBaseFromDocument();
if (!apiBase) throw new Error("The Phase 7I server endpoint is not configured.");
const response = await fetch(new URL("sessions", apiBase), {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ scenarioId: entry.id }),
});
const opened = await parseResponse(response);
return new Phase7RemoteChatSession(
apiBase,
opened.sessionId,
opened.artifact,
opened.nextPrompt,
);
}
artifact(): Phase7ChatSessionArtifact {
return this.currentArtifact;
}
nextPrompt(): string | null {
return this.suggestedPrompt;
}
async send(prompt: string): Promise<Phase7ChatSessionArtifact> {
return await this.mutate("turn", { prompt });
}
async replay(): Promise<Phase7ChatSessionArtifact> {
return await this.mutate("replay", {});
}
async close(): Promise<void> {
await fetch(new URL(`sessions/${this.sessionId}`, this.apiBase), {
method: "DELETE",
credentials: "same-origin",
keepalive: true,
}).catch(() => undefined);
}
private async mutate(action: "turn" | "replay", body: Record<string, unknown>): Promise<Phase7ChatSessionArtifact> {
const response = await fetch(new URL(`sessions/${this.sessionId}/${action}`, this.apiBase), {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const updated = await parseResponse(response);
this.sessionId = updated.sessionId;
this.currentArtifact = updated.artifact;
this.suggestedPrompt = updated.nextPrompt;
return this.currentArtifact;
}
}
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
set -euo pipefail
umask 077
ulimit -c 0
ulimit -n 128
ulimit -f 2048
: "${PHASE7I_PUBLIC_ORIGIN:?PHASE7I_PUBLIC_ORIGIN is required}"
: "${PHASE7I_COMMIT_SHA:?PHASE7I_COMMIT_SHA is required}"
: "${PHASE7I_PROXY_PORT:?PHASE7I_PROXY_PORT is required}"
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
package_root="$(cd "$script_dir/.." && pwd)"
workspace_root="$(cd "$package_root/../.." && pwd)"
socket_dir="${PHASE7I_SOCKET_DIR:-/tmp/paperclip-phase7i-pap-16917}"
socket_path="$socket_dir/phase7i.sock"
install -d -m 700 "$socket_dir"
rm -f "$socket_path"
server_pid=""
proxy_pid=""
cleanup() {
trap - EXIT INT TERM
if [[ -n "$proxy_pid" ]]; then kill -TERM "$proxy_pid" 2>/dev/null || true; fi
if [[ -n "$server_pid" ]]; then kill -TERM "$server_pid" 2>/dev/null || true; fi
if [[ -n "$proxy_pid" ]]; then wait "$proxy_pid" 2>/dev/null || true; fi
if [[ -n "$server_pid" ]]; then wait "$server_pid" 2>/dev/null || true; fi
rm -f "$socket_path"
}
trap cleanup EXIT INT TERM
unshare -Un \
--map-user=65534 \
--map-group=65534 \
--kill-child=SIGTERM \
--fork \
env -i \
PATH=/usr/local/bin:/usr/bin:/bin \
NODE_ENV=production \
PHASE7I_PUBLIC_ORIGIN="$PHASE7I_PUBLIC_ORIGIN" \
PHASE7I_BASE_PATH="${PHASE7I_BASE_PATH:-}" \
PHASE7I_COMMIT_SHA="$PHASE7I_COMMIT_SHA" \
PHASE7I_SOCKET_PATH="$socket_path" \
node \
--max-old-space-size=256 \
--permission \
--allow-fs-read="$package_root/dist" \
--allow-fs-read="$package_root/dist-phase7" \
--allow-fs-read="$package_root/node_modules" \
--allow-fs-read="$package_root/package.json" \
--allow-fs-read="$package_root/spec/phase-07/eval-traceability.yaml" \
--allow-fs-read="$workspace_root/node_modules" \
--allow-fs-write="$socket_dir" \
"$package_root/dist/phase7/phase7i-demo-server.js" &
server_pid=$!
for _ in $(seq 1 200); do
if [[ -S "$socket_path" ]]; then break; fi
if ! kill -0 "$server_pid" 2>/dev/null; then wait "$server_pid"; fi
sleep 0.05
done
if [[ ! -S "$socket_path" ]]; then
echo '{"event":"startup_failed","code":"socket_not_ready"}' >&2
exit 1
fi
env -i \
PATH=/usr/local/bin:/usr/bin:/bin \
NODE_ENV=production \
PHASE7I_PROXY_PORT="$PHASE7I_PROXY_PORT" \
PHASE7I_SOCKET_PATH="$socket_path" \
node \
--max-old-space-size=64 \
--permission \
--allow-fs-read="$package_root/dist/phase7/phase7i-loopback-proxy.js" \
--allow-fs-read="$socket_dir" \
--allow-fs-write="$socket_dir" \
"$package_root/dist/phase7/phase7i-loopback-proxy.js" &
proxy_pid=$!
wait -n "$server_pid" "$proxy_pid"
@@ -103,13 +103,29 @@ export class Phase7ChatSession {
static async open(
entry: Phase7ScenarioIndexEntry,
options: Phase7ChatSessionOptions = {},
): Promise<Phase7ChatSession> {
const fixture = phase7ScenarioFixture(entry);
const adapter = new Phase7MockControlPlaneAdapter(fixture.seed);
return await Phase7ChatSession.openWithMockAdapter(entry, adapter, options);
}
/**
* Server composition seam for the hardened Phase 7I demo.
*
* The deployment entrypoint constructs the concrete mock adapter itself and
* passes that already-selected implementation here. HTTP input, environment
* variables, and dependency injection cannot select a different adapter.
*/
static async openWithMockAdapter(
entry: Phase7ScenarioIndexEntry,
adapter: Phase7MockControlPlaneAdapter,
options: Phase7ChatSessionOptions = {},
): Promise<Phase7ChatSession> {
const mode = options.mode ?? "fake";
const fixture = phase7ScenarioFixture(entry);
const script = phase7ChatScript(entry, fixture);
const runId = `run-phase7-chat-${entry.id}`;
const adapter = new Phase7MockControlPlaneAdapter(fixture.seed);
await adapter.start();
const beforeSeed = snapshotOf(adapter);
@@ -0,0 +1,101 @@
import { readFile } from "node:fs/promises";
import { dirname, extname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import {
assertPhase7iEnvironmentSafe,
forbiddenPhase7iEnvironmentNames,
Phase7iDemoRuntime,
} from "./phase7i-demo-server.js";
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const sourceRoot = resolve(packageRoot, "src");
describe("Phase 7I deployment boundary", () => {
it("constructs the concrete mock and initializes a ready, bounded runtime", async () => {
const runtime = new Phase7iDemoRuntime({
publicOrigin: "https://phase7i.example.ts.net",
basePath: "/phase7i",
commitSha: "deadbeef",
packageRoot,
log: () => undefined,
});
await runtime.initialize();
expect(runtime.health()).toMatchObject({
ready: true,
commitSha: "deadbeef",
controlPlane: "phase7-mock",
providerAuthentication: "server-side",
credentialsExposed: false,
networkEgress: "denied",
retention: "memory-only",
activeSessions: 0,
});
await runtime.shutdown();
});
it("rejects ambient credential families by name without inspecting values", () => {
const unsafe = {
PATH: "/usr/bin",
PAPERCLIP_API_KEY: "not-read",
AWS_SECRET_ACCESS_KEY: "not-read",
OPENAI_API_KEY: "not-read",
VERCEL_TOKEN: "not-read",
};
expect(forbiddenPhase7iEnvironmentNames(unsafe)).toEqual([
"AWS_SECRET_ACCESS_KEY",
"OPENAI_API_KEY",
"PAPERCLIP_API_KEY",
"VERCEL_TOKEN",
]);
expect(() => assertPhase7iEnvironmentSafe(unsafe)).toThrow(/forbidden environment names/);
expect(() => assertPhase7iEnvironmentSafe({
NODE_ENV: "production",
PATH: "/usr/bin",
PHASE7I_COMMIT_SHA: "deadbeef",
PHASE7I_PUBLIC_ORIGIN: "https://phase7i.example.ts.net",
})).not.toThrow();
});
it("keeps the deployed source closure package-local, mock-only, and free of dynamic imports", async () => {
const closure = await sourceClosure(resolve(sourceRoot, "phase7/phase7i-demo-server.ts"));
const rootSource = await readFile(resolve(sourceRoot, "phase7/phase7i-demo-server.ts"), "utf8");
expect(rootSource).toContain('import { Phase7MockControlPlaneAdapter }');
expect(rootSource).toContain("new Phase7MockControlPlaneAdapter(fixture.seed)");
expect(closure.size).toBeGreaterThan(10);
for (const [path, source] of closure) {
expect(source, path).not.toMatch(/\bimport\s*\(/);
expect(source, path).not.toMatch(/PaperclipControlPlanePort|PAPERCLIP_API_(?:URL|KEY)|@paperclipai\/db/);
for (const specifier of importSpecifiers(source)) {
expect(specifier, `${path} imports outside the standalone boundary`).not.toMatch(
/^(?:server|ui|cli)(?:\/|$)|^@paperclipai\/(?!paperclip-runner(?:\/|$))/,
);
}
}
});
});
function importSpecifiers(source: string): string[] {
const matches = source.matchAll(/\b(?:import|export)\s+(?:type\s+)?(?:[^"'`;]{0,500}?\s+from\s+)?["']([^"']+)["']/g);
return [...matches].map((match) => match[1]!);
}
async function sourceClosure(entry: string): Promise<Map<string, string>> {
const found = new Map<string, string>();
const visit = async (path: string): Promise<void> => {
if (found.has(path)) return;
const source = await readFile(path, "utf8");
found.set(path, source);
for (const specifier of importSpecifiers(source)) {
if (!specifier.startsWith(".")) continue;
const imported = resolve(dirname(path), specifier);
const target = extname(imported) === ".js" ? `${imported.slice(0, -3)}.ts` : imported;
await visit(target);
}
};
await visit(entry);
return found;
}
@@ -0,0 +1,662 @@
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
import { readFile } from "node:fs/promises";
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
import { fileURLToPath } from "node:url";
import { dirname, extname, isAbsolute, relative, resolve } from "node:path";
import { Phase7MockControlPlaneAdapter } from "../mock-core/phase7-mock-control-plane-adapter.js";
import { Phase7ChatSession } from "./chat-session.js";
import { buildPhase7ScenarioIndex } from "./scenario-index.js";
import { phase7ScenarioFixture } from "./scenario-fixtures.js";
import type {
Phase7ChatSessionArtifact,
Phase7ScenarioIndex,
Phase7ScenarioIndexEntry,
} from "./scenario-explorer-types.js";
const REQUEST_BODY_BYTES = 64 * 1024;
const PROMPT_BYTES = 16 * 1024;
const MAX_ACTIVE_SESSIONS = 8;
const SESSION_IDLE_MS = 5 * 60_000;
const SESSION_LIFETIME_MS = 30 * 60_000;
const MAX_SESSION_EVENTS = 1_024;
const MAX_SERIALIZED_STATE_BYTES = 1024 * 1024;
const TURN_DEADLINE_MS = 30_000;
const CAPABILITY_COOKIE = "__Host-paperclip_phase7i";
const FORBIDDEN_ENV_NAME = /^(?:PAPERCLIP_|GITHUB_|AWS_|GOOGLE_|AZURE_)|(?:API_KEY|TOKEN|SECRET|PASSWORD|DATABASE_URL|CODEX_HOME)$/i;
const CONTENT_TYPES: Record<string, string> = {
".css": "text/css; charset=utf-8",
".html": "text/html; charset=utf-8",
".ico": "image/x-icon",
".js": "text/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".png": "image/png",
".svg": "image/svg+xml",
".txt": "text/plain; charset=utf-8",
".woff": "font/woff",
".woff2": "font/woff2",
};
type SessionRecord = {
id: string;
capabilityHash: Buffer;
identityHash: string;
createdAt: number;
lastActiveAt: number;
activeTurn: boolean;
chat: Phase7ChatSession;
};
type RuntimeOptions = {
publicOrigin: string;
basePath: string;
commitSha: string;
packageRoot?: string;
now?: () => number;
log?: (record: Record<string, unknown>) => void;
};
class RequestFailure extends Error {
constructor(
readonly status: number,
readonly code: string,
message: string,
) {
super(message);
}
}
function normalizeBasePath(value: string): string {
const trimmed = value.trim();
if (trimmed === "" || trimmed === "/") return "";
return `/${trimmed.replace(/^\/+|\/+$/g, "")}`;
}
function sha256(value: string): Buffer {
return createHash("sha256").update(value).digest();
}
function shortHash(value: string): string {
return sha256(value).toString("hex").slice(0, 16);
}
function header(req: IncomingMessage, name: string): string {
const value = req.headers[name];
return Array.isArray(value) ? value[0] ?? "" : value ?? "";
}
function requestIdentity(req: IncomingMessage): string {
const login = header(req, "tailscale-user-login").trim();
if (login) return `tailnet:${login.toLowerCase()}`;
const forwardedFor = header(req, "x-forwarded-for").split(",")[0]?.trim();
return `tailnet-peer:${forwardedFor || "serve-proxy"}`;
}
function parseCookies(req: IncomingMessage): Map<string, string> {
const result = new Map<string, string>();
for (const item of header(req, "cookie").split(";")) {
const separator = item.indexOf("=");
if (separator <= 0) continue;
result.set(item.slice(0, separator).trim(), item.slice(separator + 1).trim());
}
return result;
}
function capabilityCookie(value: string, maxAgeSeconds = 1_800): string {
return `${CAPABILITY_COOKIE}=${value}; Path=/; Max-Age=${maxAgeSeconds}; HttpOnly; Secure; SameSite=Strict`;
}
function applySecurityHeaders(res: ServerResponse): void {
res.setHeader("Cache-Control", "no-store");
res.setHeader(
"Content-Security-Policy",
"default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; object-src 'none'",
);
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
res.setHeader("Cross-Origin-Resource-Policy", "same-origin");
res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=(), usb=()");
res.setHeader("Referrer-Policy", "no-referrer");
res.setHeader("Strict-Transport-Security", "max-age=31536000");
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("X-Frame-Options", "DENY");
}
function sendJson(res: ServerResponse, status: number, value: unknown): void {
applySecurityHeaders(res);
const body = JSON.stringify(value);
res.statusCode = status;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.setHeader("Content-Length", Buffer.byteLength(body));
res.end(body);
}
function sendError(res: ServerResponse, error: RequestFailure): void {
sendJson(res, error.status, { error: { code: error.code, message: error.message } });
}
async function readJsonBody(req: IncomingMessage): Promise<Record<string, unknown>> {
if (!header(req, "content-type").toLowerCase().startsWith("application/json")) {
throw new RequestFailure(415, "json_required", "Mutation requests require application/json.");
}
const declaredLength = Number(header(req, "content-length"));
if (Number.isFinite(declaredLength) && declaredLength > REQUEST_BODY_BYTES) {
throw new RequestFailure(413, "request_too_large", "Request body exceeds the 64 KiB limit.");
}
const chunks: Buffer[] = [];
let total = 0;
let exceeded = false;
for await (const chunk of req) {
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
total += buffer.length;
if (total > REQUEST_BODY_BYTES) {
exceeded = true;
continue;
}
chunks.push(buffer);
}
if (exceeded) {
throw new RequestFailure(413, "request_too_large", "Request body exceeds the 64 KiB limit.");
}
try {
const parsed: unknown = JSON.parse(Buffer.concat(chunks).toString("utf8"));
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
throw new Error("not an object");
}
return parsed as Record<string, unknown>;
} catch {
throw new RequestFailure(400, "invalid_json", "Request body must be a JSON object.");
}
}
function exactKeys(body: Record<string, unknown>, expected: readonly string[]): void {
const expectedSet = new Set(expected);
const unknown = Object.keys(body).find((key) => !expectedSet.has(key));
if (unknown) {
throw new RequestFailure(422, "unexpected_field", "Request contains an unsupported field.");
}
}
function withDeadline<T>(promise: Promise<T>): Promise<T> {
let timer: NodeJS.Timeout | undefined;
return Promise.race([
promise,
new Promise<never>((_resolve, reject) => {
timer = setTimeout(
() => reject(new RequestFailure(504, "turn_timeout", "The turn exceeded its 30 second deadline.")),
TURN_DEADLINE_MS,
);
}),
]).finally(() => {
if (timer) clearTimeout(timer);
});
}
function assertArtifactBounds(artifact: Phase7ChatSessionArtifact): void {
if (artifact.timeline.length > MAX_SESSION_EVENTS) {
throw new RequestFailure(507, "event_limit", "The session exceeded its retained event limit.");
}
if (Buffer.byteLength(JSON.stringify(artifact)) > MAX_SERIALIZED_STATE_BYTES) {
throw new RequestFailure(507, "state_limit", "The session exceeded its serialized state limit.");
}
}
export function forbiddenPhase7iEnvironmentNames(env: NodeJS.ProcessEnv): string[] {
return Object.keys(env).filter((name) => FORBIDDEN_ENV_NAME.test(name)).sort();
}
export function assertPhase7iEnvironmentSafe(env: NodeJS.ProcessEnv): void {
const forbidden = forbiddenPhase7iEnvironmentNames(env);
if (forbidden.length > 0) {
throw new Error(`Phase 7I refused forbidden environment names: ${forbidden.join(", ")}`);
}
}
export class Phase7iDemoRuntime {
readonly publicOrigin: URL;
readonly basePath: string;
readonly commitSha: string;
private readonly packageRoot: string;
private readonly staticRoot: string;
private readonly now: () => number;
private readonly logRecord: (record: Record<string, unknown>) => void;
private readonly sessions = new Map<string, SessionRecord>();
private readonly rateBuckets = new Map<string, number[]>();
private scenarioIndex: Phase7ScenarioIndex | null = null;
private ready = false;
private closing = false;
constructor(options: RuntimeOptions) {
this.publicOrigin = new URL(options.publicOrigin);
if (this.publicOrigin.protocol !== "https:") {
throw new Error("PHASE7I_PUBLIC_ORIGIN must use HTTPS");
}
this.basePath = normalizeBasePath(options.basePath);
this.commitSha = options.commitSha.trim() || "unknown";
this.packageRoot = options.packageRoot ?? resolve(dirname(fileURLToPath(import.meta.url)), "../..");
this.staticRoot = resolve(this.packageRoot, "dist-phase7");
this.now = options.now ?? Date.now;
this.logRecord = options.log ?? ((record) => process.stdout.write(`${JSON.stringify(record)}\n`));
}
async initialize(): Promise<void> {
const manifest = await readFile(
resolve(this.packageRoot, "spec/phase-07/eval-traceability.yaml"),
"utf8",
);
this.scenarioIndex = buildPhase7ScenarioIndex(manifest);
const probe = this.scenarioIndex.entries[0];
if (!probe) throw new Error("Phase 7 scenario corpus is empty");
const chat = await this.openConcreteMockSession(probe);
await chat.close();
this.ready = true;
}
createServer(): Server {
const server = createServer((req, res) => void this.handle(req, res));
server.maxConnections = 32;
server.requestTimeout = 35_000;
server.headersTimeout = 10_000;
server.keepAliveTimeout = 5_000;
return server;
}
health(): Record<string, unknown> {
return {
status: this.ready && !this.closing ? "ok" : "unavailable",
ready: this.ready && !this.closing && this.sessions.size < MAX_ACTIVE_SESSIONS,
commitSha: this.commitSha,
controlPlane: "phase7-mock",
providerAuthentication: "server-side",
providerMode: "deterministic-scripted-no-provider",
credentialsExposed: false,
networkEgress: "denied",
retention: "memory-only",
activeSessions: this.sessions.size,
limits: {
requestBodyBytes: REQUEST_BODY_BYTES,
promptBytes: PROMPT_BYTES,
activeSessions: MAX_ACTIVE_SESSIONS,
activeTurnsPerSession: 1,
turnDeadlineSeconds: TURN_DEADLINE_MS / 1_000,
idleExpirySeconds: SESSION_IDLE_MS / 1_000,
absoluteLifetimeSeconds: SESSION_LIFETIME_MS / 1_000,
retainedEventsPerSession: MAX_SESSION_EVENTS,
serializedStateBytesPerSession: MAX_SERIALIZED_STATE_BYTES,
},
};
}
async shutdown(): Promise<void> {
if (this.closing) return;
this.closing = true;
const records = [...this.sessions.values()];
this.sessions.clear();
await Promise.allSettled(records.map((record) => record.chat.close()));
this.ready = false;
}
async expireSessions(): Promise<number> {
const now = this.now();
const expired = [...this.sessions.values()].filter(
(record) => now - record.lastActiveAt >= SESSION_IDLE_MS || now - record.createdAt >= SESSION_LIFETIME_MS,
);
for (const record of expired) this.sessions.delete(record.id);
await Promise.allSettled(expired.map((record) => record.chat.close()));
return expired.length;
}
private async openConcreteMockSession(entry: Phase7ScenarioIndexEntry): Promise<Phase7ChatSession> {
const fixture = phase7ScenarioFixture(entry);
// The concrete adapter choice is fixed here. No request or environment
// value can supply a module, URL, path, or alternate control plane.
const adapter = new Phase7MockControlPlaneAdapter(fixture.seed);
return await Phase7ChatSession.openWithMockAdapter(entry, adapter, { mode: "fake" });
}
private stripBasePath(rawPath: string): string {
if (this.basePath && rawPath === this.basePath) return "/";
if (this.basePath && rawPath.startsWith(`${this.basePath}/`)) {
return rawPath.slice(this.basePath.length) || "/";
}
return rawPath;
}
private assertProxyAdmission(req: IncomingMessage): void {
if (header(req, "x-phase7i-proxy") !== "v1") {
throw new RequestFailure(403, "trusted_proxy_required", "Request did not arrive through the trusted proxy.");
}
if (header(req, "x-forwarded-proto").split(",")[0]?.trim().toLowerCase() !== "https") {
throw new RequestFailure(403, "https_required", "HTTPS is required.");
}
const forwardedHost = header(req, "x-forwarded-host").split(",")[0]?.trim().toLowerCase();
if (forwardedHost !== this.publicOrigin.host.toLowerCase()) {
throw new RequestFailure(403, "host_denied", "Forwarded host is not allowed.");
}
}
private assertMutationAdmission(req: IncomingMessage): void {
this.assertProxyAdmission(req);
if (header(req, "origin") !== this.publicOrigin.origin) {
throw new RequestFailure(403, "origin_denied", "Request origin is not allowed.");
}
const fetchSite = header(req, "sec-fetch-site").toLowerCase();
if (fetchSite && fetchSite !== "same-origin" && fetchSite !== "none") {
throw new RequestFailure(403, "fetch_metadata_denied", "Cross-site mutation requests are not allowed.");
}
}
private enforceRate(key: string, limit: number, windowMs = 60_000): void {
const now = this.now();
const bucket = (this.rateBuckets.get(key) ?? []).filter((timestamp) => now - timestamp < windowMs);
if (bucket.length >= limit) {
throw new RequestFailure(429, "rate_limited", "Request rate limit exceeded.");
}
bucket.push(now);
this.rateBuckets.set(key, bucket);
}
private requireSession(req: IncomingMessage, id: string): SessionRecord {
const record = this.sessions.get(id);
if (!record) throw new RequestFailure(404, "session_not_found", "Session was not found.");
const capability = parseCookies(req).get(CAPABILITY_COOKIE) ?? "";
const capabilityHash = sha256(capability);
const identityHash = shortHash(requestIdentity(req));
if (
capability.length < 32 ||
capabilityHash.length !== record.capabilityHash.length ||
!timingSafeEqual(capabilityHash, record.capabilityHash) ||
identityHash !== record.identityHash
) {
throw new RequestFailure(404, "session_not_found", "Session was not found.");
}
const now = this.now();
if (now - record.lastActiveAt >= SESSION_IDLE_MS || now - record.createdAt >= SESSION_LIFETIME_MS) {
this.sessions.delete(record.id);
void record.chat.close();
throw new RequestFailure(404, "session_not_found", "Session was not found.");
}
record.lastActiveAt = now;
return record;
}
private async createSession(req: IncomingMessage, res: ServerResponse, body: Record<string, unknown>): Promise<void> {
exactKeys(body, ["scenarioId"]);
const scenarioId = typeof body.scenarioId === "string" ? body.scenarioId : "";
const entry = this.scenarioIndex?.entries.find((candidate) => candidate.id === scenarioId);
if (!entry) throw new RequestFailure(422, "scenario_unknown", "Scenario is not available.");
if (this.sessions.size >= MAX_ACTIVE_SESSIONS) {
throw new RequestFailure(429, "session_capacity", "The active session limit has been reached.");
}
const identityHash = shortHash(requestIdentity(req));
this.enforceRate("global:create", 40);
this.enforceRate(`${identityHash}:create`, 10);
const capability = randomBytes(32).toString("base64url");
const chat = await this.openConcreteMockSession(entry);
const artifact = chat.artifact();
assertArtifactBounds(artifact);
const now = this.now();
const record: SessionRecord = {
id: randomBytes(16).toString("hex"),
capabilityHash: sha256(capability),
identityHash,
createdAt: now,
lastActiveAt: now,
activeTurn: false,
chat,
};
this.sessions.set(record.id, record);
res.setHeader("Set-Cookie", capabilityCookie(capability));
sendJson(res, 201, { sessionId: record.id, artifact, nextPrompt: chat.nextPrompt() });
}
private async runTurn(
req: IncomingMessage,
res: ServerResponse,
record: SessionRecord,
body: Record<string, unknown>,
): Promise<void> {
exactKeys(body, ["prompt"]);
if (typeof body.prompt !== "string" || body.prompt.trim() === "") {
throw new RequestFailure(422, "prompt_required", "Prompt must be a non-empty string.");
}
const prompt = body.prompt.trim();
if (Buffer.byteLength(prompt, "utf8") > PROMPT_BYTES) {
throw new RequestFailure(422, "prompt_too_large", "Prompt exceeds the 16 KiB limit.");
}
if (record.activeTurn) throw new RequestFailure(409, "turn_active", "A turn is already active.");
this.enforceRate("global:turn", 120);
this.enforceRate(`${record.identityHash}:turn`, 30);
record.activeTurn = true;
try {
const artifact = await withDeadline(record.chat.send(prompt));
assertArtifactBounds(artifact);
sendJson(res, 200, { sessionId: record.id, artifact, nextPrompt: record.chat.nextPrompt() });
} catch (error) {
if (error instanceof RequestFailure && error.code === "turn_timeout") {
this.sessions.delete(record.id);
await record.chat.close();
}
throw error;
} finally {
record.activeTurn = false;
}
}
private async replaySession(res: ServerResponse, record: SessionRecord): Promise<void> {
if (record.activeTurn) throw new RequestFailure(409, "turn_active", "A turn is already active.");
this.enforceRate("global:turn", 120);
this.enforceRate(`${record.identityHash}:turn`, 30);
record.activeTurn = true;
try {
const artifact = await withDeadline(record.chat.replay());
assertArtifactBounds(artifact);
sendJson(res, 200, { sessionId: record.id, artifact, nextPrompt: record.chat.nextPrompt() });
} finally {
record.activeTurn = false;
}
}
private async resetSession(req: IncomingMessage, res: ServerResponse, record: SessionRecord): Promise<void> {
this.enforceRate(`${record.identityHash}:reset`, 10);
this.sessions.delete(record.id);
const entry = record.chat.entry;
await record.chat.close();
const capability = randomBytes(32).toString("base64url");
const chat = await this.openConcreteMockSession(entry);
const artifact = chat.artifact();
assertArtifactBounds(artifact);
const now = this.now();
const replacement: SessionRecord = {
id: randomBytes(16).toString("hex"),
capabilityHash: sha256(capability),
identityHash: record.identityHash,
createdAt: now,
lastActiveAt: now,
activeTurn: false,
chat,
};
this.sessions.set(replacement.id, replacement);
res.setHeader("Set-Cookie", capabilityCookie(capability));
sendJson(res, 200, { sessionId: replacement.id, artifact, nextPrompt: chat.nextPrompt() });
}
private async deleteSession(res: ServerResponse, record: SessionRecord): Promise<void> {
this.sessions.delete(record.id);
await record.chat.close();
res.setHeader("Set-Cookie", capabilityCookie("deleted", 0));
sendJson(res, 200, { closed: true });
}
private async serveStatic(req: IncomingMessage, res: ServerResponse, pathname: string): Promise<void> {
this.assertProxyAdmission(req);
if (req.method !== "GET" && req.method !== "HEAD") {
throw new RequestFailure(405, "method_not_allowed", "Method is not allowed.");
}
const decoded = decodeURIComponent(pathname);
const relativePath = decoded === "/" ? "phase7-explorer/index.html" : decoded.replace(/^\/+/, "");
const filePath = resolve(this.staticRoot, relativePath);
const relativeToRoot = relative(this.staticRoot, filePath);
if (relativeToRoot.startsWith("..") || isAbsolute(relativeToRoot)) {
throw new RequestFailure(404, "not_found", "Resource was not found.");
}
let body: Buffer;
try {
body = await readFile(filePath);
} catch {
throw new RequestFailure(404, "not_found", "Resource was not found.");
}
if (relativePath === "phase7-explorer/index.html") {
const html = body.toString("utf8").replace(
"</head>",
'<meta name="phase7i-api-base" content="api/phase7" /></head>',
);
body = Buffer.from(html);
}
applySecurityHeaders(res);
res.statusCode = 200;
res.setHeader("Content-Type", CONTENT_TYPES[extname(filePath).toLowerCase()] ?? "application/octet-stream");
res.setHeader("Content-Length", body.length);
res.end(req.method === "HEAD" ? undefined : body);
}
private async handle(req: IncomingMessage, res: ServerResponse): Promise<void> {
const startedAt = this.now();
const requestId = randomBytes(8).toString("hex");
let route = "unknown";
let outcome = "error";
try {
if (this.closing) throw new RequestFailure(503, "shutting_down", "Service is shutting down.");
const url = new URL(req.url ?? "/", "http://phase7i.invalid");
const pathname = this.stripBasePath(url.pathname);
if (pathname === "/api/phase7/health" && req.method === "GET") {
route = "health";
this.assertProxyAdmission(req);
sendJson(res, this.ready ? 200 : 503, this.health());
outcome = "ok";
return;
}
if (pathname.startsWith("/api/phase7/")) {
this.assertMutationAdmission(req);
const identityHash = shortHash(requestIdentity(req));
this.enforceRate("global:request", 300);
this.enforceRate(`${identityHash}:request`, 90);
if (pathname === "/api/phase7/sessions" && req.method === "POST") {
route = "session_create";
await this.createSession(req, res, await readJsonBody(req));
outcome = "created";
return;
}
const match = pathname.match(/^\/api\/phase7\/sessions\/([a-f0-9]{32})(?:\/(turn|replay|reset))?$/);
if (!match) throw new RequestFailure(404, "not_found", "Route was not found.");
const record = this.requireSession(req, match[1]!);
const action = match[2] ?? "session";
route = action;
if (action === "turn" && req.method === "POST") {
await this.runTurn(req, res, record, await readJsonBody(req));
outcome = "settled";
return;
}
if (action === "replay" && req.method === "POST") {
exactKeys(await readJsonBody(req), []);
await this.replaySession(res, record);
outcome = "settled";
return;
}
if (action === "reset" && req.method === "POST") {
exactKeys(await readJsonBody(req), []);
await this.resetSession(req, res, record);
outcome = "reset";
return;
}
if (action === "session" && req.method === "DELETE") {
await this.deleteSession(res, record);
outcome = "closed";
return;
}
throw new RequestFailure(405, "method_not_allowed", "Method is not allowed.");
}
route = "static";
await this.serveStatic(req, res, pathname);
outcome = "ok";
} catch (error) {
const failure = error instanceof RequestFailure
? error
: new RequestFailure(500, "internal_error", "The request could not be completed.");
if (!res.headersSent) sendError(res, failure);
outcome = failure.code;
} finally {
this.logRecord({
timestamp: new Date().toISOString(),
requestId,
route,
method: req.method ?? "UNKNOWN",
status: res.statusCode,
outcome,
latencyMs: Math.max(0, this.now() - startedAt),
identity: shortHash(requestIdentity(req)),
});
}
}
}
async function main(): Promise<void> {
assertPhase7iEnvironmentSafe(process.env);
const socketPath = process.env.PHASE7I_SOCKET_PATH?.trim();
const publicOrigin = process.env.PHASE7I_PUBLIC_ORIGIN?.trim();
if (!socketPath || !isAbsolute(socketPath)) throw new Error("PHASE7I_SOCKET_PATH must be absolute");
if (!publicOrigin) throw new Error("PHASE7I_PUBLIC_ORIGIN is required");
const runtime = new Phase7iDemoRuntime({
publicOrigin,
basePath: process.env.PHASE7I_BASE_PATH ?? "",
commitSha: process.env.PHASE7I_COMMIT_SHA ?? "unknown",
});
await runtime.initialize();
const server = runtime.createServer();
const expiryTimer = setInterval(() => void runtime.expireSessions(), 30_000);
expiryTimer.unref();
const stop = async (signal: string): Promise<void> => {
clearInterval(expiryTimer);
process.stdout.write(`${JSON.stringify({ timestamp: new Date().toISOString(), event: "shutdown", signal })}\n`);
server.close();
await runtime.shutdown();
process.exit(0);
};
process.once("SIGTERM", () => void stop("SIGTERM"));
process.once("SIGINT", () => void stop("SIGINT"));
await new Promise<void>((resolveListen, rejectListen) => {
server.once("error", rejectListen);
server.listen(socketPath, () => {
server.off("error", rejectListen);
resolveListen();
});
});
process.stdout.write(`${JSON.stringify({
timestamp: new Date().toISOString(),
event: "ready",
controlPlane: "phase7-mock",
commitSha: runtime.commitSha,
networkEgress: "denied",
})}\n`);
}
const entryPath = process.argv[1] ? resolve(process.argv[1]) : "";
if (entryPath === fileURLToPath(import.meta.url)) {
void main().catch((error) => {
process.stderr.write(`${JSON.stringify({
timestamp: new Date().toISOString(),
event: "startup_failed",
code:
typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
? error.code
: error instanceof Error
? error.name
: "Error",
})}\n`);
process.exitCode = 1;
});
}
@@ -0,0 +1,97 @@
import { request as proxyRequest, createServer, type IncomingHttpHeaders } from "node:http";
const MAX_BODY_BYTES = 64 * 1024;
const FORWARDED_HEADERS = new Set([
"accept",
"accept-encoding",
"content-length",
"content-type",
"cookie",
"host",
"origin",
"sec-fetch-dest",
"sec-fetch-mode",
"sec-fetch-site",
"tailscale-user-login",
"tailscale-user-name",
"user-agent",
"x-forwarded-for",
"x-forwarded-host",
"x-forwarded-proto",
]);
function selectedHeaders(headers: IncomingHttpHeaders): Record<string, string | string[]> {
const selected: Record<string, string | string[]> = {};
for (const [name, value] of Object.entries(headers)) {
if (!FORWARDED_HEADERS.has(name) || value === undefined) continue;
selected[name] = value;
}
selected["x-forwarded-proto"] = Array.isArray(headers["x-forwarded-proto"])
? headers["x-forwarded-proto"][0] ?? "https"
: headers["x-forwarded-proto"] ?? "https";
selected["x-forwarded-host"] = Array.isArray(headers["x-forwarded-host"])
? headers["x-forwarded-host"][0] ?? String(headers.host ?? "")
: headers["x-forwarded-host"] ?? String(headers.host ?? "");
selected["x-phase7i-proxy"] = "v1";
return selected;
}
function main(): void {
const socketPath = process.env.PHASE7I_SOCKET_PATH?.trim();
const port = Number(process.env.PHASE7I_PROXY_PORT);
if (!socketPath || !Number.isInteger(port) || port < 1 || port > 65_535) {
throw new Error("PHASE7I_SOCKET_PATH and a valid PHASE7I_PROXY_PORT are required");
}
const server = createServer((req, res) => {
const declaredLength = Number(req.headers["content-length"]);
if (Number.isFinite(declaredLength) && declaredLength > MAX_BODY_BYTES) {
res.writeHead(413, { "Cache-Control": "no-store", "Content-Type": "application/json" });
res.end('{"error":{"code":"request_too_large","message":"Request body exceeds the 64 KiB limit."}}');
return;
}
const upstream = proxyRequest({
socketPath,
method: req.method,
path: req.url,
headers: selectedHeaders(req.headers),
timeout: 35_000,
}, (upstreamResponse) => {
res.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);
upstreamResponse.pipe(res);
});
upstream.once("timeout", () => upstream.destroy(new Error("upstream timeout")));
upstream.once("error", () => {
if (res.headersSent) {
res.destroy();
return;
}
res.writeHead(502, { "Cache-Control": "no-store", "Content-Type": "application/json" });
res.end('{"error":{"code":"upstream_unavailable","message":"Demo service is unavailable."}}');
});
req.pipe(upstream);
});
server.maxConnections = 32;
server.requestTimeout = 35_000;
server.headersTimeout = 10_000;
server.keepAliveTimeout = 5_000;
server.listen(port, "127.0.0.1");
const stop = (): void => {
server.close(() => process.exit(0));
};
process.once("SIGTERM", stop);
process.once("SIGINT", stop);
}
try {
main();
} catch (error) {
process.stderr.write(`${JSON.stringify({
timestamp: new Date().toISOString(),
event: "proxy_startup_failed",
code: error instanceof Error ? error.name : "Error",
})}\n`);
process.exitCode = 1;
}
@@ -18,6 +18,7 @@ const packageRoot = fileURLToPath(new URL(".", import.meta.url));
*/
export default defineConfig({
root: resolve(packageRoot, "examples"),
base: "./",
plugins: [react(), phase7ExplorerFixturesPlugin()],
resolve: {
alias: [