fix(runner): support direct tailnet HTTP demo

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-09 17:10:17 +00:00
1 parent 4bc1c8e133
commit 7d6ec9fbf3
6 files changed
+234 -58

No files matched your search

@@ -42,6 +42,7 @@ unshare -Un \
PHASE7I_PUBLIC_ORIGIN="$PHASE7I_PUBLIC_ORIGIN" \
PHASE7I_BASE_PATH="${PHASE7I_BASE_PATH:-}" \
PHASE7I_COMMIT_SHA="$PHASE7I_COMMIT_SHA" \
PHASE7I_ALLOW_INSECURE_HTTP="${PHASE7I_ALLOW_INSECURE_HTTP:-}" \
PHASE7I_SOCKET_PATH="$socket_path" \
node \
--max-old-space-size=256 \
@@ -71,6 +72,8 @@ env -i \
NODE_ENV=production \
PHASE7I_PUBLIC_ORIGIN="$PHASE7I_PUBLIC_ORIGIN" \
PHASE7I_PROXY_PORT="$PHASE7I_PROXY_PORT" \
PHASE7I_PROXY_HOST="${PHASE7I_PROXY_HOST:-127.0.0.1}" \
PHASE7I_ALLOW_INSECURE_HTTP="${PHASE7I_ALLOW_INSECURE_HTTP:-}" \
PHASE7I_SOCKET_PATH="$socket_path" \
node \
--max-old-space-size=64 \
@@ -7,6 +7,7 @@ import { describe, expect, it } from "vitest";
import {
assertPhase7iEnvironmentSafe,
forbiddenPhase7iEnvironmentNames,
phase7iCapabilityCookieForOrigin,
Phase7iDemoRuntime,
} from "./phase7i-demo-server.js";
@@ -36,6 +37,34 @@ describe("Phase 7I deployment boundary", () => {
await runtime.shutdown();
});
it("requires an explicit opt-in for tailnet HTTP and uses an HTTP-compatible capability cookie", async () => {
expect(() => new Phase7iDemoRuntime({
publicOrigin: "http://phase7i.example.ts.net:4197",
basePath: "/phase7i",
commitSha: "deadbeef",
packageRoot,
log: () => undefined,
})).toThrow(/explicitly enabled/);
const publicOrigin = "http://phase7i.example.ts.net:4197";
const runtime = new Phase7iDemoRuntime({
publicOrigin,
basePath: "/phase7i",
commitSha: "deadbeef",
allowInsecureHttp: true,
packageRoot,
log: () => undefined,
});
await runtime.initialize();
expect(runtime.health()).toMatchObject({ ready: true, commitSha: "deadbeef" });
const setCookie = phase7iCapabilityCookieForOrigin(new URL(publicOrigin), "capability");
expect(setCookie).toContain("paperclip_phase7i=capability");
expect(setCookie).not.toContain("__Host-");
expect(setCookie).not.toMatch(/; Secure(?:;|$)/);
expect(setCookie).toContain("SameSite=Strict");
await runtime.shutdown();
});
it("rejects ambient credential families by name without inspecting values", () => {
const unsafe = {
PATH: "/usr/bin",
@@ -22,7 +22,8 @@ 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 SECURE_CAPABILITY_COOKIE = "__Host-paperclip_phase7i";
const TAILNET_HTTP_CAPABILITY_COOKIE = "paperclip_phase7i";
const FORBIDDEN_ENV_NAME = /^(?:PAPERCLIP_|GITHUB_|AWS_|GOOGLE_|AZURE_)|(?:API_KEY|TOKEN|SECRET|PASSWORD|DATABASE_URL|CODEX_HOME)$/i;
@@ -53,6 +54,7 @@ type RuntimeOptions = {
publicOrigin: string;
basePath: string;
commitSha: string;
allowInsecureHttp?: boolean;
packageRoot?: string;
now?: () => number;
log?: (record: Record<string, unknown>) => void;
@@ -104,8 +106,28 @@ function parseCookies(req: IncomingMessage): Map<string, string> {
return result;
}
function capabilityCookie(value: string, maxAgeSeconds = 1_800): string {
return `${CAPABILITY_COOKIE}=${value}; Path=/; Max-Age=${maxAgeSeconds}; HttpOnly; Secure; SameSite=Strict`;
function capabilityCookie(
name: string,
value: string,
secure: boolean,
maxAgeSeconds = 1_800,
): string {
const secureAttribute = secure ? "; Secure" : "";
return `${name}=${value}; Path=/; Max-Age=${maxAgeSeconds}; HttpOnly${secureAttribute}; SameSite=Strict`;
}
export function phase7iCapabilityCookieForOrigin(
publicOrigin: URL,
value: string,
maxAgeSeconds = 1_800,
): string {
const secure = publicOrigin.protocol === "https:";
return capabilityCookie(
secure ? SECURE_CAPABILITY_COOKIE : TAILNET_HTTP_CAPABILITY_COOKIE,
value,
secure,
maxAgeSeconds,
);
}
function applySecurityHeaders(res: ServerResponse): void {
@@ -218,6 +240,8 @@ export class Phase7iDemoRuntime {
readonly basePath: string;
readonly commitSha: string;
private readonly secureTransport: boolean;
private readonly capabilityCookieName: string;
private readonly packageRoot: string;
private readonly staticRoot: string;
private readonly now: () => number;
@@ -230,9 +254,16 @@ export class Phase7iDemoRuntime {
constructor(options: RuntimeOptions) {
this.publicOrigin = new URL(options.publicOrigin);
if (this.publicOrigin.protocol !== "https:") {
throw new Error("PHASE7I_PUBLIC_ORIGIN must use HTTPS");
this.secureTransport = this.publicOrigin.protocol === "https:";
if (
!this.secureTransport
&& !(this.publicOrigin.protocol === "http:" && options.allowInsecureHttp === true)
) {
throw new Error("PHASE7I_PUBLIC_ORIGIN must use HTTPS unless tailnet HTTP is explicitly enabled");
}
this.capabilityCookieName = this.secureTransport
? SECURE_CAPABILITY_COOKIE
: TAILNET_HTTP_CAPABILITY_COOKIE;
this.basePath = normalizeBasePath(options.basePath);
this.commitSha = options.commitSha.trim() || "unknown";
this.packageRoot = options.packageRoot ?? resolve(dirname(fileURLToPath(import.meta.url)), "../..");
@@ -328,8 +359,9 @@ export class Phase7iDemoRuntime {
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 forwardedProtocol = header(req, "x-forwarded-proto").split(",")[0]?.trim().toLowerCase();
if (forwardedProtocol !== this.publicOrigin.protocol.slice(0, -1)) {
throw new RequestFailure(403, "transport_denied", "Forwarded transport is not allowed.");
}
const forwardedHost = header(req, "x-forwarded-host").split(",")[0]?.trim().toLowerCase();
if (forwardedHost !== this.publicOrigin.host.toLowerCase()) {
@@ -361,7 +393,7 @@ export class Phase7iDemoRuntime {
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 capability = parseCookies(req).get(this.capabilityCookieName) ?? "";
const capabilityHash = sha256(capability);
const identityHash = shortHash(requestIdentity(req));
if (
@@ -408,7 +440,10 @@ export class Phase7iDemoRuntime {
chat,
};
this.sessions.set(record.id, record);
res.setHeader("Set-Cookie", capabilityCookie(capability));
res.setHeader(
"Set-Cookie",
phase7iCapabilityCookieForOrigin(this.publicOrigin, capability),
);
sendJson(res, 201, { sessionId: record.id, artifact, nextPrompt: chat.nextPrompt() });
}
@@ -479,14 +514,20 @@ export class Phase7iDemoRuntime {
chat,
};
this.sessions.set(replacement.id, replacement);
res.setHeader("Set-Cookie", capabilityCookie(capability));
res.setHeader(
"Set-Cookie",
phase7iCapabilityCookieForOrigin(this.publicOrigin, 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));
res.setHeader(
"Set-Cookie",
phase7iCapabilityCookieForOrigin(this.publicOrigin, "deleted", 0),
);
sendJson(res, 200, { closed: true });
}
@@ -612,6 +653,7 @@ async function main(): Promise<void> {
publicOrigin,
basePath: process.env.PHASE7I_BASE_PATH ?? "",
commitSha: process.env.PHASE7I_COMMIT_SHA ?? "unknown",
allowInsecureHttp: process.env.PHASE7I_ALLOW_INSECURE_HTTP === "1",
});
await runtime.initialize();
const server = runtime.createServer();
@@ -1,52 +1,14 @@
import { request as proxyRequest, createServer, type IncomingHttpHeaders } from "node:http";
import { request as proxyRequest, createServer } from "node:http";
import {
resolvePhase7iProxyConfig,
selectedPhase7iProxyHeaders,
} from "./phase7i-proxy-config.js";
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",
]);
function selectedHeaders(
headers: IncomingHttpHeaders,
publicHost: string,
): 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.host = publicHost;
selected["x-forwarded-proto"] = "https";
selected["x-forwarded-host"] = publicHost;
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);
const publicOrigin = new URL(process.env.PHASE7I_PUBLIC_ORIGIN ?? "");
if (
!socketPath
|| !Number.isInteger(port)
|| port < 1
|| port > 65_535
|| publicOrigin.protocol !== "https:"
) {
throw new Error("PHASE7I_SOCKET_PATH, PHASE7I_PUBLIC_ORIGIN, and a valid PHASE7I_PROXY_PORT are required");
}
const { socketPath, port, bindHost, publicOrigin } = resolvePhase7iProxyConfig(process.env);
const server = createServer((req, res) => {
const declaredLength = Number(req.headers["content-length"]);
@@ -60,7 +22,7 @@ function main(): void {
socketPath,
method: req.method,
path: req.url,
headers: selectedHeaders(req.headers, publicOrigin.host),
headers: selectedPhase7iProxyHeaders(req.headers, publicOrigin, req.socket.remoteAddress),
timeout: 35_000,
}, (upstreamResponse) => {
res.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);
@@ -81,7 +43,7 @@ function main(): void {
server.requestTimeout = 35_000;
server.headersTimeout = 10_000;
server.keepAliveTimeout = 5_000;
server.listen(port, "127.0.0.1");
server.listen(port, bindHost);
const stop = (): void => {
server.close(() => process.exit(0));
@@ -0,0 +1,65 @@
import { describe, expect, it } from "vitest";
import {
resolvePhase7iProxyConfig,
selectedPhase7iProxyHeaders,
} from "./phase7i-proxy-config.js";
const baseEnv = {
PHASE7I_SOCKET_PATH: "/tmp/phase7i.sock",
PHASE7I_PROXY_PORT: "4197",
};
describe("Phase 7I proxy configuration", () => {
it("keeps HTTPS loopback-only by default", () => {
expect(resolvePhase7iProxyConfig({
...baseEnv,
PHASE7I_PUBLIC_ORIGIN: "https://phase7i.example.ts.net",
})).toMatchObject({
bindHost: "127.0.0.1",
port: 4197,
socketPath: "/tmp/phase7i.sock",
});
expect(() => resolvePhase7iProxyConfig({
...baseEnv,
PHASE7I_PUBLIC_ORIGIN: "https://phase7i.example.ts.net",
PHASE7I_PROXY_HOST: "0.0.0.0",
})).toThrow(/allowed .*bind mode/);
});
it("requires explicit tailnet HTTP opt-in before binding beyond loopback", () => {
expect(() => resolvePhase7iProxyConfig({
...baseEnv,
PHASE7I_PUBLIC_ORIGIN: "http://phase7i.example.ts.net:4197",
PHASE7I_PROXY_HOST: "0.0.0.0",
})).toThrow(/allowed .*bind mode/);
expect(resolvePhase7iProxyConfig({
...baseEnv,
PHASE7I_PUBLIC_ORIGIN: "http://phase7i.example.ts.net:4197",
PHASE7I_PROXY_HOST: "0.0.0.0",
PHASE7I_ALLOW_INSECURE_HTTP: "1",
})).toMatchObject({
bindHost: "0.0.0.0",
port: 4197,
});
});
it("derives direct-tailnet identity from the socket instead of spoofable headers", () => {
const headers = selectedPhase7iProxyHeaders({
cookie: "paperclip_phase7i=capability",
"tailscale-user-login": "spoof@example.com",
"tailscale-user-name": "Spoofed User",
"x-forwarded-for": "203.0.113.10",
}, new URL("http://phase7i.example.ts.net:4197"), "100.64.0.2");
expect(headers).toMatchObject({
host: "phase7i.example.ts.net:4197",
cookie: "paperclip_phase7i=capability",
"x-forwarded-for": "100.64.0.2",
"x-forwarded-host": "phase7i.example.ts.net:4197",
"x-forwarded-proto": "http",
"x-phase7i-proxy": "v1",
});
expect(headers).not.toHaveProperty("tailscale-user-login");
expect(headers).not.toHaveProperty("tailscale-user-name");
});
});
@@ -0,0 +1,75 @@
import { isIP } from "node:net";
import type { IncomingHttpHeaders } from "node:http";
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",
]);
export type Phase7iProxyConfig = {
socketPath: string;
port: number;
bindHost: string;
publicOrigin: URL;
};
export function resolvePhase7iProxyConfig(env: NodeJS.ProcessEnv): Phase7iProxyConfig {
const socketPath = env.PHASE7I_SOCKET_PATH?.trim() ?? "";
const port = Number(env.PHASE7I_PROXY_PORT);
const bindHost = env.PHASE7I_PROXY_HOST?.trim() || "127.0.0.1";
const allowInsecureHttp = env.PHASE7I_ALLOW_INSECURE_HTTP === "1";
const publicOrigin = new URL(env.PHASE7I_PUBLIC_ORIGIN ?? "");
const directTailnetHttp = publicOrigin.protocol === "http:" && allowInsecureHttp;
const loopbackBind = bindHost === "127.0.0.1" || bindHost === "::1";
if (
!socketPath
|| !Number.isInteger(port)
|| port < 1
|| port > 65_535
|| isIP(bindHost) === 0
|| (publicOrigin.protocol !== "https:" && !directTailnetHttp)
|| (!loopbackBind && !directTailnetHttp)
) {
throw new Error(
"PHASE7I_SOCKET_PATH, an allowed PHASE7I_PUBLIC_ORIGIN/bind mode, and a valid PHASE7I_PROXY_PORT are required",
);
}
return { socketPath, port, bindHost, publicOrigin };
}
export function selectedPhase7iProxyHeaders(
headers: IncomingHttpHeaders,
publicOrigin: URL,
remoteAddress: string | undefined,
): Record<string, string | string[]> {
const selected: Record<string, string | string[]> = {};
const directTailnetHttp = publicOrigin.protocol === "http:";
for (const [name, value] of Object.entries(headers)) {
if (!FORWARDED_HEADERS.has(name) || value === undefined) continue;
if (
directTailnetHttp
&& (name === "tailscale-user-login" || name === "tailscale-user-name" || name === "x-forwarded-for")
) {
continue;
}
selected[name] = value;
}
selected.host = publicOrigin.host;
selected["x-forwarded-proto"] = publicOrigin.protocol.slice(0, -1);
selected["x-forwarded-host"] = publicOrigin.host;
if (directTailnetHttp) selected["x-forwarded-for"] = remoteAddress ?? "tailnet-peer";
selected["x-phase7i-proxy"] = "v1";
return selected;
}