Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
33a2e59
fix(security): isolate remote project execution
kojiwakayama Aug 2, 2026
9a9d53b
fix(security): enforce host outbound egress policy
kojiwakayama Aug 2, 2026
6f331f8
docs(security): document host egress ceiling
kojiwakayama Aug 2, 2026
03f6e7f
fix(security): close outbound module escape paths
kojiwakayama Aug 2, 2026
1cfa47a
fix(security): isolate tenant execution request boundaries
kojiwakayama Aug 2, 2026
5409c2e
fix(security): close shared runtime execution paths
kojiwakayama Aug 2, 2026
4bab373
fix(security): guard source-controlled host transports
kojiwakayama Aug 2, 2026
26f4819
fix(security): bind privileged transports to approved origins
kojiwakayama Aug 2, 2026
cbac43c
fix(security): bound cloud blob response lifecycles
kojiwakayama Aug 2, 2026
0c0cfea
fix(security): close privileged egress escape paths
kojiwakayama Aug 2, 2026
7fec2df
fix(security): close runtime egress review gaps
kojiwakayama Aug 2, 2026
539311e
fix(security): preserve pinned transport parity in Node
kojiwakayama Aug 2, 2026
c6d75aa
fix(security): preserve dedicated runtime execution boundaries
kojiwakayama Aug 3, 2026
c6940f5
test(oauth): use routable dispatcher endpoints
kojiwakayama Aug 3, 2026
b1cc95b
fix(data): bound isolated request exposure
kojiwakayama Aug 3, 2026
77064e1
test(security): exercise guarded egress deterministically
kojiwakayama Aug 3, 2026
97fb09b
test(security): use routable guarded-egress fixtures
kojiwakayama Aug 3, 2026
fc4481f
test(embedding): use routable signed upload fixtures
kojiwakayama Aug 3, 2026
695cb70
test(security): make guarded egress fixtures portable
kojiwakayama Aug 3, 2026
d1abeaf
fix(security): surface isolated middleware requirement
kojiwakayama Aug 3, 2026
aaa38c8
fix(security): preserve CSP provenance after rebase
kojiwakayama Aug 3, 2026
ed780ba
fix(security): honor host execution denial
kojiwakayama Aug 3, 2026
45f8c24
fix(security): contain prepared API dependency reads
kojiwakayama Aug 3, 2026
2104322
fix(security): snapshot prepared API sources
kojiwakayama Aug 3, 2026
24df369
test(routing): ratchet security config typecheck
kojiwakayama Aug 3, 2026
446a778
test(cache): use guarded API transport fixture
kojiwakayama Aug 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ REDIS_URL=
# NODE_ENV=production
# CHANNEL_DISPATCH_SIGNING_PUBLIC_KEY=

# Host outbound network policy
# Remote modules and remote MCP calls may reach public HTTP(S) endpoints. The
# runtime rejects loopback, private, link-local, metadata, and other non-global
# destinations, including after DNS resolution and on every redirect hop.
#
# Emergency compatibility override. This disables that host SSRF ceiling and
# must remain unset in shared runtimes. Project environment overlays cannot set
# this host-owned option.
# VERYFRONT_HOST_ALLOW_INTERNAL_EGRESS=1

# Binary compilation
# Set to 1 to always rebuild binary, even if source unchanged
VERYFRONT_BINARY_FRESH=1
Expand Down
2 changes: 2 additions & 0 deletions cli/commands/eval/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1172,6 +1172,8 @@ export async function runEvalCommand(
fsAdapter: adapter.fs,
cacheKey: configCacheKey,
verbose: options.debug,
// The CLI executes source from the operator-selected local project.
allowHostProjectCodeExecution: true,
});
const evals = getDiscoveredEvals(projectRuntime);

Expand Down
9 changes: 5 additions & 4 deletions cli/commands/schedule/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const originalEnvironment = Object.fromEntries(
const projectId = "22222222-2222-4222-8222-222222222222";
const scheduleId = "33333333-3333-4333-8333-333333333333";
const runId = "run_11111111-1111-4111-8111-111111111111";
const TEST_PUBLIC_API_ORIGIN = "https://93.184.216.34";

class ExitSentinel extends Error {
constructor(readonly code: number) {
Expand Down Expand Up @@ -165,7 +166,7 @@ describe("schedule command", () => {
await Deno.writeTextFile(
`${projectDir}/veryfront.json`,
JSON.stringify({
apiUrl: "https://api.from-config.test",
apiUrl: TEST_PUBLIC_API_ORIGIN,
apiToken: "config-token",
projectSlug: "json-only-project",
}) + "\n",
Expand Down Expand Up @@ -216,9 +217,9 @@ describe("schedule command", () => {

assertEquals(exitCode, 0);
assertEquals(requests.map((request) => request.url), [
"https://api.from-config.test/projects/json-only-project/schedules?status=active&source_trigger_id=process-job-submissions",
`https://api.from-config.test/projects/json-only-project/schedules/${scheduleId}/runs`,
`https://api.from-config.test/runs/${encodeURIComponent(runId)}`,
`${TEST_PUBLIC_API_ORIGIN}/projects/json-only-project/schedules?status=active&source_trigger_id=process-job-submissions`,
`${TEST_PUBLIC_API_ORIGIN}/projects/json-only-project/schedules/${scheduleId}/runs`,
`${TEST_PUBLIC_API_ORIGIN}/runs/${encodeURIComponent(runId)}`,
]);
assertEquals(
requests.map((request) => new Headers(request.init?.headers).get("Authorization")),
Expand Down
14 changes: 12 additions & 2 deletions cli/commands/schedule/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ function formatSchedule(schedule: ScheduleDefinition): string {
async function handleScheduleList(_args: ParsedArgs): Promise<void> {
const projectDir = Deno.cwd();
await withProjectSourceContext(projectDir, async ({ adapter, config }) => {
const result = await discoverSchedules({ projectDir, adapter, config });
const result = await discoverSchedules({
projectDir,
adapter,
config,
allowHostProjectCodeExecution: true,
});
await outputTriggerList({
command: "schedules",
items: result.items,
Expand Down Expand Up @@ -205,7 +210,12 @@ export async function handleScheduleCommand(args: ParsedArgs): Promise<void> {
await withProjectSourceContext(projectDir, async (context) => {
const { adapter, config, configCacheKey, projectId } = context;
const input = opts.input ? await readJsonFile(opts.input, "--input JSON file") : undefined;
const result = await discoverSchedules({ projectDir, adapter, config });
const result = await discoverSchedules({
projectDir,
adapter,
config,
allowHostProjectCodeExecution: true,
});
if (result.errors.length > 0) {
throw DEPLOYMENT_ERROR.create({
detail: `Schedule discovery failed: ${result.errors[0]?.message}`,
Expand Down
7 changes: 6 additions & 1 deletion cli/commands/schedules/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ function formatSchedule(schedule: ScheduleDefinition): string {
export async function handleSchedulesCommand(_args: ParsedArgs): Promise<void> {
const projectDir = Deno.cwd();
await withProjectSourceContext(projectDir, async ({ adapter, config }) => {
const result = await discoverSchedules({ projectDir, adapter, config });
const result = await discoverSchedules({
projectDir,
adapter,
config,
allowHostProjectCodeExecution: true,
});
await outputTriggerList({
command: "schedules",
items: result.items,
Expand Down
1 change: 1 addition & 0 deletions cli/commands/task/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export async function taskCommand(options: TaskOptions): Promise<void> {
fsAdapter: adapter.fs,
cacheKey: configCacheKey,
debug: options.debug,
allowHostProjectCodeExecution: true,
});
logRuntimeDiscoveryWarnings(discovery.errors, options.debug);

Expand Down
14 changes: 12 additions & 2 deletions cli/commands/webhook/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ function formatWebhook(webhook: WebhookDefinition): string {
async function handleWebhookList(_args: ParsedArgs): Promise<void> {
const projectDir = Deno.cwd();
await withProjectSourceContext(projectDir, async ({ adapter, config }) => {
const result = await discoverWebhooks({ projectDir, adapter, config });
const result = await discoverWebhooks({
projectDir,
adapter,
config,
allowHostProjectCodeExecution: true,
});
await outputTriggerList({
command: "webhooks",
items: result.items,
Expand Down Expand Up @@ -68,7 +73,12 @@ export async function handleWebhookCommand(args: ParsedArgs): Promise<void> {

await withProjectSourceContext(projectDir, async (context) => {
const { adapter, config, configCacheKey, projectId } = context;
const result = await discoverWebhooks({ projectDir, adapter, config });
const result = await discoverWebhooks({
projectDir,
adapter,
config,
allowHostProjectCodeExecution: true,
});
if (result.errors.length > 0) {
throw new Error(`Webhook discovery failed: ${result.errors[0]?.message}`);
}
Expand Down
7 changes: 6 additions & 1 deletion cli/commands/webhooks/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ function formatWebhook(webhook: WebhookDefinition): string {
export async function handleWebhooksCommand(_args: ParsedArgs): Promise<void> {
const projectDir = Deno.cwd();
await withProjectSourceContext(projectDir, async ({ adapter, config }) => {
const result = await discoverWebhooks({ projectDir, adapter, config });
const result = await discoverWebhooks({
projectDir,
adapter,
config,
allowHostProjectCodeExecution: true,
});
await outputTriggerList({
command: "webhooks",
items: result.items,
Expand Down
4 changes: 2 additions & 2 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@
"docs": "deno run --allow-read --allow-write --allow-run --allow-env scripts/docs/generate-api-reference.ts",
"docs:coverage": "deno run --allow-read scripts/docs/docs-coverage.ts",
"docs:copy": "rm -rf ../../docs/docs/code/api-reference && cp -r docs/api-reference/ ../../docs/docs/code/api-reference/",
"docs:validate": "deno run --allow-read scripts/docs/validate-api-reference.ts && deno run --allow-read scripts/docs/validate-guides.ts && deno run --allow-read scripts/docs/validate-public-docs.ts && deno test --config=scripts/test.deno.json --no-check --allow-read scripts/docs/docs-coverage.test.ts && deno test --no-check --allow-read tests/docs/guide-contracts.test.ts tests/docs/guide-content.test.ts && deno test --no-check --allow-all tests/docs/guide-examples.test.ts tests/docs/guide-code-examples.test.ts && deno run -A scripts/lint/check-doc-links.ts",
"docs:validate": "deno run --allow-read scripts/docs/validate-api-reference.ts && deno run --allow-read scripts/docs/validate-guides.ts && deno run --allow-read scripts/docs/validate-public-docs.ts && deno test --config=scripts/test.deno.json --no-check --allow-read scripts/docs/docs-coverage.test.ts && deno test --no-check --allow-read tests/docs/guide-contracts.test.ts tests/docs/guide-content.test.ts && DENO_TESTING=1 deno test --no-check --allow-all tests/docs/guide-examples.test.ts tests/docs/guide-code-examples.test.ts && deno run -A scripts/lint/check-doc-links.ts",
"docs:verify-npm": "node scripts/docs/verify-npm-exports.mjs && node scripts/docs/verify-npm-node.mjs",
"docs:check-links": "deno run -A scripts/lint/check-doc-links.ts",
"lint:ban-zod": "deno run --allow-read scripts/lint/ban-zod-imports.ts",
Expand Down Expand Up @@ -528,7 +528,7 @@
"test:all-runtimes": "deno task test:unit && deno task test:node && deno task test:bun",
"test:e2e": "deno task test:e2e:playwright",
"test:e2e:playwright": "PW_DISABLE_TS_ESM=1 npx playwright test --config=tests/e2e/playwright.config.cjs",
"test:e2e:rsc-browser": "deno task generate && VF_DISABLE_LRU_INTERVAL=1 SSR_TRANSFORM_PER_PROJECT_LIMIT=0 REVALIDATION_PER_PROJECT_LIMIT=0 NODE_ENV=production LOG_FORMAT=text deno test --no-check --allow-all tests/e2e/regressions/rsc-proxy-hydration.test.ts tests/e2e/regressions/2026-07-27-legacy-router-hydration.test.ts tests/e2e/regressions/2026-07-27-release-asset-page-island-hydration.test.ts --unstable-worker-options --unstable-net",
"test:e2e:rsc-browser": "deno task generate && DENO_TESTING=1 VF_DISABLE_LRU_INTERVAL=1 SSR_TRANSFORM_PER_PROJECT_LIMIT=0 REVALIDATION_PER_PROJECT_LIMIT=0 NODE_ENV=production LOG_FORMAT=text deno test --no-check --allow-all tests/e2e/regressions/rsc-proxy-hydration.test.ts tests/e2e/regressions/2026-07-27-legacy-router-hydration.test.ts tests/e2e/regressions/2026-07-27-release-asset-page-island-hydration.test.ts --unstable-worker-options --unstable-net",
"test:e2e:binary": "deno task generate && deno test --allow-all tests/integration/compiled-binary-e2e.test.ts",
"test:e2e:binary:fresh": "deno task generate && VERYFRONT_BINARY_FRESH=1 deno test --allow-all tests/integration/compiled-binary-e2e.test.ts",
"test:e2e:templates": "deno run --allow-all scripts/test/template-runtime-e2e.ts",
Expand Down
2 changes: 0 additions & 2 deletions scripts/lint/test-typecheck-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,9 @@
"src/rendering/orchestrator/config.test.ts",
"src/rendering/utils/react-helpers.test.ts",
"src/resource/registry.test.ts",
"src/routing/api/module-loader/security-config.test.ts",
"src/routing/api/openapi/mcp-resource.test.ts",
"src/runs/schemas.test.ts",
"src/server/build-service-worker.test.ts",
"src/server/handlers/response/cors.test.ts",
"src/transforms/import-rewriter/strategies/import-map-strategy.test.ts",
"src/transforms/md/compiler/md-compiler.test.ts",
"src/transforms/mdx/compiler/index.test.ts",
Expand Down
1 change: 1 addition & 0 deletions scripts/test/coverage-ci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface LcovLineRecord {

const UNIT_COVERAGE_ROOTS = ["src", "cli"];
const UNIT_COVERAGE_ENV = {
DENO_TESTING: "1",
VF_DISABLE_LRU_INTERVAL: "1",
SSR_TRANSFORM_PER_PROJECT_LIMIT: "0",
REVALIDATION_PER_PROJECT_LIMIT: "0",
Expand Down
24 changes: 21 additions & 3 deletions src/agent/ag-ui/detached-start.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,10 +282,20 @@ describe("agent/ag-ui-detached-start", () => {
const response = await executeAgUiDetachedStart(
{
sessionManager,
context: { tenant: "acme" },
startDetachedExecution: async ({ request, context }) => {
context: (request) => {
assertEquals(request.headers.get("authorization"), "Bearer public-user");
assertEquals(request.headers.get("cookie"), "session=public");
assertEquals(request.headers.get("x-token"), null);
assertEquals(request.headers.get("x-project-id"), null);
return { tenant: "acme" };
},
startDetachedExecution: async ({ request, context, rawRequest, requestOrCtx }) => {
assertEquals(request.runId, "run_1");
assertEquals(context, { tenant: "acme" });
assertEquals(rawRequest.headers.get("authorization"), "Bearer public-user");
assertEquals(rawRequest.headers.get("x-forwarded-host"), null);
assertEquals(rawRequest.headers.get("x-veryfront-control-plane-jws"), null);
assertEquals(requestOrCtx, rawRequest);
},
onAccepted: ({ runId }) => {
acceptedRunId = runId;
Expand All @@ -306,7 +316,15 @@ describe("agent/ag-ui-detached-start", () => {
}),
rawRequest: new Request("http://localhost/api/runs", {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: {
"Content-Type": "application/json",
Authorization: "Bearer public-user",
Cookie: "session=public",
"x-token": "host-secret",
"x-project-id": "infrastructure-project",
"x-forwarded-host": "trusted-proxy.example",
"x-veryfront-control-plane-jws": "signed-infrastructure-token",
},
}),
},
);
Expand Down
16 changes: 12 additions & 4 deletions src/agent/ag-ui/detached-start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from "../runtime/index.ts";
import type { Agent } from "../types.ts";
import type { ChatUiMessage, MessageMetadata } from "#veryfront/chat/types.ts";
import { createApplicationRequest } from "#veryfront/security/http/application-request.ts";

const AGENT_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
const getAgUiDetachedRunIdSchema = defineSchema((v) =>
Expand Down Expand Up @@ -302,7 +303,11 @@ export async function executeAgUiDetachedStart(
input: ExecuteAgUiDetachedStartInput,
): Promise<Response> {
const rawRequest = assertDetachedStartRawRequest(options, input);
const context = await resolveDetachedStartContext(options, input);
const applicationRequest = rawRequest ? createApplicationRequest(rawRequest) : undefined;
const context = await resolveDetachedStartContext(options, {
...input,
rawRequest: applicationRequest,
});

try {
const abortSignal = options.sessionManager.startRun({
Expand All @@ -321,8 +326,8 @@ export async function executeAgUiDetachedStart(
if (options.startDetachedExecution) {
await options.startDetachedExecution({
request: input.request,
requestOrCtx: input.requestOrCtx,
rawRequest: rawRequest!,
requestOrCtx: applicationRequest,
rawRequest: applicationRequest!,
context,
abortSignal,
});
Expand Down Expand Up @@ -410,9 +415,12 @@ export function createAgUiDetachedStartHandler(

return async function POST(requestOrCtx: unknown): Promise<Response> {
const request = extractRequest(requestOrCtx);
const applicationRequest = createApplicationRequest(request);

try {
const parsed = getAgUiDetachedStartRequestSchema().parse(await parseAgUiJsonBody(request));
const parsed = getAgUiDetachedStartRequestSchema().parse(
await parseAgUiJsonBody(applicationRequest),
);
return await executeAgUiDetachedStart(options, {
request: parsed,
rawRequest: request,
Expand Down
41 changes: 30 additions & 11 deletions src/agent/ag-ui/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,23 +433,42 @@ describe("agent/ag-ui-handler", () => {
const testAgent = createTestAgent();
const handler = createAgUiHandler({
agent: testAgent.agent,
context: { tenant: "acme" },
beforeStream: ({ lastUserText, context }) => ({
prepend: [{
role: "user",
parts: [{
type: "text",
text: `Retrieved context for: ${lastUserText}`,
context: (request) => {
assertEquals(request.headers.get("authorization"), "Bearer public-user");
assertEquals(request.headers.get("cookie"), "session=public");
assertEquals(request.headers.get("x-token"), null);
assertEquals(request.headers.get("x-project-id"), null);
return { tenant: "acme" };
},
beforeStream: ({ request, lastUserText, context }) => {
assertEquals(request.headers.get("authorization"), "Bearer public-user");
assertEquals(request.headers.get("x-forwarded-host"), null);
assertEquals(request.headers.get("x-veryfront-dispatch-jws"), null);
return {
prepend: [{
role: "user",
parts: [{
type: "text",
text: `Retrieved context for: ${lastUserText}`,
}],
}],
}],
context: { ...context, retrieval: "complete" },
}),
context: { ...context, retrieval: "complete" },
};
},
});

const response = await handler(
new Request("http://localhost/api/ag-ui", {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: {
"Content-Type": "application/json",
Authorization: "Bearer public-user",
Cookie: "session=public",
"x-token": "host-secret",
"x-project-id": "infrastructure-project",
"x-forwarded-host": "trusted-proxy.example",
"x-veryfront-dispatch-jws": "signed-infrastructure-token",
},
body: JSON.stringify({
messages: [{
id: "msg-1",
Expand Down
12 changes: 7 additions & 5 deletions src/agent/ag-ui/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
} from "./host-support.ts";
import { extractRequest } from "./request-shared.ts";
import { type AgUiResumeValue, buildMergedAgUiTools } from "./tool-shared.ts";
import { createApplicationRequest } from "#veryfront/security/http/application-request.ts";

export {
type AgUiContextItem,
Expand Down Expand Up @@ -523,6 +524,7 @@ export function createAgUiHandler(
) {
return async function POST(requestOrCtx: unknown): Promise<Response> {
const request = extractRequest(requestOrCtx);
const applicationRequest = createApplicationRequest(request);

let agent: Agent | undefined;

Expand All @@ -548,7 +550,7 @@ export function createAgUiHandler(
}

try {
const parsed = await parseAgUiRequestOrError(request);
const parsed = await parseAgUiRequestOrError(applicationRequest);
if (isResponseLike(parsed)) {
return parsed;
}
Expand All @@ -565,13 +567,13 @@ export function createAgUiHandler(
}

const context = typeof options?.context === "function"
? await options.context(request)
? await options.context(applicationRequest)
: options?.context ?? {};

return await createAgUiInjectedToolsStreamResponse(
agent,
parsed,
request,
applicationRequest,
context,
options.sessionManager,
options?.beforeStream,
Expand All @@ -580,13 +582,13 @@ export function createAgUiHandler(
}

const context = typeof options?.context === "function"
? await options.context(request)
? await options.context(applicationRequest)
: options?.context ?? {};

return await createAgUiDirectStreamResponse(
agent,
parsed,
request,
applicationRequest,
context,
options?.beforeStream,
options?.onComplete,
Expand Down
Loading