-
Notifications
You must be signed in to change notification settings - Fork 93
fix(environments): make daemon XDG platform-token and device-id env-aware #25497
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; | ||
| import { tmpdir } from "node:os"; | ||
| import { join } from "node:path"; | ||
| import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; | ||
|
|
||
| // Suppress logger output before importing the module under test. | ||
| mock.module("../util/logger.js", () => ({ | ||
| getLogger: () => | ||
| new Proxy({} as Record<string, unknown>, { | ||
| get: () => () => {}, | ||
| }), | ||
| })); | ||
|
|
||
| import { getDeviceId, resetDeviceIdCache } from "../util/device-id.js"; | ||
|
|
||
| const originalVellumEnvironment = process.env.VELLUM_ENVIRONMENT; | ||
| const originalXdgConfigHome = process.env.XDG_CONFIG_HOME; | ||
| const originalIsContainerized = process.env.IS_CONTAINERIZED; | ||
|
|
||
| let tempDir: string; | ||
|
|
||
| beforeEach(() => { | ||
| tempDir = mkdtempSync(join(tmpdir(), "vellum-device-id-test-")); | ||
| resetDeviceIdCache(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| resetDeviceIdCache(); | ||
|
|
||
| if (originalVellumEnvironment == null) { | ||
| delete process.env.VELLUM_ENVIRONMENT; | ||
| } else { | ||
| process.env.VELLUM_ENVIRONMENT = originalVellumEnvironment; | ||
| } | ||
| if (originalXdgConfigHome == null) { | ||
| delete process.env.XDG_CONFIG_HOME; | ||
| } else { | ||
| process.env.XDG_CONFIG_HOME = originalXdgConfigHome; | ||
| } | ||
| if (originalIsContainerized == null) { | ||
| delete process.env.IS_CONTAINERIZED; | ||
| } else { | ||
| process.env.IS_CONTAINERIZED = originalIsContainerized; | ||
| } | ||
|
|
||
| try { | ||
| rmSync(tempDir, { recursive: true, force: true }); | ||
| } catch { | ||
| /* best-effort */ | ||
| } | ||
| }); | ||
|
|
||
| describe("getDeviceId env-awareness", () => { | ||
| test("non-prod (dev) writes device.json under $XDG_CONFIG_HOME/vellum-dev", () => { | ||
| // Guarantee we're not containerized — the test-preload deletes this, | ||
| // but be defensive. | ||
| delete process.env.IS_CONTAINERIZED; | ||
| process.env.VELLUM_ENVIRONMENT = "dev"; | ||
| process.env.XDG_CONFIG_HOME = tempDir; | ||
|
|
||
| const id = getDeviceId(); | ||
| expect(typeof id).toBe("string"); | ||
| expect(id.length).toBeGreaterThan(0); | ||
|
|
||
| const expectedPath = join(tempDir, "vellum-dev", "device.json"); | ||
| expect(existsSync(expectedPath)).toBe(true); | ||
|
|
||
| const parsed = JSON.parse(readFileSync(expectedPath, "utf-8")); | ||
| expect(parsed.deviceId).toBe(id); | ||
| }); | ||
|
|
||
| test("staging environment writes under $XDG_CONFIG_HOME/vellum-staging", () => { | ||
| delete process.env.IS_CONTAINERIZED; | ||
| process.env.VELLUM_ENVIRONMENT = "staging"; | ||
| process.env.XDG_CONFIG_HOME = tempDir; | ||
|
|
||
| getDeviceId(); | ||
|
|
||
| const expectedPath = join(tempDir, "vellum-staging", "device.json"); | ||
| expect(existsSync(expectedPath)).toBe(true); | ||
| }); | ||
|
|
||
| test("unknown environment does NOT write under $XDG_CONFIG_HOME/vellum-<unknown>", () => { | ||
| // Unknown env names fall back to the legacy production behavior. | ||
| // We can't assert the exact legacy path without mocking homedir(), | ||
| // but we can assert that the XDG env-scoped dir is NOT created. | ||
| delete process.env.IS_CONTAINERIZED; | ||
| process.env.VELLUM_ENVIRONMENT = "no-such-env"; | ||
| process.env.XDG_CONFIG_HOME = tempDir; | ||
|
|
||
| getDeviceId(); | ||
|
|
||
| // No `vellum-no-such-env` directory created under our XDG tempdir. | ||
| const envScopedPath = join(tempDir, "vellum-no-such-env", "device.json"); | ||
| expect(existsSync(envScopedPath)).toBe(false); | ||
| // Legacy fallback would write under `${homedir()}/.vellum` — not touched. | ||
| const productionXdgPath = join(tempDir, "vellum", "device.json"); | ||
| expect(existsSync(productionXdgPath)).toBe(false); | ||
| }); | ||
|
|
||
| test("production does NOT write under $XDG_CONFIG_HOME/vellum", () => { | ||
| // Production path is ~/.vellum/device.json, never XDG_CONFIG_HOME. | ||
| delete process.env.IS_CONTAINERIZED; | ||
| delete process.env.VELLUM_ENVIRONMENT; | ||
| process.env.XDG_CONFIG_HOME = tempDir; | ||
|
|
||
| getDeviceId(); | ||
|
|
||
| const xdgPath = join(tempDir, "vellum", "device.json"); | ||
| expect(existsSync(xdgPath)).toBe(false); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,11 +6,14 @@ | |
| * extensible for future per-device metadata. | ||
| * | ||
| * Path resolution: | ||
| * - Containerized (IS_CONTAINERIZED=true): uses /home/assistant (the assistant | ||
| * user's persistent home dir) so device.json lives on the assistant's own | ||
| * filesystem rather than the shared data volume. | ||
| * - Local (single or multi-instance): uses homedir() so all instances on the | ||
| * same machine share a single device ID. | ||
| * - Containerized (IS_CONTAINERIZED=true): `/home/assistant/.vellum/device.json` | ||
| * — the assistant user's persistent home dir, kept off the shared data | ||
| * volume. Not affected by VELLUM_ENVIRONMENT because the container fs | ||
| * has no cross-process contract with the Swift client. | ||
| * - Non-containerized production: `~/.vellum/device.json` (legacy, shared | ||
| * across all local instances on the same machine). | ||
| * - Non-containerized non-production: `$XDG_CONFIG_HOME/vellum-<env>/device.json`, | ||
| * matching Swift's `VellumPaths.deviceIdFile`. | ||
| * | ||
| * The value is cached in memory after the first successful read/write. | ||
| * Falls back to a generated UUID if the file cannot be read or written. | ||
|
|
@@ -23,6 +26,7 @@ import { join } from "node:path"; | |
|
|
||
| import { getIsContainerized } from "../config/env-registry.js"; | ||
| import { getLogger } from "./logger.js"; | ||
| import { getXdgVellumConfigDirName } from "./platform.js"; | ||
|
|
||
| const log = getLogger("device-id"); | ||
|
|
||
|
|
@@ -44,6 +48,35 @@ export function getDeviceIdBaseDir(): string { | |
| return homedir(); | ||
| } | ||
|
|
||
| /** | ||
| * Resolve the directory and file path for `device.json` based on the | ||
| * runtime environment. See the module docblock for the resolution table. | ||
| * | ||
| * Production and containerized modes preserve the legacy `~/.vellum` / | ||
| * `/home/assistant/.vellum` paths. Non-production, non-containerized | ||
| * deployments route through `$XDG_CONFIG_HOME/vellum-<env>` to match | ||
| * the Swift client's `VellumPaths.deviceIdFile`. | ||
| */ | ||
| function resolveDeviceIdPaths(): { dir: string; file: string } { | ||
| if (getIsContainerized()) { | ||
| const dir = join("/home/assistant", ".vellum"); | ||
| return { dir, file: join(dir, "device.json") }; | ||
| } | ||
|
|
||
| const configDirName = getXdgVellumConfigDirName(); | ||
| if (configDirName === "vellum") { | ||
| // Production: device.json lives at ~/.vellum/device.json, shared | ||
| // across all local instances on the same machine. | ||
| const dir = join(homedir(), ".vellum"); | ||
| return { dir, file: join(dir, "device.json") }; | ||
| } | ||
|
|
||
| const configHome = | ||
| process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config"); | ||
| const dir = join(configHome, configDirName); | ||
| return { dir, file: join(dir, "device.json") }; | ||
| } | ||
|
Comment on lines
+60
to
+78
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Migration 003-seed-device-id writes device.json to wrong path for non-production environments The PR changes Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| /** | ||
| * Get the stable device ID for this machine. | ||
| * | ||
|
|
@@ -60,8 +93,7 @@ export function getDeviceId(): string { | |
| return cached; | ||
| } | ||
|
|
||
| const vellumDir = join(getDeviceIdBaseDir(), ".vellum"); | ||
| const filePath = join(vellumDir, "device.json"); | ||
| const { dir: vellumDir, file: filePath } = resolveDeviceIdPaths(); | ||
| const generated = randomUUID(); | ||
|
|
||
| try { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This path switch sends all non-production runs to
$XDG_CONFIG_HOME/vellum-<env>/device.json, butgetDeviceId()no longer reads the legacy~/.vellum/device.jsonfirst. After upgrading, existing dev/staging/test installs that already have a persisted ID in the old location (including IDs seeded by003-seed-device-id) will silently mint a new UUID, which breaks device/installation continuity for telemetry and Sentry correlation. Add a legacy read fallback (or one-time copy) before generating a new ID.Useful? React with 👍 / 👎.