diff --git a/.gitignore b/.gitignore index 7a1ca28e..ebc95e84 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -.state/ -logs/ +.state +logs node_modules/ package-lock.json npm-debug.log* diff --git a/.worktreecopy b/.worktreecopy index c7485c1c..40e0881a 100644 --- a/.worktreecopy +++ b/.worktreecopy @@ -1,5 +1,5 @@ # Non-secret local runtime hints copied by `bun scripts/cli.ts dev-env worktree add`. -# Canonical credentials live under /root/.unidesk/.env and are never copied. +# Canonical credentials and mutable state live under /root/.unidesk/.env and /root/.unidesk/.state; neither is copied. config/*.local.yaml config/*.local.yml config/*.local.json diff --git a/config.json b/config.json index f14b9d9f..637abe01 100644 --- a/config.json +++ b/config.json @@ -78,7 +78,7 @@ "hostProjectRoot": "/root/unidesk", "workspacePath": "/workspace", "composeFile": "docker-compose.yml", - "composeEnvFile": ".state/docker-compose.env", + "composeEnvFile": "/root/.unidesk/.state/docker-compose.env", "composeProject": "unidesk", "service": "provider-gateway", "runnerImage": "unidesk_provider-gateway" @@ -987,8 +987,8 @@ } ], "paths": { - "stateDir": ".state", - "logsDir": "logs", + "stateDir": "/root/.unidesk/.state", + "logsDir": "/root/.unidesk/.state/logs", "docsReferenceDir": "docs/reference" }, "sshForwarding": { diff --git a/config/agentrun.yaml b/config/agentrun.yaml index 6cf891be..a9150e63 100644 --- a/config/agentrun.yaml +++ b/config/agentrun.yaml @@ -398,7 +398,7 @@ controlPlane: key: GH_TOKEN unideskSshToken: id: tool-unidesk-ssh-token - sourceRef: /root/unidesk/.state/docker-compose.env + sourceRef: /root/.unidesk/.state/docker-compose.env sourceKey: UNIDESK_SSH_CLIENT_TOKEN targetRef: namespace: agentrun-v02 diff --git a/config/deploy-ssh-identities.yaml b/config/deploy-ssh-identities.yaml index 29e53176..c41d9968 100644 --- a/config/deploy-ssh-identities.yaml +++ b/config/deploy-ssh-identities.yaml @@ -11,7 +11,7 @@ defaults: identityId: github.com sources: - root: /root/unidesk/.state/secrets + root: /root/.unidesk/.state/secrets identities: github.com: diff --git a/config/hwlab-access-control-secrets.yaml b/config/hwlab-access-control-secrets.yaml index 9b9aa003..923e8f2d 100644 --- a/config/hwlab-access-control-secrets.yaml +++ b/config/hwlab-access-control-secrets.yaml @@ -8,7 +8,7 @@ metadata: - 2187 sources: - root: /root/unidesk/.state/secrets + root: /root/.unidesk/.state/secrets files: - sourceRef: hwlab/d601-v03-preset-users.env type: env diff --git a/config/hwlab-test-accounts.yaml b/config/hwlab-test-accounts.yaml index bf8c7e68..289b88eb 100644 --- a/config/hwlab-test-accounts.yaml +++ b/config/hwlab-test-accounts.yaml @@ -9,7 +9,7 @@ metadata: - 1237 - 1236 -sourceRoot: /root/unidesk/.state/secrets +sourceRoot: /root/.unidesk/.state/secrets targets: - id: d601-v03 diff --git a/config/platform-db/postgres-nc01.yaml b/config/platform-db/postgres-nc01.yaml index 4aee2eca..e37454f6 100644 --- a/config/platform-db/postgres-nc01.yaml +++ b/config/platform-db/postgres-nc01.yaml @@ -116,7 +116,7 @@ postgres: secrets: source: master-local - root: /root/unidesk/.state/secrets + root: /root/.unidesk/.state/secrets entries: - name: agentrun-nc01-v02-db-credentials sourceRef: platform-db/agentrun-nc01-v02-db.env diff --git a/config/platform-db/postgres-pk01.yaml b/config/platform-db/postgres-pk01.yaml index 6b360b4e..949b05c9 100644 --- a/config/platform-db/postgres-pk01.yaml +++ b/config/platform-db/postgres-pk01.yaml @@ -362,7 +362,7 @@ postgres: secrets: source: master-local - root: /root/unidesk/.state/secrets + root: /root/.unidesk/.state/secrets entries: - name: sub2api-db-credentials sourceRef: platform-db/sub2api-db.env diff --git a/config/platform-infra/gitea.yaml b/config/platform-infra/gitea.yaml index 28df0e38..5f165163 100644 --- a/config/platform-infra/gitea.yaml +++ b/config/platform-infra/gitea.yaml @@ -286,7 +286,7 @@ app: publicExposure: enabled: true publicBaseUrl: https://gitea.pikapython.com - secretRoot: /root/unidesk/.state/secrets + secretRoot: /root/.unidesk/.state/secrets dns: hostname: gitea.pikapython.com expectedA: 82.156.23.220 diff --git a/config/platform-infra/host-proxy.yaml b/config/platform-infra/host-proxy.yaml index 52c3031b..05116cb5 100644 --- a/config/platform-infra/host-proxy.yaml +++ b/config/platform-infra/host-proxy.yaml @@ -18,7 +18,7 @@ server: serviceName: sub2api-master-egress-proxy containerName: unidesk-sub2api-master-egress-proxy image: ghcr.io/shadowsocks/ssserver-rust:latest - configPath: /root/unidesk/.state/secrets/platform-infra/sub2api-master-egress-proxy.config.json + configPath: /root/.unidesk/.state/secrets/platform-infra/sub2api-master-egress-proxy.config.json listenHost: 0.0.0.0 listenPort: 18792 health: @@ -95,11 +95,11 @@ templates: mode: trans-static-binary upstreamUrl: https://github.com/SagerNet/sing-box/releases/download/v1.13.14/sing-box-1.13.14-linux-amd64.tar.gz version: v1.13.14 - archiveCachePath: .state/artifacts/platform-infra/sing-box-1.13.14-linux-amd64.tar.gz + archiveCachePath: /root/.unidesk/.state/artifacts/platform-infra/sing-box-1.13.14-linux-amd64.tar.gz archiveSha256: f48703461a15476951ac4967cdad339d986f4b8096b4eb3ff0829a500502d697 archiveInstallPath: /var/cache/unidesk/host-egress-proxy/sing-box-1.13.14-linux-amd64.tar.gz binaryMember: sing-box-1.13.14-linux-amd64/sing-box - binaryCachePath: .state/artifacts/platform-infra/sing-box-1.13.14-linux-amd64 + binaryCachePath: /root/.unidesk/.state/artifacts/platform-infra/sing-box-1.13.14-linux-amd64 binarySha256: 68aeab83cc4ab2659a5b92232261a20746ccdafc3b3d1e19b2d63247eec3bbf7 installPath: /usr/local/bin/sing-box configPath: /etc/unidesk/host-egress-proxy/sing-box.json diff --git a/config/platform-infra/langbot.yaml b/config/platform-infra/langbot.yaml index 568c0e68..6bbada99 100644 --- a/config/platform-infra/langbot.yaml +++ b/config/platform-infra/langbot.yaml @@ -67,7 +67,7 @@ runtime: sslMode: require appDatabaseValue: langbot?ssl=require secrets: - root: /root/unidesk/.state/secrets + root: /root/.unidesk/.state/secrets appSourceRef: platform-infra/langbot.env storage: data: diff --git a/config/platform-infra/n8n.yaml b/config/platform-infra/n8n.yaml index feb334ea..f794eb09 100644 --- a/config/platform-infra/n8n.yaml +++ b/config/platform-infra/n8n.yaml @@ -67,7 +67,7 @@ runtime: sslMode: require sslRejectUnauthorized: false secrets: - root: /root/unidesk/.state/secrets + root: /root/.unidesk/.state/secrets appSourceRef: platform-infra/n8n.env encryptionKey: N8N_ENCRYPTION_KEY storage: diff --git a/config/platform-infra/sub2api-master-egress-proxy.compose.yaml b/config/platform-infra/sub2api-master-egress-proxy.compose.yaml index edc3990c..7ca32e65 100644 --- a/config/platform-infra/sub2api-master-egress-proxy.compose.yaml +++ b/config/platform-infra/sub2api-master-egress-proxy.compose.yaml @@ -12,5 +12,5 @@ services: ports: - "0.0.0.0:18792:18792/tcp" volumes: - - /root/unidesk/.state/secrets/platform-infra/sub2api-master-egress-proxy.config.json:/etc/shadowsocks-rust/config.json:ro + - /root/.unidesk/.state/secrets/platform-infra/sub2api-master-egress-proxy.config.json:/etc/shadowsocks-rust/config.json:ro logging: *unidesk-log-rotation diff --git a/config/platform-infra/sub2api.yaml b/config/platform-infra/sub2api.yaml index 35684c28..598cf0d2 100644 --- a/config/platform-infra/sub2api.yaml +++ b/config/platform-infra/sub2api.yaml @@ -441,7 +441,7 @@ runtime: sslMode: require pendingAllowed: true secrets: - root: /root/unidesk/.state/secrets + root: /root/.unidesk/.state/secrets appSourceRef: platform-infra/sub2api.env redis: serviceName: sub2api-redis diff --git a/config/platform-infra/wechat-archive.yaml b/config/platform-infra/wechat-archive.yaml index 9483ec0a..9ce936d1 100644 --- a/config/platform-infra/wechat-archive.yaml +++ b/config/platform-infra/wechat-archive.yaml @@ -46,7 +46,7 @@ n8n: archiveCallback: publicUrl: http://74.48.78.17:18081/webhooks/wechat-archive - secretRoot: /root/unidesk/.state/secrets + secretRoot: /root/.unidesk/.state/secrets tokenSourceRef: platform-infra/wechat-archive.env tokenKey: UNIDESK_WECHAT_ARCHIVE_TOKEN timeoutMs: 90000 @@ -160,7 +160,7 @@ baiduNetdisk: proxyMode: backend-core-microservice-proxy configRef: config.json#microservices.baidu-netdisk staging: - hostRoot: /root/unidesk/.state/baidu-netdisk/staging + hostRoot: /root/.unidesk/.state/baidu-netdisk/staging containerRoot: /data/staging outboxDir: wechat-archive/outbox pullDir: wechat-archive/pulls diff --git a/config/secrets-distribution.yaml b/config/secrets-distribution.yaml index 6ae4b032..ff82b27f 100644 --- a/config/secrets-distribution.yaml +++ b/config/secrets-distribution.yaml @@ -11,7 +11,7 @@ metadata: - 2256 sources: - root: /root/unidesk/.state/secrets + root: /root/.unidesk/.state/secrets files: - sourceRef: platform-db/langbot-db.env type: env diff --git a/config/unidesk-cli.yaml b/config/unidesk-cli.yaml index b379cdef..ab63ea57 100644 --- a/config/unidesk-cli.yaml +++ b/config/unidesk-cli.yaml @@ -62,27 +62,27 @@ gc: enabled: false keepDays: 14 fileRoots: - e2e: .state/e2e - validation: .state/validation - jobs: .state/jobs - codex-queue-output-archive: .state/codex-queue/output-archive + e2e: /root/.unidesk/.state/e2e + validation: /root/.unidesk/.state/validation + jobs: /root/.unidesk/.state/jobs + codex-queue-output-archive: /root/.unidesk/.state/codex-queue/output-archive dirRoots: - deploy-exports: .state/deploy/exports - deploy-resolve: .state/deploy/resolve + deploy-exports: /root/.unidesk/.state/deploy/exports + deploy-resolve: /root/.unidesk/.state/deploy/resolve stateStaleScratch: enabled: false keepHours: 24 fileRoots: - playwright-cli-screenshots: .state/playwright-cli/screenshots - playwright-cli-sessions: .state/playwright-cli/sessions - perf: .state/perf - tmp: .state/tmp - web-observe: .state/web-observe + playwright-cli-screenshots: /root/.unidesk/.state/playwright-cli/screenshots + playwright-cli-sessions: /root/.unidesk/.state/playwright-cli/sessions + perf: /root/.unidesk/.state/perf + tmp: /root/.unidesk/.state/tmp + web-observe: /root/.unidesk/.state/web-observe dirRoots: - hwlab-cd: .state/hwlab-cd - codex-queue-stats-verify: .state/codex-queue-stats-verify - codex-queue-perf: .state/codex-queue-perf - tmp: .state/tmp + hwlab-cd: /root/.unidesk/.state/hwlab-cd + codex-queue-stats-verify: /root/.unidesk/.state/codex-queue-stats-verify + codex-queue-perf: /root/.unidesk/.state/codex-queue-perf + tmp: /root/.unidesk/.state/tmp legacyDockerImages: enabled: true minAgeHours: 12 diff --git a/config/unidesk-host-k8s.yaml b/config/unidesk-host-k8s.yaml index 75b16cd2..a928fd68 100644 --- a/config/unidesk-host-k8s.yaml +++ b/config/unidesk-host-k8s.yaml @@ -108,7 +108,7 @@ services: logFile: /var/log/unidesk/decision-center.jsonl database: sourceRef: - path: .state/secrets/decision-center/nc01-db.env + path: /root/.unidesk/.state/secrets/decision-center/nc01-db.env key: DATABASE_URL secretName: decision-center-runtime-secrets secretKey: DATABASE_URL @@ -162,7 +162,7 @@ services: logFile: /var/log/unidesk/todo-note.jsonl database: sourceRef: - path: .state/secrets/decision-center/nc01-db.env + path: /root/.unidesk/.state/secrets/decision-center/nc01-db.env key: DATABASE_URL secretName: decision-center-runtime-secrets secretKey: DATABASE_URL @@ -240,7 +240,7 @@ runtimeAuth: runtimeSecrets: sourceRef: - path: .state/secrets/unidesk-host-k8s.env + path: /root/.unidesk/.state/secrets/unidesk-host-k8s.env target: name: unidesk-runtime-secrets keys: diff --git a/docker-compose.yml b/docker-compose.yml index 903b83be..0f2cde0e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -87,7 +87,7 @@ services: UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-512MiB}" volumes: - ${UNIDESK_LOG_DIR}:/var/log/unidesk - - ./.state/baidu-netdisk/staging:/data/baidu-netdisk-staging + - /root/.unidesk/.state/baidu-netdisk/staging:/data/baidu-netdisk-staging healthcheck: test: - "CMD-SHELL" @@ -171,7 +171,7 @@ services: UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-512MiB}" volumes: - ${UNIDESK_LOG_DIR}:/var/log/unidesk - - ./.state/code-agent-sandbox:/var/lib/unidesk/code-agent-sandbox + - /root/.unidesk/.state/code-agent-sandbox:/var/lib/unidesk/code-agent-sandbox healthcheck: test: ["CMD", "bun", "-e", "fetch('http://127.0.0.1:4260/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] interval: 5s @@ -362,7 +362,7 @@ services: UNIDESK_DEPLOY_REQUESTED_COMMIT: "${UNIDESK_BAIDU_NETDISK_DEPLOY_REQUESTED_COMMIT:-}" volumes: - ${UNIDESK_LOG_DIR}:/var/log/unidesk - - ./.state/baidu-netdisk/staging:/data/staging + - /root/.unidesk/.state/baidu-netdisk/staging:/data/staging healthcheck: test: ["CMD", "bun", "-e", "fetch('http://127.0.0.1:4244/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] interval: 5s @@ -493,7 +493,7 @@ services: command: ["bun", "/workspace/scripts/runtime/pk01-postgres-relay.js"] volumes: - .:/workspace:ro - - ${UNIDESK_LOG_DIR:-./.state/logs}:/var/log/unidesk + - ${UNIDESK_LOG_DIR:-/root/.unidesk/.state/logs}:/var/log/unidesk healthcheck: test: ["CMD", "bun", "/workspace/scripts/runtime/pk01-postgres-relay.js", "--healthcheck"] interval: 10s diff --git a/docs/reference/devops-hygiene.md b/docs/reference/devops-hygiene.md index 9b82f4e5..b0195f78 100644 --- a/docs/reference/devops-hygiene.md +++ b/docs/reference/devops-hygiene.md @@ -60,9 +60,9 @@ The root UniDesk checkout at `/root/unidesk` is the fixed main worktree and must ## Worktree-Independent Local State -Tracked source, YAML and scripts belong to the selected checkout; operator credentials and mutable runtime state do not. UniDesk's canonical owner-level roots are `/root/.unidesk/.env` for credentials and `/root/.unidesk/.state` for mutable state, caches, generated artifacts and Secret source files. YAML `sourceRef`, CLI path configuration and source defaults that consume these materials must use those fixed absolute roots. They must not derive the path from `process.cwd()`, the module's task-worktree root, or a relative `.env`/`.state` path. +Tracked source, YAML and scripts belong to the selected checkout; operator credentials and mutable runtime state do not. UniDesk's canonical owner-level roots are `/root/.unidesk/.env` for credentials and `/root/.unidesk/.state` for mutable state, logs, caches, generated artifacts and Secret source files. YAML `sourceRef`, CLI path configuration and source defaults that consume these materials must use those fixed absolute roots. They must not derive the path from `process.cwd()`, the module's task-worktree root, or a relative `.env`/`.state`/`logs` path. -`/root/unidesk/.env` and `/root/unidesk/.state` are compatibility symlinks to the canonical owner-level roots. They preserve old main-worktree entrypoints but are not source authority. `.worktreecopy` must not copy either tree, and task worktrees must not receive private copies or task-specific compatibility links. A command that fails only because a task worktree lacks `.env`, `.state`, `secrets`, cache or generated state has a path-contract defect: fix the owning config/helper to consume the canonical absolute path before continuing the original operation. +`/root/unidesk/.env`, `/root/unidesk/.state` and `/root/unidesk/logs` are compatibility symlinks to the canonical owner-level roots. They preserve old main-worktree entrypoints but are not source authority. `.worktreecopy` must not copy these trees, and task worktrees must not receive private copies or task-specific compatibility links. A command that fails only because a task worktree lacks `.env`, `.state`, `logs`, `secrets`, cache or generated state has a path-contract defect: fix the owning config/helper to consume the canonical absolute path before continuing the original operation. When diagnosing a missing worktree file, first classify ownership. A tracked file is repaired through remote/base synchronization; an ephemeral test fixture uses an explicit temporary directory; durable local credentials or state move to the canonical owner-level root and are referenced absolutely. Never repair the latter by copying secrets/state into every worktree, silently falling back to the current directory, embedding values in Git, or maintaining divergent per-worktree data. A migration must preserve existing contents, keep the canonical directories owner-only, verify the resolved symlink target and data size, and leave the main-repo compatibility symlink in place. diff --git a/docs/reference/nc01.md b/docs/reference/nc01.md index a51affc1..929b9ccf 100644 --- a/docs/reference/nc01.md +++ b/docs/reference/nc01.md @@ -25,7 +25,7 @@ NC01 runs the UniDesk server/backend-core on Kubernetes in namespace `unidesk`, NC01 database placement is selected by each owning YAML, not by a host-wide assumption. `config/unidesk-host-k8s.yaml`, `config/agentrun.yaml` and `config/hwlab-node-lanes.yaml` independently declare whether their workloads consume host-native/external PostgreSQL or a lane-local Kubernetes PostgreSQL. Validation must report the selected mode and corresponding Service/SecretRef; it must not require objects belonging to an unselected mode. -For HWLAB v0.3, `runtimeStore.postgres.mode` in `config/hwlab-node-lanes.yaml` owns this choice. A platform-service mode uses the YAML-declared external PostgreSQL bridge such as `nc01-host-postgres`; a local-k3s mode uses its lane-local PostgreSQL objects and must report the external bridge as not required. Runtime database Secrets come only from the selected mode's YAML sourceRef and may be reported as presence/fingerprint metadata, never as full values. +For HWLAB v0.3, `runtimeStore.postgres.mode` in `config/hwlab-node-lanes.yaml` owns this choice. A platform-service mode uses the YAML-declared external PostgreSQL bridge such as `nc01-host-postgres`; a local-k3s mode uses its lane-local PostgreSQL objects and must report the external bridge as not required. Runtime database Secrets come only from the selected mode's YAML sourceRef, with local source material under `/root/.unidesk/.state/secrets/hwlab/*`, and may be reported as presence/fingerprint metadata, never as full values. ## Decision Center diff --git a/scripts/src/agentrun-lanes.ts b/scripts/src/agentrun-lanes.ts index 0d93352c..0c772f5f 100644 --- a/scripts/src/agentrun-lanes.ts +++ b/scripts/src/agentrun-lanes.ts @@ -435,7 +435,7 @@ export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record ({ 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, diff --git a/scripts/src/agentrun/secrets.ts b/scripts/src/agentrun/secrets.ts index 86f86522..dab4ef07 100644 --- a/scripts/src/agentrun/secrets.ts +++ b/scripts/src/agentrun/secrets.ts @@ -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), diff --git a/scripts/src/artifact-registry/artifact-probe.ts b/scripts/src/artifact-registry/artifact-probe.ts index 713345ea..7661b8ab 100644 --- a/scripts/src/artifact-registry/artifact-probe.ts +++ b/scripts/src/artifact-registry/artifact-probe.ts @@ -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), diff --git a/scripts/src/artifact-registry/catalog.ts b/scripts/src/artifact-registry/catalog.ts index 19f5926e..17dcc489 100644 --- a/scripts/src/artifact-registry/catalog.ts +++ b/scripts/src/artifact-registry/catalog.ts @@ -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 = { diff --git a/scripts/src/ci/help.ts b/scripts/src/ci/help.ts index 5c4f8953..da3bee9e 100644 --- a/scripts/src/ci/help.ts +++ b/scripts/src/ci/help.ts @@ -98,7 +98,7 @@ export function ciHelp(): Record { }, 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 ", 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.", diff --git a/scripts/src/code-queue-perf.ts b/scripts/src/code-queue-perf.ts index 90438b4f..00d1d71e 100644 --- a/scripts/src/code-queue-perf.ts +++ b/scripts/src/code-queue-perf.ts @@ -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> { - 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 { + 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(); diff --git a/scripts/src/codex-trace.ts b/scripts/src/codex-trace.ts index e617ac33..e0cd253d 100644 --- a/scripts/src/codex-trace.ts +++ b/scripts/src/codex-trace.ts @@ -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 { "bun scripts/cli.ts codex trace grep --session --messages --pattern 'Playwright|web-probe'", "bun scripts/cli.ts codex trace grep --session --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/] [--limit 30]", + "bun scripts/cli.ts codex trace collect [--root ~/.codex] [--output /root/.unidesk/.state/codex-trace/] [--limit 30]", "bun scripts/cli.ts codex trace show --session [--root ~/.codex] [--tail-bytes 12000]", ], safety: [ @@ -97,7 +97,7 @@ export function codexTraceHelp(): Record { ], options: { "--root ": "Codex home or another trace root. Defaults to ~/.codex.", - "--output ": "Collect destination. Defaults to .state/codex-trace/.", + "--output ": "Collect destination. Defaults to /root/.unidesk/.state/codex-trace/.", "--limit ": `Maximum included files, default ${defaultLimit}, max ${maxLimit}.`, "--max-depth ": `Recursive scan depth, default ${defaultMaxDepth}, max ${maxDepthLimit}.`, "--max-file-bytes ": "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 | 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[] = []; if (!options.dryRun) mkdirSync(outputDir, { recursive: true, mode: 0o700 }); for (const candidate of selected) { diff --git a/scripts/src/commander.ts b/scripts/src/commander.ts index fa54afe8..7a75ef50 100644 --- a/scripts/src/commander.ts +++ b/scripts/src/commander.ts @@ -67,7 +67,7 @@ function processDiscoveryPlan(sessionId: string): Record { sessionId, mutation: false, signals: [ - ".state/commander/sessions/.json", + "/root/.unidesk/.state/commander/sessions/.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 { function stateFileValidation(sessionId: string): Record { 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 { "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", ], diff --git a/scripts/src/config.ts b/scripts/src/config.ts index 80e64e11..841471a0 100644 --- a/scripts/src/config.ts +++ b/scripts/src/config.ts @@ -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); } diff --git a/scripts/src/deploy/scripts.ts b/scripts/src/deploy/scripts.ts index ea70f832..c690e5e4 100644 --- a/scripts/src/deploy/scripts.ts +++ b/scripts/src/deploy/scripts.ts @@ -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"), diff --git a/scripts/src/docker.ts b/scripts/src/docker.ts index 1bc74458..3e82e873 100644 --- a/scripts/src/docker.ts +++ b/scripts/src/docker.ts @@ -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); diff --git a/scripts/src/gc.ts b/scripts/src/gc.ts index b8e7df6d..b2dacb99 100644 --- a/scripts/src/gc.ts +++ b/scripts/src/gc.ts @@ -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 }; }); } diff --git a/scripts/src/help.ts b/scripts/src/help.ts index 05974e8d..4df182f6 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -94,9 +94,9 @@ export function rootHelp(): unknown { { command: "codex steer-confirm --steer-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 ", 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 [--tail-bytes N]", description: "Show job state with a structured progress summary and bounded stdout/stderr tails." }, - { command: "job cancel ", description: "Cancel a queued/running async job through the .state/jobs control entry and keep a terminal canceled record." }, + { command: "job cancel ", 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 ", description: "Show bounded host.ssh.tcp-pool labels for one provider, including ready/claimed/desired/lastError." }, { command: "debug egress-proxy ", 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 --messages --pattern 'Playwright|web-probe'", "bun scripts/cli.ts codex trace grep --session --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/] [--limit 30]", + "bun scripts/cli.ts codex trace collect [--root ~/.codex] [--output /root/.unidesk/.state/codex-trace/] [--limit 30]", "bun scripts/cli.ts codex trace show --session [--root ~/.codex] [--tail-bytes 12000]", "bun scripts/cli.ts codex deploy # 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 , --messages, --tools, --tool , 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//manifest.json", + collectOutput: "/root/.unidesk/.state/codex-trace//manifest.json", note: "Use --root 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 [--tail-bytes N]", "bun scripts/cli.ts job cancel ", ], - 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.", }; } diff --git a/scripts/src/hwlab-fake-model-provider.ts b/scripts/src/hwlab-fake-model-provider.ts index 3e212d41..73d565e2 100644 --- a/scripts/src/hwlab-fake-model-provider.ts +++ b/scripts/src/hwlab-fake-model-provider.ts @@ -87,7 +87,7 @@ export function fakeModelProviderHelp(): Record { ], 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; } diff --git a/scripts/src/hwlab-node-web-sentinel-cicd-shared.ts b/scripts/src/hwlab-node-web-sentinel-cicd-shared.ts index 575d7376..2b566b44 100644 --- a/scripts/src/hwlab-node-web-sentinel-cicd-shared.ts +++ b/scripts/src/hwlab-node-web-sentinel-cicd-shared.ts @@ -321,11 +321,7 @@ export function monitorWebBuildkitStatePlan(cicd: Record): 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 { diff --git a/scripts/src/hwlab-node-web-sentinel-cicd.ts b/scripts/src/hwlab-node-web-sentinel-cicd.ts index 1e4b162e..d7521ee9 100644 --- a/scripts/src/hwlab-node-web-sentinel-cicd.ts +++ b/scripts/src/hwlab-node-web-sentinel-cicd.ts @@ -2666,7 +2666,7 @@ function readSentinelSecretSourceValue(source: Record): 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): { 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 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]; diff --git a/scripts/src/hwlab-node-web-sentinel-p5-observe.ts b/scripts/src/hwlab-node-web-sentinel-p5-observe.ts index 9911e9d6..59b6ea85 100644 --- a/scripts/src/hwlab-node-web-sentinel-p5-observe.ts +++ b/scripts/src/hwlab-node-web-sentinel-p5-observe.ts @@ -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 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")) : {}; diff --git a/scripts/src/hwlab-node/public-exposure.ts b/scripts/src/hwlab-node/public-exposure.ts index 2b0ba8bd..e91add63 100644 --- a/scripts/src/hwlab-node/public-exposure.ts +++ b/scripts/src/hwlab-node/public-exposure.ts @@ -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}=` }, + next: { fixSecretSource: `create ${rootPath(".state", "secrets", exposure.tokenSourceRef)} with ${exposure.tokenSourceKey}=` }, }; } 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, result: CommandResult): Record { diff --git a/scripts/src/hwlab-node/web-probe.ts b/scripts/src/hwlab-node/web-probe.ts index 18bd833e..9172b9cb 100644 --- a/scripts/src/hwlab-node/web-probe.ts +++ b/scripts/src/hwlab-node/web-probe.ts @@ -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 }; diff --git a/scripts/src/microservices.ts b/scripts/src/microservices.ts index 94a211f9..2e2498ee 100644 --- a/scripts/src/microservices.ts +++ b/scripts/src/microservices.ts @@ -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), diff --git a/scripts/src/platform-infra-host-proxy.ts b/scripts/src/platform-infra-host-proxy.ts index a5108507..3fccd70a 100644 --- a/scripts/src/platform-infra-host-proxy.ts +++ b/scripts/src/platform-infra-host-proxy.ts @@ -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, 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, key: string, label: str return value; } -function repoStatePathField(obj: Record, key: string, label: string): string { +function canonicalStatePathField(obj: Record, 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; } diff --git a/scripts/src/playwright-cli.ts b/scripts/src/playwright-cli.ts index 5cc580bb..0a90934f 100644 --- a/scripts/src/playwright-cli.ts +++ b/scripts/src/playwright-cli.ts @@ -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([ diff --git a/scripts/src/provider-attach.ts b/scripts/src/provider-attach.ts index 09f07372..fb0689ae 100644 --- a/scripts/src/provider-attach.ts +++ b/scripts/src/provider-attach.ts @@ -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}`, diff --git a/src/components/backend-core/src/overview.rs b/src/components/backend-core/src/overview.rs index 800731d7..70a62a4f 100644 --- a/src/components/backend-core/src/overview.rs +++ b/src/components/backend-core/src/overview.rs @@ -228,7 +228,7 @@ pub async fn codex_queue_load_test( chrono::Utc::now().timestamp_millis(), rand::random::() ); - let dir = ".state/code-queue-perf"; + let dir = "/root/.unidesk/.state/code-queue-perf"; let output_path = format!("{dir}/{run_id}.json"); let stderr_path = format!("{dir}/{run_id}.stderr"); let exit_path = format!("{dir}/{run_id}.exit"); @@ -239,7 +239,7 @@ pub async fn codex_queue_load_test( .map(|value| format!(" --url {}", shell_quote(value))) .unwrap_or_default(); let start_command = format!( - "mkdir -p {}; rm -f {} {} {}; (PLAYWRIGHT_BROWSERS_PATH=.state/playwright-browsers bun scripts/src/code-queue-perf.ts --json --timeout-ms {} --target-ms {}{} > {}; printf '%s' \"$?\" > {}) > {} 2>&1 & printf '%s\\n' {}", + "mkdir -p {}; rm -f {} {} {}; (PLAYWRIGHT_BROWSERS_PATH=/root/.unidesk/.state/playwright-browsers bun scripts/src/code-queue-perf.ts --json --timeout-ms {} --target-ms {}{} > {}; printf '%s' \"$?\" > {}) > {} 2>&1 & printf '%s\\n' {}", shell_quote(dir), shell_quote(&output_path), shell_quote(&stderr_path), diff --git a/src/components/backend-core/src/overview.ts b/src/components/backend-core/src/overview.ts index 6afd14e6..29ed2cc6 100644 --- a/src/components/backend-core/src/overview.ts +++ b/src/components/backend-core/src/overview.ts @@ -237,8 +237,8 @@ export async function codexQueueLoadTest(req: Request): Promise { const timeoutMs = numberFromUnknown(body.timeoutMs, 90_000, 5_000, 180_000); const targetMs = numberFromUnknown(body.targetMs, 1_000, 100, 60_000); const runId = safePerfRunId(); - const dir = ".state/code-queue-perf"; - const browsersPath = ".state/playwright-browsers"; + const dir = "/root/.unidesk/.state/code-queue-perf"; + const browsersPath = "/root/.unidesk/.state/playwright-browsers"; const outputPath = `${dir}/${runId}.json`; const stderrPath = `${dir}/${runId}.stderr`; const exitPath = `${dir}/${runId}.exit`; diff --git a/src/components/microservices/k3sctl-adapter/k3s/code-queue.g14.k8s.yaml b/src/components/microservices/k3sctl-adapter/k3s/code-queue.g14.k8s.yaml index 0ee0d109..d7769cb6 100644 --- a/src/components/microservices/k3sctl-adapter/k3s/code-queue.g14.k8s.yaml +++ b/src/components/microservices/k3sctl-adapter/k3s/code-queue.g14.k8s.yaml @@ -253,11 +253,11 @@ spec: type: Directory - name: logs hostPath: - path: /root/unidesk/.state/code-queue/logs + path: /root/.unidesk/.state/code-queue/logs type: DirectoryOrCreate - name: state hostPath: - path: /root/unidesk/.state/code-queue + path: /root/.unidesk/.state/code-queue type: DirectoryOrCreate --- apiVersion: apps/v1 @@ -505,11 +505,11 @@ spec: type: Directory - name: logs hostPath: - path: /root/unidesk/.state/code-queue/logs + path: /root/.unidesk/.state/code-queue/logs type: DirectoryOrCreate - name: state hostPath: - path: /root/unidesk/.state/code-queue + path: /root/.unidesk/.state/code-queue type: DirectoryOrCreate --- apiVersion: v1 @@ -1219,11 +1219,11 @@ spec: type: Directory - name: logs hostPath: - path: /root/unidesk/.state/code-queue/logs + path: /root/.unidesk/.state/code-queue/logs type: DirectoryOrCreate - name: state hostPath: - path: /root/unidesk/.state/code-queue + path: /root/.unidesk/.state/code-queue type: DirectoryOrCreate --- apiVersion: v1