fix: use owner-level unidesk state

This commit is contained in:
Codex
2026-07-10 05:09:13 +02:00
parent 01ff35d0f8
commit fbd9dbbae1
48 changed files with 133 additions and 148 deletions
+1 -1
View File
@@ -435,7 +435,7 @@ export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record<string, unkn
},
secrets: spec.secrets.map((secret) => ({
id: secret.id,
sourceRef: secret.sourceMode === "codex-config" ? secret.sourceRef : secret.sourceRef.startsWith("/") ? secret.sourceRef : `.state/secrets/${secret.sourceRef}`,
sourceRef: secret.sourceMode === "codex-config" ? secret.sourceRef : secret.sourceRef.startsWith("/") ? secret.sourceRef : rootPath(".state", "secrets", secret.sourceRef),
sourceMode: secret.sourceMode,
sourceKey: secret.sourceKey,
sourceFormat: secret.sourceFormat,
+1 -1
View File
@@ -739,7 +739,7 @@ export function readSecretSourceValue(spec: AgentRunLaneSpec, source: LaneSecret
if (value.length === 0) throw new Error(`secret source ${sourceRef} is empty`);
value = transformSecretSourceValue(spec, source, value);
return {
redactedPath: source.sourceMode === "codex-config" ? sourceRef : sourceRef.startsWith("/") ? redactAbsoluteSecretPath(sourceRef) : `.state/secrets/${sourceRef}`,
redactedPath: source.sourceMode === "codex-config" ? sourceRef : redactAbsoluteSecretPath(sourcePath ?? sourceRef),
value,
valueBytes: Buffer.byteLength(value, "utf8"),
fingerprint: sha256Fingerprint(value),
@@ -78,7 +78,7 @@ export function verifyLocalArtifactLabels(
export function devFrontendAuthPatchScript(config: UniDeskConfig): string {
const sshClientToken = composeRuntimeEnvValue("UNIDESK_SSH_CLIENT_TOKEN");
if (sshClientToken === null) throw new Error("UNIDESK_SSH_CLIENT_TOKEN must be present in .state/docker-compose.env before deploying dev frontend");
if (sshClientToken === null) throw new Error(`UNIDESK_SSH_CLIENT_TOKEN must be present in ${rootPath(".state", "docker-compose.env")} before deploying dev frontend`);
const sshClientRouteAllowlist = composeRuntimeEnvValue("UNIDESK_SSH_CLIENT_ROUTE_ALLOWLIST") ?? "G14,G14:*,D601,D601:*";
const secretData = {
AUTH_USERNAME: base64(config.auth.username),
+1 -1
View File
@@ -38,7 +38,7 @@ export const baiduNetdiskRuntimeSecretRequirements: RuntimeSecretRequirement[] =
export const baiduNetdiskAuthHealthGate: AuthHealthGate = {
requiredFields: ["configured", "clientIdConfigured", "clientSecretConfigured", "tokenKeyConfigured", "loggedIn"],
recoveryHint: "Restore UNIDESK_BAIDU_NETDISK_CLIENT_ID, UNIDESK_BAIDU_NETDISK_CLIENT_SECRET, and UNIDESK_BAIDU_NETDISK_TOKEN_KEY in the canonical .state/docker-compose.env, recreate only baidu-netdisk with the canonical env file, then verify microservice health baidu-netdisk.",
recoveryHint: "Restore UNIDESK_BAIDU_NETDISK_CLIENT_ID, UNIDESK_BAIDU_NETDISK_CLIENT_SECRET, and UNIDESK_BAIDU_NETDISK_TOKEN_KEY in /root/.unidesk/.state/docker-compose.env, recreate only baidu-netdisk with the canonical env file, then verify microservice health baidu-netdisk.",
};
export const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
+1 -1
View File
@@ -98,7 +98,7 @@ export function ciHelp(): Record<string, unknown> {
},
install: {
defaultMode: "async-job",
waitFlag: "Use --wait only for explicit synchronous debugging; the default returns a .state/jobs job immediately with install-status follow-up.",
waitFlag: "Use --wait only for explicit synchronous debugging; the default returns a /root/.unidesk/.state/jobs job immediately with install-status follow-up.",
statusCommand: "bun scripts/cli.ts ci install-status <jobId|latest>",
prewarmDefault: true,
skipPrewarm: "Use --skip-prewarm only to refresh Tekton/CI manifests when runtime images are already present or prewarm is blocked by provider root/containerd permissions.",
+16 -9
View File
@@ -1,6 +1,7 @@
import { chromium, type BrowserContext, type Request } from "playwright";
import { existsSync } from "node:fs";
import { resolve } from "node:path";
import { homedir } from "node:os";
import { join, resolve } from "node:path";
import { readConfig } from "./config";
interface CodeQueuePerfOptions {
@@ -20,6 +21,8 @@ interface ApiTiming {
durationMs: number;
}
const isCheckMode = process.execArgv.includes("--check");
function argValue(args: string[], name: string): string | null {
const index = args.indexOf(name);
if (index === -1) return null;
@@ -90,7 +93,7 @@ function parseNumberAttr(value: string | null): number | null {
}
async function runCodeQueuePerf(options: CodeQueuePerfOptions): Promise<Record<string, unknown>> {
const browsersPath = process.env.PLAYWRIGHT_BROWSERS_PATH || ".state/playwright-browsers";
const browsersPath = process.env.PLAYWRIGHT_BROWSERS_PATH || join(homedir(), ".unidesk", ".state", "playwright-browsers");
const fullChromePath = resolve(browsersPath, "chromium-1217/chrome-linux64/chrome");
const launchArgs = [
"--no-sandbox",
@@ -211,11 +214,15 @@ async function runCodeQueuePerf(options: CodeQueuePerfOptions): Promise<Record<s
}
}
const options = readOptions();
const result = await runCodeQueuePerf(options);
if (options.json) {
console.log(JSON.stringify(result));
} else {
console.log(JSON.stringify(result, null, 2));
async function main(): Promise<void> {
const options = readOptions();
const result = await runCodeQueuePerf(options);
if (options.json) {
console.log(JSON.stringify(result));
} else {
console.log(JSON.stringify(result, null, 2));
}
if (result.ok !== true) process.exitCode = 1;
}
if (result.ok !== true) process.exitCode = 1;
if (import.meta.main && !isCheckMode) await main();
+4 -4
View File
@@ -3,7 +3,7 @@ import { homedir } from "node:os";
import path from "node:path";
import { createInterface } from "node:readline";
import { spawnSync } from "node:child_process";
import { repoRoot } from "./config";
import { rootPath } from "./config";
import type { RenderedCliResult } from "./output";
type CodexTraceAction = "help" | "list" | "collect" | "show" | "grep" | "active";
@@ -85,7 +85,7 @@ export function codexTraceHelp(): Record<string, unknown> {
"bun scripts/cli.ts codex trace grep --session <id> --messages --pattern 'Playwright|web-probe'",
"bun scripts/cli.ts codex trace grep --session <id> --failed-only [--tool exec_command]",
"bun scripts/cli.ts codex trace grep --pattern 'playwright|auth-login-failed' [--file sessions/...jsonl] [--since ISO]",
"bun scripts/cli.ts codex trace collect [--root ~/.codex] [--output .state/codex-trace/<timestamp>] [--limit 30]",
"bun scripts/cli.ts codex trace collect [--root ~/.codex] [--output /root/.unidesk/.state/codex-trace/<timestamp>] [--limit 30]",
"bun scripts/cli.ts codex trace show --session <id> [--root ~/.codex] [--tail-bytes 12000]",
],
safety: [
@@ -97,7 +97,7 @@ export function codexTraceHelp(): Record<string, unknown> {
],
options: {
"--root <dir>": "Codex home or another trace root. Defaults to ~/.codex.",
"--output <dir>": "Collect destination. Defaults to .state/codex-trace/<timestamp>.",
"--output <dir>": "Collect destination. Defaults to /root/.unidesk/.state/codex-trace/<timestamp>.",
"--limit <n>": `Maximum included files, default ${defaultLimit}, max ${maxLimit}.`,
"--max-depth <n>": `Recursive scan depth, default ${defaultMaxDepth}, max ${maxDepthLimit}.`,
"--max-file-bytes <n|25MiB>": "Skip files larger than this during collect/list inclusion.",
@@ -247,7 +247,7 @@ function codexTraceList(options: CodexTraceOptions, candidates: CodexTraceCandid
function codexTraceCollect(options: CodexTraceOptions, candidates: CodexTraceCandidate[]): Record<string, unknown> | RenderedCliResult {
const selected = selectedCandidates(options, candidates);
const outputDir = options.outputDir ?? path.join(repoRoot, ".state", "codex-trace", timestampForPath());
const outputDir = options.outputDir ?? rootPath(".state", "codex-trace", timestampForPath());
const copied: Record<string, unknown>[] = [];
if (!options.dryRun) mkdirSync(outputDir, { recursive: true, mode: 0o700 });
for (const candidate of selected) {
+3 -3
View File
@@ -67,7 +67,7 @@ function processDiscoveryPlan(sessionId: string): Record<string, unknown> {
sessionId,
mutation: false,
signals: [
".state/commander/sessions/<sessionId>.json",
"/root/.unidesk/.state/commander/sessions/<sessionId>.json",
"host process table filtered by executable and cwd markers",
"PTY/stdio bridge heartbeat file",
"last prompt/trace event sequence",
@@ -280,7 +280,7 @@ function healthEndpointValidation(): Record<string, unknown> {
function stateFileValidation(sessionId: string): Record<string, unknown> {
return {
surface: "state file",
storageRoot: ".state/commander/",
storageRoot: "/root/.unidesk/.state/commander/",
validationMethod: "write and read a session record only under a temporary directory during dry-run/manual inspection",
files: [
`sessions/${sessionId}.json`,
@@ -294,7 +294,7 @@ function stateFileValidation(sessionId: string): Record<string, unknown> {
"temporary state root is deleted after the smoke contract",
],
noRuntimeSideEffects: [
"do not touch the live .state/commander directory",
"do not touch the live /root/.unidesk/.state/commander directory",
"do not patch database state",
"do not discover or signal live host Codex processes",
],
+8 -1
View File
@@ -1,5 +1,6 @@
import { existsSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { homedir } from "node:os";
import { dirname, isAbsolute, join } from "node:path";
import { fileURLToPath } from "node:url";
export interface UniDeskConfig {
@@ -101,8 +102,14 @@ export interface UniDeskMicroserviceConfig {
const moduleDir = dirname(fileURLToPath(import.meta.url));
export const repoRoot = join(moduleDir, "..", "..");
export const stateRoot = join(homedir(), ".unidesk", ".state");
export function rootPath(...parts: string[]): string {
const first = parts[0];
if (first === undefined) return repoRoot;
if (isAbsolute(first)) return join(first, ...parts.slice(1));
if (first === ".state") return join(stateRoot, ...parts.slice(1));
if (first.startsWith(".state/")) return join(stateRoot, first.slice(".state/".length), ...parts.slice(1));
return join(repoRoot, ...parts);
}
+1 -1
View File
@@ -36,7 +36,7 @@ import { k8sKubeconfig, nativeK3sCtrAddress, nativeK3sImage, nativeK3sInstallVer
export function syncDevFrontendAuthScript(config: UniDeskConfig): string {
const sshClientToken = composeRuntimeEnvValue("UNIDESK_SSH_CLIENT_TOKEN");
if (sshClientToken === null) throw new Error("UNIDESK_SSH_CLIENT_TOKEN must be present in .state/docker-compose.env before deploying dev frontend");
if (sshClientToken === null) throw new Error(`UNIDESK_SSH_CLIENT_TOKEN must be present in ${rootPath(".state", "docker-compose.env")} before deploying dev frontend`);
const sshClientRouteAllowlist = composeRuntimeEnvValue("UNIDESK_SSH_CLIENT_ROUTE_ALLOWLIST") ?? "G14,G14:*,D601,D601:*";
const data = {
AUTH_USERNAME: Buffer.from(config.auth.username, "utf8").toString("base64"),
+1 -6
View File
@@ -175,22 +175,17 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean):
if (previous.length > 0 && previous !== legacyDefault) return previous;
return defaultValue;
};
let logRoot: string;
const logRoot = resolve(rootPath(config.paths.logsDir));
let logDay: string;
let logPrefix: string;
if (!freshLogPrefix && previousRaw.length > 0) {
const previousLogDir = previousValue("UNIDESK_LOG_DIR");
const previousLogRoot = previousLogDir && /^\d{8}$/u.test(basename(previousLogDir)) ? dirname(previousLogDir) : previousLogDir;
logRoot = previousLogRoot || rootPath(config.paths.logsDir);
logPrefix = previousValue("UNIDESK_LOG_PREFIX") || localDateParts(new Date()).stamp;
logDay = previousValue("UNIDESK_LOG_DAY") || logPrefix.match(/^\d{8}/u)?.[0] || localDateParts(new Date()).day;
} else {
const parts = localDateParts(new Date());
logRoot = resolve(rootPath(config.paths.logsDir));
logDay = parts.day;
logPrefix = parts.stamp;
}
logRoot = resolve(logRoot);
mkdirSync(join(logRoot, logDay), { recursive: true });
chmodSync(logRoot, 0o777);
chmodSync(join(logRoot, logDay), 0o777);
+6 -5
View File
@@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
import { closeSync, existsSync, ftruncateSync, lstatSync, mkdirSync, opendirSync, openSync, readdirSync, readFileSync, readSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
import { basename, dirname, join, resolve } from "node:path";
import { type UniDeskConfig, repoRoot, rootPath } from "./config";
import { type UniDeskConfig, repoRoot, rootPath, stateRoot } from "./config";
import { runRemoteGcCommand } from "./gc-remote";
type GcRisk = "low" | "medium" | "high" | "blocked";
@@ -928,11 +928,12 @@ function yamlStateRoots(value: unknown, label: string): GcPathRoot[] {
return Object.entries(record).map(([id, rawPath]) => {
if (!/^[a-z0-9._-]+$/iu.test(id)) throw new Error(`${label}.${id} must use a simple id`);
const displayPath = yamlString(rawPath, `${label}.${id}`);
if (displayPath.startsWith("/") || displayPath.includes("..") || displayPath.includes("\\")) {
throw new Error(`${label}.${id} must be a repo-relative .state path without ..`);
if (displayPath.includes("..") || displayPath.includes("\\")) {
throw new Error(`${label}.${id} must be a canonical state path without ..`);
}
if (!displayPath.startsWith(".state/")) throw new Error(`${label}.${id} must start with .state/`);
return { id, displayPath, path: rootPath(...displayPath.split("/")) };
const path = resolvePath(displayPath);
if (!path.startsWith(`${stateRoot}/`)) throw new Error(`${label}.${id} must be under ${stateRoot}/`);
return { id, displayPath, path };
});
}
+6 -6
View File
@@ -94,9 +94,9 @@ export function rootHelp(): unknown {
{ command: "codex steer-confirm <taskId> --steer-id <id> [--raw]", description: "Read-only lookup for a steerId in task trace so deliveryUnconfirmed can be resolved without resending the corrective prompt." },
{ command: "codex interrupt|cancel <taskId>", description: "Request interrupt for a running Code Queue task, or cancel a queued/retry_wait task, through the same private proxy." },
{ command: "codex queues [--full|--all] [--limit N] [--page N|--offset N]", description: "Read legacy Code Queue archive summaries. Legacy queue create/merge and move are frozen; use agentrun create/apply/get/cancel for new work." },
{ command: "job|jobs list [--limit N] [--include-command]", description: "List async jobs from .state/jobs with a bounded default page and progress summaries." },
{ command: "job|jobs list [--limit N] [--include-command]", description: "List async jobs from /root/.unidesk/.state/jobs with a bounded default page and progress summaries." },
{ command: "job status|get|read <jobId|latest> [--tail-bytes N]", description: "Show job state with a structured progress summary and bounded stdout/stderr tails." },
{ command: "job cancel <jobId>", description: "Cancel a queued/running async job through the .state/jobs control entry and keep a terminal canceled record." },
{ command: "job cancel <jobId>", description: "Cancel a queued/running async job through the /root/.unidesk/.state/jobs control entry and keep a terminal canceled record." },
{ command: "debug health", description: "Probe internal core, nodes, system/Docker status, frontend, provider ingress, and public boundary." },
{ command: "debug ssh-pool <providerId>", description: "Show bounded host.ssh.tcp-pool labels for one provider, including ready/claimed/desired/lastError." },
{ command: "debug egress-proxy <providerId>", description: "Show provider-gateway egress proxy tunnel counts, stale tunnel diagnosis, active target summaries, and recent closed tunnel lifecycle without URL credential leakage." },
@@ -358,7 +358,7 @@ function gcHelp(): unknown {
"--limit N": "number of candidates returned and executed by run when --full is not set; default 50",
"--result-limit N": "number of per-candidate run results returned when --full is not set; default 50",
"--full|--raw": "return and run against all candidates rather than the default bounded page",
"--include-browser-cache": "also remove repo-local .state/playwright-browsers cache",
"--include-browser-cache": "also remove /root/.unidesk/.state/playwright-browsers cache",
"--include-tool-caches": "local and remote explicit opt-in: remove rebuildable npm/npx/Bun package caches from fixed allowlisted paths",
"--include-state-artifacts": "manual local gc only: opt in to stale UniDesk .state artifact retention for allowlisted diagnostic files and deploy artifact direct directories",
"--state-artifact-keep-days N": "keep recent UniDesk .state artifacts for N days; default 14; must be a positive integer",
@@ -428,7 +428,7 @@ function codexHelp(): unknown {
"bun scripts/cli.ts codex trace grep --session <id> --messages --pattern 'Playwright|web-probe'",
"bun scripts/cli.ts codex trace grep --session <id> --failed-only [--tool exec_command]",
"bun scripts/cli.ts codex trace grep --pattern 'playwright|auth-login-failed' [--file sessions/...jsonl] [--since ISO]",
"bun scripts/cli.ts codex trace collect [--root ~/.codex] [--output .state/codex-trace/<timestamp>] [--limit 30]",
"bun scripts/cli.ts codex trace collect [--root ~/.codex] [--output /root/.unidesk/.state/codex-trace/<timestamp>] [--limit 30]",
"bun scripts/cli.ts codex trace show --session <id> [--root ~/.codex] [--tail-bytes 12000]",
"bun scripts/cli.ts codex deploy <commitId> # disabled legacy deployment entry",
"bun scripts/cli.ts agentrun get tasks --queue commander --limit 20",
@@ -484,7 +484,7 @@ function codexHelp(): unknown {
active: "codex trace active finds open Codex session JSONL files from /proc without requiring lsof and prints copyable session ids.",
grep: "codex trace grep supports --session <id>, --messages, --tools, --tool <name>, and --failed-only. It defaults to active/recent sessions, uses rg/raw prefiltering for speed, prioritizes messages/tool inputs, and folds tool outputs unless --include-output or -o wide is explicit.",
output: "Default output is concise text/table; use -o json|yaml|name|wide for machine or wider output.",
collectOutput: ".state/codex-trace/<timestamp>/manifest.json",
collectOutput: "/root/.unidesk/.state/codex-trace/<timestamp>/manifest.json",
note: "Use --root <dir> to scan another local Codex trace directory; collect copies bounded files locally and never uploads or deletes source files.",
},
unreadTriage: {
@@ -544,7 +544,7 @@ function jobHelp(): unknown {
"bun scripts/cli.ts jobs get <jobId|latest> [--tail-bytes N]",
"bun scripts/cli.ts job cancel <jobId>",
],
description: "Inspect or cancel fire-and-forget job state from .state/jobs with structured progress summaries and bounded log tails. `jobs get/read` are compatibility aliases for `job status`; server lifecycle commands return this drill-down by default and reserve full JSON for --full/--raw.",
description: "Inspect or cancel fire-and-forget job state from /root/.unidesk/.state/jobs with structured progress summaries and bounded log tails. `jobs get/read` are compatibility aliases for `job status`; server lifecycle commands return this drill-down by default and reserve full JSON for --full/--raw.",
};
}
+1 -5
View File
@@ -87,7 +87,7 @@ export function fakeModelProviderHelp(): Record<string, unknown> {
],
actions: {
plan: "Read YAML configRefs and show local source/materialization and target k3s objects without mutation.",
materialize: "Create/update local .state/secrets source files for provider auth, Codex config, and sentinel prompt set.",
materialize: "Create/update local /root/.unidesk/.state/secrets source files for provider auth, Codex config, and sentinel prompt set.",
apply: "Materialize local sources, then apply ConfigMap/Secret/Deployment/Service to the selected node k3s namespace.",
status: "Inspect remote Deployment/Service/Secret/ConfigMap/pod readiness and /healthz without printing values.",
smoke: "Exec a deterministic ECHO streaming and non-ECHO error check inside the fake provider pod.",
@@ -941,9 +941,5 @@ function repoRelative(path: string): string {
function displayPath(path: string): string {
if (path.startsWith(`${repoRoot}/`)) return path.slice(repoRoot.length + 1);
const marker = "/.state/secrets/";
const index = path.indexOf(marker);
if (index >= 0) return `.state/secrets/${path.slice(index + marker.length)}`;
if (path.endsWith("/.state/secrets")) return ".state/secrets";
return path;
}
@@ -321,11 +321,7 @@ export function monitorWebBuildkitStatePlan(cicd: Record<string, unknown>): Reco
export function secretSourcePaths(sourceRef: string): string[] {
if (isAbsolute(sourceRef)) return [sourceRef];
const paths = [join(repoRoot, ".state", "secrets", sourceRef)];
const marker = "/.worktree/";
const index = repoRoot.indexOf(marker);
if (index >= 0) paths.push(join(repoRoot.slice(0, index), ".state", "secrets", sourceRef));
return [...new Set(paths)];
return [rootPath(".state", "secrets", sourceRef)];
}
export function parseEnvFile(textValue: string): Record<string, string> {
+3 -3
View File
@@ -2666,7 +2666,7 @@ function readSentinelSecretSourceValue(source: Record<string, unknown>): Record<
const sourceKey = stringAt(source, "sourceKey");
const sourceLine = numberAtNullable(source, "sourceLine");
const paths = secretSourcePaths(sourceRef);
const sourcePath = paths.find((item) => existsSync(item)) ?? paths[0] ?? join(repoRoot, ".state", "secrets", sourceRef);
const sourcePath = paths.find((item) => existsSync(item)) ?? paths[0] ?? rootPath(".state", "secrets", sourceRef);
if (!existsSync(sourcePath)) return { ok: false, error: "secret-source-missing", sourceRef, sourceKey, sourcePath: displayPath(sourcePath), valuesRedacted: true };
const textValue = readFileSync(sourcePath, "utf8");
const value = sourceLine === null ? parseEnvFile(textValue)[sourceKey] : textValue.split(/\r?\n/u)[sourceLine - 1]?.replace(/\r$/u, "");
@@ -2688,7 +2688,7 @@ function readSentinelWebAccountUsername(source: Record<string, unknown>): { ok:
const sourceLine = numberAtNullable(source, "usernameSourceLine");
if (sourceRef === null || sourceLine === null) return { ok: false, error: "web-account-json-username-missing" };
const paths = secretSourcePaths(sourceRef);
const sourcePath = paths.find((item) => existsSync(item)) ?? paths[0] ?? join(repoRoot, ".state", "secrets", sourceRef);
const sourcePath = paths.find((item) => existsSync(item)) ?? paths[0] ?? rootPath(".state", "secrets", sourceRef);
if (!existsSync(sourcePath)) return { ok: false, error: "web-account-json-username-source-missing" };
const value = readFileSync(sourcePath, "utf8").split(/\r?\n/u)[sourceLine - 1]?.replace(/\r$/u, "") ?? "";
return value.length === 0 ? { ok: false, error: "web-account-json-username-line-missing" } : { ok: true, value };
@@ -2720,7 +2720,7 @@ function readSentinelFrpcMaterial(state: SentinelCicdState): Record<string, unkn
const sourceRef = stringAt(state.publicExposure, "frpc.tokenSourceRef");
const sourceKey = stringAt(state.publicExposure, "frpc.tokenSourceKey");
const paths = secretSourcePaths(sourceRef);
const sourcePath = paths.find((item) => existsSync(item)) ?? paths[0] ?? join(repoRoot, ".state", "secrets", sourceRef);
const sourcePath = paths.find((item) => existsSync(item)) ?? paths[0] ?? rootPath(".state", "secrets", sourceRef);
if (!existsSync(sourcePath)) return { ok: false, error: "frp-token-source-missing", sourceRef, sourceKey, sourcePath: displayPath(sourcePath), valuesRedacted: true };
const values = parseEnvFile(readFileSync(sourcePath, "utf8"));
const token = values[sourceKey];
@@ -3,7 +3,6 @@
// Responsibility: Quick-verify observe orchestration and artifact interpretation for web-probe sentinel P5 validation.
import { createHash, randomUUID } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import type { CommandResult } from "./command";
import { runCommand } from "./command";
import { resolveCliChildJsonCommandResult } from "./cli-child-json-recovery";
@@ -1959,7 +1958,7 @@ function readPromptSetForScenario(state: SentinelCicdState, scenario: Record<str
const sourceRef = stringAt(promptSet, "promptSourceRef");
const key = stringAt(promptSet, "promptSourceKey");
const paths = secretSourcePaths(sourceRef);
const sourcePath = paths.find((item) => existsSync(item)) ?? paths[0] ?? join(repoRoot, ".state", "secrets", sourceRef);
const sourcePath = paths.find((item) => existsSync(item)) ?? paths[0] ?? rootPath(".state", "secrets", sourceRef);
const summary = { sourceRef, sourceKey: key, sourcePath: displayPath(sourcePath), valuesRedacted: true };
const runtimeRaw = process.env[key];
const values = existsSync(sourcePath) ? parseEnvFile(readFileSync(sourcePath, "utf8")) : {};
+3 -7
View File
@@ -493,7 +493,7 @@ export function runNodePublicExposure(options: NodePublicExposureOptions): Recor
source,
mutation: false,
valuesRedacted: true,
next: { fixSecretSource: `create .state/secrets/${exposure.tokenSourceRef} with ${exposure.tokenSourceKey}=<redacted>` },
next: { fixSecretSource: `create ${rootPath(".state", "secrets", exposure.tokenSourceRef)} with ${exposure.tokenSourceKey}=<redacted>` },
};
}
const caddyResult = runTransHostScript(exposure.caddyRoute, publicExposureCaddyScript(options, exposure), "", options.timeoutSeconds);
@@ -581,7 +581,7 @@ function publicExposureSecretApplyStatus(options: NodePublicExposureOptions, exp
export function readPublicExposureTokenSource(exposure: HwlabRuntimePublicExposureSpec): { ok: boolean; path: string; checkedPaths: string[]; key: string; value: string | null; fingerprint: string | null; error?: string } {
const checkedPaths = publicExposureTokenSourcePaths(exposure);
const path = checkedPaths.find((candidate) => existsSync(candidate)) ?? checkedPaths[0] ?? join(repoRoot, ".state", "secrets", exposure.tokenSourceRef);
const path = checkedPaths.find((candidate) => existsSync(candidate)) ?? checkedPaths[0] ?? rootPath(".state", "secrets", exposure.tokenSourceRef);
if (!existsSync(path)) return { ok: false, path, checkedPaths, key: exposure.tokenSourceKey, value: null, fingerprint: null, error: "secret-source-missing" };
const values = parseEnvFile(readFileSync(path, "utf8"));
const value = values[exposure.tokenSourceKey];
@@ -597,11 +597,7 @@ export function readPublicExposureTokenSource(exposure: HwlabRuntimePublicExposu
}
export function publicExposureTokenSourcePaths(exposure: HwlabRuntimePublicExposureSpec): string[] {
const paths = [join(repoRoot, ".state", "secrets", exposure.tokenSourceRef)];
const marker = "/.worktree/";
const index = repoRoot.indexOf(marker);
if (index >= 0) paths.push(join(repoRoot.slice(0, index), ".state", "secrets", exposure.tokenSourceRef));
return [...new Set(paths)];
return [rootPath(".state", "secrets", exposure.tokenSourceRef)];
}
export function publicExposureSecretStatus(fields: Record<string, string>, result: CommandResult): Record<string, unknown> {
+4 -16
View File
@@ -740,15 +740,7 @@ export function readLocalPostgresPasswordMaterial(input: { sourceRef: string; so
export function localSecretSourcePaths(sourceRef: string): string[] {
if (isAbsolute(sourceRef)) return [sourceRef];
const marker = "/.worktree/";
const index = repoRoot.indexOf(marker);
const paths = index >= 0
? [
join(repoRoot.slice(0, index), ".state", "secrets", sourceRef),
join(repoRoot, ".state", "secrets", sourceRef),
]
: [join(repoRoot, ".state", "secrets", sourceRef)];
return [...new Set(paths)];
return [rootPath(".state", "secrets", sourceRef)];
}
export function shortSecretFingerprint(value: string): string {
@@ -1274,7 +1266,7 @@ export function externalPostgresSecretSetupScript(spec: HwlabRuntimeLaneSpec, dr
export function externalPostgresSecretSourceRoot(spec: HwlabRuntimeLaneSpec): string {
const configRef = spec.externalPostgres?.configRef;
if (configRef === undefined) return join(repoRoot, ".state", "secrets");
if (configRef === undefined) return rootPath(".state", "secrets");
const configPath = rootPath(configRef);
const parsed = Bun.YAML.parse(readFileSync(configPath, "utf8")) as unknown;
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`${configRef} must be a YAML object`);
@@ -1296,11 +1288,7 @@ export function readSecretSourceValue(secretRoot: string, sourceRef: string, key
export function secretSourcePaths(sourceRef: string): string[] {
if (isAbsolute(sourceRef)) return [sourceRef];
const paths = [join(repoRoot, ".state", "secrets", sourceRef)];
const marker = "/.worktree/";
const index = repoRoot.indexOf(marker);
if (index >= 0) paths.push(join(repoRoot.slice(0, index), ".state", "secrets", sourceRef));
return [...new Set(paths)];
return [rootPath(".state", "secrets", sourceRef)];
}
export function displayRepoPath(path: string): string {
@@ -1392,7 +1380,7 @@ function readBootstrapAdminUsername(spec: RuntimeSecretSpec): { ok: true; value:
function readSecretSourceScalar(sourceRef: string, sourceKey: string, sourceLine: number | null): { ok: true; value: string; sourcePath: string; sourcePresent: boolean } | { ok: false; sourcePath: string; sourcePresent: boolean; error: string } {
const paths = secretSourcePaths(sourceRef);
const sourcePath = paths.find((candidate) => existsSync(candidate)) ?? paths[0] ?? join(repoRoot, ".state", "secrets", sourceRef);
const sourcePath = paths.find((candidate) => existsSync(candidate)) ?? paths[0] ?? rootPath(".state", "secrets", sourceRef);
const runtimeValue = process.env[sourceKey];
if (!existsSync(sourcePath)) {
if (runtimeValue !== undefined && runtimeValue.length > 0) return { ok: true, value: runtimeValue, sourcePath, sourcePresent: false };
+2 -2
View File
@@ -1,7 +1,7 @@
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { runCommand } from "./command";
import { type UniDeskConfig, repoRoot } from "./config";
import { type UniDeskConfig, repoRoot, rootPath } from "./config";
import { jsonByteLength, previewJson } from "./preview";
// Todo Note misleading-404 rewrite (issue #198) for legacy deployments.
@@ -279,7 +279,7 @@ export function coreInternalFetch(path: string, init?: { method?: string; body?:
const result = runCommand(command, repoRoot, { timeoutMs: init?.timeoutMs });
if (result.exitCode !== 0) {
if (backendCoreContainerMissing(result.stderr)) {
const envPath = join(repoRoot, ".state", "docker-compose.env");
const envPath = rootPath(".state", "docker-compose.env");
return backendCoreUnavailableDiagnostic({
exitCode: result.exitCode,
stdoutTail: result.stdout.slice(-1200),
+6 -6
View File
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
import type { UniDeskConfig } from "./config";
import { rootPath } from "./config";
import { rootPath, stateRoot } from "./config";
import { runCommand, type CommandResult } from "./command";
import { resolveEgressProxySourceRef, type MasterShadowsocksSourceSpec } from "./egress-proxy-sources";
import type { RenderedCliResult } from "./output";
@@ -332,11 +332,11 @@ function managedClientSpec(raw: Record<string, unknown>, label: string): HostPro
mode,
upstreamUrl: httpsUrlField(raw, "upstreamUrl", label),
version: stringField(raw, "version", label),
archiveCachePath: repoStatePathField(raw, "archiveCachePath", label),
archiveCachePath: canonicalStatePathField(raw, "archiveCachePath", label),
archiveSha256: sha256Field(raw, "archiveSha256", label),
archiveInstallPath: absolutePathField(raw, "archiveInstallPath", label),
binaryMember: relativePathField(raw, "binaryMember", label),
binaryCachePath: repoStatePathField(raw, "binaryCachePath", label),
binaryCachePath: canonicalStatePathField(raw, "binaryCachePath", label),
binarySha256: sha256Field(raw, "binarySha256", label),
installPath: absolutePathField(raw, "installPath", label),
configPath: absolutePathField(raw, "configPath", label),
@@ -1314,10 +1314,10 @@ function repoYamlPathField(obj: Record<string, unknown>, key: string, label: str
return value;
}
function repoStatePathField(obj: Record<string, unknown>, key: string, label: string): string {
function canonicalStatePathField(obj: Record<string, unknown>, key: string, label: string): string {
const value = stringField(obj, key, label);
if (value.startsWith("/") || value.includes("..") || !value.startsWith(".state/")) {
throw new Error(`${label}.${key} must be a repo-relative .state/ path without ..`);
if (value.includes("..") || !value.startsWith(`${stateRoot}/`)) {
throw new Error(`${label}.${key} must be under ${stateRoot}/ without ..`);
}
return value;
}
+2 -2
View File
@@ -1,7 +1,7 @@
import { existsSync, mkdirSync, rmSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { chromium, firefox, webkit, type Browser, type BrowserContext, type BrowserType, type Page } from "playwright";
import { repoRoot } from "./config";
import { repoRoot, rootPath } from "./config";
import { emitError, emitJson } from "./output";
type BrowserName = "chromium" | "firefox" | "webkit";
@@ -22,7 +22,7 @@ interface ParsedOptions {
fullPage: boolean;
}
const stateRoot = join(repoRoot, ".state", "playwright-cli");
const stateRoot = rootPath(".state", "playwright-cli");
const defaultScreenshotPath = join(stateRoot, "screenshots", "latest.png");
const supportedCommands = ["open", "screenshot", "eval", "session-list", "session-delete"];
const unsupportedInteractiveCommands = new Set([
+2 -2
View File
@@ -83,7 +83,7 @@ function parseAttachOptions(config: UniDeskConfig, args: string[]): ProviderAtta
}
const envFile = optionValue(args, "--env-file") ?? rootPath(".state", `provider-${providerId}.env`);
const composeFile = optionValue(args, "--compose-file") ?? rootPath(`provider-${providerId}.yml`);
const logDir = optionValue(args, "--log-dir") ?? rootPath("logs", `provider-${providerId}`);
const logDir = optionValue(args, "--log-dir") ?? rootPath(".state", "logs", `provider-${providerId}`);
return {
providerId,
masterServer: optionValue(args, "--master-server") ?? optionValue(args, "--master") ?? defaultMasterServer(config),
@@ -109,7 +109,7 @@ function envContent(options: ProviderAttachOptions): string {
`PROVIDER_UPGRADE_HOST_PROJECT_ROOT=${repoRoot}`,
"PROVIDER_UPGRADE_WORKSPACE_PATH=/workspace",
`PROVIDER_UPGRADE_COMPOSE_FILE=provider-${options.providerId}.yml`,
`PROVIDER_UPGRADE_ENV_FILE=.state/provider-${options.providerId}.env`,
`PROVIDER_UPGRADE_ENV_FILE=${options.envFile}`,
`PROVIDER_UPGRADE_COMPOSE_PROJECT=unidesk-${slug}`,
"PROVIDER_UPGRADE_SERVICE=provider-gateway",
`PROVIDER_UPGRADE_RUNNER_IMAGE=unidesk_provider-gateway:${slug}`,