From 50248499ff980711319437264789b9954aac576f Mon Sep 17 00:00:00 2001 From: Max Schaefer Date: Wed, 11 Nov 2026 06:26:09 +0000 Subject: [PATCH 1/4] Consolidate workstation services and sandboxed browsing in dotfiles --- AGENTS.md | 2 + README.md | 10 +- home/.chezmoidata.toml | 12 ++ home/.chezmoiexternal.toml | 10 ++ home/.chezmoiignore | 6 + home/.chezmoitemplates/nono-profile | 6 +- .../sandbox/private_config.json.tmpl | 4 + .../skills/symlink_agent-browser.tmpl | 1 + .../systemd/browserless.container.tmpl | 14 +++ home/dot_config/mise/config.toml | 1 + home/dot_config/mise/tasks/services.toml | 26 +++++ .../services/browserless/compose.yaml.tmpl | 12 ++ .../onepassword-connect/compose.yaml.tmpl | 19 ++++ .../onepassword-connect-api.container.tmpl | 5 + .../onepassword-connect-sync.container.tmpl | 6 + .../onepassword-connect.pod | 5 + tests/services.test.mjs | 106 ++++++++++++++++++ 17 files changed, 241 insertions(+), 4 deletions(-) create mode 100644 home/dot_agent-browser/namespaces/sandbox/private_config.json.tmpl create mode 100644 home/dot_claude/skills/symlink_agent-browser.tmpl create mode 100644 home/dot_config/containers/systemd/browserless.container.tmpl create mode 100644 home/dot_config/mise/tasks/services.toml create mode 100644 home/dot_config/services/browserless/compose.yaml.tmpl create mode 100644 home/dot_config/services/onepassword-connect/compose.yaml.tmpl create mode 100644 home/dot_config/services/onepassword-connect/onepassword-connect-api.container.tmpl create mode 100644 home/dot_config/services/onepassword-connect/onepassword-connect-sync.container.tmpl create mode 100644 home/dot_config/services/onepassword-connect/onepassword-connect.pod create mode 100644 tests/services.test.mjs diff --git a/AGENTS.md b/AGENTS.md index 8e39494..00150d0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,8 @@ - Every tool a CI job runs must be in its `with.tools` or declared by the repository with `project-tools: true`. - `mise use --global` and `mise upgrade --global` edit live files; reconcile those changes back into `home/`. - Linux hosts apply this source too: limit macOS-only targets with `.chezmoiignore` or `.chezmoi.os` blocks. +- Workstation services live in the mirrored source: macOS Compose stacks and Linux system Quadlet sources under `.config/services`, Linux user Quadlets under `.config/containers/systemd`. Activation is explicit through the service Mise tasks. +- Preserve Connect's runtime identity: system-level Podman units, secret and volume on Linux; the `onepassword-connect` Compose project, named volume and OrbStack hostname on macOS. Service relocation must not recreate credentials or data. - Keep application-managed hooks, caches, history, account state and generated host inventories outside this repository. - Preserve application destination paths and unrelated local state when changing source attributes or templates. - Use chezmoi's normal conflict handling for apply; do not force routine updates. diff --git a/README.md b/README.md index c8790c4..daa06a5 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,8 @@ modify templates to preserve unrelated application settings and local state. MCP endpoints are defined in `home/.chezmoidata.toml`. Bifrost keys are stored in the 1Password Automation vault and referenced through `home/dot_config/fnox/config.toml`. Fnox and the 1Password CLI read vaults -through the machine's 1Password Connect server: on macOS from -[local services](https://github.com/maxexcloo/local-services), on Linux from -[Homelab](https://github.com/maxexcloo/homelab). Save its token as `token` beside +through the machine's 1Password Connect server, managed here with the +`services:connect` Mise task. Save its token as `token` beside the Connect credentials file. `chezmoi apply` writes each agent's Bifrost key into its private configuration and the `GitHub Agents` token into `~/.config/gh-agents`. Keep key values out of Git and command output. @@ -36,6 +35,7 @@ then install the global tools it declares: mise exec chezmoi@latest -- chezmoi --source "$PWD" apply "$HOME/.config/mise" mise install mise run setup +mise run services:connect mise run check chezmoi status chezmoi apply @@ -81,6 +81,10 @@ Agent logins, sessions and history stay on each machine: sign in to each agent once per machine. `CLAUDE_CONFIG_DIR` keeps Claude's state file inside `~/.claude`, because the sandbox cannot replace files directly in the home directory. +[agent-browser](https://agent-browser.dev) uses the Browserless container started +by `mise run services:browserless`. Mise installs the CLI; chezmoi configures its +skill, sandbox access and endpoint. + ## Shared Agent Guidance Clone the Agents repository beside this checkout. Chezmoi links its shared diff --git a/home/.chezmoidata.toml b/home/.chezmoidata.toml index 15514d2..2976bd2 100644 --- a/home/.chezmoidata.toml +++ b/home/.chezmoidata.toml @@ -24,3 +24,15 @@ url = "https://bifrost.excloo.com/mcp/reactive-resume" [mcp.unifi_network] url = "https://bifrost.excloo.com/mcp/unifi-network" + +[services.browserless] +concurrent = 4 +port = 9222 +queued = 4 +timeout = 3600000 +# renovate: datasource=docker depName=ghcr.io/browserless/chromium +version = "v2.62.0" + +[services.connect] +# renovate: datasource=docker depName=1password/connect-api +version = "1.8.4" diff --git a/home/.chezmoiexternal.toml b/home/.chezmoiexternal.toml index 352d2b6..b3bb296 100644 --- a/home/.chezmoiexternal.toml +++ b/home/.chezmoiexternal.toml @@ -1,3 +1,13 @@ +[".agents/skills/agent-browser/LICENSE"] +type = "file" +# renovate: datasource=github-releases depName=vercel-labs/agent-browser +url = "https://raw.githubusercontent.com/vercel-labs/agent-browser/v0.42.0/LICENSE" + +[".agents/skills/agent-browser/SKILL.md"] +type = "file" +# renovate: datasource=github-releases depName=vercel-labs/agent-browser +url = "https://raw.githubusercontent.com/vercel-labs/agent-browser/v0.42.0/skills/agent-browser/SKILL.md" + [".agents/skills/humanizer"] type = "archive" exact = true diff --git a/home/.chezmoiignore b/home/.chezmoiignore index 6341fff..7bc0b28 100644 --- a/home/.chezmoiignore +++ b/home/.chezmoiignore @@ -1,4 +1,10 @@ +{{- if ne .chezmoi.os "linux" }} +.config/containers +.config/services/onepassword-connect/*.container +.config/services/onepassword-connect/*.pod +{{- end }} {{- if ne .chezmoi.os "darwin" }} +.config/services/*/compose.yaml .gitconfig .hammerspoon .ssh diff --git a/home/.chezmoitemplates/nono-profile b/home/.chezmoitemplates/nono-profile index 0f04e7a..0c63f61 100644 --- a/home/.chezmoitemplates/nono-profile +++ b/home/.chezmoitemplates/nono-profile @@ -2,6 +2,8 @@ "extends": "nolabs-ai/{{ .agent }}", "environment": { "set_vars": { + "AGENT_BROWSER_CONFIG": "$HOME/.agent-browser/namespaces/sandbox/config.json", + "AGENT_BROWSER_SOCKET_DIR": "$HOME/.agent-browser", "GH_CONFIG_DIR": "$HOME/.config/gh-agents", "GIT_CONFIG_GLOBAL": "$HOME/.config/git/config", "TMPDIR": "$XDG_CACHE_HOME", @@ -10,13 +12,15 @@ }, "filesystem": { "allow": [ + "$HOME/.agent-browser/namespaces/sandbox", "$HOME/.config/gh-agents", "$HOME/.local/state/mise/tracked-configs", "$HOME/Library/Application Support/orca/agent-hooks", "$XDG_CACHE_HOME", "/var/tmp" ], - "read": ["$HOME/.agents", "$HOME/.orca/agent-hooks", "$HOME/Projects/agents", "/usr/libexec/git-core"] + "read": ["$HOME/.agents", "$HOME/.orca/agent-hooks", "$HOME/Projects/agents", "/usr/libexec/git-core"], + "unix_socket_subtree_bind": ["$HOME/.agent-browser/namespaces/sandbox"] }, "groups": { "include": ["mise_manager"] }, "meta": { "name": "{{ .agent }}" } diff --git a/home/dot_agent-browser/namespaces/sandbox/private_config.json.tmpl b/home/dot_agent-browser/namespaces/sandbox/private_config.json.tmpl new file mode 100644 index 0000000..42444f8 --- /dev/null +++ b/home/dot_agent-browser/namespaces/sandbox/private_config.json.tmpl @@ -0,0 +1,4 @@ +{ + "cdp": "ws://127.0.0.1:{{ .services.browserless.port }}/chromium", + "namespace": "sandbox" +} diff --git a/home/dot_claude/skills/symlink_agent-browser.tmpl b/home/dot_claude/skills/symlink_agent-browser.tmpl new file mode 100644 index 0000000..3bd7e20 --- /dev/null +++ b/home/dot_claude/skills/symlink_agent-browser.tmpl @@ -0,0 +1 @@ +{{ joinPath .chezmoi.homeDir ".agents" "skills" "agent-browser" }} diff --git a/home/dot_config/containers/systemd/browserless.container.tmpl b/home/dot_config/containers/systemd/browserless.container.tmpl new file mode 100644 index 0000000..e804fa7 --- /dev/null +++ b/home/dot_config/containers/systemd/browserless.container.tmpl @@ -0,0 +1,14 @@ +[Container] +Environment=CONCURRENT={{ .services.browserless.concurrent }} +Environment=QUEUED={{ .services.browserless.queued }} +Environment=TIMEOUT={{ .services.browserless.timeout }} +Image=ghcr.io/browserless/chromium:{{ .services.browserless.version }} +PublishPort=127.0.0.1:{{ .services.browserless.port }}:3000 +ShmSize=1g + +[Service] +Restart=on-failure +TimeoutStartSec=300 + +[Install] +WantedBy=default.target diff --git a/home/dot_config/mise/config.toml b/home/dot_config/mise/config.toml index d06d607..1a443b9 100644 --- a/home/dot_config/mise/config.toml +++ b/home/dot_config/mise/config.toml @@ -16,6 +16,7 @@ claude-code = "latest" codex = "latest" k9s = "latest" nono = "latest" +"npm:agent-browser" = "latest" opencode = "latest" pi = "latest" rclone = "latest" diff --git a/home/dot_config/mise/tasks/services.toml b/home/dot_config/mise/tasks/services.toml new file mode 100644 index 0000000..6c299c8 --- /dev/null +++ b/home/dot_config/mise/tasks/services.toml @@ -0,0 +1,26 @@ +["services:browserless"] +description = "Start Browserless" +run = ''' +{% if os() == "macos" %} +chezmoi apply "$HOME/.config/services" +docker compose --file "$HOME/.config/services/browserless/compose.yaml" up --detach +{% else %} +chezmoi apply "$HOME/.config/containers" +systemctl --user daemon-reload +systemctl --user start browserless.service +{% endif %} +''' + +["services:connect"] +description = "Start 1Password Connect" +run = ''' +chezmoi apply "$HOME/.config/services" +{% if os() == "macos" %} +docker compose --file "$HOME/.config/services/onepassword-connect/compose.yaml" up --detach +{% else %} +service_directory="$HOME/.config/services/onepassword-connect" +sudo install -D -m 0644 -t /etc/containers/systemd "$service_directory/onepassword-connect-api.container" "$service_directory/onepassword-connect-sync.container" "$service_directory/onepassword-connect.pod" +sudo systemctl daemon-reload +sudo systemctl start onepassword-connect-pod.service +{% endif %} +''' diff --git a/home/dot_config/services/browserless/compose.yaml.tmpl b/home/dot_config/services/browserless/compose.yaml.tmpl new file mode 100644 index 0000000..e692d96 --- /dev/null +++ b/home/dot_config/services/browserless/compose.yaml.tmpl @@ -0,0 +1,12 @@ +name: browserless +services: + browserless: + image: ghcr.io/browserless/chromium:{{ .services.browserless.version }} + restart: unless-stopped + shm_size: 1gb + environment: + CONCURRENT: {{ .services.browserless.concurrent }} + QUEUED: {{ .services.browserless.queued }} + TIMEOUT: {{ .services.browserless.timeout }} + ports: + - "127.0.0.1:{{ .services.browserless.port }}:3000" diff --git a/home/dot_config/services/onepassword-connect/compose.yaml.tmpl b/home/dot_config/services/onepassword-connect/compose.yaml.tmpl new file mode 100644 index 0000000..5d120df --- /dev/null +++ b/home/dot_config/services/onepassword-connect/compose.yaml.tmpl @@ -0,0 +1,19 @@ +name: onepassword-connect +services: + connect-api: + image: 1password/connect-api:{{ .services.connect.version }} + restart: unless-stopped + volumes: &connect-volumes + - data:/home/opuser/.op/data + - type: bind + read_only: true + source: ${XDG_CONFIG_HOME:-${HOME}/.config}/op/connect/1password-credentials.json + target: /home/opuser/.op/1password-credentials.json + bind: + create_host_path: false + connect-sync: + image: 1password/connect-sync:{{ .services.connect.version }} + restart: unless-stopped + volumes: *connect-volumes +volumes: + data: {} diff --git a/home/dot_config/services/onepassword-connect/onepassword-connect-api.container.tmpl b/home/dot_config/services/onepassword-connect/onepassword-connect-api.container.tmpl new file mode 100644 index 0000000..48a3da4 --- /dev/null +++ b/home/dot_config/services/onepassword-connect/onepassword-connect-api.container.tmpl @@ -0,0 +1,5 @@ +[Container] +Image=docker.io/1password/connect-api:{{ .services.connect.version }} +Pod=onepassword-connect.pod +Secret=onepassword-connect-credentials,target=/home/opuser/.op/1password-credentials.json,uid=999,mode=0400 +Volume=onepassword-connect:/home/opuser/.op/data diff --git a/home/dot_config/services/onepassword-connect/onepassword-connect-sync.container.tmpl b/home/dot_config/services/onepassword-connect/onepassword-connect-sync.container.tmpl new file mode 100644 index 0000000..7358aa7 --- /dev/null +++ b/home/dot_config/services/onepassword-connect/onepassword-connect-sync.container.tmpl @@ -0,0 +1,6 @@ +[Container] +Environment=OP_HTTP_PORT=8081 +Image=docker.io/1password/connect-sync:{{ .services.connect.version }} +Pod=onepassword-connect.pod +Secret=onepassword-connect-credentials,target=/home/opuser/.op/1password-credentials.json,uid=999,mode=0400 +Volume=onepassword-connect:/home/opuser/.op/data diff --git a/home/dot_config/services/onepassword-connect/onepassword-connect.pod b/home/dot_config/services/onepassword-connect/onepassword-connect.pod new file mode 100644 index 0000000..c21ec53 --- /dev/null +++ b/home/dot_config/services/onepassword-connect/onepassword-connect.pod @@ -0,0 +1,5 @@ +[Pod] +PublishPort=127.0.0.1:8080:8080 + +[Install] +WantedBy=multi-user.target diff --git a/tests/services.test.mjs b/tests/services.test.mjs new file mode 100644 index 0000000..5c19269 --- /dev/null +++ b/tests/services.test.mjs @@ -0,0 +1,106 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; + +const root = fileURLToPath(new URL("..", import.meta.url)); +const base = ["--source", root, "--config", "/dev/null", "--config-format", "toml"]; +const execute = (args, input = "") => + execFileSync("chezmoi", [...base, ...args], { input, encoding: "utf8" }); +const render = (path, services) => + execute([ + ...(services ? ["--override-data", JSON.stringify({ services })] : []), + "execute-template", + "--file", + `home/${path}`, + ]); +const yaml = (input) => + JSON.parse( + execute( + ["execute-template", "--with-stdin", "{{ .chezmoi.stdin | fromYaml | toJson }}"], + input, + ), + ); +const serviceRoot = "dot_config/services/onepassword-connect"; + +test("Service configuration can bootstrap into a fresh home before credential templates", () => { + const temp = mkdtempSync(join(tmpdir(), "dotfiles-services-")); + try { + for (const platform of ["linux", "darwin"]) { + const destination = join(temp, platform); + mkdirSync(join(destination, ".config"), { recursive: true }); + execFileSync( + "chezmoi", + [ + ...base, + "--destination", + destination, + "--persistent-state", + join(destination, "state.boltdb"), + "--cache", + join(temp, "cache"), + "--override-data", + JSON.stringify({ chezmoi: { os: platform } }), + "apply", + join(destination, ".config/services"), + ], + { env: { ...process.env, PATH: `${root}tests/bin:${process.env.PATH}` } }, + ); + const file = platform === "linux" ? "onepassword-connect-api.container" : "compose.yaml"; + assert.ok(existsSync(join(destination, ".config/services/onepassword-connect", file))); + } + } finally { + rmSync(temp, { recursive: true, force: true }); + } +}); + +test("Connect keeps its macOS project, named volume and read-only credential mount", () => { + const config = yaml(render(`${serviceRoot}/compose.yaml.tmpl`)); + assert.equal(config.name, "onepassword-connect"); + assert.deepEqual(config.volumes, { data: {} }); + for (const name of ["connect-api", "connect-sync"]) { + const service = config.services[name]; + assert.equal(service.restart, "unless-stopped"); + assert.equal(service.volumes[0], "data:/home/opuser/.op/data"); + assert.deepEqual(service.volumes[1], { + type: "bind", + read_only: true, + source: "${XDG_CONFIG_HOME:-${HOME}/.config}/op/connect/1password-credentials.json", + target: "/home/opuser/.op/1password-credentials.json", + bind: { create_host_path: false }, + }); + } +}); + +test("Connect keeps its existing Linux pod, secret and data volume", () => { + for (const name of ["api", "sync"]) { + const lines = render(`${serviceRoot}/onepassword-connect-${name}.container.tmpl`).split("\n"); + assert.ok(lines.includes("Pod=onepassword-connect.pod")); + assert.ok(lines.includes("Volume=onepassword-connect:/home/opuser/.op/data")); + assert.ok( + lines.includes( + "Secret=onepassword-connect-credentials,target=/home/opuser/.op/1password-credentials.json,uid=999,mode=0400", + ), + ); + } + const pod = render(`${serviceRoot}/onepassword-connect.pod`); + assert.match(pod, /^PublishPort=127\.0\.0\.1:8080:8080$/m); + assert.match(pod, /^WantedBy=multi-user\.target$/m); +}); + +test("Browserless uses the same configured port across both runtimes and the client", () => { + const services = JSON.parse(execute(["execute-template", "{{ .services | toJson }}"])); + services.browserless.port = 19333; + const compose = yaml(render("dot_config/services/browserless/compose.yaml.tmpl", services)); + assert.equal(compose.name, "browserless"); + assert.deepEqual(compose.services.browserless.ports, ["127.0.0.1:19333:3000"]); + const quadlet = render("dot_config/containers/systemd/browserless.container.tmpl", services); + assert.match(quadlet, /^PublishPort=127\.0\.0\.1:19333:3000$/m); + const client = JSON.parse( + render("dot_agent-browser/namespaces/sandbox/private_config.json.tmpl", services), + ); + assert.equal(client.cdp, "ws://127.0.0.1:19333/chromium"); +}); From 31b4bfe3b99176ca1d4cba21828feb2a63ebc3c8 Mon Sep 17 00:00:00 2001 From: Max Schaefer Date: Thu, 12 Nov 2026 01:51:08 +0000 Subject: [PATCH 2/4] Run project Playwright browsers in temporary containers --- README.md | 6 +++ home/dot_config/mise/tasks/services.toml | 16 ++++++ tests/services.test.mjs | 63 +++++++++++++++++++++++- 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index daa06a5..c217116 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,12 @@ because the sandbox cannot replace files directly in the home directory. by `mise run services:browserless`. Mise installs the CLI; chezmoi configures its skill, sandbox access and endpoint. +For project Playwright tests, run `mise run services:playwright` from the project +root outside the agent sandbox. Set the two environment variables it prints in +the test terminal, then run the project's normal browser tests. The temporary +container supplies Chromium, Firefox and WebKit at the installed project's +Playwright version, without mounting project or home files; Ctrl-C removes it. + ## Shared Agent Guidance Clone the Agents repository beside this checkout. Chezmoi links its shared diff --git a/home/dot_config/mise/tasks/services.toml b/home/dot_config/mise/tasks/services.toml index 6c299c8..75f9db7 100644 --- a/home/dot_config/mise/tasks/services.toml +++ b/home/dot_config/mise/tasks/services.toml @@ -24,3 +24,19 @@ sudo systemctl daemon-reload sudo systemctl start onepassword-connect-pod.service {% endif %} ''' + +["services:playwright"] +description = "Start Project Playwright Browsers" +run = ''' +set -eu +version=$(node -p "require('playwright/package.json').version") +{% if os() == "macos" %} +runtime=docker +{% else %} +runtime=podman +{% endif %} +echo "export PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:$usage_port/" +echo "export PW_TEST_CONNECT_EXPOSE_NETWORK=''" +exec "$runtime" run --rm --init --user pwuser --workdir /home/pwuser --shm-size 1g --publish "127.0.0.1:$usage_port:3000" "mcr.microsoft.com/playwright:v$version-noble" npx --yes "playwright@$version" run-server --port 3000 --host 0.0.0.0 +''' +usage = 'flag "--port " default="9223" help="Loopback port for the Playwright server"' diff --git a/tests/services.test.mjs b/tests/services.test.mjs index 5c19269..6da7753 100644 --- a/tests/services.test.mjs +++ b/tests/services.test.mjs @@ -1,6 +1,14 @@ import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; -import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs"; +import { + chmodSync, + existsSync, + mkdirSync, + mkdtempSync, + rmSync, + symlinkSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { test } from "node:test"; @@ -104,3 +112,56 @@ test("Browserless uses the same configured port across both runtimes and the cli ); assert.equal(client.cdp, "ws://127.0.0.1:19333/chromium"); }); + +test("Project browser server matches its installed client and exposes only loopback", () => { + const temp = mkdtempSync(join(tmpdir(), "dotfiles-playwright-")); + try { + mkdirSync(join(temp, "bin")); + symlinkSync(process.execPath, join(temp, "bin/node")); + mkdirSync(join(temp, "node_modules/playwright"), { recursive: true }); + writeFileSync( + join(temp, "node_modules/playwright/package.json"), + JSON.stringify({ version: "1.65.0" }), + ); + writeFileSync( + join(temp, ".mise.toml"), + `[task_config]\nincludes = [${JSON.stringify(join(root, "home/dot_config/mise/tasks/services.toml"))}]\n`, + ); + for (const runtime of ["docker", "podman"]) { + const path = join(temp, "bin", runtime); + writeFileSync( + path, + "#!/usr/bin/env node\nconsole.log(JSON.stringify(process.argv.slice(2)));\n", + ); + chmodSync(path, 0o755); + } + const output = execFileSync( + "mise", + ["run", "--skip-tools", "services:playwright", "--port", "19334"], + { + cwd: temp, + encoding: "utf8", + env: { + ...process.env, + MISE_CONFIG_DIR: join(temp, "config"), + MISE_GLOBAL_CONFIG_FILE: join(temp, "global.toml"), + MISE_DATA_DIR: join(temp, "data"), + MISE_CACHE_DIR: join(temp, "cache"), + MISE_TRUSTED_CONFIG_PATHS: temp, + PATH: `${join(temp, "bin")}:${process.env.PATH}`, + }, + }, + ); + assert.match(output, /PW_TEST_CONNECT_WS_ENDPOINT=ws:\/\/127\.0\.0\.1:19334\//); + assert.match(output, /PW_TEST_CONNECT_EXPOSE_NETWORK=''/); + const args = JSON.parse(output.trim().split("\n").at(-1)); + assert.equal(args[args.indexOf("--publish") + 1], "127.0.0.1:19334:3000"); + assert.ok(args.includes("mcr.microsoft.com/playwright:v1.65.0-noble")); + assert.ok(args.includes("playwright@1.65.0")); + assert.ok(args.includes("--rm")); + assert.equal(args.includes("--volume"), false); + assert.equal(args.includes("--mount"), false); + } finally { + rmSync(temp, { recursive: true, force: true }); + } +}); From dfcf4cc019c6d8dbaae06a1882af82841953df74 Mon Sep 17 00:00:00 2001 From: Max Schaefer Date: Thu, 12 Nov 2026 02:15:41 +0000 Subject: [PATCH 3/4] Separate Connect ownership from browser tooling --- AGENTS.md | 2 +- README.md | 10 --- home/.chezmoidata.toml | 8 -- home/.chezmoiexternal.toml | 10 --- home/.chezmoiignore | 1 - home/.chezmoitemplates/nono-profile | 6 +- .../sandbox/private_config.json.tmpl | 4 - .../skills/symlink_agent-browser.tmpl | 1 - .../systemd/browserless.container.tmpl | 14 --- home/dot_config/mise/config.toml | 1 - home/dot_config/mise/tasks/services.toml | 29 ------- .../services/browserless/compose.yaml.tmpl | 12 --- tests/services.test.mjs | 85 +------------------ 13 files changed, 4 insertions(+), 179 deletions(-) delete mode 100644 home/dot_agent-browser/namespaces/sandbox/private_config.json.tmpl delete mode 100644 home/dot_claude/skills/symlink_agent-browser.tmpl delete mode 100644 home/dot_config/containers/systemd/browserless.container.tmpl delete mode 100644 home/dot_config/services/browserless/compose.yaml.tmpl diff --git a/AGENTS.md b/AGENTS.md index 00150d0..2767b1c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ - Every tool a CI job runs must be in its `with.tools` or declared by the repository with `project-tools: true`. - `mise use --global` and `mise upgrade --global` edit live files; reconcile those changes back into `home/`. - Linux hosts apply this source too: limit macOS-only targets with `.chezmoiignore` or `.chezmoi.os` blocks. -- Workstation services live in the mirrored source: macOS Compose stacks and Linux system Quadlet sources under `.config/services`, Linux user Quadlets under `.config/containers/systemd`. Activation is explicit through the service Mise tasks. +- Workstation services live in the mirrored source: macOS Compose stacks and Linux system Quadlet sources under `.config/services`. Activation is explicit through the service Mise tasks. - Preserve Connect's runtime identity: system-level Podman units, secret and volume on Linux; the `onepassword-connect` Compose project, named volume and OrbStack hostname on macOS. Service relocation must not recreate credentials or data. - Keep application-managed hooks, caches, history, account state and generated host inventories outside this repository. - Preserve application destination paths and unrelated local state when changing source attributes or templates. diff --git a/README.md b/README.md index c217116..b617b23 100644 --- a/README.md +++ b/README.md @@ -81,16 +81,6 @@ Agent logins, sessions and history stay on each machine: sign in to each agent once per machine. `CLAUDE_CONFIG_DIR` keeps Claude's state file inside `~/.claude`, because the sandbox cannot replace files directly in the home directory. -[agent-browser](https://agent-browser.dev) uses the Browserless container started -by `mise run services:browserless`. Mise installs the CLI; chezmoi configures its -skill, sandbox access and endpoint. - -For project Playwright tests, run `mise run services:playwright` from the project -root outside the agent sandbox. Set the two environment variables it prints in -the test terminal, then run the project's normal browser tests. The temporary -container supplies Chromium, Firefox and WebKit at the installed project's -Playwright version, without mounting project or home files; Ctrl-C removes it. - ## Shared Agent Guidance Clone the Agents repository beside this checkout. Chezmoi links its shared diff --git a/home/.chezmoidata.toml b/home/.chezmoidata.toml index 2976bd2..2b19424 100644 --- a/home/.chezmoidata.toml +++ b/home/.chezmoidata.toml @@ -25,14 +25,6 @@ url = "https://bifrost.excloo.com/mcp/reactive-resume" [mcp.unifi_network] url = "https://bifrost.excloo.com/mcp/unifi-network" -[services.browserless] -concurrent = 4 -port = 9222 -queued = 4 -timeout = 3600000 -# renovate: datasource=docker depName=ghcr.io/browserless/chromium -version = "v2.62.0" - [services.connect] # renovate: datasource=docker depName=1password/connect-api version = "1.8.4" diff --git a/home/.chezmoiexternal.toml b/home/.chezmoiexternal.toml index 09d0498..15bfac4 100644 --- a/home/.chezmoiexternal.toml +++ b/home/.chezmoiexternal.toml @@ -1,13 +1,3 @@ -[".agents/skills/agent-browser/LICENSE"] -type = "file" -# renovate: datasource=github-releases depName=vercel-labs/agent-browser -url = "https://raw.githubusercontent.com/vercel-labs/agent-browser/v0.42.0/LICENSE" - -[".agents/skills/agent-browser/SKILL.md"] -type = "file" -# renovate: datasource=github-releases depName=vercel-labs/agent-browser -url = "https://raw.githubusercontent.com/vercel-labs/agent-browser/v0.42.0/skills/agent-browser/SKILL.md" - [".agents/skills/humanizer"] type = "archive" exact = true diff --git a/home/.chezmoiignore b/home/.chezmoiignore index 4d543ba..23f0773 100644 --- a/home/.chezmoiignore +++ b/home/.chezmoiignore @@ -1,5 +1,4 @@ {{- if ne .chezmoi.os "linux" }} -.config/containers .config/services/onepassword-connect/*.container .config/services/onepassword-connect/*.pod {{- end }} diff --git a/home/.chezmoitemplates/nono-profile b/home/.chezmoitemplates/nono-profile index 0c63f61..0f04e7a 100644 --- a/home/.chezmoitemplates/nono-profile +++ b/home/.chezmoitemplates/nono-profile @@ -2,8 +2,6 @@ "extends": "nolabs-ai/{{ .agent }}", "environment": { "set_vars": { - "AGENT_BROWSER_CONFIG": "$HOME/.agent-browser/namespaces/sandbox/config.json", - "AGENT_BROWSER_SOCKET_DIR": "$HOME/.agent-browser", "GH_CONFIG_DIR": "$HOME/.config/gh-agents", "GIT_CONFIG_GLOBAL": "$HOME/.config/git/config", "TMPDIR": "$XDG_CACHE_HOME", @@ -12,15 +10,13 @@ }, "filesystem": { "allow": [ - "$HOME/.agent-browser/namespaces/sandbox", "$HOME/.config/gh-agents", "$HOME/.local/state/mise/tracked-configs", "$HOME/Library/Application Support/orca/agent-hooks", "$XDG_CACHE_HOME", "/var/tmp" ], - "read": ["$HOME/.agents", "$HOME/.orca/agent-hooks", "$HOME/Projects/agents", "/usr/libexec/git-core"], - "unix_socket_subtree_bind": ["$HOME/.agent-browser/namespaces/sandbox"] + "read": ["$HOME/.agents", "$HOME/.orca/agent-hooks", "$HOME/Projects/agents", "/usr/libexec/git-core"] }, "groups": { "include": ["mise_manager"] }, "meta": { "name": "{{ .agent }}" } diff --git a/home/dot_agent-browser/namespaces/sandbox/private_config.json.tmpl b/home/dot_agent-browser/namespaces/sandbox/private_config.json.tmpl deleted file mode 100644 index 42444f8..0000000 --- a/home/dot_agent-browser/namespaces/sandbox/private_config.json.tmpl +++ /dev/null @@ -1,4 +0,0 @@ -{ - "cdp": "ws://127.0.0.1:{{ .services.browserless.port }}/chromium", - "namespace": "sandbox" -} diff --git a/home/dot_claude/skills/symlink_agent-browser.tmpl b/home/dot_claude/skills/symlink_agent-browser.tmpl deleted file mode 100644 index 3bd7e20..0000000 --- a/home/dot_claude/skills/symlink_agent-browser.tmpl +++ /dev/null @@ -1 +0,0 @@ -{{ joinPath .chezmoi.homeDir ".agents" "skills" "agent-browser" }} diff --git a/home/dot_config/containers/systemd/browserless.container.tmpl b/home/dot_config/containers/systemd/browserless.container.tmpl deleted file mode 100644 index e804fa7..0000000 --- a/home/dot_config/containers/systemd/browserless.container.tmpl +++ /dev/null @@ -1,14 +0,0 @@ -[Container] -Environment=CONCURRENT={{ .services.browserless.concurrent }} -Environment=QUEUED={{ .services.browserless.queued }} -Environment=TIMEOUT={{ .services.browserless.timeout }} -Image=ghcr.io/browserless/chromium:{{ .services.browserless.version }} -PublishPort=127.0.0.1:{{ .services.browserless.port }}:3000 -ShmSize=1g - -[Service] -Restart=on-failure -TimeoutStartSec=300 - -[Install] -WantedBy=default.target diff --git a/home/dot_config/mise/config.toml b/home/dot_config/mise/config.toml index 03bd38e..5e4c467 100644 --- a/home/dot_config/mise/config.toml +++ b/home/dot_config/mise/config.toml @@ -17,7 +17,6 @@ codex = "latest" k9s = "latest" nono = "latest" "npm:@deepseek-ai/dsh" = "latest" -"npm:agent-browser" = "latest" opencode = "latest" pi = "latest" rclone = "latest" diff --git a/home/dot_config/mise/tasks/services.toml b/home/dot_config/mise/tasks/services.toml index 75f9db7..977de54 100644 --- a/home/dot_config/mise/tasks/services.toml +++ b/home/dot_config/mise/tasks/services.toml @@ -1,16 +1,3 @@ -["services:browserless"] -description = "Start Browserless" -run = ''' -{% if os() == "macos" %} -chezmoi apply "$HOME/.config/services" -docker compose --file "$HOME/.config/services/browserless/compose.yaml" up --detach -{% else %} -chezmoi apply "$HOME/.config/containers" -systemctl --user daemon-reload -systemctl --user start browserless.service -{% endif %} -''' - ["services:connect"] description = "Start 1Password Connect" run = ''' @@ -24,19 +11,3 @@ sudo systemctl daemon-reload sudo systemctl start onepassword-connect-pod.service {% endif %} ''' - -["services:playwright"] -description = "Start Project Playwright Browsers" -run = ''' -set -eu -version=$(node -p "require('playwright/package.json').version") -{% if os() == "macos" %} -runtime=docker -{% else %} -runtime=podman -{% endif %} -echo "export PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:$usage_port/" -echo "export PW_TEST_CONNECT_EXPOSE_NETWORK=''" -exec "$runtime" run --rm --init --user pwuser --workdir /home/pwuser --shm-size 1g --publish "127.0.0.1:$usage_port:3000" "mcr.microsoft.com/playwright:v$version-noble" npx --yes "playwright@$version" run-server --port 3000 --host 0.0.0.0 -''' -usage = 'flag "--port " default="9223" help="Loopback port for the Playwright server"' diff --git a/home/dot_config/services/browserless/compose.yaml.tmpl b/home/dot_config/services/browserless/compose.yaml.tmpl deleted file mode 100644 index e692d96..0000000 --- a/home/dot_config/services/browserless/compose.yaml.tmpl +++ /dev/null @@ -1,12 +0,0 @@ -name: browserless -services: - browserless: - image: ghcr.io/browserless/chromium:{{ .services.browserless.version }} - restart: unless-stopped - shm_size: 1gb - environment: - CONCURRENT: {{ .services.browserless.concurrent }} - QUEUED: {{ .services.browserless.queued }} - TIMEOUT: {{ .services.browserless.timeout }} - ports: - - "127.0.0.1:{{ .services.browserless.port }}:3000" diff --git a/tests/services.test.mjs b/tests/services.test.mjs index 6da7753..a1b8cab 100644 --- a/tests/services.test.mjs +++ b/tests/services.test.mjs @@ -1,14 +1,6 @@ import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; -import { - chmodSync, - existsSync, - mkdirSync, - mkdtempSync, - rmSync, - symlinkSync, - writeFileSync, -} from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { test } from "node:test"; @@ -18,13 +10,7 @@ const root = fileURLToPath(new URL("..", import.meta.url)); const base = ["--source", root, "--config", "/dev/null", "--config-format", "toml"]; const execute = (args, input = "") => execFileSync("chezmoi", [...base, ...args], { input, encoding: "utf8" }); -const render = (path, services) => - execute([ - ...(services ? ["--override-data", JSON.stringify({ services })] : []), - "execute-template", - "--file", - `home/${path}`, - ]); +const render = (path) => execute(["execute-template", "--file", `home/${path}`]); const yaml = (input) => JSON.parse( execute( @@ -98,70 +84,3 @@ test("Connect keeps its existing Linux pod, secret and data volume", () => { assert.match(pod, /^PublishPort=127\.0\.0\.1:8080:8080$/m); assert.match(pod, /^WantedBy=multi-user\.target$/m); }); - -test("Browserless uses the same configured port across both runtimes and the client", () => { - const services = JSON.parse(execute(["execute-template", "{{ .services | toJson }}"])); - services.browserless.port = 19333; - const compose = yaml(render("dot_config/services/browserless/compose.yaml.tmpl", services)); - assert.equal(compose.name, "browserless"); - assert.deepEqual(compose.services.browserless.ports, ["127.0.0.1:19333:3000"]); - const quadlet = render("dot_config/containers/systemd/browserless.container.tmpl", services); - assert.match(quadlet, /^PublishPort=127\.0\.0\.1:19333:3000$/m); - const client = JSON.parse( - render("dot_agent-browser/namespaces/sandbox/private_config.json.tmpl", services), - ); - assert.equal(client.cdp, "ws://127.0.0.1:19333/chromium"); -}); - -test("Project browser server matches its installed client and exposes only loopback", () => { - const temp = mkdtempSync(join(tmpdir(), "dotfiles-playwright-")); - try { - mkdirSync(join(temp, "bin")); - symlinkSync(process.execPath, join(temp, "bin/node")); - mkdirSync(join(temp, "node_modules/playwright"), { recursive: true }); - writeFileSync( - join(temp, "node_modules/playwright/package.json"), - JSON.stringify({ version: "1.65.0" }), - ); - writeFileSync( - join(temp, ".mise.toml"), - `[task_config]\nincludes = [${JSON.stringify(join(root, "home/dot_config/mise/tasks/services.toml"))}]\n`, - ); - for (const runtime of ["docker", "podman"]) { - const path = join(temp, "bin", runtime); - writeFileSync( - path, - "#!/usr/bin/env node\nconsole.log(JSON.stringify(process.argv.slice(2)));\n", - ); - chmodSync(path, 0o755); - } - const output = execFileSync( - "mise", - ["run", "--skip-tools", "services:playwright", "--port", "19334"], - { - cwd: temp, - encoding: "utf8", - env: { - ...process.env, - MISE_CONFIG_DIR: join(temp, "config"), - MISE_GLOBAL_CONFIG_FILE: join(temp, "global.toml"), - MISE_DATA_DIR: join(temp, "data"), - MISE_CACHE_DIR: join(temp, "cache"), - MISE_TRUSTED_CONFIG_PATHS: temp, - PATH: `${join(temp, "bin")}:${process.env.PATH}`, - }, - }, - ); - assert.match(output, /PW_TEST_CONNECT_WS_ENDPOINT=ws:\/\/127\.0\.0\.1:19334\//); - assert.match(output, /PW_TEST_CONNECT_EXPOSE_NETWORK=''/); - const args = JSON.parse(output.trim().split("\n").at(-1)); - assert.equal(args[args.indexOf("--publish") + 1], "127.0.0.1:19334:3000"); - assert.ok(args.includes("mcr.microsoft.com/playwright:v1.65.0-noble")); - assert.ok(args.includes("playwright@1.65.0")); - assert.ok(args.includes("--rm")); - assert.equal(args.includes("--volume"), false); - assert.equal(args.includes("--mount"), false); - } finally { - rmSync(temp, { recursive: true, force: true }); - } -}); From 43bee04bb02f34cbc7e52ce3d0d06b263d2f3f3f Mon Sep 17 00:00:00 2001 From: Max Schaefer Date: Thu, 12 Nov 2026 15:42:02 +1100 Subject: [PATCH 4/4] Use shared Compose for workstation Connect --- AGENTS.md | 4 +- README.md | 15 +++++++ home/.chezmoiignore | 5 --- home/dot_config/mise/tasks/services.toml | 6 +-- .../onepassword-connect/compose.yaml.tmpl | 25 ++++++++++++ .../onepassword-connect-api.container.tmpl | 5 --- .../onepassword-connect-sync.container.tmpl | 6 --- .../onepassword-connect.pod | 5 --- tests/services.test.mjs | 40 ++++++++++++------- 9 files changed, 68 insertions(+), 43 deletions(-) delete mode 100644 home/dot_config/services/onepassword-connect/onepassword-connect-api.container.tmpl delete mode 100644 home/dot_config/services/onepassword-connect/onepassword-connect-sync.container.tmpl delete mode 100644 home/dot_config/services/onepassword-connect/onepassword-connect.pod diff --git a/AGENTS.md b/AGENTS.md index 2767b1c..7132d4e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,8 +8,8 @@ - Every tool a CI job runs must be in its `with.tools` or declared by the repository with `project-tools: true`. - `mise use --global` and `mise upgrade --global` edit live files; reconcile those changes back into `home/`. - Linux hosts apply this source too: limit macOS-only targets with `.chezmoiignore` or `.chezmoi.os` blocks. -- Workstation services live in the mirrored source: macOS Compose stacks and Linux system Quadlet sources under `.config/services`. Activation is explicit through the service Mise tasks. -- Preserve Connect's runtime identity: system-level Podman units, secret and volume on Linux; the `onepassword-connect` Compose project, named volume and OrbStack hostname on macOS. Service relocation must not recreate credentials or data. +- Workstation Compose definitions live under `.config/services`; Homelab owns Mercury’s boot configuration. Mise service tasks start already-applied definitions. +- Preserve Connect’s existing rootful Podman secret and volume on Linux; keep the `onepassword-connect` Compose project, named volume and OrbStack hostname on macOS. A runtime migration must not recreate credentials or data. - Keep application-managed hooks, caches, history, account state and generated host inventories outside this repository. - Preserve application destination paths and unrelated local state when changing source attributes or templates. - Use chezmoi's normal conflict handling for apply; do not force routine updates. diff --git a/README.md b/README.md index 2ce6a36..54860c0 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ then install the global tools it declares: mise exec chezmoi@latest -- chezmoi --source "$PWD" apply "$HOME/.config/mise" mise install mise run setup +chezmoi apply "$HOME/.config/services" mise run services:connect mise run check chezmoi status @@ -50,6 +51,20 @@ and run the same commands. Changes to live files aren't imported automatically. `mise use --global` and `mise upgrade --global` edit the live Mise configuration, so copy those edits back into `home/`. +## Workstation Services + +Compose definitions live in `~/.config/services`. Apply them with chezmoi before +using the small `services:*` Mise tasks to start a service. macOS uses Docker +Compose through OrbStack; Mercury uses rootful `podman-compose` so Connect keeps +its existing Podman volume and secret. Homelab owns Mercury’s boot service. + +Mercury’s switch from Quadlets to Compose requires an approved handover: stop the +old Connect pod, start the Compose stack, then verify credential-backed access and +retained data before removing the old Quadlet files. The Compose definition uses +the existing volume and secret as external resources; do not recreate them or run +both owners at once. Keep the old definitions available until that verification. +The macOS project, data volume, credential mount and OrbStack endpoint stay the same. + ## Agent Launch Launch Claude Code, Codex, OpenCode and Pi directly. In Orca, reset each agent's diff --git a/home/.chezmoiignore b/home/.chezmoiignore index 23f0773..d9e2b5b 100644 --- a/home/.chezmoiignore +++ b/home/.chezmoiignore @@ -1,9 +1,4 @@ -{{- if ne .chezmoi.os "linux" }} -.config/services/onepassword-connect/*.container -.config/services/onepassword-connect/*.pod -{{- end }} {{- if ne .chezmoi.os "darwin" }} -.config/services/*/compose.yaml .gitconfig .hammerspoon .ssh diff --git a/home/dot_config/mise/tasks/services.toml b/home/dot_config/mise/tasks/services.toml index 977de54..1578b37 100644 --- a/home/dot_config/mise/tasks/services.toml +++ b/home/dot_config/mise/tasks/services.toml @@ -1,13 +1,9 @@ ["services:connect"] description = "Start 1Password Connect" run = ''' -chezmoi apply "$HOME/.config/services" {% if os() == "macos" %} docker compose --file "$HOME/.config/services/onepassword-connect/compose.yaml" up --detach {% else %} -service_directory="$HOME/.config/services/onepassword-connect" -sudo install -D -m 0644 -t /etc/containers/systemd "$service_directory/onepassword-connect-api.container" "$service_directory/onepassword-connect-sync.container" "$service_directory/onepassword-connect.pod" -sudo systemctl daemon-reload -sudo systemctl start onepassword-connect-pod.service +sudo podman-compose --file "$HOME/.config/services/onepassword-connect/compose.yaml" up --detach {% endif %} ''' diff --git a/home/dot_config/services/onepassword-connect/compose.yaml.tmpl b/home/dot_config/services/onepassword-connect/compose.yaml.tmpl index 5d120df..2cf0e6a 100644 --- a/home/dot_config/services/onepassword-connect/compose.yaml.tmpl +++ b/home/dot_config/services/onepassword-connect/compose.yaml.tmpl @@ -3,17 +3,42 @@ services: connect-api: image: 1password/connect-api:{{ .services.connect.version }} restart: unless-stopped +{{- if eq .chezmoi.os "linux" }} + ports: + - "127.0.0.1:8080:8080" + secrets: &connect-secrets + - source: onepassword-connect-credentials + target: /home/opuser/.op/1password-credentials.json + uid: "999" + mode: "0400" +{{- end }} volumes: &connect-volumes - data:/home/opuser/.op/data +{{- if ne .chezmoi.os "linux" }} - type: bind read_only: true source: ${XDG_CONFIG_HOME:-${HOME}/.config}/op/connect/1password-credentials.json target: /home/opuser/.op/1password-credentials.json bind: create_host_path: false +{{- end }} connect-sync: image: 1password/connect-sync:{{ .services.connect.version }} restart: unless-stopped +{{- if eq .chezmoi.os "linux" }} + environment: + OP_HTTP_PORT: "8081" + secrets: *connect-secrets +{{- end }} volumes: *connect-volumes volumes: +{{- if eq .chezmoi.os "linux" }} + data: + external: true + name: onepassword-connect +secrets: + onepassword-connect-credentials: + external: true +{{- else }} data: {} +{{- end }} diff --git a/home/dot_config/services/onepassword-connect/onepassword-connect-api.container.tmpl b/home/dot_config/services/onepassword-connect/onepassword-connect-api.container.tmpl deleted file mode 100644 index 48a3da4..0000000 --- a/home/dot_config/services/onepassword-connect/onepassword-connect-api.container.tmpl +++ /dev/null @@ -1,5 +0,0 @@ -[Container] -Image=docker.io/1password/connect-api:{{ .services.connect.version }} -Pod=onepassword-connect.pod -Secret=onepassword-connect-credentials,target=/home/opuser/.op/1password-credentials.json,uid=999,mode=0400 -Volume=onepassword-connect:/home/opuser/.op/data diff --git a/home/dot_config/services/onepassword-connect/onepassword-connect-sync.container.tmpl b/home/dot_config/services/onepassword-connect/onepassword-connect-sync.container.tmpl deleted file mode 100644 index 7358aa7..0000000 --- a/home/dot_config/services/onepassword-connect/onepassword-connect-sync.container.tmpl +++ /dev/null @@ -1,6 +0,0 @@ -[Container] -Environment=OP_HTTP_PORT=8081 -Image=docker.io/1password/connect-sync:{{ .services.connect.version }} -Pod=onepassword-connect.pod -Secret=onepassword-connect-credentials,target=/home/opuser/.op/1password-credentials.json,uid=999,mode=0400 -Volume=onepassword-connect:/home/opuser/.op/data diff --git a/home/dot_config/services/onepassword-connect/onepassword-connect.pod b/home/dot_config/services/onepassword-connect/onepassword-connect.pod deleted file mode 100644 index c21ec53..0000000 --- a/home/dot_config/services/onepassword-connect/onepassword-connect.pod +++ /dev/null @@ -1,5 +0,0 @@ -[Pod] -PublishPort=127.0.0.1:8080:8080 - -[Install] -WantedBy=multi-user.target diff --git a/tests/services.test.mjs b/tests/services.test.mjs index a1b8cab..82d2240 100644 --- a/tests/services.test.mjs +++ b/tests/services.test.mjs @@ -10,7 +10,14 @@ const root = fileURLToPath(new URL("..", import.meta.url)); const base = ["--source", root, "--config", "/dev/null", "--config-format", "toml"]; const execute = (args, input = "") => execFileSync("chezmoi", [...base, ...args], { input, encoding: "utf8" }); -const render = (path) => execute(["execute-template", "--file", `home/${path}`]); +const render = (path, platform = "darwin") => + execute([ + "--override-data", + JSON.stringify({ chezmoi: { os: platform } }), + "execute-template", + "--file", + `home/${path}`, + ]); const yaml = (input) => JSON.parse( execute( @@ -43,7 +50,7 @@ test("Service configuration can bootstrap into a fresh home before credential te ], { env: { ...process.env, PATH: `${root}tests/bin:${process.env.PATH}` } }, ); - const file = platform === "linux" ? "onepassword-connect-api.container" : "compose.yaml"; + const file = "compose.yaml"; assert.ok(existsSync(join(destination, ".config/services/onepassword-connect", file))); } } finally { @@ -69,18 +76,21 @@ test("Connect keeps its macOS project, named volume and read-only credential mou } }); -test("Connect keeps its existing Linux pod, secret and data volume", () => { - for (const name of ["api", "sync"]) { - const lines = render(`${serviceRoot}/onepassword-connect-${name}.container.tmpl`).split("\n"); - assert.ok(lines.includes("Pod=onepassword-connect.pod")); - assert.ok(lines.includes("Volume=onepassword-connect:/home/opuser/.op/data")); - assert.ok( - lines.includes( - "Secret=onepassword-connect-credentials,target=/home/opuser/.op/1password-credentials.json,uid=999,mode=0400", - ), - ); +test("Linux Compose reuses Connect’s rootful external volume and secret", () => { + const config = yaml(render(`${serviceRoot}/compose.yaml.tmpl`, "linux")); + assert.equal(config.name, "onepassword-connect"); + assert.deepEqual(config.volumes, { data: { external: true, name: "onepassword-connect" } }); + assert.deepEqual(config.secrets, { "onepassword-connect-credentials": { external: true } }); + assert.deepEqual(config.services["connect-api"].ports, ["127.0.0.1:8080:8080"]); + for (const name of ["connect-api", "connect-sync"]) { + assert.deepEqual(config.services[name].volumes, ["data:/home/opuser/.op/data"]); + assert.deepEqual(config.services[name].secrets, [ + { + source: "onepassword-connect-credentials", + target: "/home/opuser/.op/1password-credentials.json", + uid: "999", + mode: "0400", + }, + ]); } - const pod = render(`${serviceRoot}/onepassword-connect.pod`); - assert.match(pod, /^PublishPort=127\.0\.0\.1:8080:8080$/m); - assert.match(pod, /^WantedBy=multi-user\.target$/m); });