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
1 change: 1 addition & 0 deletions integrations/isolation/acq-kits/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ they are exempt from — and must stay out of — the `kits.yaml` registry. See
| Template | Purpose |
|----------|---------|
| [`examples/team-kit/`](examples/team-kit/) | A team's scope-layer mixin: conventions and agent settings through the OpenCode config tier, applied via `ACQ_EXTRA_KITS` after the global kits. Pattern and composition rules: [`../docs/scope-layers.md`](../docs/scope-layers.md). |
| [`examples/personal-kit/`](examples/personal-kit/) | A person's own mixin, stacked last: shell drop-ins, git preferences, and an OpenCode TUI theme. Its verify proves the global, team, and personal stack. |

## What is a hybrid/v1 kit?

Expand Down
133 changes: 133 additions & 0 deletions integrations/isolation/acq-kits/examples/personal-kit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# personal-kit (template for your own acq mixin kit, `hybrid/v1`)

A **template, not a deployable kit**: the starting point for your personal
scope-layer mixin, the last kit in the stack, on top of the global kits and
your team's kit. `acq` never applies this directory; you copy it into a repo
you own (a dotfiles repo is ideal), rename it, and grow it. The pattern, the
per-field composition rules, the authoring gotchas, the backend differences,
and the parser rules to check against your `acq` are in
`integrations/isolation/docs/scope-layers.md` in
[agentic-coding-patterns](https://github.com/GSA-TTS/agentic-coding-patterns)
(the pattern doc); the placement decision is ADR 0005 beside it, in
`docs/decisions/`.

> **Tested on msb only.** This template and its `scripts/verify` have not been
> tested on sbx and may not work there. Run `scripts/verify` on sbx before
> relying on it.

```text
global zscaler-ca-certificate · usai-provider · agentic-coding-playbook · git-ssh-sign (acq, everyone)
agent the agent's own kit (opencode for `acq run opencode`) (acq, per agent)
team your team's kit (ACQ_EXTRA_KITS, first)
personal your copy of this kit (ACQ_EXTRA_KITS, last)
repo the repo's AGENTS.md / .opencode/ (read by the agent)
```

## Use

1. Copy this directory into a repo you own and **rename both the directory
and `name:` in `spec.yaml`** (kebab-case; keep them equal). Update
`displayName:` and `description:`.
2. Replace every value marked `REPLACE` in `spec.yaml` and in the files under
`files/`. Each extension point ships one live, harmless value so the schema
and `scripts/verify` exercise it; leave a value in place only if you
actually want it.
3. Apply it last, after your team's kit. List it in `PERSONAL_KITS` before
your shell rc sources the team kit's `env.sh`, which puts your kits after
the team's (see "Host-side defaults" in the pattern doc), then start
sandboxes as usual:

```bash
# shell rc (bash or zsh)
export TEAM_KIT=/path/to/team-repo/acq-kits/team-kit
PERSONAL_KITS="/path/to/dotfiles/personal-kit"
. "$TEAM_KIT/env.sh"
```

```bash
acq run opencode /path/to/project
```

4. Validate on every change, and run the live check when the spec or a payload
changes:

```bash
acq kit validate /path/to/dotfiles/personal-kit
TEAM_KIT=/path/to/team-repo/acq-kits/team-kit /path/to/dotfiles/personal-kit/scripts/verify
```

Your kit is last, so wherever a field is last-wins (`environment`, `files[]`
by path) yours silently shadows the team's. Override a team setting only where
the team kit's README says a personal kit may.

## What it carries (as shipped)

| Extension point | Live value | Replace with |
|-----------------|------------|--------------|
| `caps.network.allow` | `example.net` (an IANA-reserved example domain no other layer allows, so `scripts/verify` can observe it) | the hosts your own tools need, or nothing |
| `files[]` | `.rc.d/50-example.sh` (one alias), `personal/tui.jsonc` (OpenCode TUI theme) | your drop-ins and personal config files (one `files[]` record per file) |
| `commands[]` | `git config --global alias.st status` | your own idempotent steps |
| `environment` | `OPENCODE_TUI_CONFIG` → your `tui.jsonc` (this overrides a team `tui.jsonc`; keep it only if the team kit's README allows that, and `scripts/verify` skips its checks once you remove it) | more non-secret personal settings |

## What to put where

| You want | Do this |
|----------|---------|
| Aliases, prompt, shell functions | A `files/home/.rc.d/NN-name.sh` drop-in plus its `files[]` record. `acq`'s `~/.profile` sources them in lexical order in every bash login shell, scripted `bash -lc` runs included, so add no loop of your own. Keep them POSIX and safe without a terminal; wrap interactive-only lines in `case $- in *i*) ... esac`. |
| zsh as your working shell | An `exec zsh` line in a `99-` drop-in, so it sorts last, guarded so it runs only in an interactive shell on a terminal, never swallows a `bash -lic '...'` command string (`BASH_EXECUTION_STRING`), and never re-fires in a `bash` you start from zsh (see `files/home/.rc.d/50-example.sh`), plus a startup step that gives `~/.zshrc` its own `~/.rc.d` loop (commented example in `spec.yaml`), since `acq`'s loop runs only in bash |
| Terminfo for your terminal | Ship the source (`infocmp -x`) and compile it in a startup step (`tic -x`); the commented example in `spec.yaml` shows how |
| Git preferences | One startup step per key, or ship `files/home/personal/gitconfig` and register it once with `include.path`; read its keys back with `git config --global --includes KEY`. A per-remote identity goes in a fragment behind `includeIf "hasconfig:remote.*.url:…"` (commented example in `spec.yaml`) |
| An OpenCode theme or keybinds | Edit `files/home/personal/tui.jsonc` |
| Config for your own tools | One `files[]` record per file under `files/home/.config/<tool>/`. These are copies of your host config kept in sync by hand; expect sandbox-specific trims, because the sandbox runs the image's pinned version of the tool (see TROUBLESHOOTING). |
| Personal CLI tools | The image first: anything the team needs belongs there. Git config that points at such a tool (`core.pager = delta`, say) goes in its own fragment, registered with `include.path` inside the install step only while `command -v` finds the tool; otherwise a skipped install leaves git broken. For a tool only you want, and only if the image ships Nix, a guarded, non-fatal `nix profile install` startup step pinned to the image's nixpkgs rev (commented in `spec.yaml`). It is a startup step, not `phase: install`, so a failed download skips that tool instead of failing the create. |
| Extra egress | `caps.network.allow` (union across kits) |

## What it does not carry, on purpose

- **Whole shared files** (`~/.bashrc`, `~/.zshrc`, `~/.gitconfig`). Files are
whole-file and last-wins, so shipping one clobbers the image's copy and any
earlier layer's. Append-if-absent startup steps and includes compose.
- **Binary files.** Kit file delivery is for text; ship sources and generate
at startup. A compiled terminfo entry in `files[]`, for example, fails the
create with "Unterminated quoted string".
- **Secrets.** `acq secret set -g <service> --host <host> --env <VAR>` on the
host; the sandbox only ever sees a placeholder.
- **Team or global settings.** Whatever your team or the global layer already
owns stays there; carry only your own delta.

## Verifying

```bash
./scripts/verify # stacks on ../team-kit/ by default
TEAM_KIT=/path/to/team-kit ./scripts/verify
KEEP=1 ./scripts/verify # keep the sandbox and work dir for inspection
```

It validates both kits, then creates a throwaway sandbox through `acq`, which
applies the pinned built-in bundle plus, via `ACQ_EXTRA_KITS`, the team kit and
then this kit. It asserts that the global and team layers are intact under your
kit, that every live value above landed, that a bash login shell sources the
drop-in exactly once (twice means some layer added its own `~/.rc.d` loop),
that a scripted `bash -lc` still runs its command, and that both kits'
variables, files, and egress hosts coexist. When you point
`TEAM_KIT` at your team's kit, also set the `TEAM_*` variables
(`TEAM_CONFIG`, `TEAM_CONVENTIONS`, `TEAM_GIT_KEY`, `TEAM_GIT_VALUE`,
`TEAM_EGRESS_HOST`) to what it ships, and `UNLISTED_EGRESS_HOST` if your team
kit allows `example.com`. Each egress host must answer at `/` with a code
other than the backend's deny response (000 on msb, 403 on sbx). Extend
its clearly marked **PERSONAL** section as your kit grows, one assertion per
mechanism you add.

## Backend parity

The kit uses no backend-specific features: it is declarative only (`caps`,
`files[]`, `commands[]`, `environment`), with no `backend_shortcuts` or
`backend_extras`, and `scripts/verify` uses only `acq` verbs. The backends
still behave differently (who runs a command with no `user:`, how kits
re-apply, unlisted files, SSH egress); "Backend differences" in the pattern
doc is the list. Keep this section in your copy and add anything
backend-specific your kit introduces.

## Troubleshooting

See [`TROUBLESHOOTING.md`](TROUBLESHOOTING.md).
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Troubleshooting — personal kit (template)

Failure modes any copy inherits. Keep this file in your copy and grow it with
your own entries. Every command is an `acq` command. The rules behind these
entries live in the pattern doc (`scope-layers.md`, see the README); each
entry names the section. For team-layer problems, see the team kit's own
TROUBLESHOOTING.

## Kit content missing (drop-ins and personal files absent)

The shell that *created* the sandbox did not have your kit in
`ACQ_EXTRA_KITS` (missing from `PERSONAL_KITS`, or the rc did not source the
team kit's `env.sh`), so it never applied. Probe something only your kit
ships. An environment variable is not a reliable probe here: a team kit may
set the same one (`OPENCODE_TUI_CONFIG`, for example), so it can be non-empty
without your kit.

```bash
acq exec <sandbox> -- sh -c 'ls ~/.rc.d/50-example.sh; git config --global alias.st' # both missing => the kit did not apply
```

Fix the rc, then recreate:

```bash
acq rm <sandbox> && acq run opencode /path/to/project
```

Re-running `acq run` alone is not enough: `acq` records a sandbox's extra kits
at creation and reuses that record on reattach. See "Recreating a sandbox" in
the pattern doc.

## A team setting has the wrong value (your kit shadows it)

Your kit is last, and `environment` (by name) and `files[]` (by path) are
last-wins (see the composition table in the pattern doc), so a variable
or file of yours with the same name or path as the team's replaces it
silently. Compare what the sandbox has with what the team kit ships:

```bash
acq exec <sandbox> -- printenv OPENCODE_CONFIG
acq kit list # the pinned kits plus THIS shell's ACQ_EXTRA_KITS
```

`acq kit list` does not show what an existing sandbox was created with, so
compare against the `ACQ_EXTRA_KITS` you used at creation.

Rename your file or variable, or drop the override unless the team kit's
README allows it.

## Aliases or prompt missing in the shell

`acq`'s `~/.profile` sources the drop-ins, so only bash **login** shells see
them: `acq shell` and `bash -l`, not a plain `bash` typed at a prompt. Check
that the loop is there and the drop-in landed:

```bash
acq exec <sandbox> -- sh -c 'cat ~/.profile; ls ~/.rc.d'
```

`acq` writes that loop only when `~/.profile` is missing, empty, or its own; an
image that ships its own `~/.profile` must source `~/.rc.d` itself. A drop-in
present in `files/` but absent from `~/.rc.d` has no `files[]` record (see
"Backend differences" in the pattern doc). If you switched to zsh, `acq`'s loop
runs only in bash; give `~/.zshrc` its own loop.

## Drop-ins run twice

Some layer still appends its own `~/.rc.d` loop to `~/.bashrc`, and `acq`'s
`~/.profile` sources `~/.bashrc` before running its own loop. Find it and
remove that kit's startup step; `acq` already wires the directory:

```bash
acq exec <sandbox> -- sh -c 'grep -n rc.d ~/.bashrc'
```

## A scripted command hangs or runs in the wrong shell

A drop-in that runs `exec zsh` replaced a shell it should not have. `acq`'s
loop sources drop-ins in scripted `bash -lc` runs too, so the line needs its
own guards: keep it in a `99-` drop-in, wrap it in `case $- in *i*) ... esac`,
require a terminal (`[ -t 0 ]`), and require `BASH_EXECUTION_STRING` to be
empty: bash sets that variable whenever it runs a command string, so
`bash -lic '...'` still runs its command. The example in
`files/home/.rc.d/50-example.sh` carries all the guards.

## Files landed, but the env var and startup effects are missing (msb)

One of the kit's `files[]` drops failed, which stops the rest of that kit's
apply on msb. The create output names the file ("could not place kit file at
..."); the usual cause is an empty payload. See "Check against your acq" in the
pattern doc.

## A startup command did nothing, and validate said OK

`acq kit validate` passes several spec shapes that run nothing or run the
wrong thing: a comment after `phase:` or `user:` (the command is skipped with
only a warning in the create output), a comment after an argv item (it becomes
part of the argument), and flow-style argv (`command: [a, b]`, parsed as
empty). See "Check against your acq" in the pattern doc for the exact rules.

## Commits in the sandbox have the wrong author

`acq` carries your host's git identity into the sandbox, but not when
`user.name` and `user.email` come from a file your host gitconfig includes
(`include.path` or `includeIf`). Check what the sandbox has:

```bash
acq exec <sandbox> -- git config --global --includes --get user.email
```

Set the identity in your kit instead: a startup step per key, or the gitconfig
your kit ships and includes (see "Git preferences" in the README).

## A tool warns about config keys that work on the host

The sandbox runs the tool version from the image's pin, not the host's, so a
config file copied from your dotfiles can carry keys that version does not
know yet (or no longer knows). The tool then warns on every run. Trim the
sandbox copy to what the pinned version accepts, and note in the file that it
is a sandbox-specific copy of your host config.

## Inspecting startup state

Startup-command output only prints during `acq run` or `acq create`. Inspect
later with a probe, and recreate to watch the output again:

```bash
acq exec <sandbox> -- sh -c 'git config --global alias.st'
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# shellcheck shell=sh
# Personal shell drop-in. acq's ~/.profile sources it in every bash login
# shell, scripted `acq exec ... bash -lc` runs included, so keep it POSIX and
# safe without a terminal, and wrap interactive-only lines in
# `case $- in *i*) ... esac`.
#
# Drop-ins run in lexical order, hence the two-digit NN- prefix (10- sorts
# before 9-). Convention: 00-09 foundations (PATH, env others read), 10-49
# tool setup, 50 independent items like this file, 90-99 must run last.
# Space by tens so inserting never renumbers.
#
# If a loop that zsh also runs sources these files, give its glob zsh's (N)
# qualifier, as the zsh step in spec.yaml does: without it, an empty ~/.rc.d
# prints "no matches found".

# REPLACE: the live value scripts/verify checks. Keep it or update verify.
alias gst='git status'

# Guard on tools installed at startup, so the first shell of a fresh sandbox
# degrades instead of erroring:
# command -v eza >/dev/null 2>&1 && alias ls='eza'

# To make zsh your working shell (sandbox entry shells are bash), put this in
# a 99- file: exec replaces the shell, so anything sorting after it never
# runs in bash. acq's loop runs only in bash, so give zsh its own ~/.rc.d loop
# with the commented zsh startup step in spec.yaml.
# The guards, in order: an interactive shell (acq's loop also runs in
# scripted `bash -lc`); a terminal; not a command string (bash sets
# BASH_EXECUTION_STRING for one, so `acq exec ... bash -lic '...'` still runs
# its command, with or without a terminal); not zsh itself; and
# not a bash started from inside the handed-off zsh (RC_D_ZSH_HANDOFF is
# exported to it), so typing `bash` in zsh still gives you bash.
# case $- in *i*) [ -t 0 ] && [ -z "${BASH_EXECUTION_STRING:-}" ] && [ -z "${ZSH_VERSION:-}" ] && [ -z "${RC_D_ZSH_HANDOFF:-}" ] && command -v zsh >/dev/null 2>&1 && RC_D_ZSH_HANDOFF=1 exec zsh ;; esac
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
// Personal OpenCode TUI settings, loaded via OPENCODE_TUI_CONFIG. Theme and
// keybinds live here, not in opencode.jsonc: OpenCode strips them from
// opencode.jsonc on load.
"$schema": "https://opencode.ai/tui.json",

// REPLACE with your theme; "system" follows the terminal's own palette.
"theme": "system"
}
Loading
Loading