security: complete local access protections

This commit is contained in:
Rohit P
2026-07-24 18:44:39 -07:00
parent 3f5ac872ca
commit ac83bc0490
20 changed files with 361 additions and 32 deletions
+4 -4
View File
@@ -2,11 +2,11 @@
// /v1/providers to catch integration drift between the GUI's expectations and the backend's
// responses. Skips cleanly when the backend is down, so it's safe to run anytime. No creds needed.
import { expect, test } from "@playwright/test";
import { BACKEND } from "./helpers";
import { backendFetch } from "./helpers";
async function backendUp(): Promise<boolean> {
try {
const res = await fetch(`${BACKEND}/v1/health`);
const res = await backendFetch("/v1/health");
return res.ok;
} catch {
return false;
@@ -15,7 +15,7 @@ async function backendUp(): Promise<boolean> {
test("health reports ok with the fields the GUI reads", async () => {
test.skip(!(await backendUp()), "backend not running on :8765");
const s = await (await fetch(`${BACKEND}/v1/health`)).json();
const s = await (await backendFetch("/v1/health")).json();
expect(s.status).toBe("ok");
// The GUI's boot reads these three off /v1/health.
expect(s).toHaveProperty("model");
@@ -24,7 +24,7 @@ test("health reports ok with the fields the GUI reads", async () => {
test("providers list has the shape the Settings pane expects", async () => {
test.skip(!(await backendUp()), "backend not running on :8765");
const providers = await (await fetch(`${BACKEND}/v1/providers`)).json();
const providers = await (await backendFetch("/v1/providers")).json();
expect(Array.isArray(providers)).toBe(true);
expect(providers.length).toBeGreaterThan(0);
// Each descriptor carries what ManageTabs renders: name/title/needs_key/fields/configured.
+23 -2
View File
@@ -1,4 +1,4 @@
import { readdirSync, statSync } from "fs";
import { readFileSync, readdirSync, statSync } from "fs";
import { homedir } from "os";
import { join } from "path";
import type { Page } from "@playwright/test";
@@ -8,10 +8,31 @@ import type { Page } from "@playwright/test";
export const BACKEND = "http://127.0.0.1:8765";
function sidecarToken(): string {
const state =
process.env.COWORKER_STATE_DIR ||
(process.platform === "win32"
? join(process.env.APPDATA || homedir(), "coworker")
: join(homedir(), ".config", "coworker"));
try {
return readFileSync(join(state, "sidecar-8765.token"), "utf8").trim();
} catch {
return "";
}
}
/** Fetch from the live sidecar with its per-launch authentication token. */
export function backendFetch(path: string, init: RequestInit = {}): Promise<Response> {
const headers = new Headers(init.headers);
const token = sidecarToken();
if (token) headers.set("X-OpenWorker-Token", token);
return fetch(`${BACKEND}${path}`, { ...init, headers });
}
/** The expanded scratch base if the backend is up and a model is ready — else null (→ skip). */
export async function scratchBaseIfReady(): Promise<string | null> {
try {
const res = await fetch(`${BACKEND}/v1/settings`);
const res = await backendFetch("/v1/settings");
const s = await res.json();
if (res.ok && s.model_ready) {
return String(s.scratch_base || "~/OpenWorker").replace(/^~(?=\/|$)/, homedir());