Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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.
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. Keep key values out of Git and command output.

Expand All @@ -35,6 +34,8 @@ 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
chezmoi apply
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions home/.chezmoidata.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@ url = "https://bifrost.excloo.com/mcp/reactive-resume"

[mcp.unifi_network]
url = "https://bifrost.excloo.com/mcp/unifi-network"

[services.connect]
# renovate: datasource=docker depName=1password/connect-api
version = "1.8.4"
9 changes: 9 additions & 0 deletions home/dot_config/mise/tasks/services.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
["services:connect"]
description = "Start 1Password Connect"
run = '''
{% if os() == "macos" %}
docker compose --file "$HOME/.config/services/onepassword-connect/compose.yaml" up --detach
{% else %}
sudo podman-compose --file "$HOME/.config/services/onepassword-connect/compose.yaml" up --detach
{% endif %}
'''
44 changes: 44 additions & 0 deletions home/dot_config/services/onepassword-connect/compose.yaml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: onepassword-connect
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 }}
96 changes: 96 additions & 0 deletions tests/services.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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, platform = "darwin") =>
execute([
"--override-data",
JSON.stringify({ chezmoi: { os: platform } }),
"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 = "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("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",
},
]);
}
});
Loading