fix: run the provider sandbox and admit a release to a named environment

The provider sandbox never ran. bubblewrap 0.9.0 stops parsing an `--args`
file at the first non-option and never hands the remainder back, so the
command written into that file was silently dropped: bwrap printed its usage
text, exited 1, and the provider produced no evidence at all. The options
still travel in the args file — that is what keeps host paths and credentials
out of `/proc/<pid>/cmdline` — but the command now rides on real argv, and
`encodeProviderBwrapInput` refuses a `--` so the drop cannot come back.

The scope wrapper then could not exit. It read the supervisor's liveness pipe
through `fs`, which runs a blocking `read(2)` on a threadpool thread; the
supervisor holds that pipe open for the scope's whole life, so the read never
returned and closing the descriptor did not interrupt it. Once bubblewrap
finished the wrapper deadlocked in `process.exit`, the scope outlived the
provider, and a completed run was reported as a timeout kill. The channel is
now read through the event loop, so teardown is observable and terminal.

Creation modes were left to the ambient umask. `mkdir(mode)` and `open(mode)`
are requests the kernel subtracts the umask from, so a runner exporting a
restrictive umask produced directories it could not enter and handed `tar` a
file it could not re-open. Private modes are pinned instead of inherited.

Promotion cleanup deleted before it checked. Removals run through a pinned
descriptor, so a leaf substituted after validation had this promotion's exact
five destroyed first and the substitution reported afterwards, leaving a
half-emptied directory a retry could not tell from a completed one. The name
is re-bound to the inode before anything is removed, so the failure is total.

Separately, release coherence proved the artifacts agreed with each other but
never that they belonged where they were going: a build whose runtime document
said `APP_ENV: local`, `AUTH_MODE: demo` and a loopback API is coherent with
itself and passed every gate. `public/` is copied verbatim into `dist/`, so
that local document shipped with every build regardless of what the build was
for. Runtime configuration now comes from a declared profile, and FE-GATE-027
refuses to admit an artifact to an environment it does not match — including
refusing an undeclared destination, so nothing is admitted by omission.

`REQUEST_TIMEOUT_MS` and `VITE_ROUTER_BASE_PATH` were validated and then
dropped: the V3 executor ran every operation on its contract's own deadline,
and Vite emitted root-absolute assets for a sub-path deployment. The timeout is
now a ceiling that may tighten a contract but never loosen one, and one base
path feeds the router, the Service Worker scope and the asset base together.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-15 16:38:19 +09:00
co-authored by Claude Opus 5
parent a0fbafb77b
commit dfb7734674
28 changed files with 1082 additions and 48 deletions
+30 -5
View File
@@ -842,12 +842,15 @@ describe("candidate archive and provider upload boundaries", () => {
expect((await readdir(path.dirname(sealedPath))).filter((leaf) => leaf.includes(".guardian-")))
.toEqual([]);
// `providerWriter` is only a path; the retry has to materialise the script
// it names or the clean-retry claim is proven by a module-not-found error.
await writeFile(fixture.providerWriter, providerV2WriterSource());
const retried = runProviderSupervisor(fixture, {
command: `node ${JSON.stringify(fixture.providerWriter)}`,
sealedPath,
});
expect(retried.status, retried.stderr).toBe(0);
}, 15_000);
}, 20_000);
it("collects the whole provider scope when its supervisor dies", async () => {
const fixture = await createProviderFixture();
@@ -1272,9 +1275,17 @@ describe("verified promotion finalizer", () => {
"provider-verification.json": valid["promotion-verification.json"],
"promotion-verification.json": valid["provider-verification.json"],
};
// `{}` never reaches the digest comparison: it fails the report schema
// first, so this case asserted a decode error while claiming to cover the
// digest branch. The substitution has to be a structurally valid report
// that simply is not the one the verification records committed to.
const divergentReport = JSON.parse(
valid["vulnerability-report.json"].toString("utf8"),
) as Record<string, any>;
divergentReport.provider = "divergent-provider";
const reportMismatch = {
...valid,
"vulnerability-report.json": Buffer.from("{}\n"),
"vulnerability-report.json": jsonBytes(divergentReport),
};
const absent = { ...valid } as Partial<typeof valid>;
delete absent["provider-verification.json"];
@@ -1513,7 +1524,10 @@ describe("verified promotion finalizer", () => {
}),
).rejects.toThrow(/leaf.*identity|staging leaf/u);
await expect(readFile(canary, "utf8")).resolves.toBe("unchanged\n");
await expect(readdir(saved)).resolves.toEqual([]);
// A substituted leaf aborts the cleanup before anything is removed, so the
// promotion this call owned is still intact and a retry sees a coherent
// directory rather than a half-emptied one.
expect((await readdir(saved)).sort()).toEqual([...PROMOTED_FILE_NAMES].sort());
}, 30_000);
it("never deletes an unrelated leaf substituted after cleanup validation", async () => {
@@ -1681,8 +1695,19 @@ async function readCgroupPids(cgroupRoot: string): Promise<number[]> {
async function waitForDirectProviderChildren(supervisorPid: number): Promise<number[]> {
for (let attempt = 0; attempt < 120; attempt += 1) {
const childrenPath = `/proc/${supervisorPid}/task/${supervisorPid}/children`;
const children = (await readFile(childrenPath, "utf8"))
.trim().split(/\s+/u).filter(Boolean).map(Number);
let listing: string;
try {
listing = await readFile(childrenPath, "utf8");
} catch (error) {
if (hasErrorCode(error, "ENOENT")) {
throw new Error(
"provider supervisor exited before its children could be observed",
{ cause: error },
);
}
throw error;
}
const children = listing.trim().split(/\s+/u).filter(Boolean).map(Number);
const arguments_ = children.flatMap((pid) => {
try {
return [showProcessArguments(pid)];
+166
View File
@@ -0,0 +1,166 @@
import { readFile, readdir } from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
DEPLOYMENT_TARGETS,
findAdmissionViolations,
isDeploymentTarget,
type AdmissionInput,
} from "../../src/contracts/deployment-admission.ts";
import { runtimeConfigV2ArtifactSchema } from "../../src/contracts/release-artifacts.ts";
import { generateRuntimeConfig } from "../../scripts/generate-runtime-config.ts";
const PRODUCTION_ARTIFACT: AdmissionInput = Object.freeze({
APP_ENV: "production",
API_BASE_URL: "https://api.example.com/",
REQUEST_TIMEOUT_MS: 10_000,
MAX_RETRY_ATTEMPTS: 2,
TELEMETRY_ENABLED: false,
AUTH_MODE: "external",
CONFIG_SCHEMA_VERSION: "2.0",
RELEASE_MANIFEST_URL: "/release-manifest.json",
BUILD_ID: "20260815.42",
RELEASE_ID: "r-2026.08.15-1",
CAPABILITY_OVERRIDES: Object.freeze({
REALTIME: "DEFAULT",
WEB_WORKER: "DEFAULT",
SERVICE_WORKER: "DEFAULT",
OFFLINE_COMMANDS: "DEFAULT",
}),
}) as AdmissionInput;
const LOCAL_ARTIFACT: AdmissionInput = Object.freeze({
...PRODUCTION_ARTIFACT,
APP_ENV: "local",
API_BASE_URL: "http://localhost:8080/",
AUTH_MODE: "demo",
BUILD_ID: "local-build",
RELEASE_ID: "local-release",
}) as AdmissionInput;
describe("deployment admission", () => {
it("admits an artifact only to the environment it declares", () => {
expect(findAdmissionViolations("production", PRODUCTION_ARTIFACT)).toEqual([]);
expect(findAdmissionViolations("local", LOCAL_ARTIFACT)).toEqual([]);
});
it("refuses the exact local build that release coherence used to approve", () => {
// The review's strongest reproduction: FE-GATE-015 passed on a build whose
// runtime document was APP_ENV=local / AUTH_MODE=demo / loopback API. Each
// of those is now an independent refusal, so fixing one does not admit it.
const violations = findAdmissionViolations("production", LOCAL_ARTIFACT);
const fields = violations.map((violation) => violation.field);
expect(fields).toContain("APP_ENV");
expect(fields).toContain("AUTH_MODE");
expect(fields).toContain("API_BASE_URL");
expect(fields).toContain("BUILD_ID");
expect(fields).toContain("RELEASE_ID");
});
it("refuses endpoints a browser on the public internet cannot reach", () => {
for (const host of [
"http://api.example.com/",
"https://localhost/",
"https://127.0.0.1/",
"https://10.0.0.5/",
"https://192.168.1.10/",
"https://172.16.4.4/",
"https://169.254.169.254/",
"https://[::1]/",
]) {
const violations = findAdmissionViolations("production", {
...PRODUCTION_ARTIFACT,
API_BASE_URL: host,
} as AdmissionInput);
expect(violations.map((violation) => violation.field), host).toContain(
"API_BASE_URL",
);
}
});
it("permits a routable public host", () => {
expect(
findAdmissionViolations("production", {
...PRODUCTION_ARTIFACT,
API_BASE_URL: "https://api.172.16.example.com/",
} as AdmissionInput),
).toEqual([]);
});
it("refuses a placeholder identity on a public target", () => {
for (const buildId of ["local-build", "local", "dev", "unknown", ""]) {
const violations = findAdmissionViolations("production", {
...PRODUCTION_ARTIFACT,
BUILD_ID: buildId,
} as AdmissionInput);
expect(violations.map((violation) => violation.field), buildId).toContain(
"BUILD_ID",
);
}
});
it("treats an unknown target as not a target at all", () => {
for (const value of ["prod", "PRODUCTION", "", undefined, null, 1]) {
expect(isDeploymentTarget(value), String(value)).toBe(false);
}
for (const target of DEPLOYMENT_TARGETS) {
expect(isDeploymentTarget(target)).toBe(true);
}
});
});
describe("runtime config profiles", () => {
it("ships one valid profile per deployment target", async () => {
const files = (await readdir("config/runtime")).sort();
expect(files).toEqual(
[...DEPLOYMENT_TARGETS].map((target) => `${target}.json`).sort(),
);
for (const target of DEPLOYMENT_TARGETS) {
const source: unknown = JSON.parse(
await readFile(path.join("config/runtime", `${target}.json`), "utf8"),
);
const parsed = runtimeConfigV2ArtifactSchema.safeParse({
...(source as Record<string, unknown>),
BUILD_ID: "20260815.42",
RELEASE_ID: "r-1",
});
expect(parsed.success, `${target}: ${JSON.stringify(parsed.error?.issues)}`).toBe(
true,
);
expect((source as Record<string, unknown>)["APP_ENV"]).toBe(target);
}
});
it("produces an admissible document for every public target", async () => {
for (const target of ["staging", "production"] as const) {
const config = await generateRuntimeConfig(target, {
VITE_BUILD_ID: "20260815.42",
RELEASE_ID: "r-2026.08.15-1",
});
expect(
findAdmissionViolations(target, config as unknown as AdmissionInput),
).toEqual([]);
}
});
it("refuses a deployment override that would make the document unservable", async () => {
await expect(
generateRuntimeConfig("production", {
VITE_BUILD_ID: "20260815.42",
RELEASE_ID: "r-1",
RUNTIME_API_BASE_URL: "http://api.example.com/",
}),
).rejects.toThrow(/runtime config is invalid/u);
});
it("keeps a developer build from carrying a released identity by default", async () => {
const config = await generateRuntimeConfig("local", {});
expect(config["BUILD_ID"]).toBe("local-build");
expect(
findAdmissionViolations("production", config as unknown as AdmissionInput)
.length,
).toBeGreaterThan(0);
});
});
+70
View File
@@ -66,6 +66,76 @@ async function flushMicrotasks(): Promise<void> {
await Promise.resolve();
}
describe("runtime request deadline ceiling", () => {
/**
* `REQUEST_TIMEOUT_MS` was validated by the runtime config schema and then
* never handed to the V3 executor, so the deployment dial did nothing and
* every operation ran on its contract's own deadline. It is a ceiling: it may
* tighten an operation, never loosen one.
*/
async function settlesWithin(
contractDeadlineMs: number,
ceilingMs: number | undefined,
advanceMs: number,
): Promise<boolean> {
vi.useFakeTimers();
try {
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
...(ceilingMs === undefined ? {} : { requestDeadlineCeilingMs: ceilingMs }),
attachCredentials: () => ({ kind: "READY", headers: {} }),
// A request that only ever ends by being cut off, so what settles it is
// exactly the deadline under test.
fetcher: (_input, init) =>
new Promise((_resolve, reject) => {
const signal = (init as RequestInit | undefined)?.signal;
signal?.addEventListener(
"abort",
() => reject(new DOMException("aborted", "AbortError")),
{ once: true },
);
}),
});
let settled = false;
const pending = executor
.execute(operation({ deadlineMs: contractDeadlineMs }), {}, {
routeId: ROUTE_ID,
scope,
})
.then(
() => { settled = true; },
() => { settled = true; },
);
await vi.advanceTimersByTimeAsync(advanceMs);
await flushMicrotasks();
const observed = settled;
if (!observed) await vi.advanceTimersByTimeAsync(contractDeadlineMs + 1_000);
await pending;
return observed;
} finally {
vi.useRealTimers();
}
}
it("applies the tighter of the contract and deployment bounds", async () => {
await expect(settlesWithin(5_000, 500, 800)).resolves.toBe(true);
await expect(settlesWithin(5_000, undefined, 800)).resolves.toBe(false);
});
it("never extends a contract deadline", async () => {
await expect(settlesWithin(500, 60_000, 800)).resolves.toBe(true);
});
it("ignores a ceiling that is not a usable duration", async () => {
for (const ceiling of [0, -1, Number.NaN]) {
await expect(settlesWithin(5_000, ceiling, 800), String(ceiling)).resolves.toBe(
false,
);
}
});
});
describe("descriptor-driven HTTP execution lifetime", () => {
it("normalizes a read-side 429 to the non-applicable effect vocabulary", async () => {
const executor = createContractHttpExecutor({
@@ -70,8 +70,19 @@ describe("selective Task 3 contract closure", () => {
"--signal=SIGKILL",
unit,
]);
// `bwrap --args FD` stops parsing at the first non-option and never hands
// the remainder back, so a command placed in the args file is dropped and
// bubblewrap exits with its usage text. Refusing `--` in the option stream
// is what keeps that silent no-sandbox launch from returning.
expect(() =>
encodeProviderBwrapInput(
["--unshare-net", "--", "/usr/bin/prlimit"],
{ PROVIDER_COMMAND: command },
),
).toThrow(/terminate the option stream/u);
const frame = encodeProviderScopeFrame({
bwrapInput: Buffer.from("private-bwrap-vector\0"),
bwrapCommand: ["/usr/bin/prlimit", "--nofile=64:64", "--", "/bin/sh", "-eu", "-c", 'exec /bin/sh -eu -c "$PROVIDER_COMMAND"'],
reportPath: "/exact/report.json",
reportDev: 12,
reportIno: 34,
@@ -81,6 +92,27 @@ describe("selective Task 3 contract closure", () => {
Buffer.from("private-bwrap-vector\0").toString("base64"),
);
expect(launch.join("\0")).not.toContain("private-bwrap-vector");
// The command vector rides on real argv, so it must never be able to carry
// the secret that the args file exists to hide.
expect(frame.subarray(4).toString("utf8")).not.toContain(credential);
expect(() =>
encodeProviderScopeFrame({
bwrapInput: Buffer.from("x\0"),
bwrapCommand: [],
reportPath: "/exact/report.json",
reportDev: 12,
reportIno: 34,
}),
).toThrow(/bwrap command is invalid/u);
expect(() =>
encodeProviderScopeFrame({
bwrapInput: Buffer.from("x\0"),
bwrapCommand: ["prlimit"],
reportPath: "/exact/report.json",
reportDev: 12,
reportIno: 34,
}),
).toThrow(/bwrap command is invalid/u);
});
it("removes only the pinned raw inode during parent-loss cleanup", async () => {