1c905480e3170eb5a4ef776b585c51693d684484
29 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ecfd2fc2e5 |
feat: bake dig/ldapsearch/xxd and refresh the stale pi-extensions rootfs floor
Two changes that share one forced base rebuild, hence one commit.
1. THREE PACKAGES, each closing a capability gap measured during the
gitea.egl.lan/FreeIPA work on 2026-09-09..10 rather than a preference:
bind9-dnsutils (~6.1 MB measured) -- dig/host/nslookup were ALL absent,
so the container could resolve names but had no way to interrogate a
SPECIFIC nameserver. `getent hosts` only follows the resolver's default
path, so diagnosing "gateway 172.16.88.1 NXDOMAINs the egl.lan zone
while 10.20.253.1 is authoritative for it" had to be hand-rolled in
python3. Split-horizon DNS is a recurring class of bug on this fleet.
Note the package name: plain `dnsutils` is transitional in trixie.
ldap-utils (1244 KB, pulls nothing extra) -- the fleet authenticates
against FreeIPA, yet every LDAP probe had to be run by SSHing to an
already-enrolled host. Simple binds only; GSSAPI would additionally
need krb5-user + libsasl2-modules-gssapi-mit, deliberately not added
as that is a Kerberos-client decision, not a tool.
xxd (198 KB) -- convenience for verifying git-crypt blob magic in
myconfigs; `od -c` from coreutils already does the same job.
netcat-openbsd was in the original proposal and is deliberately NOT
here: measured redundant, because socat is already baked and bash's
/dev/tcp does reachability checks with zero packages (verified against
gitea.egl.lan:3000). Recorded in the Dockerfile so the omission reads
as a decision rather than an oversight.
2. ROOTFS FLOOR REFRESH: rootfs/.../pi-extensions/SKILL.md was 34284 B,
unchanged since
|
||
|
|
f561acc89a |
skills: refresh vendored mempalace snapshot a12fe5e -> e9e09d9, re-pin the canary
Folded into v1.8.13 at zero marginal cost: the snapshot is hashed into
base_tag, but Dockerfile.base already changed this release, so the ~67 min base
rebuild was already being paid. vendor-mempalace-skill.sh --check reported exit
0 (stale-but-truthful) beforehand, so skipping was sanctioned -- this is the
deliberate call the release checklist asks for. Upstream content: the bare
project-name wing convention and the <harness>@<device> added_by rule, both
downstream of the attribution defect measured on this device 2026-09-06.
The canary re-pin matters more than the refresh. Its old pair ("Provenance is
stamped for you" present / "Attribute what you file yourself" absent) still
PASSED against the new snapshot, so leaving it would have yielded a canary
green on both old and new bytes -- blind to exactly the refresh it exists to
witness, the same false-green family as the pre-v1.8.5 canary. New pair chosen
by measuring direction against both files rather than reading the diff
("Diaries self-heal; plain drawers do not" new=1/old=0; "Agent diaries live in"
new=0/old=1), then tested two-sided: PASS on refreshed bytes, FAIL on the old
bytes recovered from git.
Gates after the change: smoke-test.sh parses, vendor --check exit 0,
check-base-hash exit 0.
|
||
|
|
c8622ece9d |
skills: correct the credential-incident-response §5 premise about chroma metadata
§5 said embedding_metadata.string_value holds "metadata fields only". False, measured directly: chroma also stores a copy of the document text there, under key chroma:document. Confirmed with a disposable sentinel drawer (pi@tor-ms22, 2026-08-30): one row in fts_content AND one row in embedding_metadata for the same drawer. This was a real mistake in shipped guidance, not a nitpick: this section's own scanning advice was written to guard against explaining a zero with a mechanism nobody verified from source, and the section itself did exactly that -- I downgraded a census to "a floor" on the strength of a metadata-blind claim I never checked against chroma's actual storage layout. The practical scan order is unchanged (fts_content is still the direct target, raw bytes are still the backstop); only the stated REASON for a metadata zero changes: it needs a different explanation now (key filter, query shape, escaping), not "structurally absent". §6's row-gone/bytes-gone claim is upgraded from asserted to measured, same sentinel: delete_by_source took both fts_content and embedding_metadata 1->0, raw bytes stayed 4->4 (freed pages persist until VACUUM). Also records the method that unblocked the measurement: not a better instrument, a disposable sentinel drawer instead of risking real fleet data. No image behaviour changes. |
||
|
|
58c22afb04 |
skills: the fingerprint advice was missing its precondition, and the skill had no section on proving absence
Docs only; no image behaviour changes. WHY THIS AND NOT A PRIVATE NOTE. pi@emb-7kj4vr4g reported itself for printing sha256[:8] fingerprints of GIT_USER_EMAIL, GIT_USER_NAME and HOST_SSH_USER, and wrote a private rule forbidding it. It had not broken a rule. It followed §2 of this skill as written, and §2 is incomplete: it says a fingerprint lets you compare a credential "without ever materialising the secret" with no condition attached. When two agents independently make the same mistake, the artifact that taught them both is the bug. §2 NOW CARRIES THE PRECONDITION. A fingerprint is 32 bits over its INPUT SPACE, so publishing fp8(x) hands anyone a MEMBERSHIP ORACLE: they can test x == v for every candidate v they can generate. Safe for a 40-char random token; a wordlist for a hostname, username, e-mail, port, path, commit SHA or weak password. "High entropy" is the usual sufficient condition, NOT the test — a commit SHA is 160-bit and still fully enumerable from the repo. Operationally: if you can imagine writing the wordlist, you cannot publish the fingerprint. Also added: candidate fingerprints are working memory and never output (an extractor hashes hostnames and paths too, so the tempting "print what the scanner saw" debug step leaks low-entropy fingerprints wholesale), and a plain statement that a fingerprint register is a CONFIRMATION ORACLE for anyone already holding a candidate corpus — which is exactly how a retired token is identified in old transcripts, and works identically for someone else holding those same files. NEW §6, "Proving absence: instrument strength, and four ways a scan lies clean", placed next to §5 on purpose: §5 optimises against false POSITIVES, and every failure in §6 is a false NEGATIVE. Triage optimises precision, a gate optimises recall, and conflating them is what produced three clean reports over secrets that were really there. Contents: instrument ranking (exact-byte value search > class/structure pass > fingerprint census) with the instruction to state which one produced your zero; census vs class passes as different questions, both failure modes measured on this fleet; the tokenisation trap where quoting alone decided detectability; scan the index or pushed tree, never the working tree; git filters never run on symlinks while check-attr claims they do; two-sided self-tests that abort, incl. the fixture-interaction artifact; row-gone is not bytes-gone. Attribution kept per finding: the census/class split and the instrument ranking's provenance are pi@emb-7kj4vr4g's; exact-byte search over index blobs is pi@tor-ms22's. The credential sense of "census" originated in this skill, not with either agent. TRAP FOR THE NEXT EDITOR, also in the CHANGELOG: the frontmatter description is now 1022 of 1024 characters. Trim before adding, or the skill silently fails to load. Verified by parsing the frontmatter (1022 chars, name intact, every prior trigger phrase retained). Deployment: baked skill -> needs an image rebuild AND a container recreate to reach a running container. |
||
|
|
9b5783f9dd |
skills: a sixth instance, found by the repo owner within the hour
Claimed "## Unreleased is a new convention in this repo" after reading
CHANGELOG.md once — minutes after
|
||
|
|
36e65fe657 |
skills: add credential-incident-response, and assert it stays baked
Carries the facts a two-day credential incident produced, not the discipline: probe the issuer FIRST (11 of 13 "exposed" credentials were already dead at the provider, which cost five HTTP requests to learn and was never checked), the 403-vs-401 trap that scoped tokens introduce into liveness probes, revocation beats deletion for anything already replicated, the three places a secret hides in a Chroma palace (FTS content, metadata, raw bytes) in coverage order, scope derivation from measured consumers, and this fleet's age store with its single-recipient weakness. Facts transfer between sessions; exhortations do not — hence a separate skill for the domain knowledge and a one-line pointer in the always-loaded block. Authored here, so baked is canonical and it is NOT added to skillset-owned.txt. Skill dirs are picked up by a glob in entrypoint-user.sh, so no registration is needed — verified rather than assumed, since an enumerated list would have left the skill inert, a fitting failure given its subject. Three smoke assertions extended so a future rebuild cannot silently drop it. |
||
|
|
f0ebea2d98 |
skills: fix the half of the negative-result rule that was wrong
pi-devbox-environment already warned that "a negative result is usually your
own filter" — baked, symlinked in at every container start, authored by an
earlier session. It survived every recreate, was available all of a later
session, and was violated five times anyway. So the gap was never persistence.
That section also closed with "a positive result needs no such scepticism — it
carries its own evidence." That is false, and it aimed the scepticism budget
one way only. Three of those five errors were positives:
- an SSH handshake SUCCEEDED and greeted me as joakimp while I believed I was
probing gitea.egl.lan — `Host gitea*` had rewritten HostName
- a 401 that was a genuine answer from an issuer which never minted the token
- a "regression" produced by diffing against a value my own -p 2222 flag set
Adds the three missing false-negative rows, replaces the false claim with the
"a positive result only proves what you actually asked" subsection, and records
the two habits that actually caught these: `ssh -G` to learn which rule
captured a hostname, and declaring the expected result before running a check.
The cross-cutting form goes in pi-global-AGENTS.append.md rather than in a
skill, because it has to fire without a task description matching it — being
loadable on demand is exactly what failed. Across all five errors, none was
caught by re-reading my reasoning; every one was caught by a second measurement
that disagreed.
|
||
|
|
495b7e3859 |
vendor: resync mempalace skill snapshot to skillset a12fe5e
scripts/vendor-mempalace-skill.sh, real refresh not --check: skillset
moved 6eb20af -> a12fe5e (mermaid-diagrams cutU normalisation, and the
from_agent identity rule this same release ships in RFC 003). --check
reported stale-but-truthful (exit 0, the sanctioned skip) but this
release's point is getting today's fixes live fleet-wide, and the base
rebuild is already forced by the entrypoint change and the floating
mempalace-toolkit ref moving -- so the incremental cost of also
bumping this pin is zero. Phrase canary in scripts/smoke-test.sh
unaffected: neither pinned phrase ('Provenance is stamped for you'
present, 'Attribute what you file yourself' absent) is in the section
that changed; both verified still correct in the new snapshot.
|
||
|
|
34cf1e3810 |
release: v1.8.8, and a notice that named the wrong remedy
Lint / hadolint (push) Successful in 14s
Lint / actionlint (push) Successful in 17s
Publish Docker Image / resolve-versions (push) Successful in 10s
Publish Docker Image / base-decide (push) Successful in 19s
Publish Docker Image / build-base (push) Successful in 1h3m31s
Publish Docker Image / smoke (push) Successful in 4m47s
Publish Docker Image / smoke-studio (push) Successful in 8m8s
Publish Docker Image / build-variant-studio (push) Successful in 20m4s
Publish Docker Image / build-variant (push) Successful in 26m9s
Publish Docker Image / promote-base-latest (push) Successful in 9s
Publish Docker Image / update-description (push) Successful in 14s
Freezes the v1.8.8 section and clears the two non-code checklist items
pi@emb-7kj4vr4g handed over (evt_20260826T134919_a614ecfc2d4f), plus the two
carried nits from its round-2 verification (evt_20260826T133356_d56792791a49).
Every claim below was re-measured here rather than taken from the handoff.
THE STALENESS NOTICE ASSERTED A DIRECTION IT NEVER TESTED — Blocker 1's shape,
one layer down, in the message I added to replace the message that named the
wrong cause. The notice fired on "recorded != HEAD" and then announced HEAD as
the newer side without testing ancestry, so a clone that was merely BEHIND got
"has moved to 82a8d3c; the snapshot describes the older 5fd0d5c" when 82a8d3c is
5fd0d5c's ANCESTOR. Found by EMB against the real state of its own host, not a
fabrication. The verdict was never wrong (rc 0, nothing mis-verified) but the
remedy it implies is a ~67-minute base rebuild when the actual fix is `git pull`
— the only one of the two carried nits with a price tag, which is why it went
first. Now tests ancestry with the merge-base --is-ancestor primitive the refresh
path 60 lines below already used, and reports three verdicts: stale (refresh),
clone behind (pull, do NOT refresh), diverged (reconcile). All three verified by
execution; only the first was correct before. --help no longer errors on the one
script whose argument order was itself a landmine. The `-s "$VENDORED"` guard is
now commented as load-bearing: it makes the empty-stdin collision unreachable by
construction, which also means no test below exercises it any more, so deleting
it as "redundant with the probes" would silently restore the false OK.
CHANGELOG: retitled, and three stale spots fixed in what becomes the permanent
record. It cited the skill at 82a8d3c (twice superseded); it RE-ASSERTED the
retracted mailbox measurement as live evidence 200 lines after withdrawing it,
which is the exact non-contradiction failure this release exists to fix; and its
warning block still described the pre-e8ddeaf world ("still records c04cd15",
"now exits 1") while quoting as exemplary the very notice whose direction was
unverified. Per EMB's steer the conclusion was kept and only the evidence
replaced: the status filter does drop broadcast noise, it just never computed
owed-ness. Honest replacement, measured on both machines: raw filter returns 2
here and 1 there, EVERY ONE already answered, derivation returns 0 for both.
SNAPSHOT RESYNCED AGAIN, 5fd0d5c -> 6eb20af, because skillset 6eb20af adds the
limit of my own seq ordering test: seq is REPLICA-LOCAL, equal to origin_seq only
because one replica authors for all four machines, so use hlc once mesh_peers
reports a peer. Recorded as reasoning not measurement — a second replica cannot
be stood up here. The durable half is the asymmetry: seq skew makes an ANSWERED
item resurface (noise, visible, self-correcting) while created_at SUPPRESSES AN
UNANSWERED ask forever (silent, permanent), so the skill now says outright that
"fixing" a resurfacing item with a timestamp trades the safe failure for the
dangerous one. The resync was free: rootfs/ was already changing, so the base
rebuild was forced regardless — the ordering warning about accidental staleness
does not apply to a deliberate refresh before the tag.
--check is a clean OK at 6eb20af with no notice, canary re-verified bidirectionally
(present 3, withdrawn 0), baked snapshot 0644, tree hash recomputed at build time
and re-verified in-container. bash -n clean; shellcheck/hadolint/actionlint remain
absent locally, so CI is still the only evidence for those.
|
||
|
|
e8ddeaf89f |
skills: a gate that could pass without checking, and a mailbox that never empties
Fixes the three blockers and seven should-fixes from pi@emb-7kj4vr4g's review (logstream correlation skills-provenance-review, full text in drawer_pi-devbox_reviews_e43e766641c9ec85217bc6ce). Every finding was reproduced by execution here before being fixed; two were refined by that reproduction rather than taken as given. BLOCKER 1 — the provenance gate could print OK and exit 0 without verifying. `git show <ref>:<path> | sha256sum` hashes EMPTY STDIN when the ref does not resolve, so at_ref was never empty and the UNKNOWN branch was dead code. Measured: a bogus ref reported MISMATCH — accusing the snapshot of lying when the real cause was an incomplete clone, and the operator's natural remedy for MISMATCH is to re-run the refresh, which rewrites provenance to silence the complaint; and with a 0-byte snapshot against a 0-byte upstream file it printed "OK: exactly skillset@aaaaaaa" with exit 0 for a ref that does not exist. The script already had the sha_empty idiom and had applied it to blob_sha but not to at_ref. Existence is now PROVEN with git cat-file -e before anything is hashed, at two levels (ref resolves / path exists at it) because those deserve different messages. Same defect class as the canary it replaces: a check that can succeed without checking. A second, unflagged instance of the same pipeline shape in blob_sha was found and fixed too. Exit codes split, because the old contract failed the sanctioned case: 0 truthful (including stale, with a NOTICE), 1 a lying record only, 2 cannot determine. AGENTS.md step 2 promised "the message distinguishes the two" and was the thing this branch was breaking; rewritten to state all three. BLOCKER 3 — VENDORED.md contradicted itself in the release whose stated invariant is non-contradiction: its hand-maintained provenance line named skillset 670f7f1, seven commits behind the ARG and itself the commit that told agents to hand-stamp added_by — the withdrawn instruction this work exists to stop shipping — while its cp recipe contradicted the "not cp" rule 20 lines above. Line removed (nothing forced it to move when the ARGs did); 670f7f1 kept only as a labelled cautionary example. The pi-extensions half was verified redundant (CI require_sha resolves PI_EXTENSIONS_REF) before removal. SHOULD-FIXES: `<root> --check`, the spelling VENDORED.md documented, silently ran a REFRESH because only $1 was parsed (both tools now parse all args and reject unknown ones); refresh at a detached/older HEAD silently rewound ref and bytes (now refused unless the recorded ref is an ancestor, --force to override); upstream_dirty was computed and never used in check mode; --no-skills --json printed human text and broke jq; --help was a hardcoded sed range this branch had already made stale; the fingerprint hashed SKILL.md alone so a live skill differing only in a sibling file reported "identical", and pi-extensions already ships two files, so it is now a per-skill TREE hash with the manifest field renamed skillset_snapshot_tree_sha256; the --no-skills smoke assertion was negative-only and passed on a crashed binary. mktemp+mv left files 0600 — CI was unaffected since the index records 100644, so the blast radius was local builds only, narrower than the review inferred. Snapshot resynced c04cd15 -> 5fd0d5c so the no-clone fallback carries the CORRECTED coordination protocol rather than the withdrawn one; --check is now OK with no staleness notice, and the bidirectional canary re-verified against the new bytes. Local validation is bash -n only (shellcheck, hadolint and actionlint are all absent in this container) — CI remains the shellcheck gate. |
||
|
|
e070e0bcbf |
skills: record the vendored snapshot's provenance, and report which copy wins
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.
|
||
|
|
dbb78798fb |
vendor: resync mempalace skill snapshot to skillset c04cd15
c04cd15 ('the withdrawal only holds where the bridge is live') landed after the
v1.8.7 snapshot was taken, so the baked fallback was already 4 lines behind the
skillset within hours of publishing. It adds the caveat this fleet is currently
living in: the bridge is baked at image build, so a container on an image older
than the stamping commit satisfies both env gates while stamping nothing, and
hand-stamping is still the only signal a hand-filed drawer gets there. It also
gives the one-line test —
grep -c MEMPALACE_PI_DEVICE "$(readlink -f ~/.pi/agent/extensions/mempalace.ts)"
which returns 0 on this v1.8.6 container, confirming the gap empirically.
Note what this instance proves about the canary fixed one commit ago: it still
PASSES on the refreshed copy, because both pinned phrases survived the edit. A
phrase canary cannot detect 'older than skillset main' — only a diff can. This is
the second drift in 24h and is the argument for the Still-open item (a CI job
diffing this file against the skillset repo, blocked on a clone credential for a
private repo). No re-pin was needed here.
|
||
|
|
657b1ad856 |
ssh sidecar: default to multiplexing, as a default and not an override
A target whose ~/.ssh/config entry never mentioned ControlMaster got no
multiplexing from the sidecar (only ControlPath was supplied), so every ssh call
opened a fresh TCP connection. On 2026-08-25 that produced ~12 connections to
one host in 15 min and a fail2ban block that looked like an outage — the tell
being that HTTPS to the same estate stayed healthy.
The correctness of this depends entirely on WHERE the block goes. ssh_config is
first-value-wins:
ControlPath before the Include -> override (the user's value points at
read-only ~/.ssh and cannot work in the container)
ControlMaster after the Include -> default (an explicit per-host
'ControlMaster no' must keep winning)
Force what is broken, default what is merely absent. The first draft put both in
the leading block and would have silently overridden an explicit 'no'.
Verified with ssh -G rather than from the man page, including the counterfactual:
under the shipped layout an explicit 'no' resolves to controlmaster false while a
silent host resolves to auto; under the rejected layout the 'no' host flips to
auto. So the test discriminates position, not presence. Plus a sandbox render of
the real script, bash -n, and shellcheck -S error (the v1.8.7 gate) clean.
Effect measured on 41 real host aliases: 22 silent entries gain auto+10m, 0
overridden. Note the fleet's one deliberate opt-out is written as absence plus a
comment ('# No ControlMaster — VPN means direct route'), which ssh cannot
distinguish from no opinion; that host now multiplexes, which its own comment
says is unnecessary rather than harmful.
Skill documents the sidecar-vs-~/.ssh trap (the failure misleads: read-only
ControlPath makes multiplexing look impossible rather than misconfigured) and
the stale-master recovery, ssh -O check / -O exit.
|
||
|
|
4f1aa0d0dd |
skills: refresh the vendored mempalace snapshot (withdrawn hand-stamping)
VENDORED.md's freshness model for `mempalace` is "Option 2 only — refreshed
manually per release", and it had drifted since 2026-08-23. The stale snapshot
still carried the instruction to hand-stamp added_by="<harness>@<device>", which
skillset 73c7c8e withdrew: the pi bridge now stamps at the edge
(mempalace-toolkit 553d865), and RFC 001 §7.3.2 ranks agent-side stamping ❌
worst-possible.
That matters specifically for the fallback case this snapshot exists to serve — a
container started WITHOUT the private skillset mounted would otherwise be the
only kind of container still being taught to do it by hand.
|
||
|
|
b5810654f6 |
skills: let the skillset own the skills it owns, and stop a dangling link from killing boot
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. |
||
|
|
fbc1f86612 |
docs: a negative result is usually your own filter (skill + AGENTS.md)
Three false negatives in one session, all self-inflicted, all convincing because the command "succeeded": a `| head -20` proved an SSH peer absent that sits at line 454 of a ~500-line config; `ssh mac 'docker ps'` proved the host had no Docker, when the non-interactive PATH simply lacks /usr/local/bin; and `grep 'ssh '` proved no ControlMaster was running, when those processes rename themselves to `ssh: <path> [mux]`. Same root cause each time, so it goes in the skill rather than in a commit message: a positive result carries its own evidence, absence has to be earned. The skill (rootfs/, symlinked into ~/.agents/skills) is BAKED, so this is an image change and is logged in CHANGELOG Unreleased accordingly. Its §3 also now records that a live ControlMaster socket makes later commands authenticate not at all -- after editing a peer's authorized_keys, "it still works" proves nothing; prove it with -o ControlPath=none, or the breakage waits for a future session that has no memory of the edit. AGENTS.md: corrected a stale CI claim while placing the pointer. It said a tag push produces two runs including lint; lint.yml has since been scoped to branches: ['**'], which excludes tag refs, and refs/tags/v1.8.4 duly produced run 571 (publish) and nothing else. Kept the head_sha + workflow path filter advice, which is cheap and guards against a future v*-triggered workflow. Added a short section on verifying this repo from inside a container, including that docker-compose.yml here is a TEMPLATE pinning :latest while a real host runs its own per-machine file -- recreating from the repo copy can silently move a host off :latest-studio. Placement note: AGENTS.md is only auto-read when the cwd is this repo, so the durable rule lives in the skill, which loads by description match in any pi-devbox session. |
||
|
|
3a509077c2 |
v1.8.3: mempalace 3.7.1, refreshed skill snapshot, census on PATH
Lint / hadolint (push) Successful in 8s
Lint / actionlint (push) Successful in 23s
Publish Docker Image / resolve-versions (push) Successful in 13s
Publish Docker Image / base-decide (push) Successful in 11s
Publish Docker Image / build-base (push) Successful in 53m49s
Publish Docker Image / smoke (push) Successful in 7m22s
Publish Docker Image / smoke-studio (push) Successful in 18m6s
Publish Docker Image / build-variant (push) Successful in 19m6s
Publish Docker Image / update-description (push) Successful in 6s
Publish Docker Image / promote-base-latest (push) Successful in 11s
Publish Docker Image / build-variant-studio (push) Successful in 27m5s
mempalace 3.6.0 -> 3.7.1. Verified against the 3.7.1 source rather than its
changelog, because the risk lands on palaces users cannot reconstruct: legacy
drawers lack the new chunk_total marker and both decision sites trust them, so
no mass re-mine; NORMALIZE_VERSION is 2 in both; chromadb stays <2 so no
index-format migration; no auto-migration exists; logstream.sqlite3 is created
lazily. Downgrade remains possible (3.6.0 has zero references to chunk_total).
Two behaviour changes documented in the CHANGELOG: ALLOW_PEER_WRITER no longer
works on local/chroma palaces, and writer-lock setup failures fail closed.
Neither affects this image's MCP-server-plus-CLI-feeder pattern, which already
serialised on the same lock under 3.6.0 -- the upstream "process-lifetime
single-writer" entry describes tightened escape hatches, not a new lease.
The motivation is the shared central palace: 3.7.1 drops the stale chromadb
SharedSystemClient cache on reconnect (3.6.0 could let a stale in-memory HNSW
segment overwrite a peer's writes, "index count going backwards"), releases the
writer lease on SIGTERM/SIGHUP, and stops treating an interrupted mine as
complete. The fleet primary was upgraded to 3.7.1 and restarted before this tag,
because 3.7.1 refuses writes when the served library drifts and reconnect cannot
clear that. opencode-devbox still pins 3.6.0, so the lockstep is broken until it
cuts its own release.
Vendored mempalace skill snapshot refreshed to skillset 936fed8 (was 63f3bf5).
This closes a gap that had been invisible for two commits: ~/.agents/skills/
mempalace symlinks to the IMAGE-BAKED copy, entrypoint-user.sh creates that link
first, and the skillset deploy never clobbers an existing name -- so in a devbox
container the vendored snapshot always wins and editing skillset alone changes
nothing a container reads. Brings the multi-machine shared-palace section and
the hand-crafted-provenance guard.
pi-global-AGENTS.append.md already carried the three shared-palace damage rules
(
|
||
|
|
a55f6369b3 |
AGENTS.md: three damage-prevention rules for a shared central palace
The managed block already tells agents to load the mempalace skill, but said nothing about the palace being shared with other machines. Three failure modes observed tonight while onboarding tor-ms22's feeders, all of which do damage rather than merely confuse: - `mempalace sync` prunes drawers whose source files look gitignored, deleted or moved. On a central palace that describes most of the content, including every other machine's. Compounded by RFC-001 7.2: feeders now stage inside the palace root, so a scoped sync can delete the drawers it just filed. - A client-side timeout is not a failure. The palace is single-writer and one large mine blocks every client for minutes, so `[mempalace ext] feed (tick) failed: mine timed out after 30000ms` usually means the mine COMPLETED. Verified: drawers from a timed-out tick were present 35 s after the client gave up. A blind retry files a duplicate. - The `mempalace` CLI has zero references to MEMPALACE_REMOTE_URL, so it always opens a palace on local disk and can silently disagree with the MCP tools. Orientation depth stays in the skill; only damage-prevention belongs here, because this file is always read and the skill's later sections often are not. |
||
|
|
fa04d2083d |
docs(rootfs): re-sync pi-extensions skill snapshot (fork boundary mechanism)
Vendored floor snapshot re-synced from pi-extensions 98eb07b, which documents the mechanism behind fork boundary violations (full parent-transcript inheritance via index.ts:47) plus the corrected claims about tool restriction and narrative invention. CI resolves PI_EXTENSIONS_REF from main HEAD, so a normal build ships the package-owned copy; this keeps the committed floor identical so scripts/smoke-test.sh's cmp assertion holds either way. |
||
|
|
209f2c2f67 |
docs(rootfs): re-sync mempalace snapshot from skillset 63f3bf5
Closes the drift found in 4d4abd9's investigation: the Temporal grounding
guidance was authored straight into this vendored fallback (
|
||
|
|
4d4abd9a9f |
skill(pi-devbox-environment): resolve a skill symlink before editing it
~/.agents/skills/ lives in the ephemeral container layer and is rebuilt by
entrypoint-user.sh on every start from two sources, so an edit made through the
symlink may vanish on the next recreate. Adds to §1 (persistence tiers):
- `readlink -f ~/.agents/skills/<name>` as the first move, with a tier table:
resolves under /workspace/skillset → edit in place; resolves under
/usr/local/share/pi-devbox/skills → image layer, edit the canonical repo and
`sudo cp` to activate for the running session.
- Canonical owner per baked skill (pi-devbox-environment → this repo;
pi-extensions → the package repo's skill/, plus this repo's floor snapshot;
mempalace → the private skillset repo), pointing at VENDORED.md as
authoritative.
- The shadowing gotcha: image-baked links are created first and only when
absent, and deploy-skills.sh --prune-stale leaves foreign links alone, so for
a name present in BOTH sources the image copy wins and a skillset edit has no
effect in the container. Documented with the live example found while writing
this: the baked mempalace snapshot carries a Temporal grounding section
(
|
||
|
|
d5c5da3f6c |
docs(rootfs): refresh vendored pi-extensions skill snapshot
Sync the image-baked "floor" copy at
rootfs/usr/local/share/pi-devbox/skills/pi-extensions/SKILL.md with
pi-extensions e73cb9f, which documents package-registration forensics
(packages[] vs whole-file grep), that /reload suffices for a newly installed
package, and a fork-output caveat — the findings from the fork-guard bug fixed
in
|
||
|
|
6625d66f3a |
docs(agents): point the global AGENTS.md managed block at agent-browser
Discoverability follow-up to baking agent-browser into the base: agents won't reach for a browser they don't know they have (the exact gap that made this capability easy to miss). Add a short pointer section to the pi-devbox managed block — capability, the preset AGENT_BROWSER_EXECUTABLE_PATH, and `agent-browser skills get core --full` for the command set. Pointer only; depth stays in the skill. rootfs change → folds into the same base-<hash> rebuild. CHANGELOG note updated. |
||
|
|
71b12a9ed4 |
docs(skill): note macOS NFD-filename gotcha for dscp/scp
Accented filenames on a macOS host are stored decomposed (NFD), so a precomposed (NFC) remote path in scp/dscp silently fails with 'No such file or directory'. Document the wildcard / list-first workaround in the pi-devbox-environment skill, next to the dssh/dscp alias table. (Hit while copying a screenshot named 'Skärmavbild ….png' from the host.) |
||
|
|
8c27894cf2 |
base: support modern terminals (ncurses-term + xterm-ghostty alias)
The base only shipped ncurses-base (xterm-256color, tmux), so SSHing in from WezTerm/Alacritty/foot/Ghostty degraded to a dumb TERM fallback. Following the maintainer's ansible common role: - Dockerfile.base: install ncurses-term (terminfo for wezterm, alacritty, foot, st, base ghostty entry, and many more). - rootfs/.../terminfo-src/ghostty.terminfo: thin xterm-ghostty alias (use=ghostty) — Ghostty connects as TERM=xterm-ghostty, which no distro packages. Compiled into the system db with 'tic -x'; build asserts it resolved via infocmp. - kitty (xterm-kitty) already covered by kitty-terminfo; iTerm2 uses xterm-256color (ncurses-base). - smoke-test: assert ncurses-term emulators + xterm-ghostty alias resolve. Validated end-to-end in a throwaway container: ncurses-term brings the entries, the alias compiles and equals the ghostty capability set. hadolint clean, bash -n OK. Base-affecting, rebuilds base-<hash>. No tag. |
||
|
|
904fe85249 |
skill(mempalace): teach temporal grounding (recreate != new day)
Baked mempalace SKILL.md now instructs agents to establish current date/time and compute the delta against the actual diary/drawer timestamp before using relative terms (yesterday/last week), and explicitly that a container recreate or fresh session is NOT a day boundary (pi-devbox restarts several times a day). Phase 1 wake-up section + anti-pattern bullet. CHANGELOG Unreleased. |
||
|
|
7551947466 |
feat(skills): add mempalace proactive-load directive for containers
Baking the mempalace fallback skill fixed *availability*, but mempalace had no proactive-load directive anywhere (pi-toolkit's global AGENTS.md only points to pi-extensions), so a new container would still surface it only via description-matching — the same under-utilisation the pi-extensions directive was created to fix. Add a session-start pointer to the pi-devbox managed AGENTS.md block (pi-global-AGENTS.append.md): gated to pi-devbox containers and conditional on the MemPalace MCP tools being present. Memory continuity matters most in a frequently-recreated container — the palace is its only cross-recreate memory. - pi-global-AGENTS.append.md: '## Session start: load the mempalace skill'. - smoke-test: assert the pointer merges into the global AGENTS.md at build. - docs: VENDORED.md, README, CHANGELOG [Unreleased]. Now both skills are complete in pi-devbox: directive + skill file. pi-extensions = directive (pi-toolkit) + baked skill; mempalace = directive (this block) + baked skill. |
||
|
|
a7d6a7d235 |
feat(skills): bake pi-extensions + mempalace fallback skills
The pi-toolkit global AGENTS.md tells every pi session to read
~/.agents/skills/pi-extensions/SKILL.md at start (the fork/recall
under-utilisation fix), but that skill lived only in the private skillset
repo — so the pointer dangled in any container started without skillset
mounted. Bake fallbacks so the pointer always resolves.
- pi-extensions (Option 1 + Option 2, layered):
* Canonical skill promoted to the public pi-extensions package repo under
skill/ (separate commit there); co-located with the code it documents.
* rootfs/ carries a committed snapshot (the floor).
* Dockerfile.variant copies /opt/pi-extensions/skill/ over the snapshot
after the pinned clone, so a normal build ships the fresh package copy
(recorded via PI_EXTENSIONS_REF) and an old-ref/mirror build still ships
the snapshot. Helper evaluate-extension-usage.py travels with it.
- mempalace (Option 2 only): snapshot in rootfs/. Its consumer skill has no
public package home (mempalace-toolkit ships a different skill,
opencode-mempalace-bridge), so no build-time refresh.
- entrypoint links both (only-when-absent; mounted skillset still wins).
- smoke-test: build-time presence + package-match check + runtime symlink
assertions; readiness gate now waits on the last-linked skill.
- docs: skills/VENDORED.md (provenance + refresh), README, AGENTS.md,
CHANGELOG [Unreleased].
Note: shipped in the NEXT release; v1.2.0 (run 409) predates this.
|
||
|
|
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). |