Commit Graph

20 Commits

Author SHA1 Message Date
joakimp 2278b22ba7 fix(ssh): wire git core.sshCommand to the writable sidecar, and assert it
Lint / hadolint (push) Successful in 13s
Lint / skill-floor (push) Successful in 14s
Lint / doc-drift (push) Successful in 18s
Lint / actionlint (push) Successful in 29s
~/.ssh is commonly bind-mounted READ-ONLY from the host, so a per-host
`ControlPath ~/.ssh/cm/%r@%h:%p` — the standard CGNAT multiplexing recipe, and
correct on the host — resolves inside an unwritable dir in the container. Every
push dies `unix_listener: cannot bind to path ...: Read-only file system`,
behind git's misleading "make sure you have the correct access rights".

setup-lan-access.sh already writes the fix: ~/.ssh-local/config overrides
ControlPath into the writable ~/.ssh-local/cm BEFORE `Include ~/.ssh/config`,
so -F repairs the socket path and keeps every per-host User/Port/IdentityFile.
entrypoint-user.sh now points git at it, guarded on the sidecar existing —
setup-lan-access.sh writes none on native Linux Docker, where -F at a missing
file would break every git-over-ssh call instead of fixing one. An existing
core.sshCommand is left alone (first-wins, as for the three git settings above).

Why code and not another doc line: the remedy was already in the global
AGENTS.md, in pi-devbox-environment SKILL.md §3, in 24 MemPalace drawers from
three devices, and printed verbatim by recreate-sanity-check.sh — and an agent
that had run that script two hours earlier still hit the failure and reinvented
a /tmp/sshcm workaround. A fifth copy was not the missing piece.

Assertions, each where it can actually pass:
- smoke-test.sh: two STATIC greps (wiring line + its [ -r ] guard). `run` uses
  --entrypoint="", so asserting the runtime value there would repeat the v1.8.0
  mistake of an assertion that cannot pass, unvalidated until the next tag.
- smoke-test.sh runtime phase: a BICONDITIONAL — sidecar present => must route
  through it; absent => must be unset. The absent arm is the one CI exercises
  (native Linux runner), so "is set" would have failed CI for a correct image.
- recreate-sanity-check.sh: the runtime assertion, plus an explicit fail for the
  inverted state (set while the sidecar is missing). The permanent "default ssh
  precedence" warning keeps its severity but now states that it is structural and
  can never reach zero, and whether git is wired, unwired, or has no sidecar.

All five arms exercised against the real script before commit; that caught a
defect in the first draft, which reported "git IS wired ... unaffected" about a
state where the sidecar was gone and every git-over-ssh call failed.

Host ~/.ssh/config needs no change: the same line is right on the host and
unusable through a read-only mount, so the fix belongs in the container layer.
2026-09-22 23:08:59 +02:00
joakimp 37fcbfcf04 fix: three v1.9.4-acceptance findings (installer WARN, init sentinel, hash collation)
Lint / hadolint (push) Successful in 10s
Lint / skill-floor (push) Successful in 9s
Lint / actionlint (push) Successful in 20s
Lint / doc-drift (push) Successful in 18s
Unreleased; no pin moves except pi-extensions master 25c1265 -> 143a214.

1. entrypoint-user.sh: first-run sentinel is now config.json, which is what
   `mempalace init` writes. The old test on palace/ (created by mining, not
   init) re-fired "Initializing MemPalace" on every boot of a container that
   never mined locally: v1.9.4 acceptance measured 1 line after one boot, 2
   after a restart. Idempotent, so harmless; the comment lied. This file is a
   base-hash input, so the next tag rebuilds the base (326fb7c03949 predicted
   with the pinned sort below; f40c4b7b103d before).

2. Dockerfile.variant: `git config --system --add safe.directory` for the
   seven root-owned /opt clones (listed, not `*`). Since git 2.35.2 any git
   command in a repo owned by another user fails "dubious ownership"; as
   `developer` that made `git -C /opt/pi-atelier rev-parse` print nothing
   (one false FAIL in the v1.9.4 acceptance) and is the root cause of the
   per-boot "WARN: pi-extensions install.sh failed" (fixed at the source in
   pi-extensions 143a214; this is the belt to that suspender). Verified live
   in a v1.9.3 container: add -> rev-parse prints 25c1265, unset -> fatal
   again; /etc/gitconfig restored to its 5 lines afterwards.

3. docker-publish.yml: `find -print0 | LC_ALL=C sort -z` in base-decide.
   sort collates per locale; identical rootfs hashed to base-f40c4b7b103d
   under C/C.UTF-8 (== run 695) and base-d8df62216a81 under sv_SE/en_US.UTF-8.
   The runner exports LANG=C.UTF-8, so the hash was stable by accident. Pin is
   hash-neutral: pinned sort + HEAD inputs reproduces f40c4b7b103d exactly.
   Per-command prefix only; nobody's locale changes.

CHANGELOG: new `## Unreleased` above v1.9.4 naming pi-extensions 143a214
(check 9 rc=0), the three fixes, and the synlig 3.10.0 hub upgrade that
v1.9.4 listed as still open. One invented URL org (gwpl) caught before commit;
upstream is elpapi42, as Dockerfile.variant:151 says.

Gates: check-doc-drift rc=0, check-base-hash rc=0, lint-shell rc=0 (16 files),
hadolint 2.15.1 rc=0, YAML parses (10 jobs), bash -n rc=0.
2026-09-22 17:20:10 +02:00
joakimp e3b38cdb0b release(v1.9.4): mempalace 3.10.0 with a pinned palace root, pi-atelier v0.10.3, pi held at 0.85.1
Lint / skill-floor (push) Successful in 8s
Lint / hadolint (push) Successful in 10s
Lint / doc-drift (push) Successful in 13s
Lint / actionlint (push) Successful in 22s
Three pins move or are deliberately held; the CHANGELOG's Unreleased section
becomes the v1.9.4 entry in this same commit, because since ee6cb9e the tag
build runs check-doc-drift.sh check 9 and every would-bake value must be named
above the last published heading.

mempalace 3.9.0 -> 3.10.0 (Dockerfile.base). Deferred at v1.9.3 for two
"Upgrade notes" items; both re-measured against the 3.10.0 wheel:

  - New installs resolve ~/.config/mempalace. The order is $MEMPALACE_CONFIG_DIR,
    then ~/.mempalace IF it holds config.json / people_map.json /
    palace/chroma.sqlite3, then XDG. An EMPTY ~/.mempalace fails that test, and
    an empty ~/.mempalace is what a freshly mounted devbox-palace volume (or
    entrypoint.sh's mkdir on a volume-less container) looks like at first boot.
    Measured with a fresh $HOME and `uvx mempalace==3.10.0 init`: config.json
    landed in ~/.config/mempalace, ~/.mempalace stayed empty, and
    entrypoint-user.sh's `[ ! -d ~/.mempalace/palace ]` would fire every start.
    Same run with MEMPALACE_CONFIG_DIR set: everything in ~/.mempalace.
    => ENV MEMPALACE_CONFIG_DIR=/home/${USER_NAME}/.mempalace, placed in the
    non-root-user section where USER_NAME is in scope, and entrypoint-user.sh
    reads PALACE_DIR from the same variable (old path as fallback). Existing
    volumes were safe either way; this is for first boots. MEMPALACE_PALACE_PATH
    is still honoured (config.py:927), so smoke-test.sh's stage test holds.
  - event_list defaults newest-first without a cursor. Server-side: the
    extension talks to synlig over MEMPALACE_REMOTE_URL, so it lands when the
    hub upgrades. mempalace-toolkit 2167a1b (floating main) makes every
    cursor-less call say order:"desc" so the mailbox reads the same window on
    either server version.

Corrected a stale sequencing comment while there: synlig serves the palace as
a `uv tool` under systemd (mempalace-serve.service; measured 2026-09-22:
mempalace 3.9.0, python 3.12.13, chromadb 1.5.9, no palace container), not via
docker-compose.mempalace.yml as the comment had said for two releases.

pi-atelier v0.10.1 -> v0.10.3 (Dockerfile.variant). Fixes only (both tags
released 2026-09-22); package.json at v0.10.3 still has zero runtime deps and no
build script, peerDeps still >=0.84.0. Annotated tags: the CHANGELOG names the
peeled SHA ed3837b, which is what resolve-versions bakes and check 9 compares.

pi HELD at 0.85.1 while 0.86.1 / 0.87.0 exist. 0.87.0 removed the
shouldStopAfterTurn agent option; pi-observational-memory 3.1.4 still uses it
and AgentContext.systemPrompt in observer/reflector/dropper (measured: 1 hit
per worker in src/agents on both the baked 3.1.3 tree and upstream master;
finishTurn 0). peerDeps are `*`, so it would break at runtime, not install.
Upstream #82, fix PR #83 open and unmerged. Bump pi and pi-obsmem together
once #83 ships. The other extensions are clean against the 0.86/0.87 lists.

Verified locally the way CI does: check-doc-drift.sh rc=0 with check 9
reporting all four moved components as named above the v1.9.3 heading;
lint-shell.sh rc=0; hadolint 2.15.1 (CI's pin, with .hadolint.yaml) rc=0 on
both Dockerfiles; PyPI 3.10.0 present, not yanked. Two claims caught by
re-measurement before commit and corrected in the text: a pi issue number
that does not exist on the 0.87.0 changelog line, and "8 shouldStopAfterTurn
hits" that was 3 (one per worker) in src/.

Still open before the tag: run the feeder from the built image against the
3.9.0 hub (dry-run first); recreate acceptance must show ~/.mempalace/palace
still passing and no ~/.config/mempalace appearing. After acceptance: upgrade
synlig's tool to 3.10.0.
2026-09-22 14:26:10 +02:00
joakimp adcf56f829 release: audited bumps (pi 0.85.1, mempalace 3.9.0, atelier v0.10.1) + two guards
Lint / hadolint (push) Successful in 12s
Lint / actionlint (push) Successful in 17s
Version audit for the next release. pi 0.84.4 -> 0.85.1, deliberately skipping
0.85.0 (it published internal experimental code and broke SDK imports,
upstream #9132). mempalace 3.8.0 -> 3.9.0. pi-atelier v0.10.0 -> v0.10.1.
PI_STUDIO_VERSION relabelled none -> v0.9.60-rc.0 so the floating main ref's
RC status is visible at docker-inspect time instead of discovered later.
PI_FORK_REF stays floating and adopts e69725c.

The pi bump was verified by running it under a pty in five combinations rather
than by reading the changelog, because this repo has already shipped a version
pair no changelog flagged (atelier < 0.7.1 hangs pi >= 0.84). CPU delta
0.00-0.01s over 5s against a ~5s sustained-CPU hang signature, two-sided via
the atelier sidebar painting identically to the 0.84.4 control.

NODE_VERSION stays 22 on purpose: node 24 is technically safe (pi's five
prebuilt addons are all NAPI, nothing declares a ceiling, agent-browser's
engines.node >=24 is vestigial for the shipped aarch64 ELF), but this release
already moves two minors and bakes an RC, and a node major would leave four
suspects if the image misbehaves. Own release, smoke suite as the gate.

Also corrects a stale claim at the mempalace ARG: synlig serves 3.8.0
server-side, not 3.7.1 (measured over ssh 2026-09-06).

agent-browser volume shadowing: the image has shipped 0.35.2, but every
session on mbp-m1-2020 ran 0.27.0 from a 2026-07-17 hand-install in
~/.pi/npm-global (a VOLUME, at PATH position 2 vs /usr/bin at 8). Third
package hit by this hazard after pi and pi-atelier, so the guard is now
generalised: entrypoint-user.sh retires the copy by moving it aside
(reversible, only when the image ships its own), recreate-sanity-check.sh
asserts resolution under /usr where the volume is real, smoke-test.sh carries
the build-time half and says in the source why it is weak. The real damage was
the stale BUNDLED SKILL (3 skillsets/17.6 KB vs 8/31.5 KB, ten subcommands
undocumented to the agent) - a stale tool errors, a stale skill quietly
teaches wrong commands.

pi-fork capability floor (extensions: []): forks were measured across four
dispatches ignoring their brief, answering in the user's voice, fabricating
self-referential measurements, and once filing a diary entry as agent_name=pi.
Cause is upstream by design - the child gets getHeader()+getBranch(), the
whole active session branch, with the brief as the final user message. Not a
model-capability problem: the same model as the fast profile obeyed the
identical brief perfectly with a fresh session and no inherited context.
extensions: [] runs children with --no-extensions, so the mempalace bridge is
absent and palace writes are impossible by construction (verified by asking a
child to enumerate its tools: read, bash, edit, write). Removes palace writes,
not filesystem writes.
2026-09-06 20:40:02 +02:00
joakimp 45850bc973 entrypoint: put back the shell state a recreate eats
Lint / hadolint (push) Successful in 8s
Lint / actionlint (push) Successful in 25s
cli_utils' install.sh reaches PATH by symlinking bin/ into ~/.local/bin.
That is persistent on a host and ephemeral in a container, so the same
installer produced opposite durability and every --force-recreate sent the
human back to typing /workspace/cli_utils/bin/git-status-all. Re-link at
start instead, and add a per-device boot hook so the next question of this
shape needs no image change at all.

Symlinks rather than a PATH edit in an rc file, deliberately: ~/.local/bin
is already ahead of /usr/local/bin in ENV PATH, so links resolve in
NON-interactive shells too (docker exec, agent tool shells, scripts). An
rc-file PATH edit cannot reach those because ~/.bashrc returns early when
not interactive — measured, that asymmetry is exactly why `command -v
git-status-all` failed in one shell and worked in another on the same box.
Shell FUNCTIONS remain the sourced file's job; a symlink cannot carry them.

Guards, because ~/.local/bin is shared: a real file is never clobbered, a
symlink pointing elsewhere is never stolen, ours are refreshed, and links
into a cli_utils/bin whose target vanished are pruned — a dangling link on
PATH reads as a broken container rather than a removed script.

The hook (~/.config/devbox-shell/init.sh) adds no trust boundary: that dir
is already sourced into every interactive shell by the baked bash_aliases,
so it is already arbitrary code from the same owner. Only WHEN it runs is
new. bash <file>, never sourced, exit status ignored, output to a log.

Caught before commit, and the reason the loops use `if` bodies instead of
`&&` chains: under `set -euo pipefail` a for-loop whose last command is a
false test exits non-zero, and with no match the /workspace/*/cli_utils
glob stays literal — so the first draft would have failed to START a
container on every machine that does not have this repo, rather than merely
skipping the links. Re-tested with set -e in place: no-cli_utils/empty-HOME
no-op, guards, idempotence, CLI_UTILS_LINK=0, a hook that exits 7, and a
hook that tries to mutate CLI_UTILS_BIN — all exit 0 with intact state.

Not covered by CI: docker-publish.yml runs only on tags and lint.yml lints
workflow run: steps, so neither executes this file. Validated by extracting
both sections and running them against fixtures, then for real in a live
v1.8.10 container. No smoke assertion added on purpose — the positive path
needs a /workspace mount smoke does not have, and asserting it there would
repeat the v1.8.0 mistake of a smoke check written against a stage that
does not exist at run time.

Moves the base hash (base-decide folds `cat entrypoint.sh
entrypoint-user.sh`), so this rides along with the next tag's ~40-minute
base rebuild rather than justifying a tag of its own.
2026-08-27 21:52:51 +02:00
joakimp e070e0bcbf skills: record the vendored snapshot's provenance, and report which copy wins
Lint / actionlint (push) Successful in 16s
Lint / hadolint (push) Successful in 16s
Found while verifying v1.8.7 from inside a fresh container: the baked mempalace
snapshot is read by no host on this fleet. devbox-skill-reconcile repoints
~/.agents/skills/mempalace at the mounted live clone (the v1.8.5 fix working as
designed), and all four compose stacks mount a workspace containing the
skillset. So the phrase canary that blocked v1.8.7's first tag polices a file
nobody opens, while the drift that could actually mislead an agent — a git pull
nobody ran in /workspace/skillset — was invisible from inside the container and
is invisible to CI by construction.

Record provenance instead of policing it, and move the check to where the
skillset actually is:

- Dockerfile.variant: ARG SKILLSET_SNAPSHOT_REF (the claim) + a sha256 of the
  shipped bytes measured in the manifest layer (the fact), as manifest siblings
  rather than components{} members, plus an OCI label. An ARG default, not a
  CI-resolved output: no credential for the private skillset, no change at any
  of the four variant build call sites, and a local docker build records what CI
  does. Variant-only, so no base rebuild — check-base-hash.sh scans
  Dockerfile.base alone, verified by running it.
- pi-devbox-version: a skills: section naming baked vs live <repo> @ <sha> per
  vendored skill, and for mempalace whether the live copy is identical to the
  baked fingerprint, at the same commit with uncommitted edits, or divergent.
  entrypoint-user.sh passes the new --no-skills, because the banner prints
  before the links exist and long before the reconcile runs.
- scripts/vendor-mempalace-skill.sh: refresh the file and rewrite the ref
  together (a cp without an ARG bump makes the manifest lie, which is worse than
  anonymity); --check verifies the claim against a real clone.
- 5 new smoke assertions (78 -> 83), mutation-tested through the real sh -c
  path: 6 fabricated manifests, where a well-formed hash of the wrong file
  proves the two manifest assertions are not redundant; the all-baked reporting
  test verified to FAIL against a live-skillset environment.

Reviewed mid-flight by pi@emb-7kj4vr4g over the logstream (correlation
skillset-vendor-drift), which retracted its own earlier recommendation of a
build-time byte-compare against skillset HEAD and supplied the better framing:
the invariant is NON-CONTRADICTION, not currency. Byte parity on a fallback
would have cost a resync commit plus a ~67-min base rebuild for each of the four
skillset commits pushed in one evening. Its warning also found a real bug here:
the script now CONSTRUCTS the snapshot from `git show HEAD:<path>` instead of
copying the working tree, because a clean `git diff` says nothing about an
untracked file — the one input the first draft would have recorded a false ref
for. Tested: untracked, unstaged and staged-but-uncommitted all refuse, atomically.

Also fixes three stale in-repo markers of the same class the canary belongs to
(true when written, silently false at release): two dangling "Unreleased"
pointers and a typst line still marked Unreleased five releases after v1.4.0.
2026-08-26 10:30:27 +02:00
joakimp b5810654f6 skills: let the skillset own the skills it owns, and stop a dangling link from killing boot
Lint / actionlint (push) Successful in 16s
Lint / hadolint (push) Successful in 20s
Baked skill links won over the live skillset clone for all three vendored
skills, so a pushed edit to skills/mempalace/SKILL.md was invisible in every
container until the next image build -- measured on two hosts (live md5
129bcc4752 vs baked 5236024fef). Cause was ordering, not intent: the baked links
are created early with a create-only-when-absent guard to close a smoke
readiness race, and the skillset deploy runs last and treats them as foreign.
The comment claimed the opposite of the behaviour.

The fix is not "skillset always wins". Ownership is per-skill: pi-extensions is
owned by its package repo and copied over the snapshot at build time, so the
skillset's lagging duplicate must keep losing; pi-devbox-environment is authored
here. Only mempalace is skillset-owned. devbox-skill-reconcile therefore runs
after the deploy and repoints only the names in skills/skillset-owned.txt,
replacing a link solely when it points into the baked tree, so a real directory
or a link pointing elsewhere is never disturbed. Precedence is now user override
-> live clone (owned names) -> baked snapshot, with the early links intact as
the fallback so the readiness race stays closed.

Reviewing that turned up a latent boot-abort in the pre-existing baked-link
block: `[ ! -e "$link" ]` is TRUE for a dangling symlink, so once a link can
point into /workspace/skillset, a vanished mount makes plain `ln -s` fail with
"File exists" -- and under `set -euo pipefail` that aborts container start
before `exec "$@"`. Reachable on `docker restart` or a host reboot, not on a
recreate, since ~/.agents is not a volume on any host. Now `ln -sfn`, which
heals the link back to the baked fallback.

Smoke additions cover what let this ship: the stale-snapshot canary grepped a
phrase present in BOTH the stale and fresh copies, so it passed throughout;
it now pins the newest section. Link targets are asserted, not just `test -L`;
the owned-list content is asserted both ways; and the reconciler's replace path
-- which no CI container exercises, since none mounts a skillset -- is covered by
fabricating one. A mutation test showed the obvious three assertions still pass
with the "is this link ours?" guard deleted, so a discriminating case was added:
an owned name whose link is a user override outside the baked tree.

Also refreshes the mempalace snapshot to skillset 670f7f1 (without it the fix
helps only hosts that mount skillset) and corrects README, which documented the
old, wrong precedence in three places.

Verified with 12 fixture cases plus 2 mutants: ownership respected against the
real trees, user overrides preserved, relative/trailing-slash/CRLF/space/glob
inputs handled, dangling link healed, read-only skills dir exits 0, idempotent.
2026-08-23 20:59:14 +02:00
Joakim Persson cbd7cf5c67 entrypoint: announce the "remote palace, no inbox" skip instead of vanishing
Lint / hadolint (push) Successful in 8s
Lint / actionlint (push) Successful in 1m8s
When MEMPALACE_REMOTE_URL is set but MEMPALACE_PI_SSH_TARGET is not, the feeder
has nowhere to ship staged transcripts, so skipping is correct. The problem was
that the branch was a bare `:` AND the skip happens before the subshell that
writes ~/.pi/agent/mempalace-catchup.log -- so a container in that state
contributed nothing to the palace and left no artifact at all, not even an empty
log, to explain why. It is indistinguishable from a healthy run that had nothing
to file, which is the worst property a memory system can have: the failure looks
exactly like success.

Surfaced while flipping the first client onto the shared palace, where this is
the single most likely way to end up quietly memory-less -- the palace is the
only thing that survives a container recreate.

The notice goes to both the container start output (docker logs) and the log
path anyone debugging looks at first. It names both variables, says what still
works (MCP tools read/write the shared palace; only this container's own
conversations go nowhere), and points at MEMPALACE_FEED=0 for anyone who meant
it -- "HTTPS first, mining later" is a documented interim state, so the notice
has to be silenceable without being ignorable.

Guarded against becoming a startup failure. mkdir -p in a branch that previously
touched no filesystem is a new risk: an unwritable ~/.pi (root-owned volume, a
classic Docker accident) fails under set -e and would abort the entire
entrypoint. It now degrades to stdout-only. Verified all five paths by executing
the extracted block under `set -euo pipefail`: the trap prints and writes the
log; unwritable ~/.pi still exits 0 and still prints; MEMPALACE_FEED=0 stays
completely silent (no message, no file); and both normal-remote and local mode
still background the feeder with no notice.

Two smoke assertions guard against a regression to the silent no-op. They test
the entrypoint as shipped in the image rather than behaviour, because this branch
only runs at container start and a `docker run` one-shot cannot reach it.

entrypoint-user.sh is COPY'd in Dockerfile.base, so this rides the base rebuild
the Unreleased feeder work already needs.
2026-08-13 16:30:55 +02:00
Joakim Persson 7c00dd6001 mempalace: drop the stage ENV pin, fix the shared-server compose
Lint / actionlint (push) Successful in 21s
Lint / hadolint (push) Successful in 29s
The feeder now defaults to <palace-root>/pi-stage upstream, so pinning
MEMPALACE_PI_STAGE into ~/.pi here is unnecessary -- and was actively wrong. It
created a second convention that could still diverge from the palace: keep the
devbox-palace volume, drop devbox-pi-config, and a scoped `mempalace sync`
prunes every conversation drawer, because dedup keys on the staged path. Both
the ENV and the entrypoint export are gone; a comment explains why adding one
back re-introduces the split it was meant to fix.

docker-compose.mempalace.yml was broken on mempalace 3.6.0 in both directions:
  - `--host 0.0.0.0` with no token in the environment makes the server refuse
    to start, crash-looping under `restart: unless-stopped`.
  - Supply a token and the healthcheck's unauthenticated `tools/list` POST 401s,
    marking a perfectly healthy server unhealthy forever.
Now the token is required via ${MEMPALACE_REMOTE_TOKEN:?...} so it fails fast at
`docker compose up` with a readable message, and the healthcheck probes the
deliberately token-free /healthz. The "no authentication of its own" security
note has been stale since 3.6.0 and is replaced with the actual posture
(bearer token + Host pin + Origin allowlist), including why browser-shaped auth
must not be put in front of it.

Dockerfile.base: mempalace-pi-session symlinked onto PATH, with a build-time
`--help` check so a broken feeder fails the image build rather than the first
session.

smoke-test: assert the stage resolves beside the palace (default, and following
$MEMPALACE_PALACE_PATH) instead of asserting the removed ENV pin. The two
behavioural guards -- a synthetic session that must be captured, an abandoned
one that must not be -- are unchanged.

.env.example: recommend `mempalace serve` on the docker0 gateway rather than
`mempalace-mcp --transport http --host 0.0.0.0`, with the two binds to avoid.
2026-08-12 17:04:14 +02:00
joakimp 43cd6e22f2 v1.7.0: bundle pi-atelier at a pinned tag; pin pi to an audited 0.84.1
Publish Docker Image / resolve-versions (push) Successful in 9s
Lint / actionlint (push) Successful in 15s
Lint / hadolint (push) Successful in 13s
Publish Docker Image / base-decide (push) Successful in 8s
Publish Docker Image / build-base (push) Successful in 41m22s
Publish Docker Image / smoke-studio (push) Successful in 5m16s
Publish Docker Image / smoke (push) Successful in 7m31s
Publish Docker Image / build-variant-studio (push) Successful in 18m31s
Publish Docker Image / build-variant (push) Successful in 27m18s
Publish Docker Image / promote-base-latest (push) Successful in 11s
Publish Docker Image / update-description (push) Successful in 12s
Two changes that belong together, because the first is what makes the second
dangerous to get wrong.

pi-atelier (TUI sidebar + status rail) is now vendored to /opt/pi-atelier at
PI_ATELIER_REF=v0.8.0 and registered by entrypoint-user.sh — the pi-fork /
pi-observational-memory / pi-studio pattern, deliberately NOT
`pi install npm:pi-atelier`, which writes into ~/.pi/npm-global on the config
volume where it shadows the image and pins nothing. Unlike its siblings it gets
no `npm install`: atelier declares zero runtime deps (peerDeps only, satisfied by
the baked pi) and has no build step, so pi loads its TypeScript straight from the
checkout via package.json `pi.extensions`.

pi is no longer resolved to npm `latest` at build time. The pin lives in
Dockerfile.variant and CI reads it from there, so a local `docker build` and a CI
release ship the same versions by construction. The pin is a CHECKPOINT, NOT A
FREEZE: bumping stays a one-line change; what stops is *unreviewed* adoption of
whatever shipped that morning, in the same build that then gets tagged and
published. CI fails when a pin is not concrete or not actually published on npm,
and warns — never adopts — when npm latest moves ahead, naming what to re-check.

Why this pairing needed care: pi-atelier 0.6.0/0.7.0 wrap pi's PRIVATE TUI
renderer, and under pi 0.84 that wrapper recurses — pi hangs at startup burning
CPU with no error. Upstream fixed the recursion in 0.7.1 and restored the
non-overlapping split in 0.7.2; 0.8.0 is additive on top. atelier's own
peerDependencies still say >=0.80.7, which does not express that floor, so
nothing in npm metadata could have warned us. The floor is therefore encoded as
an executable rule — pi >= 0.84 => pi-atelier >= 0.7.1 — asserted in both
smoke-test.sh (build time) and recreate-sanity-check.sh (after a real recreate),
verified against a 4x4 version matrix.

Existing volumes needed migration, not just vendoring: a hand-installed
`npm:pi-atelier` entry is counted as already-registered by the entrypoint guard,
so every existing volume would have kept its unpinned npm copy — and a 0.6.x copy
next to pi 0.84 is exactly the startup hang. The entrypoint now drops that one
exact string (settings.json.bak.atelier.<ts> backup, distinct prefix so it cannot
clobber the template merge's backup in the same second) and lets the pinned /opt
copy register. Tested against a real settings.json: only that entry removed,
other packages and all keys intact, idempotent, and unparseable JSON leaves the
file untouched. DEVBOX_ATELIER=0 opts out entirely — in the entrypoint rather
than via `pi uninstall`, because this component's failure mode is "pi will not
start", which cannot be repaired from inside pi.

0.84.1 was audited for this release, not merely adopted: theme/TUI changes are
additive, the session format is unchanged (CURRENT_SESSION_VERSION = 3 in both
0.83.0 and 0.84.1 with an identical migrateV1ToV2/migrateV2ToV3 ladder, so
existing transcripts are neither migrated nor at risk and pi-session-repair stays
valid), and the Node engine floor is unmoved at >=22.19.0. CI resolves the
atelier tag to its PEELED commit SHA — atelier uses annotated tags, so the
unpeeled ref is a tag object, not a commit; pi-studio's lightweight tags never
exposed that distinction.

Also: docs for overriding the read-only ~/.ssh/config from the container —
container-only keys in ~/.ssh-local, hardened authorized_keys, the fact that
`from=` must allow the HOST's addresses because container egress is NAT'd through
it, and the macOS-only-keyword trap (`UseKeychain` is fatal to Linux OpenSSH and
takes out dssh/pi --ssh while the host keeps working). Corrects two claims in
"Naming LAN peers": ssh-lan.conf is not ProxyJump-only, and first-time creation
does need one restart because the Include is emitted only when the file already
exists at start.
2026-08-07 21:34:41 +02:00
joakimp 8248688d58 fix(entrypoint,tests): register pi-fork — guard matched its own config block
Lint / actionlint (push) Successful in 32s
Lint / hadolint (push) Successful in 1m25s
The `pi install /opt/<pkg>` loop in entrypoint-user.sh guarded on a
whole-file substring grep of ~/.pi/agent/settings.json. settings.example.json
ships a top-level "pi-fork" CONFIG block (fork effort profiles, pi-toolkit
adb6907, 2026-06-17), so `grep -q pi-fork settings.json` matched the config
key itself and `pi install /opt/pi-fork` never ran — on fresh or preserved
volumes. The `fork` tool has therefore been absent since v1.0.0.

The non-destructive template merge runs earlier in the same startup than the
install loop, so the mechanism that delivers new template keys to an old
volume is what plants the string that defeats the guard. pi-observational-
memory and pi-studio escaped only by luck: the template key is
"observational-memory" (no pi- prefix) and there is no studio block.

Guard now inspects the `packages` array via jq, with a grep fallback on the
stored `.../opt/<name>"` path form, which a config key can never produce.
Existing volumes self-heal on the next container start.

Both test suites asserted the bug as green — smoke-test.sh:244 and
recreate-sanity-check.sh:204 used the same whole-file grep, so "pi-fork
registered (fork tool)" passed on every build and recreate while the tool was
missing. Both now assert against packages[] with the entrypoint's predicate,
labels say packages[], and the smoke readiness wait loop uses the array check
plus `docker exec -u developer` + $HOME instead of a hard-coded
/home/developer path.

Evidence: zero `fork` tool calls across all 19 sessions on this volume; the
v1.6.3 session that tuned pi-fork.deep to opus-5 was configuring a tool that
never loaded.
2026-07-29 19:21:49 +02:00
joakimp fb49828826 base: add pi-devbox-version command + startup banner
Lint / hadolint (push) Successful in 8s
Lint / actionlint (push) Successful in 1m11s
Wraps /etc/pi-devbox/build-manifest.json (already written at docker-build
time in Dockerfile.variant) into a human-readable summary instead of
requiring users to know the manifest path and pipe it through jq
themselves.

- rootfs/usr/local/bin/pi-devbox-version: human (default) / --json /
  --quiet output modes. Also flags live drift — compares the baked
  pi_version against the actually-running `pi --version` and warns
  on mismatch rather than trusting the manifest blindly (same
  ground-truth philosophy as the manifest generation itself). Exits 1
  with a short stderr notice on images built before the manifest
  existed, instead of failing silently.
- entrypoint-user.sh: calls it as the very first line. CMD is
  `bash -l` with tty:true in compose, so this banner lands directly
  above the first prompt on container start — no separate motd/bashrc
  hook needed (deliberately not wired into .bash_aliases, which would
  reprint on every docker exec -it).
- Dockerfile.base: COPY + chmod, same pattern as dot-watch/studio-expose.
- scripts/smoke-test.sh: 4 new checks (binary present+executable, human
  output has release tag, --json round-trips the manifest, --quiet is
  a single line).
- README.md / AGENTS.md / CHANGELOG.md updated.
2026-07-15 14:50:47 +02:00
pi cc53877328 feat: bake global gitignore (core.excludesFile) into image
Seed ~/.gitignore_global from /etc/skel-devbox (seed-if-absent, like
.bash_aliases/.inputrc, so user edits survive recreate) and wire it via
git config --global core.excludesFile, guarded so a user-set excludesFile
is never overridden. Ignores *.bak, *.bak.*, *~, *.orig, *.swp, *.tmp
across all repos without per-repo .gitignore entries.
2026-06-28 11:52:02 +02:00
Joakim Persson d619a6e2ec fix(entrypoint,smoke): link image-baked skills early to fix smoke race
Publish Docker Image / resolve-versions (push) Successful in 21s
Publish Docker Image / base-decide (push) Successful in 7s
Publish Docker Image / build-base (push) Successful in 33m43s
Publish Docker Image / smoke-studio (push) Successful in 4m5s
Publish Docker Image / smoke (push) Successful in 5m42s
Publish Docker Image / build-variant (push) Successful in 15m55s
Publish Docker Image / promote-base-latest (push) Successful in 7s
Publish Docker Image / build-variant-studio (push) Successful in 17m45s
Publish Docker Image / update-description (push) Successful in 56s
The runtime 'pi-devbox-environment skill linked' smoke assertion failed in
CI run 408 (gating build-variant). Root cause: the skill-linking block ran
AFTER the pi-toolkit/extensions deploy, but the smoke readiness gate only
waits on pi-deploy markers (keybindings.json, mempalace.ts) — which land
before the skill symlink — so the assertion sampled too early.

- entrypoint-user.sh: move the image-baked-skills symlink loop to run early
  (before the pi deploy block), so it completes before any readiness marker.
  Still before the skillset deploy, so foreign-link semantics are unchanged.
- smoke-test.sh: add the skill symlink to the readiness gate as well.

Build-time checks (baked skill, append snippet, merged AGENTS marker) all
passed in 408; only the timing of the runtime check was wrong.
2026-06-23 14:29:52 +02:00
Joakim Persson 2abfee141b feat: image-baked agent skills + pi-devbox-environment skill (v1.2.0)
Publish Docker Image / resolve-versions (push) Successful in 35s
Publish Docker Image / base-decide (push) Successful in 23s
Publish Docker Image / build-base (push) Successful in 41m32s
Publish Docker Image / smoke-studio (push) Failing after 4m5s
Publish Docker Image / build-variant-studio (push) Has been skipped
Publish Docker Image / smoke (push) Failing after 5m46s
Publish Docker Image / build-variant (push) Has been skipped
Publish Docker Image / update-description (push) Has been skipped
Publish Docker Image / promote-base-latest (push) Has been skipped
Ship skills inside the image (independent of any mounted skillset repo):
- rootfs/usr/local/share/pi-devbox/skills/<name>/ symlinked into
  ~/.agents/skills/ by entrypoint-user.sh (foreign-link, survives volume
  recreate, never clobbers a skillset/user skill of the same name).
- New pi-devbox-environment skill: persistence model, host/LAN SSH
  reachability, split-DNS mechanisms, interactive-vs-tool-shell alias
  gotcha, tmux 0-index, uv-first Python, pi-studio reachability. Agnostic
  to host OS / hostnames / domains / nameservers (discovered at runtime).
- Dockerfile.variant appends pi-global-AGENTS.append.md onto pi-toolkit's
  pi-global-AGENTS.md (single global slot) so the skill is loaded
  proactively; gated on /usr/local/lib/pi-devbox/. Idempotent.
- smoke-test: baked-skill + append-snippet + merged-marker presence and a
  runtime symlink assertion.
- docs: README 'Agent skills' section, AGENTS.md layout, DOCKER_HUB.md;
  moved studio-tex roadmap to v1.3.0.

pi 0.79.7 -> 0.79.10 (auto-resolved from npm latest at build).
2026-06-23 12:49:13 +02:00
Joakim Persson a0abacaafb fix(ssh): survive read-only ~/.ssh ControlPath; render sidecar on all host OSes
Publish Docker Image / smoke (push) Successful in 3m22s
Publish Docker Image / smoke-studio (push) Successful in 3m42s
Publish Docker Image / build-variant (push) Successful in 15m29s
Publish Docker Image / update-description (push) Successful in 11s
Publish Docker Image / build-variant-studio (push) Successful in 16m49s
Publish Docker Image / promote-base-latest (push) Successful in 14s
Publish Docker Image / resolve-versions (push) Successful in 8s
Publish Docker Image / base-decide (push) Successful in 8s
Publish Docker Image / build-base (push) Successful in 33m44s
Coordinated with the pi-extensions ssh-controlmaster fix (picked up at build via
PI_EXTENSIONS_REF=main), this makes `pi --ssh <host>` and `dssh`/`dscp` robust
to a user ~/.ssh/config whose per-host ControlPath points under the read-only
~/.ssh bind-mount (e.g. `ControlPath ~/.ssh/cm/%r@%h:%p`). A system default can
never override a user's per-host value, so the fix lives in two layers.

- setup-lan-access.sh: always render the writable ~/.ssh-local/config sidecar
  (Host * ControlPath redirect into ~/.ssh-local/cm + Include ~/.ssh/config) on
  EVERY host OS. Previously the script exited early (no-op) on native Linux,
  leaving dssh/dscp broken when ~/.ssh was read-only there too. The host-jump
  block, its key generation, and the authorize hints stay gated on VM-backed
  detection / DEVBOX_LAN_ACCESS=jump (new NEED_JUMP flag).
- Dockerfile.base: document that the /etc/ssh drop-in default cannot override a
  user per-host ControlPath; cross-ref the two handling layers.
- entrypoint-user.sh: correct the now-stale "no-op on native Linux" comment.
- README.md / DOCKER_HUB.md: document read-only-~/.ssh ControlPath handling.

CHANGELOG: v1.1.5 (Fixed + Changed + pi 0.79.6 -> 0.79.7 auto-resolved bump).
2026-06-18 21:59:18 +02:00
Joakim Persson 41c2c2b716 feat(entrypoint): non-destructively merge new template keys into settings.json
The settings.json bootstrap only fires when the file is ABSENT, so a
settings.json on a preserved named volume never picks up config added in a
later image (e.g. the observational-memory / pi-fork blocks, a newly-enabled
model). Users had to hand-merge after every upgrade.

On start, when settings.json already exists, deep-merge the template into it
with 'jq -s ".[0] * .[1]"' (template first, live second) so the user's values
always win and only MISSING keys are filled from the template. Arrays are
leaves (a model the user removed is not re-added). Rewrites only when the
merge changes something, backs up the original first, and skips safely (no
clobber) if either file is invalid JSON. Opt out with PI_SETTINGS_MERGE=0.

Add a recreate-sanity-check assertion that settings.json carries the
observational-memory + pi-fork blocks after recreate.
2026-06-17 20:49:41 +02:00
pi 7d8ee4cea1 feat(studio): bundle studio-expose bridge + socat (opt-in STUDIO_EXPOSE)
pi-studio binds the container's 127.0.0.1, which a published Docker port
can't reach. Add a robust, portable bridge rather than a doc-only one-liner:

- Dockerfile.base: add socat (~1 MB, generally useful TCP relay).
- rootfs/usr/local/bin/studio-expose: socat TCP relay listening on the
  container's egress IPv4 (not 0.0.0.0 — that would EADDRINUSE against
  Studio's loopback listener) forwarding to 127.0.0.1:PORT on the SAME
  port, so Studio's printed token URL works verbatim. Robust egress-IP
  detection (hostname -I, loopback-filtered; ip route get fallback),
  --help, port validation, foreground.
- entrypoint-user.sh: opt-in STUDIO_EXPOSE=1 auto-starts the bridge in the
  background (studio variant only). Default OFF — Studio stays loopback-only
  (its secure default) unless explicitly opted in.
- README: 'Using pi-studio' now documents host-networking (A) and the
  studio-expose/STUDIO_EXPOSE bridge (B) with a security note; ssh -L for
  remote, mosh caveat retained.
- smoke-test: assert socat + studio-expose present (base-level).
- CHANGELOG/AGENTS updated.

No tag — stopping for review.
2026-06-10 23:33:44 +02:00
pi a78e59fb5b feat(studio): add :latest-studio variant (PR-3)
Bundle pi-studio (omaclaren/pi-studio) as a new -studio image variant:
browser prompt editor, KaTeX/Mermaid preview, tmux-backed literate REPLs,
/studio command + studio_* agent tools.

- Dockerfile.variant: INSTALL_STUDIO + PI_STUDIO_REPO/REF args; vendor
  pi-studio to /opt/pi-studio (no build step — prebuilt client in git;
  npm install --omit=dev for 3 prod deps). STUDIO_PORT=8765 advisory.
- entrypoint-user.sh: register /opt/pi-studio via the existing pi install
  local-path loop (auto-skips in non-studio variant).
- smoke-test.sh: auto-detected studio assertions (clone + prebuilt client
  + pi install registration).
- CI: resolve PI_STUDIO_REF to a SHA; independent smoke-studio +
  build-variant-studio jobs that gate ONLY the -studio tags, so a studio
  failure never blocks the core :latest release.
- README: 'Using pi-studio' section documenting the container access
  reality — pi-studio hard-binds 127.0.0.1 (index.ts .listen(port,
  '127.0.0.1'), no --host flag), so -p publish alone can't reach it.
  Documents host-networking and loopback-bridge paths, the remote ssh -L
  forward, and the mosh caveat (no port forwarding; run parallel ssh -L).
- CHANGELOG/AGENTS/DOCKER_HUB updated. Will tag as v1.1.0 (minor).

No tag created — stopping for review.
2026-06-10 23:15:29 +02:00
pi c1154f1fa6 v1.0.0: decouple from opencode-devbox
Publish Docker Image / resolve-versions (push) Successful in 5s
Publish Docker Image / base-decide (push) Successful in 12s
Publish Docker Image / build-base (push) Successful in 45m47s
Publish Docker Image / smoke (push) Successful in 8m18s
Publish Docker Image / build-variant (push) Successful in 22m41s
Publish Docker Image / update-description (push) Failing after 9s
Publish Docker Image / promote-base-latest (push) Successful in 14s
Self-contained build chain — own Dockerfile.base + Dockerfile.variant
+ entrypoint scripts + rootfs + CI pipeline. Previously v0.79.0 and
earlier were thin re-brands of opencode-devbox's pi-only variant
(joakimp/pi-devbox:base-pi-only built by opencode-devbox CI).

Architectural changes:
- Replace 5-line Dockerfile shim with full base+variant pair.
- Adapt CI workflow from opencode-devbox/docker-publish-split.yml,
  simplified to a single variant. Includes content-addressed base hash,
  PI_VERSION concrete-resolution to defeat registry-buildcache footgun,
  crane-based base-latest promotion, and the c6f9d11 smoke-test gate.
- pi-devbox releases no longer require rebuilding opencode-devbox first.

Base image additions:
- pandoc, graphviz, imagemagick, yq — broadly useful, ~260 MB total.
- tldr (tealdeer) — Rust port replaces Node tldr global, saves 135 MB.
- /etc/tmux.conf with base-index 0 + pane-base-index 0 — required for
  the planned :latest-studio variant; pi-studio hard-codes :0.0 target.

Smoke test:
- New checks for pandoc, graphviz, imagemagick, yq, tldr, tmux config,
  /tmp/sshcm directory.
- Image-size measurement now sums docker history layers (the prior
  inspect --format='{{.Size}}' returned only the variant-unique layer
  with the new base/variant split, understating by 2+ GB).
- Threshold 2850 → 3500 MB to absorb base additions + arch margin.

Image size:
- Local arm64 build: 3.20 GB. ~390 MB up from prior pi-only equivalent.
- Will tighten threshold once amd64 actuals settle in CI.

Pre-1.0 history preserved at tag pre-v1.0.0-decouple-backup.

Future work:
- v1.1.0: :latest-studio variant (adds pi-studio).
- v1.2.0: :latest-studio-tex variant (adds texlive-xetex for PDF).
- opencode-devbox v2.0.0 will retire INSTALL_PI / pi-only paths.
2026-06-10 01:14:07 +02:00