mirror of
https://github.com/morluto/rea.git
synced 2026-10-23 14:22:30 -05:00
Replace fixed Linux Hopper release pins with current vendor feed metadata, complete detached-PGP package verification under the stable publisher primary key, and authenticated package evidence for offline admission. Share parsed publisher facts across approved setup, doctor and the pre-execution guard. Preserve existing ownership, cancellation, private-display and cleanup rules. Fixes #3577 Normalize empty XDG settings, preserve backups during approved cache repair, and retain publisher cleanup through cancellation and shutdown. Exercise cold offline verification without reusing process-local proof state. Serialize approved cache publication across processes, restore old evidence on failure, and share the existing lease owner from process without changing its identities. Validation: 438 focused tests (six platform-related skips), full static/documentation checks, real Ubuntu 24.04 official Hopper 6.6.0 setup/CLI/MCP, offline ordinary-user admission, launcher mutation rejection, and cold/warm verification measurements. Official RPM metadata mismatch is tracked separately in #3578 and fails closed. BREAKING CHANGE: Older Linux Hopper builds admitted by the fixed allowlist now require retained publisher-signed package evidence or an approved update to the current public demo. Update explicit HOPPER_LAUNCHER_PATH values when migrating; the live feed exposes only its current release. Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
80 lines
2.7 KiB
TypeScript
80 lines
2.7 KiB
TypeScript
import { chmod, lstat, mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
import type { FileHandle } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { afterEach, expect, it, vi } from "vitest";
|
|
|
|
const replacement = vi.hoisted(() => ({
|
|
selected: "",
|
|
retained: "",
|
|
handle: undefined as FileHandle | undefined,
|
|
}));
|
|
|
|
vi.mock("node:fs/promises", async (importOriginal) => {
|
|
const original = await importOriginal<typeof import("node:fs/promises")>();
|
|
return {
|
|
...original,
|
|
open: async (...args: Parameters<typeof original.open>) => {
|
|
const handle = await original.open(...args);
|
|
if (args[0] === replacement.selected) {
|
|
replacement.handle = handle;
|
|
try {
|
|
await original.rename(replacement.selected, replacement.retained);
|
|
await original.mkdir(replacement.selected);
|
|
await original.chmod(replacement.selected, 0o755);
|
|
} catch (cause) {
|
|
await handle.close();
|
|
throw cause;
|
|
}
|
|
}
|
|
return handle;
|
|
},
|
|
};
|
|
});
|
|
|
|
import { acquireLinuxHopperApplicationLease } from "../../src/process/HopperLease.js";
|
|
|
|
afterEach(() => {
|
|
replacement.selected = "";
|
|
replacement.retained = "";
|
|
replacement.handle = undefined;
|
|
});
|
|
|
|
it.skipIf(process.platform === "win32")(
|
|
"secures only the admitted directory descriptor when its path is replaced",
|
|
async () => {
|
|
// macOS limits Unix-domain socket paths to 104 bytes. Keep the selected
|
|
// directory path short enough for the production lease socket beneath it.
|
|
const root = await mkdtemp("/tmp/r-");
|
|
const selected = join(root, "selected");
|
|
const retained = join(root, "retained");
|
|
await mkdir(selected);
|
|
await chmod(selected, 0o755);
|
|
const before = await lstat(selected);
|
|
replacement.selected = selected;
|
|
replacement.retained = retained;
|
|
const acquired = await acquireLinuxHopperApplicationLease({
|
|
directory: selected,
|
|
runId: "descriptor-permissions",
|
|
});
|
|
if (!acquired.acquired) {
|
|
await rm(root, { recursive: true, force: true });
|
|
throw new Error("Expected the fixture lease");
|
|
}
|
|
try {
|
|
expect((await lstat(retained)).ino).toBe(before.ino);
|
|
expect((await lstat(retained)).mode & 0o777).toBe(0o700);
|
|
expect((await lstat(selected)).ino).not.toBe(before.ino);
|
|
expect((await lstat(selected)).mode & 0o777).toBe(0o755);
|
|
await expect(replacement.handle?.stat()).rejects.toMatchObject({
|
|
code: "EBADF",
|
|
});
|
|
} finally {
|
|
await acquired.lease.release();
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
// This asserts descriptor permission ownership only. Later socket
|
|
// operations still resolve the path, as the production comment states.
|
|
},
|
|
);
|