This file tracks repository-level (infrastructure, workflows, governance, cross-cutting documentation) changes.
Application-specific changes live in per-directory CHANGELOGs. See the index below.
Format: Keep a Changelog 1.1
Versioning: Per VERSIONING_WEOWNVER.md β vSEASON.MONTH.WEEK.ITERATION
| Component | Changelog |
|---|---|
| AnythingLLM | anythingllm/CHANGELOG.md |
| Gitea runner Docker | gitea-runner-docker/template/CHANGELOG.md.jinja |
| Landing/Purchase | landing-purchase/CHANGELOG.md |
| Matomo | matomo/CHANGELOG.md |
| n8n | n8n/CHANGELOG.md |
| Nextcloud | nextcloud/CHANGELOG.md |
| Sandbox Docker | sandbox-docker/template/CHANGELOG.md.jinja |
| SearXNG Docker | searxng-docker/template/CHANGELOG.md.jinja |
| SigNoz Docker | signoz-docker/template/CHANGELOG.md.jinja |
| Vaultwarden | vaultwarden/CHANGELOG.md |
| WordPress | wordpress/CHANGELOG.md |
| WordPress Dev | wordpress-dev/docs/CHANGELOG.md |
Changes in this section will be promoted to a dated release entry on merge to main.
-
Fixes that missed #320's merge (2026-10-12, #323).
anythingllm-dockerembed-filter: a HEAD, 204, 205 or 304 withapplication/jsonpasses through instead of becoming a 502, and an empty 200 still fails closed.scripts/publish_to_buzz.py: hostname redaction is case-insensitive, soexample.XN--P1AIno longer leaves<host>--P1AIin a public note.beta-weown-chatgets the embed-filter change on its next deploy; Buzz reports on their next run. -
anythingllm-docker β a fresh wrap token is never ignored (2026-10-12, weown-fleet#168). The openbao deploy's host unwrap used the token in
.bao-wrap.tokenwhenever that file was non-empty. After a failed unwrap the file kept the spent token, so every laterBAO_WRAP_TOKENwas ignored and the deploy failed until someone blanked the file by hand. A supplied token now always replaces the file and always rotates the secret-id, even one that still works; a failed unwrap blanks the file, and if the current secret-id works the deploy warns and keeps it. Template and thebeta-weown-chatrender; applies on that instance's next deploy. Two deploys of one instance are serialized by a lock held from the validity check through the restarts; overlapping rotations could unwrap each other's token and leave one live. Test:anythingllm-docker/tests/deploy-unwrap-check.sh, 40 checks against a real OpenBao dev server;main's task fails the stale-token and rotation checks. -
Folded fixes from the
ai-grokseat (2026-10-12, #320). Six stranded PRs (#286β#289, #299, #300) and the redaction fix onmerge/nik-mac(#309), cherry-picked onto currentmain:- billing: an OIDC login links by
subor verified e-mail only, never onto staff - anythingllm-docker: MCP children get an env allowlist; embed-filter strips reasoning from JSON chat history and fails closed (502) on a body over 16 MiB or one cut short
- Node 24 LTS pinned by digest, with a CI check
- Matomo and Nextcloud securityContext (weown-fleet#102)
((X++))underset -e(weown-fleet#148)- IP addresses and dotted hostnames redacted from Buzz notes (weown-fleet#128)
Fixed in review: Matomo's init container ran as root with every capability dropped, so its
chownand config edits failed silently; it now runs as the pod's non-root user 33. The Nextcloud cron job ran as root, whichcron.phprefuses; it now runs as 33. Two of the source branches predated weown-fleet#48 and would have reverted the tenant document scope; the fold keeps it. The CI-flow change from the same seat (fca598a4) is not adopted: the current auto-PR behaviour stays. Live renders change on their next deploy (beta-weown-chat,sites/billing); the helm charts change on their next upgrade. - billing: an OIDC login links by
-
gitea-docker β hardening (2026-10-09, weown-fleet#163 group 6). Caddy serves TLS 1.3 only.
ssh_source_cidrs, which opens the admin sshd, has no default (it was the whole internet) and every entry must be a /24 or narrower (IPv6 /64), thegitea-runner-dockerrule; it is no longer a copier answer, so an admin CIDR never lands in a committed render.ansible/deploy.ymlrefuses a box that keeps Gitea volumes under another project name, so a mismatched render cannot start Gitea on new, empty volumes. Applies to the live forge on its next deploy (Caddyfile, play) and plan (the variable; the live value already passes). -
gitea-docker β manual MI rotation refreshes the containers' auth copy (2026-10-08, weown-fleet#163).
scripts/rotate-mi-manual.shswapped v2 into the host's auth file but not the copy the containers read (written by the deploy). Revoking v1, the next step of the runbook, would then leave any container that restarts before the next deploy unable to log in to Infisical. The swap now refreshes that copy too, keeping its owner and mode. Found while rotating the live forge, whose v1 bootstrap secret was confirmed still valid by--verify. -
gitea-docker β pinned installs (2026-10-09, weown-fleet#163 group 3). Cloud-init installs Docker from its signed apt repository (fingerprint-checked key, pinned and held versions) instead of
get.docker.com, and the Infisical CLI from a sha256-pinned release.debinstead ofcurl | bash, asgitea-runner-dockerdoes. Theawscliapt package, which does not exist on Ubuntu 24.04 and failed the whole package step, is removed. Ansible collections are exact pins. Applies on a rebuild (cloud-init) and the next collection install. -
gitea-docker β bootstrap rotation marks done only after v1 is proven revoked (2026-10-08, weown-fleet#163). The first-boot Machine Identity rotation wrote
.rotation-completeeven when v1 could not be identified or its revoke was not confirmed, and it took "the oldest active secret that is not v2" for v1, so with another older secret active it revoked the wrong one and left v1, which sits in terraform state and droplet user_data, valid. Thegitea-runner-dockerrotation (#302) is ported to the template and the live forge render: the marker records the proof (v1-401 <time>) only when a login with the bootstrap client id and secret from user_data answers 401 "Invalid credentials",ansible/deploy.ymlrefuses to deploy without that proof, andscripts/rotate-mi-manual.shplus a README section (the failure log pointed at one that did not exist) finish it by hand. Cloud-init changes apply on a rebuild. The deploy gate applies on the forge's next deploy: the empty marker the old script wrote no longer passes, so the operator runsrotate-mi-manual.sh --verifyonce first, which proves v1 dead there or says it is still alive. Verification:gitea-docker/tests/rotation-check.sh gitea-docker/sites/gitandgitea-docker/tests/manual-rotation-check.sh gitea-docker/sites/git. -
gitea-docker β secrets off command lines and backups owner-only (2026-10-08, weown-fleet#163 group 1).
backup.shpassed the database password todocker exec -e, visible inpsfor the whole dump: it now reachespg_dumpon stdin, and the backup runs underumask 077in a 0700 directory (dumps and archives were 0644). The first-boot rotation script put the Machine Identity secret and bearer tokens on curl's argv: logins now send JSON on stdin (--data @-) and tokens go in a curl config on stdin (-K -). The README's OAuth setup now prefers the web UI and feeds the CLI secret on stdin, saying plainly what remains on argv. The Quick Start usesitofu.sh(secrets from Infisical at run time);init.sh, which reads credentials from a localterraform.tfvars, warns that it is deprecated. Also removed one em-dash from the template's cloud-init (DigitalOcean cloud-init must be ASCII-only). Applies to the live forge on its next deploy (backup.sh) or rebuild (cloud-init). -
gitea-docker β consistent backups, safe and recoverable restores (2026-10-09, weown-fleet#163 group 4).
backup.shstops Gitea while it dumps the database and copies the data volume, so the two match; a trap restarts it.restore.shaccepts only a namebackup.shproduces and passes it to the root-run program as an argument (it was spliced into the program text), in amktempfile instead of a fixed/tmppath. The archive is checked while Gitea still serves; after the stop the current database and volumes are copied, and a restore that fails part-way loads that copy back and restarts Gitea.psqlstops on the first SQL error. Retention keeps a backup whose date does not parse (it was deleted as epoch 0); off-box copies get the same policy, report-only untilREMOTE_RETENTION=enforce. Applies to the live forge on its next deploy. -
gitea-docker β AWS CLI pinned (2026-10-09, weown-fleet#163 group 3). The deploy play installs AWS CLI 2.37.12 from AWS's versioned zip, checked against a recorded sha256 (AWS's PGP signature, key
FB5D B77F D5C1 18B8 0511 ADA8 A631 0ACC 4672 475C, verified when the pin was taken), instead of whatever the unversioned zip served, unchecked. A box on another version is moved to the pin on its next deploy. The other 18 unpinned AWS CLI installs (anythingllm, billing, keycloak, openclaw, sandbox, supabase) are listed on weown-fleet#164. -
gitea-docker/sites/git β the live Gitea render is committed (2026-10-08, weown-fleet#159). The forge's own render had only ever existed as an untracked directory in an operator checkout, so its infrastructure (firewall included) ran from code no PR had reviewed. It is now the canonical committed render, with two changes from the copy that built the box: the dormant
10.0.0.0/8 β 5432firewall rule is removed (as for the templates in #303), and the monitoringalert_emaildefault is the role addressalerts@weown.netinstead of a personal address from another engagement. The nexttofu applychanges both on the live firewall and monitors; the operator runs it. -
billing-docker β backups and restores keep secrets off command lines and files owner-only (2026-10-12, weown-fleet#166 review). For both backends:
backup.shpassed the database password todocker exec -e PGPASSWORD=..., so it was in the docker client's argv for the whole dump. It now reachespg_dumpon stdin, and backups run underumask 077(archives were 0644).restore.shremote mode wrote its program to a fixed/tmp/weown-restore-cmds.sh; it now uses amktempfile removed by an EXIT trap. TheAWS S3remote-storage choice is removed, becausebackup.shnever uploaded to it: ans3render made only a local archive while the cron reported success. On anopenbaorender, a login the store refuses (HTTP 400 or 403) is not retried. It is recorded in/opt/<project>/bao-state/login-refused, and no consumer logs in again until a deploy clears it. Before, provisioning_watch alone would have used up the shared role's lockout budget in five ticks. ABAO_WRAP_TOKENsupplied to a deploy now always rotates the secret-id, even one that still works; if its unwrap fails, the deploy warns and keeps the working one. The old secret-id still logs in until a secrets-admin destroys it (the box's AppRole cannot), so the deploy prints the new accessor and the README gives the destroy step.backup.shalso tightens archives an earlier version left 0644.restore.sh(both backends) accepts only a backup name of the shapebackup.shproduces, or ans3://URL in this site's own location, and checks it again inside the program it runs as root; a quote in the name was spliced into that program. Thebackup.shandrestore.shfixes are also applied to the livesites/billingrender (re-rendered with its name and domain; the diff is only these lines):backup.shreaches the box on its next deploy, andrestore.shruns from the operator's checkout. Tests:backup-bao-check.sh30 checks,deploy-unwrap-check.sh33,entrypoint-bao-check.sh36; each new check fails on the head before it. -
billing-docker β OpenBao backend, stage 5: the runbook (2026-10-12, weown-fleet#166). An
openbaorender's README covers the steps before a first deploy (mint the AppRole, seed the document with the listed keys, open the store's 8200 allowlist to the droplet, keep a break-glass copy), the deploy with a hidden wrap-token prompt, the backup that must be seen landing afterwards, changing a value withbao kv patch KEY=-(value on stdin; checked against a dev server), and what to do after the store refuses the secret-id. It also says that POSTGRES_* only seed an empty database: rotating the password starts in Postgres. TheinfisicalREADME is unchanged. -
billing-docker β OpenBao backend, stage 4: backups and restores read OpenBao (2026-10-12, weown-fleet#166). Billing holds customer and payment data, so an openbao box never runs without backups. A new
scripts/bao-run.sh(openbao renders only) runs a command on the host with the backup keys (POSTGRES_*, plusSPACES_*with DO Spaces) loaded throughentrypoint-bao.sh, the wrapper the containers use. The daily backup cron,backup.sh's remote mode andrestore.sh's remote mode run through it. A missing key or a refused login stops the run beforebackup.shstarts, after one login attempt and with no cooldown sleep, so the cron fails at once and adds one failed login, not two, to the lockout count of the role db and web share. The provisioning_watch cron also makes one login attempt per tick. The stage-3 refusal is removed: an openbao deploy now installs the backup cron. Aninfisicalrender is byte-identical to before (CHANGELOG aside). Test:billing-docker/tests/backup-bao-check.sh, 14 checks against a real OpenBao dev server; removing the one-attempt setting fails it. -
billing-docker β OpenBao backend, stage 3: the deploy playbook (2026-10-11, weown-fleet#166). An
openbaorender'sansible/deploy.ymlinstalls a pinned bao CLI, places the store CA, fills the store URL into the entrypoint on the box fromBAO_ADDR_INSTANCE, probes the store, and unwraps the AppRole secret-id on the host fromBAO_WRAP_TOKENon stdin, restartingdbandwebwhen it is replaced. Infisical-only tasks (MI checks, rotation proof, registry login, container auth file, Infisical backup cron) are skipped;scripts/deploy.shasks forBAO_ADDR_INSTANCEinstead ofINFISICAL_PROJECT_ID. The bao CLI step checks/usr/bin/bao(the path compose mounts) at the pinned version, so a pin bump upgrades the box and a bao elsewhere onPATHdoes not count. Fixed while porting from anythingllm-docker: after a failed unwrap the spent token stayed in.bao-wrap.tokenand was preferred over every freshBAO_WRAP_TOKEN. The wrap token now never touches disk: it goes from stdin straight intobao unwrapand is unset whether the unwrap works or not, and a.bao-wrap.tokenleft by the earlier logic is removed.bao_cli_versionmust be a plain release version, since it is rendered into shell. Until stage 4 the openbao deploy refused before touching the box, because billing never runs without backups. Aninfisicalrender is byte-identical to before. Test:billing-docker/tests/deploy-unwrap-check.sh, 24 checks against a real OpenBao dev server (the three no-file checks fail on the earlier logic). -
billing-docker β OpenBao backend, stage 2: containers, cloud-init, terraform (2026-10-10, weown-fleet#166). An
openbaorender wiresdb(--keys) andweb(--document) to the stage-1 entrypoint, drops every Infisical step from cloud-init, tags the dropletopenbao-consumerand stops injecting a per-site Infisical folder initofu.sh; compose comments name OpenBao, not Infisical, as the store. Both backends now tag the dropletbillinginstead of thekeycloakandssotags copied from keycloak-docker, which put billing in every SSO-widescripts/manage-droplets.shrun; apart from that tag line theinfisicalrender is byte-identical to main. The livesites/billingrender is not re-rendered here and keeps the old tags (weown-fleet#167). -
billing-docker β OpenBao backend, stage 1: container entrypoint (2026-10-09, weown-fleet#166). Wave 2 of the Infisical β OpenBao move starts with billing (the forge is held by the custody gate, WeOwnCloud/openbao#16). The template gains a
secret_backendchoice (defaultinfisical; the render is unchanged) and an OpenBao entrypoint that works inpostgres:16and the Python web image (AnythingLLM's needs node). Compose, deploy, backup and restore wiring follow in later stages; no live values move until Nik's go. Verification:billing-docker/tests/entrypoint-bao-check.shruns it against a real OpenBao 2.7.0 dev server (32 checks). -
billing-docker β bootstrap rotation marks done only after v1 is proven revoked (2026-10-09, weown-fleet#164). The
gitea-dockerfix (#307, #308) ported to the billing template and the livesites/billingrender: v1 is proven dead by a login that answers 401 "Invalid credentials" before.rotation-completeis written (v1-401 <time>); the old script took "the oldest active secret that is not v2" for v1 and wrote the marker regardless; secrets leave curl's argv; runs are serialized.ansible/deploy.ymlgates on the proof andscripts/rotate-mi-manual.shfinishes by hand. Cloud-init applies on a rebuild; the gate applies on the next deploy, which stops untilrotate-mi-manual.sh --verifyproves v1 dead on the live box. Verification:gitea-docker/tests/rotation-check.sh billing-docker/sites/billingandgitea-docker/tests/manual-rotation-check.sh billing-docker/sites/billing. -
anythingllm-docker β dashboard delete purged nothing, and could purge shared files once fixed (2026-10-08, weown-fleet#157).
remove-documentsreceived an empty body (a DELETE withoutContent-Length), so "deleted" documents stayed in storage while the dashboard reported success. Fixed with explicit body framing, a purge only for files inside the tenant's folder (detach-only for workspace documents stored elsewhere), and a 502 when the purge is refused; replace-on-duplicate follows the same folder rule, and every AnythingLLM call is pinned toALLM_URL's origin. Three hand-derived cases intest-library-scope.mjsfail on the old code and pass now. Applies on the next redeploy of each dashboard instance (template andbeta-weown-chat). -
gitea-runner-docker β a Gitea Actions runner on its own droplet (2026-10-07, WeOwnCloud/openbao#8). A copier template and the
weown-ci-runnersite. The runner serves one repository, and every CI job runs in a Docker-in-Docker daemon that is created for it and destroyed after it: containers, volumes, images and daemon settings never carry over to the next job. The droplet has its own VPC and no inbound port but admin SSH, and the SSH CIDRs and the VPC range are required tofu variables, never in git. Containers are refused the metadata service, and the deploy proves that before any job runs. Docker comes from its signed apt repo (key fingerprint checked, versions held), and the Infisical CLI from a sha256-pinned release.deb. The bootstrap Machine Identity secret is marked rotated only once a login with it answers 401. The registration token is the only secret; a one-shotregisterstep alone holds the Infisical credential, and the runner that handles jobs mounts neither. Decision record:.github/ADR-008-gitea-runner-dedicated-droplet.md(Accepted). Cost: one droplet (s-2vcpu-4gb-amdby default). Compliance: NIST CSF 2.0 PR.AA-05, PR.DS-1, PR.PS-01, DE.CM. Verification:gitea-runner-docker/tests/live-check.sh gitea-runner-docker/sites/weown-ci-runner. -
gitea / keycloak / supabase β dormant Postgres firewall rules removed (2026-10-07, weown-fleet#159). Each firewall admitted tcp/5432 from
10.0.0.0/8("only from within the VPC", but that range is every private network). No production compose template publishes Postgres on the host (the apps reach it asdb:5432on the compose network; only thecompose.localdev templates publish 5432, on a workstation), so the rule admitted nothing on a deployed box today. It was a trap: the day anyone published 5432, every box in the shared VPC could reach it. Removed from the three templates and the livekeycloak-docker/sites/ssoandsupabase-docker/sites/supabase-devrenders; the retiredsso.weown.devrender is untouched. Applying it is a firewall change, so the operator runs it. -
anythingllm-docker β the dashboard document library listed every file on the instance, not only the tenant's (2026-10-07, weown-fleet#48). The library merged the tenant's two workspaces with AnythingLLM's system document store, which is instance-wide. A signed-in customer saw the titles of operator test files, files attached to no workspace, and another brand's documents on the same instance. Replace-on-duplicate matched uploads against the same list, so it could pick another brand's same-named file to remove. The store pass now reads only the tenant's own folder, named by the new
DASHBOARD_DOCUMENT_FOLDER(copier answerdashboard_document_folder), and dashboard uploads are written to that folder. Unset, malformed, orcustom-documents(AnythingLLM's shared default) fails closed: the store is not read and the library lists workspace documents only. The handlers that take a docpath from the client (/api/documents/content,/scope,/delete,/lock) checked only that the string was well-formed. A docpath of another brand, which earlier listings had shown, could be read, attached to the tenant's workspace and chatted with, or detached. All four now pass one gate,tenantMayTouch(). A docpath is allowed only if it is embedded in one of the tenant's workspaces (read fresh per request) or exists at exactly that path in the tenant folder. Anything else gets 404, so the answer does not confirm the file exists, and nothing about it is sent to AnythingLLM./contentlooked documents up by file name, and AnythingLLM answers from the first folder holding that name. It now reads the stored file at the exact authorized path first and, when that file is mounted, does not ask AnythingLLM at all. Without the mounted store it uses AnythingLLM's answer only when that answer names the same path. New CI jobdashboard-testsruns every dashboard test in the template and the committed sites.test-library-scope.mjsfails 32 of its 46 checks on the old server and passes all 46 on the new one. -
do_backup_stale_alert.yml β findings and delivery failures use different exit codes (2026-10-03). A delivered audit with no stale or unbacked resources exits 0. A delivered audit that found them exits 3, and the workflow stays green. An API error, an incomplete snapshot list, or a Buzz publish that did not succeed exits 1, so the job goes red. Compliance: NIST CSF 2.0 DE.CM, PR.IP-4. Verification:
python check_stale_backups.py --self-check. -
Cost and backup notes β executive summary and optional mentions (2026-10-02). Both reports open with cost or compliance KPIs. The itemized inventory sits inside
<details>.NOTIFICATION_PUBKEYS(comma-separated 64-hex public keys) adds aptag per person. A backup audit with zero violations publishes two lines, "All backups compliant.", and does not add those tags. A private key in that list is refused and not printed. Compliance: NIST CSF 2.0 ID.AM-1, PR.IP-4, PR.DS-1. Verification: both reporters'--self-checkandpython scripts/publish_to_buzz.py --self-check. -
scripts/do-cost-reporter β Buzz gets a short infrastructure summary (2026-10-02). The channel note is the spend, droplet, DOKS, volume, and load-balancer counts, plus how many volume-backed droplets have no backup feature. The team table sits in a collapsed
<details>block. Resource names stay in the localDO_MONTHLY_COST_REPORT.md. Compliance: NIST CSF 2.0 ID.AM-1, PR.DS-1. Verification:python do_cost_reporter.py --self-check. -
Late review findings on Shahid's Buzz and DigitalOcean report PRs (#277β#283), fixed (2026-10-02).
publish_to_buzz.pyrefuses a channel id or name that carries the signing key:nostr:nsec1β¦(the shape guard ran before the prefix was stripped), and the key's own 64-hex form, which is shaped like an event id. Every tag is compared with the key before anything is signed. Its--self-checknow covers thensecload path and publishes through a local NIP-42 relay on 127.0.0.1, so it fails ifuniffi_set_event_loopis removed. Both reporters'requirements.txtare hash-locked with--require-hashes, dependencies included. A 401 names the variable that holds the rejected token (DO_EXTRA_TEAM_TOKENSteams included), and the workflows README lists the secrets and refusal semantics. -
Late review findings on #272/#276, fixed (2026-10-02). sso scripts (
create-weown-user,kc-set-temp-password,kc-configure-smtp,kc-set-login-theme) refuse any argument that is not a plain Keycloak name, e-mail or host, before ssh: a quote plus shell syntax in an argument ran as root on the SSO host. The FIFO secret writers (bootstrap-product,store-sendgrid-key,provision-openrouter-key) are killed by the EXIT trap, so a TERM/HUP to the script no longer leaves one holding the secret. Backup exits 1, before retention, when the dashboard could not be quiesced (the archive is still stored); restore prints "restore complete" only on success.dev/up.shclassifies enable-multi-user by status + exact reply, and its.env.devrewrites leave no temp copy. A committed dashboard test (test-origins.mjs) covers userinfo origins and the position-only 400. -
scripts/publish_to_buzz.py β channel notes are also Kind 9 (2026-10-02). Kind 1 reached the relay and did not show in the channel view. A set
BUZZ_CHANNEL_IDnow signs the same markdown twice: Kind 1 withe(root),h,t(Cloud&Infrastructure), andp(sender pubkey), then Kind 9 withh,t, andpso the channel index can show it. Kind 9 has noetag, because that marks a reply. If the relay answersunknown event kindfor Kind 9, that refusal is printed and the Kind 1 note still stands. Markdown line breaks are kept. The key is never printed. Compliance: NIST CSF 2.0 PR.DS-1. Verification:python scripts/publish_to_buzz.py --self-checksigns both kinds offline and does not connect. -
scripts/publish_to_buzz.py β reports are Kind 1 notes (2026-10-02). The relay rejects Kind 42 with
restricted: unknown event kind, so the signed note is always Kind 1. WhenBUZZ_CHANNEL_IDis set the tags aree(root, relay URL of this send),h(that channel id), andt(Cloud&Infrastructure). The key is never printed. Compliance: NIST CSF 2.0 PR.DS-1. Verification:python scripts/publish_to_buzz.py --self-checksigns a Kind 1 event and does not connect. -
scripts/publish_to_buzz.py β relay refusals are printed, not replaced (2026-10-01).
NOSTR_PRIVATE_KEYis stripped of spaces, newlines, and wrapping quotes, thenKeys.parseaccepts hex ornsec(annsecis decoded to hex before use). NIP-42 still usesSignerAuthenticator. Asend_eventor authentication refusal is printed as the relay's own message and raised, so the job exits non-zero. A successful send prints[SUCCESS] Published event <id> to channel <id> on <relay>. The key is never printed. Compliance: NIST CSF 2.0 PR.DS-1. Verification:python scripts/publish_to_buzz.py --self-check. -
Superseded (2026-10-02) by "relay refusals are printed, not replaced" above: a refused login now fails the job. scripts/publish_to_buzz.py β a rejected relay login does not fail the report (2026-10-01).
NOSTR_PRIVATE_KEYis stripped, then parsed withKeys.parseas hex ornsec. NIP-42 still usesSignerAuthenticator(nostr-sdk 0.45 has noClient.handle_auth). If the relay answersauthentication failed, the script logs[WARNING] Nostr publish failed: authentication failedand returnsauth_failedso the cost and backup jobs keep the report they already wrote (exit 0 unless the audit itself is bad). The key is never printed. Compliance: NIST CSF 2.0 PR.DS-1. Verification:python scripts/publish_to_buzz.py --self-check. -
scripts/publish_to_buzz.py β NIP-42 AUTH callback has an event loop (2026-10-01). After #280 the monthly cost run still exited 1. The report was written, then
make_auth_eventraisedRuntimeError: no running event loopon the Rust thread and the relay dropped the socket (relay not connected). nostr-sdk 0.45 documents this: calluniffi_set_event_loopwith the running loop before connect, and clear it afterward. The key is still never printed. Compliance: NIST CSF 2.0 PR.DS-1, PR.AA-05. Verification:python scripts/publish_to_buzz.py --self-checksigns a kind 22242 AUTH event from a worker thread againstwss://relay.example.com/and does not open a socket. -
scripts/publish_to_buzz.py β NIP-42 AUTH when the relay requires it (2026-10-01). The monthly cost run wrote the report, then the relay answered
auth-required: not authenticatedand the job exited 1. The client now signs that challenge with the same key that signs the note. The key is still never printed. Compliance: NIST CSF 2.0 PR.DS-1, PR.AA-05. Verification:python scripts/publish_to_buzz.py --self-checkbuilds the authenticated client offline and does not connect. -
scripts/publish_to_buzz.py β channel id is a public identifier (2026-10-01). The monthly cost workflow wrote the report, then exited 1 because
BUZZ_CHANNEL_IDwas set and was not a 64-character hex event id. A 64-hex id is still accepted, and so is a short public channel id (channel-1, a UUID,note1β¦).nsecanddop_v1values are still refused, and the value is never printed. Compliance: NIST CSF 2.0 PR.DS-1. Verification:python scripts/publish_to_buzz.py --self-check. -
scripts/do-cost-reporter and scripts/do-backup-monitor β webhook host check (2026-10-01).
WEBHOOK_URLis treated as Discord only when the URL hostname isdiscord.com,discordapp.com, or a subdomain of those. A host that merely ends with those names, or a query string that contains them, stays on the generic webhook path. Compliance: NIST CSF 2.0 PR.DS-1. Verification: both scripts'--self-check. -
Billing deploy helpers pinned and offline (2026-09-30). The state-file purge and the state-volume owner check (#274) ran
alpine:3.19(end of life, floating tag); both now runalpine:3.24pinned by manifest digest, with--network nonesince they only touch a local volume. The repo's otheralpine:3.19uses (backup/restore scripts) are unchanged here. -
billing-docker β the provisioning state file had its own volume in name only (2026-09-30). In the live
sites/billingcompose,ops_stateandstatic_databoth resolved toweown_billing_static_data(the volumename:sat under the wrong key), soprovisioning_watch's state file lived inside the web-served static root. The live compose now givesops_stateits own volume (weown_billing_ops_state; static files keep theirs). The deploy removes the stale state files from the static volume beforeup, makes sure the new state volume is owned by the app user, and fails unless/static/provisioning.jsonanswers 404 from the running app. The template gets the same tasks. Follow-up to #269, which fixed the template but left the live render aliased. -
Every
*-dockerdeploy play: the Infisical auth-file copy no longer prints its content under--diff(2026-09-26).Create container-readable Infisical auth file (ADR-006)copies.infisical-auth.env, which holds the Machine Identity secret, and had neitherno_lognordiff: false.render-deploy.sh --app-checkruns ansible--check --diffand is the documented first step on boxes holding customer data, so after any rotation it would print the secret to the operator terminal and any log. Added task-leveldiff: false(the task still runs and still reports changed) in 10 templates and 5 committed site renders (15 files). Found while running--app-checkon the sales box before the weown-fleet#92 deploy; that run printed nothing because the file had not changed. Verification: non-jinja files parse as YAML;ansible-playbook --syntax-checkpasses on the committed site renders. -
*-dockertemplates: a look-alike registry host could receive the Minimus pull token (2026-09-30, Copilot on #265).registry_token_keypickedMINIMUS_TOKENwhenimage_registrymerely started withreg.mini.dev, soreg.mini.dev.evil.examplegot the Minimus credential. It now requires the host to be exactlyreg.mini.dev(a namespace path is still fine); verified by render, andmainfails the same test. Also: the.env*.examplefiles in billing, gitea, keycloak and supabase are now templates (they already began with{{ project_name }}and rendered it literally), so they followimage_registryandregistry_token_key; the wordpress README no longer says "Minimus" for a custom registry. -
anythingllm-docker β dashboard SVG logo,
/healthzand embed-filter hardening (2026-09-30). A customer-uploaded SVG logo could run script on the instance origin when/app/brand/logowas opened directly (entity-encoded and namespace-prefixed payloads passed the regex check)./app/brand/*now sends a sandboxing CSP plusnosniff, and the upload check refuses DTDs,&#references, prefixed elements and non-fragmenthref/url()./healthzno longer returns raw socket errors to unauthenticated callers. embed-filter fixes the upstream host fromALLM_URL(a request target could name another host) and runs as uid 1000 on a read-only root filesystem with no capabilities andno-new-privileges. Template andsites/beta-weown-chatkept identical; details inanythingllm-docker/template/CHANGELOG.md.jinja. -
anythingllm-docker β OpenBao deploy review sweep: the store's private address left the public render; NUL key names, a leaked login token and the wrap token on argv closed (2026-09-30). Fixes open Copilot threads on #198β#262, in the template and the committed
sites/beta-weown-chatrender. Store address: the render committed the store's VPC address in four places.ansible/deploy.ymlnow takesbao_addrfrom the copier answer when given (weown-fleet's ephemeral renders, unchanged), else fromBAO_ADDR_INSTANCEat deploy time (or-e bao_addr=), and refuses to run without anhttps://host[:port]value.entrypoint-bao.shholds a placeholder that the deploy fills in as it uploads the file, so the address exists only on the box, and--check --diffdoes not print it (diff: falseon that task). Deploy change: every deploy ofbeta-weown-chatfrom this repo now needsBAO_ADDR_INSTANCE. History still holds the old address. Host key gate: a key NAME containing NUL passed the #262 name check as two names and shifted the export loop (dummy JSON exportedCOMPOSE_PROJECT_NAME=evil); NUL is now refused in names as well as values. The container gate already refused it. Tokens: the template's registry-token fetch now revokes its AppRole login token on every exit path; the wrap token reaches the host on stdin underno_log, not viaenvironment:(process list,-vvv). Also: a faileddocker restartof a consumer fails the deploy; store keys named like entrypoint helpers are no longer unset;embed-filterruns as uid 1000 with no capabilities;bootstrap-product.shkeeps the admin JWT and API key off curl's argv;devsec.hardeningpinned to10.6.0; copier validators for thebao_*answers;entrypoint-bao.shrendered only for OpenBao anddeploy.shdetects the backend from compose;site.sh smoke-testsets its repo root; README, cloud-init and comment corrections. CI's render check adds a render with nobao_addr(the committed-site shape). Compliance: NIST CSF 2.0 PR.DS-1, PR.AA-05, PR.PS-01; CIS v8 3.3, 4.1, 16.1. Verification (mainas the negative control each time): the real playbook tasks run on localhost under bash 5 with stubbedbao/docker, the real entrypoint in a muslnode:22-alpinecontainer, and the real curl lines against a local server. NUL key:mainran compose withCOMPOSE_PROJECT_NAME=evil, the branch refuses and compose never runs, a benign document still deploys. Login token:main0 revokes, the branch 1 on success and on a failed read. Wrap token:mainput it on the process list and in-vvv, the branch in neither, and the token still unwraps. Failed restart:mainexited 0, the branch fails with the message. Address: missing or malformed value refused before any change; the uploaded entrypoint differs from the committed file on exactly the one line. Helper-named keys:maindropped 3 of 4, the branch keeps all.embed-filter: uid 1000, CapEff 0,/healthz200. Bearer:main2 tokens on argv, the branch 0, and the server still authenticates. Plusansible-playbook --syntax-checkon all render shapes,bash -n/dash -n, shellcheck (no new warnings), and a YAML/ASCII check of both cloud-init renders. -
keycloak-docker/sites/sso β admin password, SendGrid key and temp passwords moved off argv (2026-09-30).
store-sendgrid-key.sh's--file=/dev/stdinpath never worked (the CLI rejects--filetogether with a positional name), so every run silently fell back toNAME=valueon argv. It now writes a YAML--filethrough a FIFO (jq--rawfilefrom stdin; the CLI keeps a YAML string exactly and expands@fileonly for a user login), and there is no argv fallback.kc-configure-smtp.sh,kc-set-login-theme.sh,kc-set-gitea-logout-redirect.sh,kc-set-temp-password.shandcreate-weown-user.shput the Keycloak admin password on docker compose's argv (-e KC_PASS=<value>) and on kcadm's (--password); kcadm now readsKC_CLI_PASSWORD(Keycloak 26+; sso runs 26.7.0), passed into the container by name. The SendGrid key left-s smtpServer.password=<key>for JSON on kcadm's stdin (update -f - --merge), and generated temporary passwords left--new-passwordforKC_CLI_PASSWORD. Same PR, same class:devbox-dockersetup-zed.sh --infisical(template +dev-weown-devboxrender) stored the member's OpenRouter key asOPENROUTER_API_KEY=@file/OPENAI_API_KEY=@file, which underINFISICAL_TOKENsaves the literal path and still reports success. It now uses one YAML document with both names through--fileon a FIFO, so the key is exact in both auth modes and the transfer adds no copy on argv or disk. The simple-path dotfile that--infisicalalso writes is unchanged. A signal cleanup means a Ctrl-C mid-store leaves no blocked writer or temp dir. Verification: the real scripts driven through stub ssh/docker/kcadm/infisical (the docker stub copies bare-e VARfrom its environment the way compose does), with every argv logged: 30/30, and every secret-on-argv check fails onmain. The test also caught an apostrophe in a new comment inside abash -c '...'block that truncated the remote script; it was fixed before commit. -
npm Dependabot alerts cleared: 12 alerts, 0 left (2026-09-30).
landing-purchase: astro 7.2.0 β 7.3.5 (the critical one) and five transitive packages.n8n/docker/custom: undici 7.30.0. Lockfiles were regenerated with--package-lock-only --ignore-scripts, and both were verified in a throwawaynode:22-alpinecontainer, so no package code ran on the operator machine. -
Operator scripts β secrets moved off argv; a bootstrapped embed was open to every site (2026-09-30). Found in the Copilot review sweep of merged PRs; each fix has a stub-CLI test that logs every argv, run against
mainas the negative control.anythingllm-docker/.../bootstrap-product.sh(template +beta-weown-chatrender): the JWT and the Developer API key were on curl's argv; now a curl config on a pipe (-K <(...)). The Infisical backend put every generated password oninfisical secrets set KEY=value; it now passes--filepointing at a FIFO namedpush.yamlin a private 0700 dir, fed byjq --rawfile v /dev/stdin, so the value is on no argv and never written to disk. Why--fileand notKEY=@file: the CLI expands@fileonly for a user login, and under a machine-identity token it stores the literal path. Why YAML: the CLI parses.yaml/.ymlas YAML and keeps the string exactly, while any other name is parsed as dotenv, which trims whitespace, strips quotes and splits on newlines.allowlist_domainswas sent as a JSON array, which AnythingLLM stores as NULL (allow every site); it is now a comma-separated string. Each entry is normalised to an origin (https://host[:port]) exactly as the dashboard'snormOrigin()does, and the instance's own origin is included. An unusable entry stops the run before anything is sent, including a URL with userinfo (https://trusted.example@evil.exampleis hostevil.example). The dashboard'snormOrigin()refuses userinfo too, so a later save can't reintroduce it. Rejected entries are reported by position only, in the script's message and in the dashboard's HTTP error, because an entry may carry a password (https://user:pw@host). A blank answer now means only the instance's own origin: before, it meant no allowlist, which lets every website use the embed and spend the customer's LLM budget. The same list is stored asEMBED_ALLOWLIST_DOMAINS, the dashboard's seed. On a re-run with an existing embed, the list is owned by the dashboard (which keeps its own copy), so the script no longer writes it to AnythingLLM. It prints the exact dashboard action instead, repeated at the end of the run. A failed store write now stops the run (it used to print one line and end in "Done"), and the generated admin password is stored before the admin is created, so a failed write can never lose it.dev/up.sh: the admin password (jq and curl argv) and the Bearer header (curl argv) now go env β stdin andcurl -K -.scripts/offboard-account.sh: the Keycloak admin password was onkcadm.sh --password(docker compose and java argv); kcadm now readsKC_CLI_PASSWORD(Keycloak 26+) passed by name with-e. TheGITEA_MODE=apiadmin password no longer touches disk (it was a temp curl config); curl reads it from a pipe per call, with\and"escaped for curl's config syntax. The one-shotwrite:admintoken (both Gitea paths) now reaches curl on stdin (-K -), not-Hon argv.- Found in passing, same class:
scripts/provision-openrouter-key.shsent the OpenRouter provisioning key as-H "Authorization: Bearer β¦", andanythingllm-docker/scripts/allm-prompt-sync.shdid the same with the admin API key, while both headers promised "never on argv". Both now use-K <(...).provision-openrouter-key.shalso stored the key withOPENROUTER_API_KEY=@file, which underINFISICAL_TOKENsaves the literal path, and its read-back only checked for a non-empty value, so it reported success. It now uses a YAML--fileon a FIFO, and the read-back compares a sha256 of the stored value with the minted key. The key no longer touches disk at all: the old mode-0600 temp copy is gone, and the private dir and FIFO are set up before minting, so a setup failure stops the run before a key exists. anythingllm-docker/scripts/allm-user-rename.shandallm-admin-account.shvalidate every value they interpolate into a remote root shell (ssh target asuser@host, container name, usernames), so a quote can no longer run commands on the droplet. The ssh target must start with a letter or digit:-Fconfig@hostmatcheduser@host, but ssh reads it as the option-F. The same check was fixed inallm-set-role.sh,scripts/offboard-account.sh(GITEA_SSH_HOST) anddevbox-dockeroffboard-user.sh.allm-user-rename.shalso refuses an email-shaped new name (FedArc D35).anythingllm-dockerembed-filter(template +beta-weown-chat), the one public-facing container, now runs as uid 1000 withcap_drop: [ALL]andno-new-privileges. Verified by starting it from the rendered compose: uid 1000, CapEff 0, NoNewPrivs 1, healthcheck healthy (main: uid 0)..github/workflows/validation.yml: the workflow token iscontents: read; only the security job addssecurity-events: writefor the SARIF upload.pull-requests: writeis gone (no job used it).
-
anythingllm-docker β an OpenBao key NAME could run commands, in the container and on the droplet host (2026-09-29). Both OpenBao paths turned every key at an instance's path into shell text and ran it, and quoted only the value. A key name containing shell syntax therefore ran as a command: in the container entrypoint (
entrypoint-bao.sh, POSIX sh, before the app starts, with every secret already in its environment), and on the host duringansibledeploy (the two openbaodocker compose uptasks, as the deploy user). Anyone who can write a key at the instance path could do it: operator tiers, store admins and the provisioner. It's the same bug as WeOwnCloud/openbao#90, which fixed the shared seam. Measured on a local OpenBao 2.6.1 dev server against the committedbeta-weown-chatrender: the old code executed the key name and booted normally both times, exportedNODE_OPTIONSinto the node app, and passed a store-suppliedDOCKER_HOSTstraight todocker compose, which would send the whole deployment, secrets included, to another daemon. Now: every key name must be a plain identifier and must not be a loader/shell/node variable (LD_*,DYLD_*,BASH_ENV,ENV,BASH_FUNC_*,SHELLOPTS,BASHOPTS,PS4,PROMPT_COMMAND,IFS,PATH,NODE_OPTIONS); on the host,DOCKER_*andCOMPOSE_*are refused as well, because compose runs next. One bad name refuses the start or deploy. It is never skipped, and a malformed name is never printed, since it may be a value pasted into the wrong field. Values containing NUL are refused. The host exports throughexport "$k=$v"and never parses store data as shell. The container is POSIX sh with noread -d ''and no jq, so node checks every name and single-quotes every value before the (now inert) text is run. Also closed, same class: the approle secret-id and the single-use wrap token were on argv at every login and unwrap (4 host sites, 2 container sites), wherepsand/proc/*/cmdlineshow them to any process. Logins now send the JSON body on stdin (jq--rawfileon the host, node from the environment in the container), and unwrap reads the token fromBAO_TOKEN. Every login token, including the host's validity-probe tokens, is now revoked withbao token revoke -selfinstead of being left live until its TTL. Verification (old code as the positive control each time): container and host each run with a benign doc (quotes,$(β¦)and backticks in values arrive intact and are not executed), a hostile key name (old: executed, rc 0; new: refused, nothing ran),NODE_OPTIONSandDOCKER_HOST(old: exported; new: refused), and a NUL value (new: refused); abaoargv logger shows the secret-id and wrap token on argv in the old code and never in the new; first-boot unwrap,sid_validverdicts (real/wrong/empty) and revocation checked. Template and render stay identical outside the site's three values;sh -n/dash -n/shellcheck -s sh,ansible-playbook --syntax-check, and a Jinja parse with copier's jinja2 3.1.6 all pass. Deploy consequence: every openbao instance keeps the vulnerable code until it is redeployed.weown-fleetrenders tenants from this template at deploy time, so this covers three live instances (beta-weown-chat,chattest2,weownacademy, pertenants.yaml2026-09-29), not only the committed render.operator.default_secret_backendisopenbao, so every new tenant uses this code too. Reported by openbao-claude. -
anythingllm-docker β the public chat widget resumed the previous visitor's conversation forever (2026-09-26) β the stock AnythingLLM widget stores its session id in
localStorage(allm_<embedId>_session_id) and resumes it with no expiry and no setting to change it. On a CPA's public site, the next person on a shared or family computer saw the last person's tax questions. Tyler reported it on the OCPA bot on 2026-09-25: "loaded this website new β¦ the old one was still there". Every instance already serves the widget script itself, soembed-filternow serves/embed/anythingllm-chat-widget.min.jswith a small ES5 session-policy shim prepended. A new tab, a reload, arriving from outside the site (a typed URL, a bookmark, a search result, another site's link, even in the same tab), or 30 idle minutes starts a fresh chat. Moving between pages of the same site, and back/forward, keep the conversation. (A tab duplicated from the current one keeps it within the idle window: same person, minutes apart. A site that sends no referrer resets on every page, toward privacy. Known limit: the widget keeps one session id per origin, so two tabs open at once share it; true per-tab isolation needs the widget to support it.) Per embed it can be changed withdata-weown-session-ttl-minutes. The shim is fully guarded (storage that throws never blocks the widget load). The shimmed script carries its own ETag andCache-Control: no-cache, andembed-filteranswers revalidation itself. A copy validated against upstream's old ETag gets the new bytes; one holding ours gets a 304. One shimmed copy is cached keyed by upstream's ETag, and upstream is asked conditionally, so AnythingLLM sends the full ~650 KB widget once, not once per page load, and a widget upgrade replaces the copy. No host-page change is needed (ocpa.group, demo.weown.dev). The Caddy route sits above the catch-all in the template and in the re-syncedbeta-weown-chatrender; the three other sites run noembed-filterand are deliberately untouched. Compliance: NIST CSF 2.0 PR.DS-1, PR.DS-5; CIS v8 3.3. Verification:embed-filter/test.js, 27 assertion groups: the shim's policy is run in a sandbox (moving between pages kept; reload dropped; back/forward kept; new tab dropped; arriving from another site, a typed URL, or a look-alike host dropped; duplicated tab kept within TTL; no Performance API still applies the storage rules; idle > TTL dropped; TTL override; garbage TTL falls back to 30; no embed id is a no-op; throwing storage doesn't escape), plus an end-to-end HTTP test through the real server against a chunked upstream. That test caught aContent-Length+Transfer-Encodingconflict before it shipped. The template Caddyfile, rendered both ways, and the beta Caddyfile passcaddy validate/caddy adaptincaddy:2. weown-fleet#92. -
.gitignore β local DigitalOcean audit exports are ignored (2026-09-24) β droplet inventory and cleanup-audit markdown/PDF files hold live addresses, account emails, and SSH fingerprints. They are local working copies and must not land on this public repo. Compliance: NIST CSF 2.0 PR.DS-1. Verification:
git check-ignoreon the six export filenames. -
anythingllm-docker β the customer dashboard's Private/Public toggle was HARD-DELETING library documents (2026-09-03) β turning a document's last scope off computed that it would be left embedded in neither workspace and then called
DELETE /api/v1/system/remove-documentsto avoid an "invisible orphan". The result was silent customer data loss: operators watched a library shrink 6 to 5, and a customer's uploaded file disappeared when they clicked the toggle. The purge is now removed from the scope path entirely β a scope toggle only adds or removes embeddings, and the explicitPOST /api/documents/delete(behind a confirm, a path check and a lock 409) is the sole destructive call site. Documents left in neither scope stay in the system store and remain listed with a Not in use badge, so the state the purge existed to prevent is now visible instead of destroyed. Also hardened:isValidDocpath(rejects leading/,.., NUL, backslash, over-512-char paths) now guards the lock, scope, delete and content endpoints, three of which previously had none. Forward-only β documents already purged by the old code are gone and need a restore from backup. The rendered site underanythingllm-docker/sites/beta-weown-chat/dashboard/is re-synced in the same change, because ansible ships the site's copy, not the template's, so a template-only fix would have left the deleting code running in production. Compliance: NIST CSF 2.0 PR.DS-1, PR.AC-4; CIS v8 3.11. Verification: everyremove-documentscall site traced on the merged tree (one comment, one explicit-delete call);node --checkclean on template and render;GET /api/v1/documentsresponse shape verified against a live AnythingLLM (viewLocalFilesreturns{name: <on-disk filename>, type: "file"}), confirming the library listing's path reconstruction is correct rather than producing phantom duplicate rows. PRs #222, #226. -
scripts/tag-droplet.sh β commit-tagging can no longer report success while the tag never lands (2026-08-30) β the DO API only attaches existing tag resources, so every fresh
commit-<sha>tag failed the droplet-action; a blanket|| true(added to tolerate "already tagged") swallowed the failure, and the deploy task reportedokwith no tag on the droplet (doctl-verified 2026-08-26 after the #188 deploy: nocommit-0cbcdab). The #185 class one level deeper: silent miss β silent false success. Now the helper creates the tag resource first, then verifies presence (add) / absence (remove) viadoctlafter every mutation and exits non-zero on a miss β which the deploy playbook's existing rc-gated surface task then prints instead of staying quiet. Per governance assert the step's REAL contract, not a proxy: "the action exited 0" was the proxy; "the tag is on the droplet" is the contract. Compliance: NIST CM-8, CIS v8 1.2. Verification:bash -nclean. -
twilio-bridge/ removed (2026-08-30) β the Twilio Voice dial-pad spike was dormant since 2026-01-06, deployed nowhere, and referenced only by CODEOWNERS, the README index, and copilot-instructions β while carrying all 35 remaining Dependabot alerts (13 high, 11 axios) on
main. Deleting it takes the repo to 0 open alerts at zero ongoing cost; git history is the undo if the spike is ever revived. References removed from CODEOWNERS, README, and copilot-instructions. -
anythingllm-docker β customer data restored to the encrypted volume; deploy now refuses to strand it (2026-07-31) β a template change had replaced the named volume
anythingllm_storage:/app/server/storagewith a host bind./storage:/app/server/storageand deleted the volume declaration. Two consequences, both silent: (1) redeploying an existing site starts the app on an EMPTY directory and orphans the old volume β the app comes up healthy with zero workspaces (this is what took chat.weown.dev's workspaces, threads and API-key registry offline on 2026-07-31; 25M of data survived only because nothing had rundown -v), and (2) the bind path/opt/...lives on the droplet's local disk, while Docker's data-root sits on the attached DO block volume that is platform-encrypted at rest β so the change also moved customer documents and vectors off encrypted storage, undoing the at-rest control from the July hardening work. Fixed by restoring the named volume for app data while keeping the declarative MCP pack as nested binds (./storage/mcp,./storage/plugins) inside the volume mount, so the git-managed tool pack stays IaC. Added a pre-upguard inansible/deploy.yml: if this project's storage volume exists and is non-empty but the compose about to run does not mount it, the deploy FAILS loudly instead of silently starting on empty storage. Compliance: NIST CSF 2.0 PR.DS-1; CIS v8 3.11. Verification: template re-rendered, compose + playbook YAML parse, guard renders with the correct per-project volume name. -
anythingllm-docker β dashboard chat-flow hardening on top of the #133 redesign (2026-07-29) β review of the redesign confirmed the four security invariants hold (uniform safe-DOM rendering, CSRF header on every non-GET, public/private boundary, non-emptyable allowlist); this follow-up fixes the correctness gaps found: (1)
/api/chatupstream failures returned HTTP 200 with the raw upstream error astext, rendering as a normal bot bubble β now a 502{error}; the UI shows the error strip, rolls the unsent user message back, and restores it into the composer; (2) a reply landing after the user switched threads was pushed into the wrong conversation β aviewSeqguard drops stale async responses (send, thread-create, history-load) for views the user has left; (3) concurrent double-send blocked with an in-flight gate that also protects rewind and the composer from truncating/clearing for a send that would no-op; (4) thread delete and history-load now surface server errors instead of pretending success. Verification:node --checkon server + extracted inline script; live smoke against a stub AnythingLLM (login, whoami, rename incl. CSRF/blank/81-char cases, chat happy + 502 paths, draft-to-thread creation with derived title) β all pass in a real browser session. -
anythingllm-docker β backup.sh gets the same off-box-upload contract as keycloak/gitea (#134), plus the ai.weown.agency bucket mismatch fix (2026-07-29) β same defect family: missing
SPACES_*creds withREMOTE_STORAGE=do-spacesproduced a WARNING and exit 0, and β worse here β GFS retention then still pruned local copies, so a silently-unuploaded site also aged out its only backups. Now: missing creds abort withexit 1BEFORE retention; afteraws s3 cpthe exact object key must exist remotely with the exact local byte size before success (asserted BEFORE the local encrypted.gpgcopy is deleted). Applied to the template and all three rendered sites. Also fixed:sites/ai.weown.agencybackup.sh AND restore.sh both pointed at the deprecatedweown-backupsbucket while restore's own header documentedweown-prod-backupsβ both now use the canonicalweown-prod-backups(no stranded objects: no backup was ever observed landing in the old bucket). Verification:bash -nclean on all four scripts + the de-jinja'd template. -
keycloak-docker + gitea-docker β backup.sh can no longer report success while silently skipping the off-box upload (2026-07-29) β root cause of the "live, backups unverified" finding: when
REMOTE_STORAGE=do-spacesbutSPACES_ACCESS_KEY/SPACES_SECRET_KEYwere absent from the injected env, the script printed aWARNING, skipped theaws s3 cp, ran retention, printed=== BACKUP FINISHED ===and exited 0 β so the daily cron wrote local-only backups for weeks while reporting green. Two contract assertions added (templates + both rendered sitessites/git,sites/sso): (1) missing Spaces credentials withdo-spacesconfigured is now a hardexit 1; (2) after the upload, the scriptaws s3 lses the exact object key and fails unless it exists with the exact local byte size βcpexiting 0 is not proof of an object. Per governance assert the step's REAL contract, not a proxy: "the script ran" was the proxy; "a non-zero object exists in the bucket" is the contract. The dead-man alert (absence of a fresh object should page) remains open on the board. Compliance: NIST CSF 2.0 PR.DS-1, RC.RP; CIS v8 11.2/11.4. Verification:bash -nclean on both rendered scripts and both de-jinja'd templates. -
anythingllm-docker β embed widget can no longer be left callable from any website (2026-07-22) β the public chat widget is the customer's brand and their capped LLM budget, and AnythingLLM treats an embed with no allowlist as allow every origin (
EmbedConfig.parseAllowedHostsreturnsnullβcanRespondskips the origin check). Two changes close that: (1)EMBED_REQUIRE_ALLOWLIST: "true"incompose.prod.yamlflips "no allowlist" to deny-all, so the failure mode is a visibly dead widget instead of a stranger silently spending the customer's budget (upstream #5759; effective on images built after 2026-06-17, ignored harmlessly on older ones); (2) the customer dashboard gains an Authorized websites panel (GET/POST /app/api/embed-domains) so adding a second site never needs an operator β entries are normalised toscheme://host[:port](lower-cased, path/trailing-slash stripped, wildcards refused, since ALLM compares the browserOriginbyte-for-byte), the instance's own origin is always retained so the list can never be emptied, and the list is persisted to a newdashboard_statevolume. The allowlist that provisioning seeds arrives asEMBED_ALLOWLIST_DOMAINS. Compliance: NIST CSF 2.0 PR.AC-4, PR.DS-2; CIS v8 4.1. Verification: 16-check live test of the dashboard (public health probe, auth gate, CSRF gate, wildcard rejection, case/path normalisation, self-origin retention, empty-submission floor, state-file persistence, comma-separated-string payload shape, snippet contents) all pass against a running server + stub AnythingLLM; ALLM's ownvalidatedCreationDatawas executed directly to confirm an array-valuedallowlist_domainsstoresNULL(allow-all) while the comma-separated string stores the array. -
anythingllm-docker β the embed snippet now carries the AI disclaimer (2026-07-22) β
/app/api/snippetemitsdata-greeting("I'm an AI assistant⦠general information only⦠don't share private information in this chat") plusdata-no-sponsor, so the PRD's non-negotiable disclosure is stated the moment the widget opens rather than depending on the model remembering to say it. -
anythingllm-docker β per-instance Infisical secret paths (
infisical_secret_path) (2026-07-19) β supports the WeOwn-Chat initiative secret model: ONE Infisical project per initiative, one folder per instance (/sites/<slug>), each droplet Machine Identity restricted to its own folder via a path-scoped project role β the client-isolation boundary (D-ISO amendment) without a project per customer. New copier varinfisical_secret_path(default/β fully backward-compatible; existing dedicated-project sites render identically) threads through every secret consumer: container entrypoint (entrypoint-infisical.shinfisical run --path),ansible/deploy.yml(newinfisical_pathvar fromINFISICAL_PATHenv; all fiveinfisical run/secrets getcalls),backup.sh/restore.sh(local + remote-ssh modes),bootstrap-product.sh,deploy.shenv pass-through, andscripts/provision-openrouter-key.sh(new--path/--operator-pathflags β the OpenRouter PROVISIONING key can now live at the project root where instance-scoped MIs cannot read it, closing the POC caveat that a compromised droplet could mint keys). Verification:bash -nclean on the shell scripts; rendered-template validation via the fleet dry run (weown-fleetrender-deploy.sh). -
anythingllm-docker β CIS host hardening play + encrypted-at-rest data volume (2026-07-15) β Phase A of porting the SOP-INFRA-012 baseline to customer instances. Hardening: new
template/ansible/harden.ymlapplies DevSecos_hardening+ssh_hardening(β CIS Ubuntu L1 β kernel/sysctl, auth policy, SSH key-only + strong KEX/ciphers) with the Docker-safeip_forward=1task-var override (defaults-level overrides silently lose to inner-role defaults β the 2026-07-03 no-egress landmine), then measures with Lynis and verifies Docker survived;ansible/requirements.ymlpins thedevsec.hardeningcollection. Phase A deliberately keeps root key-only SSH (all fleet tooling SSHes asroot@); Phase B (ops-user model, root-off, per SOP-INFRA-012) is gated on fleet-tooling migration and tracked for pre-first-customer. Data at rest: newdigitalocean_volume(default 50 GB,prevent_destroy,data_volume_size_gb=0to disable) β DO block storage is platform-encrypted at rest, droplet local disk is not; cloud-initsetup-data-volume.shwaits out the attach race, formats/mounts, points the Docker data-root (chats/vectors/uploads) and the backups dir at it before Docker installs. Compliance: FTC Safeguards/GLBA at-rest expectation; NIST CSF 2.0 PR.DS-1, PR.PS; CIS v8 4.1/3.11. Verification: renderedharden.ymlpassesansible-playbook --syntax-check;setup-data-volume.shbash -nclean; renderedmain.tfparses (tofu fmt). -
anythingllm-docker β client-side encrypted backups (GPG) + ZDR LLM routing posture (2026-07-15) β closes two compliance gaps for customer instances holding financial/personal records (GLBA-service-provider posture). Backups:
backup.shnow encrypts the tarball with OpenPGP/GPG (per-customer ed25519/cv25519 public key,BACKUP_GPG_PUBLIC_KEYinjected viainfisical run; keyring-free--recipient-file+ ephemeralGNUPGHOME) before the Spaces upload β the object at rest is ciphertext to DigitalOcean; the private key lives ONLY in the operator store (BACKUP_GPG_PRIVATE_KEY_<project>, auto-generated bydeploy-new-site.shin an ephemeral keyring). Fail-loud when encryption is configured butgpgis missing; loud warning on unencrypted uploads.restore.shfetches.tar.gz.gpg, pulls the private key in-process from operator-tools via the logged-in infisical CLI (paste fallback), stages it into droplet tmpfs (0600, ephemeral GNUPGHOME, shredded on exit via trap), decrypts, and proceeds; legacy plaintext objects still restore. GPG chosen overagefor auditor familiarity (owner call);gnupgwas already in cloud-init packages. LLM routing: per-customer OpenRouter keys operate under the account-level Zero-Data-Retention guardrail (ZDR-only endpoints β no provider retention or training; OpenRouter itself retains nothing);provision-openrouter-key.shprints the posture requirement, and the compliance facts table indocs/CUSTOMER_INSTANCE_PROVISIONING.mddocuments both. Compliance: FTC Safeguards (GLBA) encryption-at-rest expectation; NIST CSF 2.0 PR.DS-1/PR.DS-2; CIS v8 3.11; supports IRC Β§7216 posture for tax data. Verification: 6-check GPG round-trip (encrypt β ciphertext-opaque β decrypt β byte-identical β wrong-key-rejected) passes with the exact script logic; rendered-shellbash -nclean. -
M2 fleet-wide β Infisical Machine-Identity secrets & DO Spaces creds moved off command-line argv (2026-07-14) β closes the AGENTS.md S2 finding that
infisical login --client-secret="$INFISICAL_CLIENT_SECRET"andtofu init -backend-config="secret_key=β¦"exposed secrets on process argv (readable viaps//proc/<pid>/cmdlineon the droplet). Infisical login now uses env-var auth βexport INFISICAL_UNIVERSAL_AUTH_CLIENT_ID/_SECRETbefore a flaglessinfisical login --method=universal-auth(the CLI reads them natively), matching the pattern openclaw's entrypoint already used. Tofu/OpenTofu now exportsAWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY(the S3 backend reads them from env) and drops the two-backend-configaccess/secret lines;sse_customer_keystays on-backend-config(no env equivalent). Applied across every*-dockertemplate and rendered site (anythingllm, keycloak, owncloud, wordpress, searxng, signoz, sandbox, openclaw, devbox) plusotel-agent/and thescripts/*otel*helpers, and the doc examples that showed the old form. Only--plain/--silentand non-secret flags remain on argv. Compliance: NIST CSF 2.0 PR.DS-1/PR.AC-4; CIS v8 3.11. Verification:bash -n+shellcheck -S errorclean on all changed scripts; rendered ansible YAML parses; 0 remaining--client-secret=/-backend-config="secret_key="in tracked code. Note: needs a live deploy test (copier render β ansible/tofu path) before promotion β the human pre-merge gate.
-
scripts/registry-rollover.py β move a committed site render off
reg.mini.dev(2026-10-02, WeOwnDev/weown-fleet#112). Minimus shuts down on 2026-10-22. The templates take the registry as one copier answer (#265); the committed renders under*-docker/sites/predate it. The script applies the template's own rule to a render: image refs move to<registry>/<path>,docker logingets the registry host and--username, andMINIMUS_TOKENbecomesREGISTRY_TOKEN. It prints a diff by default (--writeapplies it). Anything it cannot rewrite safely, such as a token on a command line or a renamed variable, is listed as MANUAL with exit 3. Verification:python3 scripts/test_registry_rollover.pyrenders all 11 registry-bearing templates both ways and requires the script's output to match the template on all 243 lines it changes; 4 mutations of the script each fail it. -
scripts/publish_to_buzz.py β Kind 1 notes on a configured Buzz relay (2026-10-01) β the cost report and the stale-backup audit sign their Markdown with
nostr-sdkand publish it only whenNOSTR_PRIVATE_KEYis set andNOSTR_RELAY_URLis an explicitwss://URL. There is no default relay. The key acceptsnsecor hex and is never printed; when it is unset the scripts still write the local report and exit normally.BUZZ_CHANNEL_IDmust be a 64-character hex event id; it posts a NIP-28 kind 42 (eroot tag plush). (Superseded under Fixed: a short public channel id is also accepted, and a channel note is Kind 1 plus Kind 9.) With no channel id the note stays a kind 1 root note. Scheduled workflows:do_monthly_cost_report.yml(01 08:00 UTC) anddo_backup_stale_alert.yml(Mondays 08:00 UTC). Actions logs discard report stdout because this repository is public. Compliance: NIST CSF 2.0 PR.DS-1 (key and relay stay in Actions secrets), PR.IP-4 (backup alert). Verification:python scripts/publish_to_buzz.py --self-checkplus both reporters'--self-check(offline sign and skip path; no relay connection, no DigitalOcean token). -
scripts/do-backup-monitor β read-only stale backup audit (2026-10-01) β
check_stale_backups.pyissues GET requests only (/v2/droplets,/v2/volumes,/v2/snapshots) and writesBACKUP_STALE_REPORT.mdas mode 0600. A Droplet or Volume isPASSwhenfeaturescontainsbackupsor a linked snapshot is under 7 days old; otherwise the row isSTALE_WARNINGorCRITICAL_NO_BACKUP. The address column is a public IPv4 or the resource id. Team labels may contain spaces. A failed snapshot list isAUDIT_INCOMPLETEso a missing response is not reported as no backup. Tokens come from the process environment, not a.envfile. The same Markdown can be posted to the configured relay orWEBHOOK_URL. The file is gitignored because it contains live names and addresses. Compliance: NIST CSF 2.0 PR.IP-4 (backups), PR.DS-1 (local file is owner-only; publish targets are explicit secrets). Verification:python -m py_compile;python check_stale_backups.py --self-check(offline rules; no token, no live API). -
scripts/do-cost-reporter β read-only DigitalOcean monthly cost report (2026-10-01) β
do_cost_reporter.pyissues GET requests only againstapi.digitalocean.com, groups list-price run-rate by project and by tag, and writesDO_MONTHLY_COST_REPORT.mdas mode 0600. The token stays inDIGITALOCEAN_TOKEN(optional extra teams name other env vars; the script never prints token values and does not read a.envfile). Team labels may contain spaces. The same Markdown can be posted to the configured relay orWEBHOOK_URL. The generated report is gitignored because it contains live resource names. Compliance: NIST CSF 2.0 ID.AM-1 (inventory), PR.DS-1 (local file is owner-only; publish targets are explicit secrets). Verification:python -m py_compile;python do_cost_reporter.py --self-check(offline pricing and grouping; no token, no live API). -
Changelog entries for tools that shipped without one (2026-09-30):
dev/local stack (dev/up.sh,dev/compose.dev.yaml,dev/README.mdβ AnythingLLM, dashboard and billing on a laptop, credentials generated into a gitignored 0600 file; PR #251);scripts/offboard-account.sh(one script for Keycloak, Gitea, GitHub orgs and the devbox, disable-over-delete, dry run by default; PR #232/#233);anythingllm-docker/scripts/allm-set-role.sh(role change and suspend/unsuspend in place, refuses to demote or suspend the last admin; PR #235). -
billing-docker β
manage.py stalled_signups, a read-only report of where every sign-up stopped (2026-09-26). Since the no-instance paywall (#252), someone can register and never finish, and nobody could see how many. The command places each Customer created on or after--since(default2026-09-10, the paywall launch) at the first funnel step it hasn't passed:no_agreementsigned_no_checkoutcheckout_unfinishedlapsedpaid_no_instanceprovisioning_stuckinstance_inactivelive
"Checkout started" is detected from the Instance row that
new_instancecreates before redirecting to Stripe, not from the Stripe customer id, which only arrives withcheckout.session.completed(a Copilot review finding, fixed with a regression test). An abandoned checkout through the legacy/subscribe/route leaves no row (that door opens Checkout without an Instance), so it lands insigned_no_checkout: the agreement signature both doors require bounds the gap, and the report prints a note saying so. A signature counts only on the ACTIVE customer template, the same test the checkout gate applies, so publishing a new agreement version moves unsigned customers back tono_agreement.It prints a count per step and a total. It prints no personal data: no emails or names;
--idsadds Customer primary keys for lookup in the admin. It writes nothing. Not covered: Keycloak registrations that never reached billing have no Customer row, and counting them needs the Keycloak admin API. Added to the committed render and the template (identical). Verification: 6 new tests. Every state lands on its own step; a legacy-route abandon issigned_no_checkout; a signature on a retired agreement version isno_agreement; no email or username appears in the output;--sinceexcludes older rows and--idsis opt-in; a malformed date is refused. The full suite passes, 80/80 in both copies, run against a throwaway Postgres 16. weown-fleet#66. -
anythingllm-docker β
allm-prompt-sync.sh, bringing a workspace system prompt under version control for the first time (2026-09-17) β perdocs/design/DESIGN-client-intake-and-agent-workflows.mdPhase 1: the agent's entire behaviour (persona, disclaimers, refusals) has lived only in the AnythingLLM runtime DB, with no review, no CI, no rollback and no drift detection, while everything around it (Terraform, compose, the MCP pack, Ansible) is strict IaC. New script (discover/pull/diff/push) closes that gap:discoveris a read-only GET that confirms the live system-prompt field name before anything is trusted (AnythingLLM's documented contract usesopenAiPrompt, but no AnythingLLM source is vendored in this repo to verify that against, and this instance's actual field has not yet been confirmed);pullbootstraps a committed file from a live workspace;diffdetects drift and exits non-zero;pushshows the live-vs-committed diff and requires a typedYESbefore applying. The admin API key is read withread -rsin the operator's own terminal and never crosses an agent's context, argv or a log β perAGENTS.md's secrets-hygiene standard, the bootstrap step is explicitly a human-run action, not one this change performs.sites/dev-weown-anythingllm/prompts/README.mdstarts the convention (prompts/{public,private}.mdper site) on the lowest-risk instance only; no prompt content is committed in this change β both files remain to be bootstrapped by a human holding the live key. Deliberately not wired intodeploy.yml.jinjayet: the mechanism needs to prove itself against one live instance before it is extended toai.weown.agency,beta-weown-chatands004.ccc.bot, or made part of the deploy path. Verification:bash -nclean; thejqextraction/construction logic (shape check, prompt extraction, missing-field failure path, and JSON round-trip through quotes/newlines/backslashes) exercised against synthetic fixtures β no live instance or secret touched in this change.jqchosen over introducingpython3as a dependency, since no other script inanythingllm-docker/scripts/uses it, and hand-built JSON string escaping was rejected as unsafe for arbitrary prompt text. -
docs/design β build plan for the agent review thread's client-intake and workflow changes (2026-09-17) β a stakeholder review of a deployed tax/accounting instance produced ten items spanning four disjoint kinds of work (prompt copy, retrieval behaviour, a new product surface, and a compliance decision), which is how a "ten small changes" estimate goes wrong.
docs/design/DESIGN-client-intake-and-agent-workflows.mdsequences them into six phases with a traceability table mapping every thread item to a plan item. Deliberately vertical-neutral: the thread's examples are tax-shaped, but the same intake workflow serves advisory, lending and legal, so every vertical-specific string, source and document type is per-instance config and atax-shaped conditional in shared code is defined as a defect (constraint C6). Findings that materially change scope: roughly half the requested workflow already exists (the dashboard is already the AOP's authenticated surface with upload, allowlist, library, previews and a private analysis workspace, and the public/private lane split is already two workspaces, not a persona choice); the system prompt is the one artifact in the product with no version control β no review, no rollback, no drift detection β while everything around it is strict IaC, so a phase is added to close that before an automated knowledge-base writer is pointed at it; andOPENROUTER_MODEL_PREFstops being a quality knob and becomes a compliance control, because OpenRouter routes to upstream providers whose retention policies differ per provider and per model, so under a no-retention requirement model choice determines which third party sees a client's documents. Two specified behaviours are changed rather than implemented as asked, with the reasoning recorded: client documents are notified by authenticated link rather than emailed as attachments (an attachment puts the document in inboxes, auto-forwards, phone syncs and at rest on the relay, contradicting the security claim being sold), and the agent detects intent with the model but executes deterministically via a UI affordance rather than emitting URLs as text (a hallucinated near-miss URL is a link to a document drop-box). Records the highest-severity failure mode for the build β client uploads must never reachWS_PUBLIC_SLUG, which grounds the public widget, or one client's document becomes retrievable context for the next anonymous visitor β and carries an agent-mode probe to run first, whose method section notes that a response's text is not evidence of a tool call and must be confirmed in the container logs. No code changes; design only, nothing approved. Verification: markdown only; no secrets, real customer URLs, or identities (booking and portal URLs are placeholders per the public-repo rule). -
anythingllm-docker β dashboard stops silent duplicate uploads, and the library text preview actually shows text (2026-09-10) β two customer-visible gaps on top of the #223/#224/#226 dashboard work. Duplicate uploads: re-uploading a file whose display name already exists (case-insensitive, ignoring the internal
β¦-<uuid>.jsonstorage suffix) now opens an in-app Keep both / Replace / Cancel modal that names how many rows match; the client sendsX-Upload-On-Duplicate: keep|replaceand the server enforces it. Replace is upload β verify β delete, never delete-first β the new file is uploaded and embedded before a single old copy is touched, so a failed upload can never leave the customer with nothing; the only pre-upload work is a locked-match check so a doomed replace fails fast. A failure while retiring the old copy is reported as a warning on the 200, not swallowed (replaceWarnings[]) β the oldremove-documentscall quietly.catch(() => null)'d. The client no longer pre-deletes (that doubled the destructive ALLM calls β Copilot review). Preview content: MintplexGET /api/v1/document/:docNamestripspageContent, so the preview was always empty and its subtitle leaked theAI-Playbook-MEGA.pdf-<uuid>.jsonstorage name. The dashboard now reads the stored JSON fromALLM_DOCUMENTS_PATH(a read-only mount of ALLM'sstorage/documentson the shared named volume), path-traversal-guarded under that root and byte-capped at 16 MiB before the read enters the heap β a preview is truncated to 100 k chars anyway, so a larger file can only be an OOM lever for an authenticated caller. The subtitle is now the de-suffixed human title only; when there is still no text the client shows a muted note, not a red error, and distinguishes an unconfigured instance ("ask WeOwn support to enable previews") from a still-indexing document. The rendered site underanythingllm-docker/sites/beta-weown-chat/dashboard/is re-synced byte-for-byte from the template in the same change (ansible ships the render, not the template β a template-only fix would never reach the box), and the render'sdashboardservice gains theanythingllm_storage:/allm-storage:romount plusALLM_DOCUMENTS_PATH, mirrored intotemplate/docker/compose.prod.yaml.jinja. Rebased onto currentmain: the four defects closed in #227 (chat body cap derived from the attachment budget, document-leveldragover/dropguard,/api/documents/contentno longer asserting an unverifiedlocation, location-mismatch log recording folders not filenames) stay fixed βmain's version was taken at every overlap. Compliance: NIST CSF 2.0 PR.DS-1 (Replace can no longer lose the only copy), PR.AC-4 (lock still gates the destructive path); CIS v8 3.11. Verification:node --checkclean on template and renderserver.jsand on the extracted inline<script>;compose.prod.yamlparses; disk-read cap and traversal guard reasoned through; render confirmed byte-identical to the template after the copy. PR #245. -
anythingllm-docker β dashboard drag-and-drop, per-message attachments, library text preview and Word
.docx(2026-09-03) β files can be dropped onto the chat or the document library, attached to individual messages, and previewed as extracted text;.docxis accepted and legacy.docis refused with a convert message. The upload allowlist is enforced server-side and is now two-sided:GET /api/upload-policyreports the server's effective list, so an instance pinningUPLOAD_ALLOWED_EXTin Infisical can no longer advertise a type the server will reject β operators pinning that variable must adddocx. Four defects found in review were fixed in the same change: the chat body cap is now derived from the attachment budget (CHAT_ATTACH_MAXxCHAT_ATTACH_MAX_B64plus overhead) rather than a fixed 6 MiB that two ordinary PDFs exceeded β which reached customers as "could not reach the server" with the attachments still queued; a document-leveldragover/dropguard stops a file dropped outside a bound zone (the document list being the obvious one) from navigating the browser away and discarding an unsent draft;/api/documents/contentno longer reports alocationit never verified; and the location-mismatch log records folders instead of customer document filenames, which on hosts running the fleet OTel agent leave the box unredacted. Verification:node --checkclean on template and render including the extracted inline script; derived cap asserted to exceed a full attachment set. PRs #223, #224, #226. -
billing-dockerβ white-label affiliate branding on the signup funnel (2026-08-26) β affiliates sell under their own brand, and the billing funnel is the only WeOwn-controlled surface an end customer sees before paying, so every pre-purchase page now wears the referring affiliate's identity.Affiliategains four optional fields (display_name,logo_url,primary_color,support_email; migration0011); a newcore.context_processors.brandingresolves the brand from?ref=β session β the signed-in customer'sreferred_by. That last fallback is load-bearing:subscribe()pops the session key once it is recorded on the Customer, so without it the page reverts to WeOwn at exactly the success/portal step β after the customer has paid. Only active affiliates may brand a page, so an unsigned or revoked affiliate cannot dress the payment funnel in their name. Blank fields fall back per-field, so existing affiliates are unchanged until an operator fills them in.create_affiliategains--display-name/--logo-url/--primary-color/--support-emailandfull_cleans, so a bad value is rejected at the operator's terminal rather than reaching the page. Security:primary_colorlands in a CSS custom property, where Django's HTML autoescaping gives no protection, and model validators do not run on a bare.save()β so the context processor re-validates the hex colour and drops any non-https://logo whatever is in the database; tests assert a CSS-breakout string, anhttp://logo, ajavascript:URL and an HTML display name are all neutralised. White-label is not anonymity: an affiliate-branded page still carries a "Service delivered on the WeOwn platform." line. 14 new tests; suite 59 passing. -
anythingllm-docker + weown-fleet β the instance now honours
subscription_active; a cancelled customer no longer keeps a working instance (2026-08-16) β billing has written this Keycloak user attribute since the entitlement work landed, and nothing read it. Diagnosing the chain end-to-end rather than patching the obvious end found the actual break: a Keycloak user ATTRIBUTE is not present inuserinfounless a protocol mapper puts it there, and the livechat-chattestclient carried exactly one mapper (groups). So the attribute was invisible to the dashboard even when set β a check bolted onto the dashboard alone would have "enforced" a claim that never arrives. Two halves therefore: weown-fleetkc-provision-tenant.shnow creates asubscription_activeclaim mapper alongside the groups mapper (idempotent; backfilling an existing tenant is just re-running the script), and the dashboard OIDC callback checks entitlement after the group check, returning 402 with a message that says the documents and workspaces are untouched. Deny only on an explicitfalseβ this asymmetry is deliberate and load-bearing: the claim is absent for every user whose client has no mapper yet and for every user billing has never touched, so failing closed on an absent claim would lock out the entire existing fleet the moment it deploys, turning an entitlement feature into an outage. Stated in the code so nobody assumes more than is true: the gate is exactly as good as billing's writes. The predicate accepts every shape Keycloak can produce β bare string, array (multivalued attribute), and real boolean (JSON-typed mapper) β rather than trusting one. Verification:node --checkclean;dashboard/test-entitlement.mjs(zero-dependency, run withnode test-entitlement.mjs) exercises 12 claim shapes including absent, null, uppercaseFALSE, padded" false ", cleared-to-empty and unexpected values β all pass.β οΈ Not yet applied to the live tenant client (blocked on SSH-to-identity-box access at the time of writing) and existing sessions survive up to 12h on the HMAC cookie, so revocation is bounded by session lifetime rather than immediate. The warn-then-lock grace period and the droplet-stop are separate owners (transactional email, and the fleet side). -
billing-docker β refund and chargeback now claw back affiliate commission (2026-08-16) β closes the exposure recorded as accepted in
OWNER-GUIDE-stripe-connect-and-splits.mdΒ§Refunds and raised directly by Tyler on the 2026-08-11 call: Stripe reverses the platform's share of a refund automatically but leaves every Connect transfer alone, so a $1,000 reversal cost WeOwn the $1,000 plus the ~$250 commission already paid out. Newcharge.refundedandcharge.dispute.created/charge.dispute.funds_withdrawnhandlers reverse every paid leg proportionally β refund 30% of a charge and each leg returns 30% of its cut β viastripe_svc.reverse_affiliate_splitsand a newSplitReversalaudit row per (event, leg). The clawback is target-based, not delta-based: each run computes what the charge's CUMULATIVE refunded amount says should be reversed per leg, subtracts what already was, and moves only the difference. That is what makes it safe against Stripe's at-least-once, sometimes-out-of-order delivery β a replayed webhook computes a delta of zero, and a partial refund followed by a full one totals 100% rather than 130%; a delta-based design over-reverses on any replay. It RETURNS failures and never raises, the same load-bearing shape aspay_affiliate_splitsand for the same reason β raising inside the caller's@transaction.atomicwould roll back audit rows for reversals that already executed at Stripe, and the retry would reverse the same money twice, which is the mirror image of the 2026-08-04 double payout.SplitPayoutgains an indexedcharge_id(populated at payout time) because a refund arrives as a CHARGE event and previously had no way to find its legs without re-deriving the invoice.charge.dispute.funds_reinstateddeliberately does not auto-repay: re-sending money is a new payment decision, not the undo of a reversal, so it logs loudly and leaves it to an operator β over-reversing is recoverable, under-reversing loses money to a party with no obligation to return it. Verification: 12 new tests (38 total, all passing) β full refund reverses the whole cut; partial refund is proportional; both tiers reverse, not just tier 1 (Tyler's explicit requirement); a replayed event moves money once; successive partialβfull refunds total exactly the cut and never more; a failing reversal returns a failure and records it rather than raising; one failing leg does not block the other; a leg that never paid out is skipped; an unknown charge is a no-op; and the webhook paths for refund, dispute and reinstatement each do the right thing.makemigrations --checkclean,migrateapplies,django checkclean. Known gap: rows written before migration 0010 have an emptycharge_id, so clawback cannot find them β backfill from Stripe before relying on it for any payout predating this change (in practice none exist yet; the first real charge has not happened). -
buzz-docker β Block Buzz relay wrapper + PRJ-432.2 deploy docs (2026-08-06) β new
buzz-docker/mirror of Block's productiondeploy/composestack (relay + Postgres + Redis + MinIO + Caddy) forbuzz.weown.tools, with placeholder.env.example, hardened Caddyfile, host-side secret bootstrap script (scripts/bootstrap-host-env.sh), and runbookdocs/PRJ-432.2-buzz-deployment.md. Live deploy uses upstreamBUZZ_COMPOSE_TLS=true ./run.sh start(image pull fromghcr.io/block/buzz, not on-droplet build). Secrets stay on the host (0600); public repo docs use<DROPLET_PUBLIC_IP>/example.complaceholders only. Follow-up: require immutableBUZZ_IMAGEdigest (reject:main/:latest), pin upstream git SHA in docs, Composecap_drop/no-new-privileges(Caddy +NET_BIND_SERVICE), bootstrap via local venv. -
billing-docker β 14-day free trial and a customer agreement gate on checkout (2026-08-11) β the two things blocking WeOwn.Chat's first revenue under the single-agency commercial model (Tyler call, 2026-08-11). Trial:
STRIPE_TRIAL_DAYS(config, never a constant β the same image has to sell the $5 drill product and the $1,000 live product, so switching is an env change plus a restart) drivessubscription_data.trial_period_dayson Checkout, withpayment_method_collection: alwaysandtrial_settings.end_behavior.missing_payment_method: cancelso a trial can never quietly become free-forever when the card is missing at conversion. Newtrialingsubscription status, entitled likeactiveβ a trial with a locked instance is not a trial β plusSubscription.trial_end, handlers forcustomer.subscription.created/trial_will_end, and a full Stripeβlocal status map (unpaidandincomplete_expiredpreviously fell through unmapped and silently left entitlement stale). The trial must not pay commission: the zero-amount check inpay_affiliate_splitsnow runs BEFORE the charge lookup, so Stripe's $0 trial invoice returns no splits and does not spend three API round-trips resolving a charge that cannot exist; it is logged at info, not warning, because it is now the normal path. Customer agreement: affiliates had click-wrap evidence (body SHA-256 + IP + UA + timestamp) and customers had nothing βnew_instancewent straight to Checkout.ContractTemplategains akind(affiliate|customer) with the single-active constraint corrected from GLOBAL to per-kind (publishing a customer agreement would otherwise have deactivated the affiliate one), andCustomerContractmirrors the affiliate signature record. Both routes to Checkout are gated, and the gate fails closed: no published agreement means no sale, because taking money without being able to prove what was accepted is the exposure the feature exists to close. Agreement text is a DB row seeded with a placeholder, so it is replaced in admin without a deploy. Review follow-up (2026-08-11) β five findings on ai#169, two of them real bugs. (1)ContractTemplateAdmin.has_change_permissionreturned False once a template had any signature, which combined with the one-active-per-kind constraint made publishing a new agreement version impossible after the first signature β v1 could not be deactivated, so v2 could not be activated, and the "a new version re-gates everyone" behaviour was unreachable through admin, the only route the business actually uses (the original tests missed it because they build templates via the ORM, bypassing admin permissions). The row is now changeable withkind/version/body_mdfrozen instead, and activating a new version retires its predecessor in the same save rather than raising IntegrityError. (2) the signature stored the LEFTMOSTX-Forwarded-Forhop, which is client-supplied β a signer could forge the very IP their click-wrap attests to. Caddy (reverse_proxy web:8000) sits in front, so plainREMOTE_ADDRis the proxy's constant container address and is no fix; the authoritative value is the RIGHTMOST hop (the one Caddy appends), with the full chain now kept verbatim in a newsigner_forwarded_forcolumn so a spoof attempt stays visible. Applied to the affiliate signature path too, which carried the identical flaw. Also: an internal ssh alias removed from the public drill doc, a comment corrected to describe the trial settings actually passed, and an unused import dropped. Verification: 26 tests (core/tests.py) all passing against Django 5.2 β trial kwargs sent/not sent, badSTRIPE_TRIAL_DAYSvalues degrade to "no trial" rather than crashing at import, $0 trial invoice moves no money and creates no payout rows, first real invoice does pay with the arithmetic checked, trialβactive clearstrial_end, cancellation de-entitles, checkout blocked unsigned via both routes, 409 with no template, signature hash pins the exact bytes shown, a new agreement version re-gates a previously-signed customer, and both agreement kinds can be active at once;makemigrations --checkclean,migrateapplies. Both review fixes carry regression tests confirmed to FAIL against the pre-fix code (5 failures) and pass after. Writing the original tests caught a third bug in this change: thetrial_end is not Noneguard skipped the explicitNoneStripe sends at conversion, so a paying customer would have kept a stale trial date forever β fixed with a sentinel default. -
New
landing-purchase/project β Astro Landing/Purchase page, plus aweownchat-designskill capturing the real brand tokens (2026-08-06) β first build-out of PRD roadmap item 2 (the top-of-funnel marketing site; the transactional purchase/billing flow itself already lives separately atbilling.weown.dev). Astro 7 (static output) + Tailwind CSS v4 via@tailwindcss/vite; started on Next.js, rewritten on Astro per direct direction to match the framework used elsewhere in the WeOwn ecosystem outside this repo (this repo itself has no other Astro usage to match conventions against β confirmed by a repo-wide search β so the setup is this project's own).npm run build(astro check && astro build) is clean and static, 0npm auditfindings. Design tokens (--bg #0e1726,--accent #00A3FF, system-ui font stack, 8/10/16/999px radius scale) are copied verbatim from the real dashboard (anythingllm-docker/template/dashboard/public/index.html's:root) and the Keycloakweownlogin theme β not invented separately β and codified as.claude/skills/weownchat-design/(SKILL.md+references/tokens.md) so future frontend work reuses this source of truth instead of drifting from it (an earlier draft of both the skill and the page shipped an independently-invented light/serif palette before this was caught and corrected). Notable: a "Live preview" section embeds the real, unmodified dashboard HTML in an iframe (landing-purchase/public/_demo/dashboard-preview.html), fed realistic fictional content (Harborview CPA, matching the existingweown-cpa-demo.pages.devplaceholder-brand convention) via awindow.fetchmock β no edits to the real template, and the Private/Public toggle drives the actual app's own hash router via vanilla JS. The hero's animated chat preview and the FAQ accordion (native<details name="faq-accordion">, zero JS) are likewise framework-free. Pricing intentionally shows no dollar figure (commercial terms are still draft/unsigned per the PRD)..gitignorenarrowed from a blanket.claude/exclusion to.claude/*+!.claude/skills/, so skills are shareable while local session state stays ignored. Scroll-reveal is hardened, not just ported: content is visible by default and only opts into the hidden-then-reveal transition once the script runs, with a 2.5s timeout that force-reveals anythingIntersectionObservernever fires for β added after testing surfaced the observer sometimes not firing at all for an element plainly in the viewport, in the specific automated-browser environment used to test this (cause unconfirmed; the fix holds regardless of cause). Open follow-ups, not yet decided: hosting/deployment target (droplet + Docker Compose, matching every sibling service, vs. a static host β Astro's static output keeps either path open β seelanding-purchase/README.md); page copy/IA beyond this first pass still needs a check against the Branding & Marketing guide's guardrails before anything ships as final. Verification:astro check+astro buildclean (0 errors/warnings/hints, static generation succeeds); manually checked at desktop/1440px/mobile widths in-browser; FAQ exclusive-accordion and mobile nav toggle behavior confirmed via direct DOM inspection, not just visual screenshots. -
anythingllm-docker dashboard β lock/unlock guard on shared library documents, plus the correctness gaps Copilot found in review (2026-08-01) β the document library is shared across every conversation in a workspace (Main + all threads all query the same private-workspace embeddings), so deleting a document doesn't just affect the chat you're in β it silently pulls grounding out from under every other conversation too. The existing two-click "Confirm?" on the trash icon wasn't enough of a speed bump for that blast radius. Added a per-document lock toggle in the document library modal (
public/index.html): locking a document shows a "Locked" badge next to its name and disables its delete button (with a tooltip explaining why); unlocking restores it. New dashboard-only state (server.js, sameSTATE_DIRread/write pattern as the embed-domain allowlist) βlocked-documents.json+POST /api/documents/lockβ since AnythingLLM's API has no concept of a locked document.GET /api/documentsnow includeslockedper document. Enforced server-side too:POST /api/documents/delete409s on a locked docpath regardless of what the client sends, so a stale UI state or a direct API call can't bypass the lock β the same defense-in-depth posture as the CSRF header and upload allowlist elsewhere in this file. Copilot PR review then caught two related gaps in/api/documents/scope, both fixed: (1) a missing/non-booleanonwas coerced to falsy and could silently detach a document nobody asked to detach β now requires an explicit boolean; (2) turning off a document's last remaining scope left it embedded in neither workspace but still sitting in ALLM's system document store β invisible to/api/documents(only shows what's embedded somewhere) and never cleaned up. Now purged there too, same as/api/documents/deleteβ and a locked document can't have its last scope turned off at all (409), closing the one path that could've bypassed the delete lock via scope toggling instead. Also fixednameOf()'s unguardedJSON.parse(d.metadata), which could throw on a malformed/legacy metadata blob and 500 the whole/api/documentsresponse. Verification:node --checkonserver.js+ the extracted inline script; live smoke against a stub backend β lock persists across reload, delete button disables/re-enables correctly, directPOST /api/documents/deleteon a locked docpath returns 409, unlock restores normal delete behavior. Second review pass (2026-08-05) hardened the remaining edges:docsOf()now treats any/api/v1/workspace/<slug>200 that doesn't yield a documents array as a failure (fail-loud) rather than an empty list β verified against a live ALLM that an empty workspace still returnsdocuments: [], so the destructive orphan-purge can never fire on a mis-parsed response;locked-documents.jsonwrites are now atomic (temp + rename) so a crash can't silently unlock everything, and a corrupt file is logged loudly rather than hidden; every lock read-modify-write and check-then-purge is serialized through an in-process mutex (no lost-update/TOCTOU on the lock set);readBodycaps the request buffer (256 KB); purged documents are dropped from the lock set so it stays bounded; and the scope/delete handlers no longer echo raw upstream ALLM error strings to the client (logged server-side instead). -
billing-docker: new copier template β the central WeOwn billing service (P0: take payment). Django 5 + Postgres + Caddy on the standard droplet pattern (Infisical ADR-006 entrypoints, proven backup/restore contract). Features: Keycloak OIDC login (same identity as the customer's instance), Stripe Checkout + Billing Portal for the single Private-AI-Instance product, 2-tier affiliate splits paid as Stripe Connect transfers (global defaults + per-affiliate overrides, append-only SplitConfig), click-wrap affiliate agreements (versioned immutable templates, sha256-pinned signature records), idempotent Stripe webhook ledger,
subscription_activeKeycloak attribute flip for elegant suspend/reactivate (no droplet pausing), and aweown-adminbreak-glass local superuser (own email, password via Infisical). -
anythingllm-docker dashboard β customer-visible
<think>leak closed + session-expiry auto-recovery (2026-07-31) β reasoning models (DeepSeek/Kimi) emit<think>β¦</think>blocks that AnythingLLM passes through verbatim; they rendered in customer chat replies (seen live in demo transcripts), narrating the model's internal reading of the customer's documents. Now stripped server-side at every text egress (live/api/chatreply + both history endpoints). Also: an expired session previously surfaced as generic errors on every action; the dashboard now detects 401 and reloads straight to the sign-in page β the exact "please log out and log back in" UX the C-Suite asked for, without the user needing to know it. Verification:node --check+ extracted-script parse; strip regex exercised against nested/multiline think blocks. -
supabase-docker β terraform/ansible/scripts/first-site layers added on top of v0.1 scaffold (2026-07-28) β the layers pending from the Jul 14 v0.1 integration (
964d5d2) now land. Layer 1 (DO Spaces remote state):template/terraform/backend.tf.jinjaβ S3-compatible endpoint, SSE-C encryption, private ACL. Layer 2 (bootstrap-secret rotation):rotate-bootstrap-secret.shembedded intemplate/terraform/templates/cloud-init.yaml.jinjaβ logs in with v1 from terraform state, mints v2 via Infisical Universal Auth API, atomically swaps the auth file, revokes v1, fails closed with logs to/var/log/<project>-rotation.log. Path C (thin cloud-init + ansible): cloud-init handles only first-boot bootstrap (Docker + Infisical CLI + Layer 2 rotation);template/ansible/deploy.yml.jinjaowns compose + Caddyfile + backup script + cron + Pop schema migration apply +docker compose up. Ansible roles:template/ansible/roles/{common,docker,supabase}/tasks authored; thesupabaserole handles Pop schema migration idempotency (skips whenpopschema already present). Pop schema migration:template/docker/migrations/001_pop_schema.sqlβ pop schema + tenants registry + 6 pop tables + 24 RLS policies +set_tenant_from_jwt()PostgREST hook +pop_adminbypassrls role (structural only). Secret generation helper:template/scripts/generate-secrets.sh.jinjamints all 10 Supabase app secrets including HS256-signedANON_KEYandSERVICE_ROLE_KEYJWTs (bash + openssl only). First site:sites/supabase-dev/β supabase-dev.weown.tools deployment rendered from the template as the pilot instance under PRJ-429.16 pgvector migration. Copier template compliance: addedtemplate/README.md.jinja+template/CHANGELOG.md.jinjamatching the keycloak-docker reference pattern. Compose hardening: Caddy service gains healthcheck (wget spider on port 80) matching the multi-service hardening baseline;depends_onblock gets jinja whitespace control for clean per-site rendering. Compliance: NIST CSF 2.0 PR.AC-4 (secret rotation), CIS v8 3.11 (encrypt sensitive data at rest via DO Spaces SSE-C), ISO 27001 A.8.24 (HS256 JWT minting). -
supabase-docker β pre-merge security/correctness fixes on the layer build-out (2026-07-29) β from the integration review:
pop.tenantsRLS enabled with a service_role-only policy (the blanket pop-schema grant had made every tenant'sapi_key/rls_keyreadable AND writable toanon/authenticatedvia PostgREST);PGRST_DB_PRE_REQUEST: set_tenant_from_jwtwired (without itapp.tenantwas never set and every RLS query returned empty);pg_dump --single-transactionremoved (invalid flag, ai#135 class); backup.sh gains the ai#134/#136 off-box upload contract (exit 1 before retention, object asserted by byte size);DASHBOARD_PASSWORD_BCRYPTminted by generate-secrets (stdin, never argv); restore.sh moves the Infisical client secret off argv +timeout 30on headless login + stopsmetaduring restore; GoTrue deploy probe fails the play on non-200; rendered site backup bucket βweown-prod-backups; copier MI varssecret: true. -
anythingllm-docker β
pgvectoras avector_dboption (2026-07-29) β adds pgvector alongside the default embedded LanceDB. Newvector_db=pgvectorcopier choice + conditional fail-loudPGVECTOR_CONNECTION_STRINGandPGVECTOR_TABLE_NAMEin bothcompose.prod.yaml.jinjaandcompose.local.yaml.jinjafor per-instance table isolation on a shared pgvector substrate. Fail-loud rationale mirrorsEMBEDDING_ENGINE: a silent fallback to LanceDB would orphan embeddings on a different DB; a silent shared table name would collide instances on shared pgvector. Password lives only in Infisical, never in compose. Existinglancedbsites render byte-identically. -
anythingllm-docker/sites/dev-weown-anythingllm β
INFISICAL_PATHcatch-up to the shared-project + folder-per-instance model β optionalINFISICAL_PATH--pathscoping in the site'sentrypoint-infisical.shandansible/deploy.yml(default empty = project root, backward-compatible). DEPLOYMENT_GUIDE Β§6.5 documentssite.confas the primary mechanism and the current container-side scoping caveat. -
anythingllm-dockertemplate β kimi-verified fleet defaults + MCP tool pack (2026-07-29) β updates the canonical copier template so new AnythingLLM sites ship with OpenRouterdeepseek/deepseek-v4-flash,OPENROUTER_TIMEOUT_MS/LLM_STREAM_TIMEOUT10000,AGENT_SEARCH_PROVIDER=searxng-engine,AGENT_SEARXNG_API_URL=https://searxng.weown.tools(copier varsearxng_api_url), bind-mounted./storage, and a declarative MCP pack (searxng / web-scraping / web-browsing / rag-memory / document-summarizer) with verified uvx/mcp pins and headless Puppeteer flags. No API keys in git β Infisical / Secure Handoff only. Ansibledeploy.yml.jinjaseedsstorage/mcp+storage/pluginson deploy. Also folds in INT-P07 deploy-hardening leftovers (Minimus token soft-fail + Caddy volume ownership) without regressing the env-var Infisical login pattern already onmain. -
anythingllm-docker dashboard β "Sign in with WeOwn" (Keycloak OIDC) (2026-07-24) β implements the customer-identity decision (one
weown-chatrealm, per-tenant OIDC client + group; weown-fleetkc-bootstrap-realm.sh/kc-provision-tenant.sh). When the tenant's Infisical folder carriesOIDC_ISSUER/OIDC_CLIENT_ID/OIDC_CLIENT_SECRET/OIDC_ALLOWED_GROUP(stored by fleet provisioning), the login page offers SSO as the primary path and tucks the password form behind a break-glass link; absent, nothing changes. Zero-dependency authorization-code flow: HMAC-signed 10-minutestate(no server-side session store), server-side code exchange, authorization by tenant-group membership in the issuer'suserinfogroupsclaim (asked directly over TLS β no local JWT verification to get wrong), same HMAC session cookie as password logins (SameSite=Laxon the SSO-set cookie so the cross-site redirect landing works; password cookie staysStrict). This finally gives Tyler/Mike/etc. individual named logins with password reset + MFA in Keycloak instead of a shared credential. Verification:node --check; 10-check live test against a stub IdP β authcfg gate, redirect shape, good-code β cookie β authenticated API call, wrong-group 403 without cookie, tampered-state 400, bad-code 401, password break-glass intact. -
anythingllm-docker β
cloudflare_proxiedmode: tenants can live behind Cloudflare's proxy (2026-07-23) β the per-brand<brand>.weown.chathosting model (Jason 2026-07-21) puts customer instances behind Cloudflare Pro (orange cloud), where the template's ACME HTTP-01 cannot complete β exactly the 525f1visa.weown.chatshows today. New copier boolcloudflare_proxied(default false β existing direct-DNS sites render identically): Caddy serves a Cloudflare Origin CA certificate instead of running ACME (tls /certs/origin.crt /certs/origin.key; zone SSL = Full (strict)). The PEMs arrive asCF_ORIGIN_CERT/CF_ORIGIN_KEYthrough the same ADR-006 Infisical entrypoint wrapper as every other secret β the caddy container gains the wrapper, and its command materialises them as 0600 files inside the container at start (never on host disk; re-fetched on restart). Origin-CA over DNS-01 was deliberate: no custom Caddy build and no DNS-edit API token sitting on customer droplets β one wildcard cert for*.weown.chat, readable only by operators at the initiative-project root, copied per-tenant-folder at provision time (droplet MIs stay path-scoped). Verification: copier renders clean in both modes and the rendered compose parses; the cert-materialise command proven against a multi-line PEM in env (content byte-identical, 0600 perms); fleet wiring (registry flag β render-deploy/provision) landed in weown-fleet. -
gitea-docker/β new copier template: Gitea with Keycloak SSO (2026-07-17) β self-hosted git for WeOwn internal use (Jason-approved Gitea + Keycloak), cloned fromkeycloak-docker/per the repo template standard and keeping its full stack: ADR-006 in-container Infisical injection, Path-C thin cloud-init + ansible app layer, Layer-2 bootstrap-secret rotation, skinny backups with DO Spaces offload, DO monitoring alerts. Gitea-specific: all logins via Keycloak OIDC (confidential client,gitea admin auth add-oauthbootstrap documented in the rendered README with a stdin-only secret prompt; local registration disabled, SSO auto-registration on,USERNAME=email,ACCOUNT_LINKING=auto), git-over-SSH on a dedicated firewall port (gitea_ssh_port, default 2222; inline-justifiedtrivy:ignore:AVD-DIG-0001),GITEA__*env-style config with only non-secret values in compose (SECRET_KEY/INTERNAL_TOKEN/JWT_SECRETfrom Infisical in prod),INSTALL_LOCK=true(env-only config, no web installer), no admin vhost (avoids keycloak-docker's D437 exposure), health via/api/healthz. Verification: template renders clean via copier; rendered compose/ansible YAML parse;tofu validatepasses; all scriptsbash -nclean; local end-to-end SSO proven β renderedcompose.local.yaml+ local Keycloak 24 (realmweown, confidentialgiteaclient withemail profile offline_accessscopes) β browser-equivalent OIDC dance β Keycloak login β callback β user auto-registered active β authenticated session. Reference configs:mcp/infragitea + keycloak_client roles. -
anythingllm-docker β customer dashboard (the product surface) + Simple-SSO + two-workspace bootstrap (2026-07-15) β the customer-facing managed-service layer. New
template/dashboard/is a zero-npm-dependency Node server (bind-mounted read-only into anode:20-alpinecontainer, no build/registry image for v0) that is the customer's ONLY surface: they authenticate to the dashboard (HMAC session cookie, sha256 password hash from Infisical, per-IP login throttle,X-DashboardCSRF header,SameSite=Strict; Secure), and the server proxies scoped actions to the AnythingLLM API with the admin Developer API key held server-side β the customer never receives ALLM credentials. Two sections: Public (upload public docs βws-public, embed-snippet screen) and Private (upload business docs βws-private, authenticated chat proxied withuserIdattribution). Caddy routes/app*β dashboard, everything else β ALLM (embed widget + WeOwn-operated admin UI).SIMPLE_SSO_ENABLEDadded to compose so the dashboard can mint single-use 1-hour login links (/api/v1/users/{id}/issue-auth-tokenβ/sso/simple) for a UI hop. Newscripts/bootstrap-product.shprovisions the two workspaces + domain-allowlisted embed widget + all dashboard secrets in-process (nothing on disk/history). ansible uploads the dashboard app; docs updated. Verification:node --checkon the server; live 8-check smoke test (healthz, unauth-reject, wrong-pw-reject, login, CSRF-reject, authed doc-list, bearer-authenticated chat proxy, logout-clears-session) all pass against a running server + stub upstream; compose YAML parses with all three services; rendered ansible parses. -
Per-customer LLM key provisioning + single-tenant instance lifecycle doc (2026-07-15) β
scripts/provision-openrouter-key.shmints a per-customer, budget-capped OpenRouter key via the OpenRouter Management API (POST /api/v1/keys,limit+limit_reset: monthly) and writes it into that customer's Infisical app project asOPENROUTER_API_KEYβ the value never touches the terminal, disk, or shell history; the provisioning key is sourced in-process from the operator Infisical project (OPENROUTER_PROVISIONING_KEY,read -rsfallback). Refuses to overwrite a live key without--force(no silent orphaning);--dry-runfor shared hosts. This automates the previously-manual "create a key in the OpenRouter dashboard and paste it" step in the per-sitebootstrap-*-infisical.shscripts, and gives each dedicated instance blast-radius isolation + a hard monthly spend cap. Newdocs/CUSTOMER_INSTANCE_PROVISIONING.mdsequences the existing deploy pieces into a per-customer provisionβdeployβoperateβdeprovision lifecycle (technical only; commercial/sales flow kept out of this public repo). Verification:bash -n+shellcheckclean. Compliance: NIST CSF 2.0 PR.AC-4 (least privilege per tenant), PR.DS-1; CIS v8 3.11. -
Open-branch consolidation β 10 branches integrated to
main, 19 retired (2026-07-14) β a single signed integration of every mergeable open branch, each individually code-reviewed (secrets/public-repo, correctness, template-standard), rebased onto currentmainso the latest template versions (incl. #78 Minimus fail-loud registry login) are preserved. New:supabase-docker/copier template (v0.1 β Postgres 15 + pgvector);anythingllm-docker/sites/dev-weown-anythingllm/(INT-P07/P08 do.weown.tools);devbox-docker/sites/dev-weown-devbox/(shared Zed remote-dev droplet, SSH-only, key-only sshd);keycloak-docker/sites/sso.weown.dev/migrated to Path-C servingsso.weown.id;openclaw-docker/sites/claw-weown-dev/+ template hardening. Changed/Fixed: anythingllm template defaults per Jason (DeepSeek V4 Flash, SearXNG agent-search); searxng bound loopback-only; wordpress core-plugin-bundle fetch task; owncloud oCIS template hardening (backup-name injection guard,ignore_changes=[user_data,tags], corrected security-model claims); smoke-test framework across all 7 templates (fails on unhealthy containers, correct auth-file path). Security/public-repo scrubs: real ProtonVPN egress IP β RFC5737; real Infisical project UUIDs β placeholders; offboarded-dev emails βalerts@example.com; openclaw ADR-006 auth file0644β0600/UID-1000; keycloak image pinned:24.0+ cloud-init{% raw %}rotation-breaker fixed. Retired (skipped + branches deleted): branches already folded intoe8496d1, the retiredsites/s004, obsolete pre-copier flat layouts, and (owner-decided) thezeroToHundredwebapp and the supersededsecurity-hardening-sshinventory. Deferred: M2 Infisical--client-secretargv pattern (pre-existing fleet-wide) tracked as a separate hardening pass. -
scripts/deploy-new-site.sh: Added--skip-infisicalflag to deploy to existing Infisical projects without creating new ones -
docs/WEOWN-APP-CLUSTER-RUNBOOK.mdΒ§5.5 β W24 Phase 3 operational findings (2026-06-11) β net-new subsection consolidating durable lessons from W24 Phase 3 dry-run execution against the live source cluster (TueβThu). Seven findings, each with reproduction commands and the evidence that surfaced them: Β§5.5.1 Target cluster sizing (combined-workload sizing, not source-match β 2-nodes-1vcpu-2gb-amdwas insufficient, scaled to 4 then 6, still 92β99 % allocated; recommend β₯7β8s-1vcpu-2gb-amdOR 3β4s-2vcpu-4gb-amd); Β§5.5.2 Velero controller memory (velero installdefault 256Mi OOM-kills on namespaces >50 items; explicit--velero-pod-mem-limit=768Mi+--node-agent-pod-mem-limit=512Minow baked into Β§4.2/Β§4.3); Β§5.5.3 Velero 4 h PVR-wait timeout Γ capacity wall (Velero marks restorePartiallyFailedafter 4 h even when no data has moved β triage via Pending pod describe Events); Β§5.5.4 Restore retry pattern β namespace nuke before re-fire (if pods scheduled on empty PVCs before PVRs ran, a re-fire produces a hollow restore βCompletedin <30 s with no PVRs; clean redo requireskubectl delete namespace <workload>-stagingfirst); Β§5.5.5 Post-restore CronJob cleanup (matomo-archive / n8n-backup CronJobs restore into staging and fire jobs against unreachable legacy destinations); Β§5.5.6 Interpreting benign warnings (table of 5 warning patterns that are skip-correct: cert-manager + cilium CRDs already exist, kube-root-ca.crt auto-managed, Helm release annotation drift, CiliumEndpoint cosmetic, 4 h PVR timeout); Β§5.5.7 AnythingLLM auth drift (post-backup admin reconfig invalidates restored password hash; informational, not a failure; soft-smoke via DB file presence). Companion edits to Β§3.1 (sizing callout) and Β§4.2 / Β§4.3 (--velero-pod-mem-limit=768Mi+--node-agent-pod-mem-limit=512Miflags). Real cluster identifiers redacted per.github/copilot-instructions.mdΒ§3.0. Source: W24 SOW Day-1 through Day-4 execution; Gate 1 evidence captured in PR comment for@nciminosign-off. -
scripts/create-signoz-oom-alert.shβ fleet-wide memcg-OOM pager (2026-06-10, #WeOwnVer v4.1.2.2) β creates one SigNoz log-based alert on the kernel'sMemory cgroup out of memory: Killed processline (already shipped to SigNoz by the fleet otel-agent's syslog receiver), closing the blind spot exposed by the INT-S004 incident: DO droplet monitor alerts watch host metrics and never fired while the AnythingLLM container was OOM-killed against its own compose memory limit (host RAM was fine). Env-only auth (SIGNOZ_BASE_URL+SIGNOZ_API_KEYviaread -rs,:?guards, nothing on argv/disk), idempotent (skips if the rule name exists), with a UI fallback recipe in the header for SigNoz schema drift. Companion:anythingllm-docker/sites/s004.ccc.bot/RESIZE_RUNBOOK.mdPhase 7. Compliance: NIST CSF 2.0 DE.CM-1; CIS v8 8.11. -
AGENTS.mdcanonical agent rules + cross-tool pointer stubs (2026-06-09, #WeOwnVer v4.1.2.1) β the secrets-hygiene standard now auto-loads into every agentic IDE/CLI, not just Zed/Copilot/Claude. New rootAGENTS.md(read natively by Codex, Cursor, Windsurf, Cline, Aider, Jules, Copilot agent mode) carries the portable standard: the agent context-leak ban (never read secrets into chat; Secure-Handoffread -rspattern), credential-sourcing rules (Infisical runtime / fail-loud env vars with fetch-from-where errors / interactive prompts β never argv), the two-layer gitleaks setup (pre-commit + CI pinned to the same version, bumped together), the 8-step mint-before-revoke rotation flow with multi-orphan cleanup, and the shell traps that have bitten production (SSH+heredoc expansion, unicode-after-$VAR,templatefile()$$). Byte-identical pointer stubs at.cursorrules/.windsurfrules/.clinerules/GEMINI.mdroute the remaining loaders to it;CLAUDE.mdgains the context-leak ban section + a first-read pointer;.rulesnow points at the canonical file and itstemplatefile()$$line is corrected to matchCLAUDE.md(the 2026-06-03 text still instructed the$$VARPID-expansion landmine that the 2026-06-02 fleet-wide fix removed). -
docs/WEOWN-APP-CLUSTER-RUNBOOK.md(2026-06-05, v4.1.1.1) β migration runbook for the W23 cluster team-move (<source_team>β<target_team>). Companion todocs/WEOWN-APP-CLUSTER-INVENTORY.md. Adopts the parallel-build + DNS-cutover pattern fromADR-005. Ten phases: target cluster provisioning at the highest available1.33.X-do.Yslug to match source's minor (per inventory Β§5.1; current operational slug is1.33.12-do.0since DOKS retired the source's exact1.33.1-do.2slug post-inventory β within-minor patch drift is API-compatible and Velero cross-restore is unaffected), addon install (ingress-nginx, cert-manager β₯v1.16 fresh, metrics-server), Velero install on both clusters pointed at shared<SPACES_BUCKET>, dry-run restore into staging namespaces with per-workload smoke tests (Gate 1), real restore on temp staging hostnames with dual-hostname cert-manager TLS pre-issuance, OTel collector to SigNoz Cloud, DNS TTL lowering 48 h ahead of cutover, non-matomo cutover (anything-llm β n8n β vaultwarden Pattern B-equivalent), matomo Pattern B with explicit freeze window +mysqldump --single-transaction+ DNS swap (Gate 2 with@nciminosign-off), 7-day soak with source stopped-but-intact, decommission. Includes Β§13 per-phase rollback table, Β§14 compliance mapping (NIST CSF 2.0 PR.IP-3 / RC.RP-1 / DE.CM-1; CIS Controls v8 11.1 / 12.4; ISO 27001:2022 A.5.30 / A.8.13). Real identifiers redacted per.github/copilot-instructions.mdΒ§3.0; operator-internal companion holds resolved values. Source: W23 SOW Day-3 deliverable. -
docs/WEOWN-APP-CLUSTER-INVENTORY.md(2026-06-03, v4.1.1.1) β pre-migration inventory of the W23 source cluster (<source_team>β<target_team>), captured via read-onlykubectl+helmagainst the source kubeconfig context. Documents 5 active Helm-managed workloads (matomo + MariaDB, anything-llm, n8n, vaultwarden), flags 2 anomalous namespaces (orphaned nextcloud PVC, endpoint-less searxng), and catalogs 10 PVCs totalling 123 Gi plus the ingress / DNS map. Β§5 records 7 scope decisions resolved with@nciminoduring 2026-06-03 review (k8s version pin at1.33.1-do.2to match source per Velero cross-restore constraint; vaultwarden Pattern B treatment; nextcloud + searxng +*-backupPVC decommission; Cilium + Hubble continuity; cert-manager fresh install β₯v1.16; ingress-nginx upstream image source). Real cluster identifiers, LB IP, node hostnames, kubeconfig context, Spaces bucket, and PV UUIDs redacted per.github/copilot-instructions.mdΒ§3.0; operator-internal runbook holds resolved values. Companion to forthcomingdocs/WEOWN-APP-CLUSTER-RUNBOOK.md(W23 follow-up PR). Compliance: NIST CSF 2.0 ID.AM-1, ID.AM-2; CIS Controls v8 1.1, 12.4. Source: W23 SOW Day-2 deliverable. -
.rules+docs/onboarding/β Zed dev-governance & setup runbook (2026-06-03, #WeOwnVer v4.1.1.1) β net-new dev-governance layer so WeOwn standards load into each developer's AI editor automatically (Zed loads only the first rules file it finds at a repo root, so the standards must be in that slot). Adds a root.rules(the first-match file: public-repo Β§3.0 redaction, Infisical secrets, branch naming, #OnlyHumanApproves, PR hygiene, with pointers to.github/copilot-instructions.md+CLAUDE.md+ the #FedArch PinnedDocs),docs/onboarding/zed-global-AGENTS.md(the cross-repo global-rules template devs copy to~/.config/zed/AGENTS.md), anddocs/onboarding/ZED_DEV_SETUP.md(step-by-step runbook: clone β global rules β project.rulesβ #FedArch RAG-sync per GUIDE-006 β Infisical β must-reads β verify). Companion business-governance doc proposed forCCCbotNet/fedarchas GUIDE-016. -
anythingllm-docker/DEPLOYMENT_GUIDE.mdβ operator onboarding guide (2026-06-03) β the single step-by-step source of truth for deploying a WeOwn AnythingLLM instance, written so other devs can build on the template. Documents the two-project Infisical secrets model (operator-onlyweown-tofuper-dev/shared infra tokens via each dev's own login, vs the per-site app project read by the droplet's Machine Identity), DO-Spaces remote tofu state (weown-prod-state, SSE-C) viaitofu.sh, the Path C + Layer 2 bootstrap, skinny GFS backups toweown-prod-backups, the reserved/static IP, Caddy auto-HTTPS, OTelβSigNoz observability (separate fleet agent), the fail-loudJWT_SECRET/OPENROUTER_API_KEY/ANYTHINGLLM_IMAGEguards, runtime env injection, the Infisical-injectedANYTHINGLLM_IMAGEversioning (in testing on AnythingLLM; Caddy still in-repo), the MinimusβDOCR mirror strategy (with an open licensing question), and thessh_source_cidrsβ Proton-VPN perimeter plan (currently open). Reference deployment: INT-S004. Linked from the template README. -
anythingllm-docker/sites/s004.ccc.bot/β INT-S004 recovery rebuild (2026-06-02, #WeOwnVer v4.1.1.1) β a fresh AnythingLLM droplet replacing the locked-outs004.ccc.bot. Re-rendered from theanythingllm-dockercopier template and validated by diff against the hardenedsites/ai.weown.agency/(INT-P01) β at render time its deployable files matchedai.weown.agencymodulo names (it has since been hardened further β see the Changed entries) (project_name=int-s004-anythingllm,domain=s004.ccc.bot, image injected from Infisical viaANYTHINGLLM_IMAGEβ deployedv1.12.1; the old box actually ran:latest(digest7a2f7157), so the restore forward-migrated the schema cleanly on first boot). Single-host Caddyfile (s004.ccc.bot, same hostname as the box it replaces), a dedicated s004 Infisical project (least privilege, distinct from INT-P01'sweown-anythingllm), a${JWT_SECRET:?...}fail-loud compose guard, andscripts/bootstrap-s004-infisical.sh(read -rsβ push to Infisical, no disk). Ships a recoveryMIGRATION_RUNBOOK.md(restore the off-boxs004_storage_<TS>.tar.gzexport). Closes the two old failures:JWT_SECRETunset on a container restart, and the never-installed daily backup cron. -
devbox-docker/β shared multi-user developer machine copier template (2026-05-30) β new*-dockertemplate for a single DigitalOcean droplet the team SSHes into for Zed (zed.dev) remote development (per Jason's 2026-05-20 "Zed is mandatory for the entire team" mandate,βΎοΈ WeOwn.Dev). Forked from the canonicalanythingllm-dockertemplate and adopts the same Layer 1 (DO Spaces remote tofu state) + Layer 2 (first-boot Infisical Machine-Identity bootstrap-secret rotation) + Path C (thin cloud-init +ansible/deploy.ymlapp layer) pattern fromdocs/INFRA_BOOTSTRAP_PATTERN.md. Diverges from the web-app templates: no Caddy/compose, no inbound 80/443 β the only inbound port is SSH (22). Security model: no shared team root β a single break-glass admin key (var.ssh_key_fingerprint) is the only root access (PermitRootLogin prohibit-password); each member gets their own non-root account (login = lowercased CCC Short ID, UID β₯ 1000) in adevsgroup gated byAllowGroups devs root, withauthorized_key exclusive: trueso a key opens only its owner's account; members get no sudo and docker-group membership is opt-in per member (root-equivalent β documented). Per-user OpenRouter keys for Zed's AI agent are handled byscripts/setup-zed.sh(read -rsprompt, stored only in a0600 ~/.config/<slug>/openrouter.env, never insettings.json/argv/logs; optional--infisicalpath keeps the key in the member's own Infisical account behind aninfisical run -- zedlauncher). The roster lives inansible/members.yml(gitignored;members.example.ymlshipped) so onboarding (scripts/add-user.sh) and offboarding (scripts/offboard-user.shβ archives the member's/homeoff the box, removes the account, then prints an Infisical/secret-rotation revocation checklist) are an edit + re-run, never atofu taint. Also ships team-standard Zed settings, a dev toolchain (git, node, opentofu, doctl, copier, ansible-lint, YAML/Python LSPs), daily skinny backups of/home+/etc(secrets hard-excluded), DO monitoring alerts, and operator + end-user docs (README.md,docs/CONNECTING-WITH-ZED.md). Validated:copierrender +tofu validateclean,bash -non all scripts, YAML parse of cloud-init/ansible/roster. -
.github/ADR-005-int-p01-doks-retirement.mdβ Proposed ADR (2026-05-25) documenting the INT-P01 retirement from DOKS, the parallel-build + DNS-cutover migration pattern, two-gate validation (Jason/Yonks staging soak; CTO production-cutover approval), and compliance mappings across NIST CSF 2.0, SOC 2, ISO/IEC 27001:2022, ISO/IEC 42001:2023, CIS Controls v8. Status moves Proposed β Accepted at close of the 7-day post-cutover soak. -
.github/ADR-006-in-container-infisical-injection.mdβ Proposed ADR (2026-05-30, #WeOwnVer v3.4.5.1) establishing in-container Infisical injection as the runtime secret-delivery standard for*-dockerservices. Documents the move from the host-sideinfisical run -- docker compose upwrap (secrets baked at container-create time β refresh only on--force-recreate/redeploy) toinfisical runas the container entrypoint (re-fetch ondocker restartβ bounce-to-refresh), reusing the Layer-2 Machine Identity + CLI already on the droplet (no rebuilt image, no new credential). Covers blast-radius scoping (per-workload, per-project identity), auto-reload (--watch/Infisical Agent), automatic rotation (consumer-side trigger), single-use/wrapping tokens (Machine Identity Token Auth, Max-Uses=1), and the K8s/K3s migration (Infisical Secrets Operator + Kubernetes-native auth +auto-reloadannotation). Pilots onanythingllm-docker/sites/s004.ccc.bot/; other services adopt in follow-up PRs. Priority sponsor: Jason (SignalβΎοΈ WeOwn.Dev2026-05-21; D383 gate on s004 working). -
docs/INFRA_BOOTSTRAP_PATTERN.mdβ new "Runtime secret injection" section (2026-05-30) added as the companion to Layers 1β2 + Path C: documents current host-side-wrap semantics, the in-container target, auto-reload/rotation/one-time-token, and the K8s mapping; updates the single-use-token bullet under "future hardening" to the Infisical-native Token-Auth form; refreshes the stalesites/s004/proving-ground reference to the lives004.ccc.bot; last-updated 2026-06-04. -
Per-
*-dockerREADME "Secret injection pattern" pointers (2026-05-30) β anythingllm, keycloak, openclaw, sandbox, searxng, signoz, wordpress READMEs now link the canonical pattern (INFRA_BOOTSTRAP_PATTERN.md + ADR-006). -
anythingllm-docker/sites/ai.weown.agency/β INT-P01 DOKS β Docker migration site (2026-05-25) β generated from theanythingllm-dockercopier template withproject_name=int-p01-anythingllm,domain=ai.weown.agency,anythingllm_image=reg.mini.dev/anythingllm:1.7.2(WeOwnLLM hardened, per D381; pinned to the version verified on s004.ccc.bot). Adopts the Path C + Layer 2 bootstrap pattern fromdocs/INFRA_BOOTSTRAP_PATTERN.md: slim cloud-init that hands off toansible/deploy.yml, Layer 2 bootstrap-secret rotation, Layer 1 DO-Spaces remote tofu state, dual-hostname Caddyfile (ai-stage.weown.agency, ai.weown.agency) so the production cutover is a DNS A-record swap with no re-deploy. Includes phased migration runbook (MIGRATION_RUNBOOK.md) with an optional local-laptop dry-run phase, and a one-shot bridge scriptscripts/migrate-from-doks.shthat streams/app/server/storageout of the DOKS pod viakubectl execand wraps it in the skinny-backup tarball layout the existingscripts/restore.shalready understands. Source plan: D383 / Tuleap A174 ([#1238]) / SignalβΎοΈ WeOwn.Dev2026-05-21 ask from Jason. -
anythingllm-docker/sites/README.mdβ new directory-level README documenting thesites/pattern (matching the existingkeycloak-docker/sites/README.mdconvention), with a current-deployments table and a "creating a new site" walkthrough. -
sandbox-docker/adopts canonical Layer 1 + Layer 2 + Path C (2026-05-26) β migrated the AIO Sandbox copier template to the WeOwn bootstrap pattern defined indocs/INFRA_BOOTSTRAP_PATTERN.md. Adds DO Spaces remote state (backend.tf.jinja+init.sh.jinja), embeddedrotate-bootstrap-secret.shin cloud-init (mints v2 Infisical Machine Identity client secret via Universal Auth API on first boot, atomically swaps.infisical-auth.env, revokes v1), thin cloud-init +ansible/deploy.yml.jinjaapp layer withcommunity.docker.docker_image_pull(CLI-based; no python3-docker SDK dependency), auto-tagging viascripts/tag-droplet.sh(commit-<sha>, feature tags),ssh_source_cidrsfirewall pinning, Caddy file logging with/var/log/caddybind mount for otel-agent ingestion, and pinnedsandbox_imagedefault (ghcr.io/agent-infra/sandbox:0.4.0, no more:latest). Switches Infisical CLI install from legacyinstall-cli.sh(capped at v0.38) to theartifacts-cli.infisical.comapt repo. -
scripts/bootstrap-otel-agent.sh(2026-05-23) β one-time per-droplet bootstrap for the OTel fleet agent. Installs the Infisical CLI if missing and writes/opt/otel-agent/.infisical-auth.env(0600 root) with the Machine Identity credentials for the Infisicalotelproject. Supports--droplet <name>,--tag <tag>, and--host <user@ip>selectors. Validates the Machine Identity can log in before exiting. Compliance: NIST PR.DS, CIS 3.11, ISO A.5.16. -
otel-agent/README.md(2026-05-23) β full operator runbook for the SigNoz Cloud observability path: architecture diagram, secrets model (Infisical runtime injection, never on disk), safety notes for existing droplets (all mounts read-only, loopback health port, 256MB cap), per-droplet bootstrap + deploy steps, verification checklist, key rotation procedure, troubleshooting table, compliance mappings.
-
*-dockertemplates β the image registry is one copier answer (2026-09-30) βreg.mini.dev(Minimus) shuts down 2026-10-22 (WeOwnDev/weown-fleet#112), and the host was hardcoded across 11 templates (anythingllm, billing, gitea, keycloak, openclaw, owncloud, sandbox, searxng, signoz, supabase, wordpress). Each now asksimage_registry(defaultreg.mini.dev) andregistry_username(default = the username that template logs in with today:minimusfor anythingllm/openclaw,tokenfor the rest); image defaults, ansible inventories and everydocker loginread them. The registry password's secret-store key is chosen BY REGISTRY, not by presence: a hidden computed answerregistry_token_keyisMINIMUS_TOKENwhileimage_registryisreg.mini.devandREGISTRY_TOKENfor any other registry, and every deploy playbook (Infisical and OpenBao paths) reads exactly that one key β no fallback. So the new registry'sREGISTRY_TOKENcan be stored in every tenant's store before a site switches without a still-on-Minimus site logging in toreg.mini.devwith the wrong key. With default answers a render is byte-identical to before, except anythingllm's login task below. Committedsites/renders are untouched; each rolls over per site under weown-fleet#112. The Terraformminimus_tokenvariable (really the DO API token) is not renamed here. anythingllm's registry login now passes the token on ansible'sstdin:(command+argv) instead of writing it into a shell script's text, which put it on bash's argv on the droplet. -
WeOwnChat onboarding (billing + landing) β Keycloak register vs sign-in + no-instance paywall (2026-09-15) β see
billing-docker/sites/billing/CHANGELOG.md,billing-docker/template/CHANGELOG.md.jinja,landing-purchase/CHANGELOG.md, and.github/ADR-007-billing-keycloak-registration-onboarding.md. PR #252. -
scripts/provision-openrouter-key.sh β
--bao-path: the minted per-customer key can land in OpenBao instead of Infisical (2026-09-04) β new weown-fleet tenants integrate with the WeOwn OpenBao platform store (Nik, 2026-09-04: "credentials should be integrated with openbao, not infisical"), so the fleet provisioner needs the customer key written toweown/platform/<slug>. The PROVISIONING key is still read from the operator Infisical project (operator tier); with--bao-paththe minted key travels env βjqβ stdin βbao kv patch(never argv) and is read back from the store before success is reported. Infisical behaviour without the flag is unchanged. Consumer:weown-fleetscripts/provision-instance.shPhase 1 viascripts/lib-secrets.sh. Verification:bash -nclean; dry-run path unchanged. -
anythingllm-dockercustomer dashboard β full UI/UX redesign to a modern chat-app standard (2026-07-29) βpublic/index.htmlandpublic/login.htmlrebuilt against the same real endpoints, unchanged constraints (zero-dependency, single self-contained file each): a collapsible sidebar with a horizontal elevated-chip tab switcher (private/public selection now reads through elevation/form rather than color β color is reserved for the semantic public/private cue, carried by apillbadge in the main header plus a warning banner on the public pane instead of the tab selector itself; a deliberate mid-review pivot from the original ask, made because two different signals β "which tab is selected" and "is this tab public" β were competing for the same color channel); a real chat surface for the private assistant (message bubbles with an accessibility fix β the original blue-fill/white-text combination measured ~2.7:1 contrast, below WCAG AA's 4.5:1 floor for body text, now a dark navy bubble at ~11.5:1; copy on every message, edit-and-resend on the ones you sent; a landing state that centers the greeting and composer together, homepage-style like modern AI chat products, instead of docking the composer at the bottom before any conversation exists); deferred conversation creation ("New conversation" opens a blank composer immediately with no upfront naming prompt β the thread is created server-side with a title derived from the first message actually sent, once it's sent, so the common case never needs a rename call); a kebab menu (rename/delete) replacing the old flat picker; confirm-before-remove (click once to arm, click again to confirm) added to document and allowed-website removal for parity with conversation delete; the public tab restyled onto card-based sections (Documents / Allowed websites / Embed snippet) with the snippet in a bordered code block that wraps instead of overflowing on narrow screens β the exact bug named in the original brief. All dynamic rendering rebuilt on safe DOM construction (createElement/textContent) instead of the previousinnerHTML-based rendering β which, checked against the pre-redesign source, mixed a sharedesc()helper (thread names, allowed-website domains) with a separate, weaker inline escape for chat text (stripped<only, not&/>/quotes) and, for document names specifically, no escaping at all (${x.name}interpolated straight intoinnerHTML). The rewrite applies the same safe-construction treatment everywhere uniformly, closing that document-name gap rather than just preserving the original's (inconsistent) behavior. The composer carries a visibly disabled file-attach affordance (paperclip icon, "coming soon" tooltip) β no upload wiring behind it, since neither AnythingLLM's nor the underlying model's chat-attachment support is confirmed yet.Two small additions to
server.js(the four named security invariants βX-DashboardCSRF header, HTML-escaping discipline, upload type/size limits, non-emptyable allowed-websites list β otherwise untouched):POST /api/threads/:slug/rename(proxies to AnythingLLM's thread-update API; path unverified against a live instance β no ALLM access in the environment this was built in, so this is the first thing to check if renaming 502s in practice) andGET /api/whoami(returns the already-existingDASHBOARD_CUSTOMER_EMAILβ previously computed server-side but never sent to the client β now shown in the sidebar; gracefully hidden on instances that don't set that optional env var).Known limitation: edit-and-resend ("rewind") truncates the conversation client-side and resends for real, but does not delete the original trailing messages from AnythingLLM's own thread history β no confirmed partial-deletion API exists (only whole-thread clearing, which would be more destructive than intended). Reloading that thread will show the old messages reappear ahead of the resend; nothing is lost, it just isn't a clean server-side history rewrite yet.
Verification: no live AnythingLLM instance was available while building this, so verification is static + simulated rather than live end-to-end:
node --checkon all three files; HTML tag-balance parsing on both pages; a scriptedjsdomsession against afetchstub matching every real endpoint's request/response contract, driving the full interaction surface (login success/failure, tab switch, thread switch/create/rename/delete, send/rewind including the thread-creation-failure rollback path, document/domain add/remove, snippet copy) with zero runtime errors;server.jsbooted locally against a throwaway dev password hash and curled directly to confirm the auth gate, the CSRF gate, and the new rename endpoint's input validation all behave correctly against the real server, not just the mock. Layout fixes (composer-width parity between the landing and docked states, mobile-width snippet overflow) verified with real browser screenshots at 1280Γ800 and 390Γ844. Before merge: needs a pass against a real AnythingLLM instance to confirm the rename endpoint and general live behavior β flagged to the reviewer in the PR. -
anythingllm-dockerdashboard β document upload redesigned into one shared library with per-surface toggles (2026-07-30) β the private and public tabs each had their own separate "upload a document" box, so a document useful for both (a service menu, say) had to be uploaded twice, and there was no single place to see everything a customer had ever uploaded.public/index.htmlreplaces both with a Documents button (header, either tab) opening one shared library modal: every uploaded document gets two independent toggle pills β Private chat and Public site β reusing the exact green/red the app already uses for the private/public distinction elsewhere, so "this is visible on the public website" reads as the same warning color everywhere, not a new one to learn. Each tab's header strip now shows a compact, live summary ("documents your private assistant uses" / "documents your website chatbot uses") instead of a duplicate upload UI. Uploading from a tab defaults the new document on for that tab's scope, so the common one-click case still works; widening it to the other surface is a separate toggle click.server.jsreplaces the three scoped routes (GET /api/documents/:scope,POST /api/upload/:scope,POST /api/remove/:scope) with four unscoped ones:GET /api/documents(the library β the union of what's embedded in the private and public AnythingLLM workspaces, merged bydocpath),POST /api/upload(now scoped by anX-Upload-Scopeheader instead of the URL β a document is always embedded into at least one workspace at upload time, so nothing is ever orphaned in the system document store with no scope pointing at it),POST /api/documents/scope(toggles one workspace's embedding on/off for an existing document), andPOST /api/documents/delete(detaches from both workspaces and purges the underlying file). All four are built entirely from ALLM calls the pre-redesign code already made and had proven working (workspaceGET,update-embeddingsadds/deletes,system/remove-documents) β no new/unverified ALLM API surface for this feature, unlike the earlier thread-rename endpoint. The four security invariants (X-DashboardCSRF header, HTML-escaping, upload type/size limits, non-emptyable allowed-websites list) are untouched.Verification:
node --checkonserver.js; HTML tag-balance check onindex.html; a stateful local stub of the four ALLM endpoints involved, withserver.jsbooted for real against it (throwaway dev password hash) β curled directly to confirm the merge-by-docpath logic (a document embedded in both workspaces, one in only private, one in only public, all reported correctly), the scope-toggle and delete round-trips, the upload-scope default, and that the CSRF/auth/upload-type guard rails are all still enforced. Then driven through a real Chromium session against the same running server (not a static mock): logged in, opened the library, toggled a document's public scope on, searched, ran the two-click delete confirm, and confirmed both tabs' header summaries and counts updated live after each action. -
anythingllm-dockerβ migrateditofu.shto the A405 folder model (shared + per-site), fixing stale deploy-specific TF_VARs (2026-07-15) β anythingllm was the last template still readingweown-tofuflat (infisical run --projectId=<weown-tofu>with no--path), so every deploy inherited whateverTF_VAR_infisical_project_idwas last set at the project root β surfaced live as a droplet pointed at a stale app project (8420b42e) while its secrets lived elsewhere. Migrated to the openclaw/claw reference model:infisical run --path=/infra/shared --path=/infra/sites/$SITE(SITE=SITE_NAMEor the renderedproject_name), so shared infra vars (do_token,spaces_*,ssh_key_fingerprints,alert_email) come from/infra/sharedand per-site vars (infisical_client_id/_secret/_project_idβ the app-project MI the droplet reads) come from/infra/sites/<site>. No deploy-specific TF_VAR at the root anymore. Consequently reverts the earlier singularssh_key_fingerprintchange (correct for the flat model) back to the pluralssh_key_fingerprints/list(string)that/infra/sharedprovides. Requires a/infra/sites/<site>folder per instance inweown-tofu. Verification: rendereditofu.shbash -n+shellcheck -S errorclean; terraform parses (tofu fmt). -
anythingllm-docker/template/ansible/deploy.yml.jinjaβ Minimus registry login + rotation marker (2026-06-12) β addsdocker login reg.mini.devvia CLI (--password-stdin, no Docker SDK dependency) usingMINIMUS_TOKENfrom Infisical, so fresh droplets can pullcaddy:2and the AnythingLLM image without manual auth. Adds fail-loud.rotation-failedmarker check in pre_tasks (gated on.rotation-completenot existing, so the warning stops after manual rotation succeeds). Cloud-initfail()now writes.rotation-failedwithchmod 0600to prevent API response leakage to non-root users. -
anythingllm-dockertemplate: bounce-proof config + right-sized memory after the INT-S004 OOM (2026-06-10, #WeOwnVer v4.1.2.2) β incident-driven hardening of the copier template (and thes004.ccc.botsite β full incident narrative inanythingllm-docker/sites/s004.ccc.bot/CHANGELOG.md). The 2026-06-10 01:00 MT memcg OOM kill of INT-S004's AnythingLLM container revealed two template-level gaps: (1)EMBEDDING_ENGINEhad a silent:-nativecompose fallback β after the auto-restart the app came back on a different embedder than the one that built the LanceDB vectors and every RAG query failed on a dimension mismatch; it is now a fail-loud:?guard likeJWT_SECRET/OPENROUTER_API_KEY/ANYTHINGLLM_IMAGE, and the compose header + template README now state the rule that Infisical is the only source of truth for runtime config (UI changes do not survive a bounce). (2) The AnythingLLM memory limit was hardcoded at 2G regardless of droplet size; it is now theanythingllm_memory_limit/anythingllm_memory_reservationcopier vars (defaults unchanged: 2G/1G on the 4 GB droplet) with sizing guidance, since agent-RAG chats with pinned docs + the in-process native reranker legitimately spike RSS. Also adds explicitresize_disk = falseto the droplet resource (template + s004): dropletsizechanges stay CPU/RAM-only, reversible, and leave the disk + Docker volumes untouched. Compliance: NIST CSF 2.0 PR.IP-3, RC.RP-1; CIS v8 4.1/4.2; ISO 27001 A.8.9. -
Refreshed all
anythingllm-dockerdocs to the current pattern (2026-06-03) β swept the template README + top-level README +sites/README.md+ both live sites' READMEs/runbooks/changelogs to remove stale "old way" content now that INT-S004 is live. Replaced thecopier copy β¦ --data-file answers.yamlrender with the realsites/<domain>+--dataform; documentedANYTHINGLLM_IMAGE(Infisical-injected image, no longer a render-time /variables.tfvalue) in every secrets table + Updating row; corrected the wrong "docker compose restarttriggers a freshinfisical run" claim everywhere to "re-rundeploy.sh(recreates the container);restartreuses the old env"; switched provisioning prose fromterraform.tfvars/tofu applytoitofu.sh/weown-tofu(TF_VAR_*, no disk); added OTelβSigNoz observability + the OpenRouter-key-from-restored-DB caveat (401 Missing Authentication headerβ re-enter in the UI). Slimmedtemplate/README.md.jinjato point atDEPLOYMENT_GUIDE.mdinstead of duplicating the flow, fixed the broken site-relative../docs/links (β../../../docs/), marked the s004.ccc.botMIGRATION_RUNBOOK.mdCOMPLETED, and trimmed the retiredsites/s004/tombstone to a banner + pointers. The top-level "Migration status" section (which still called the retiredsites/s004/the "first deployment" and said other templates "should migrate to match") is now "Pattern status" pointing at the lives004.ccc.botreference. -
Infisical-native OpenTofu provisioning (2026-06-02) β added
terraform/itofu.sh(+ template) that runstofuunderinfisical runagainst a separate operator-onlyweown-tofuInfisical project, so theTF_VAR_*infra secrets (DO API token, Spaces keys, droplet Machine Identity) never touch a committed file, a localterraform.tfvars, or the s004 app project (whose Machine Identity the droplet can read).initforwards the Spaces creds to the S3 backend.terraform.tfvars+init.shremain a gitignored fallback. Runbook Phase 1 documents theweown-tofusetup + the isolation rationale. -
Team SSH access standard for AnythingLLM ops boxes (2026-06-02) β
ansible/deploy.yml(+ the template) writes the team's public keys from the InfisicalOPS_AUTHORIZED_KEYSvar to root'sauthorized_keys.weown-ops(managed exclusively, alongside the untouched DO break-glass key) and adds it to sshd'sAuthorizedKeysFilevia a drop-in. Grant/revoke = editOPS_AUTHORIZED_KEYS+ re-rundeploy.sh(revoke-on-termination supported);sshd -tgates the reload so a bad config can't lock the box.scripts/bootstrap-s004-infisical.shprompts for the key list. Piloted ons004.ccc.bot. -
Standardized DO Spaces bucket names (2026-06-02) β
weown-terraform-stateβweown-prod-state(state) andweown-backupsβweown-prod-backups(backups), in the template +s004.ccc.bot. Other sites/services keep the old names (separate coordinated migration to avoid orphaning live state). -
Renamed terraform var
minimus_tokenβdo_tokeninanythingllm-docker(2026-06-02) β the variable feedsprovider "digitalocean" { token = ... }, so it is the DigitalOcean API token (droplet host), not a Minimus credential; the Minimus image-registry token is the separate optionalMINIMUS_TOKENInfisical secret (reg.mini.devpulls). Renamed in the template.jinja+ thes004.ccc.botsite (+ its runbookTF_VAR_do_token), with the distinction documented in the tfvars example.ai.weown.agencyand the retireds004/keepminimus_tokenfor now (renaming a live site's var breaks its localterraform.tfvarsβ separate coordinated change). -
Fail-loud
JWT_SECRETguard +/api/pingcaveat acrossanythingllm-docker(2026-06-02) β added a${JWT_SECRET:?...}guard to the template (anythingllm-docker/template/docker/compose.prod.yaml.jinja) and both existing sites (ai.weown.agencyand the retireds004), so adocker compose upthat bypassesinfisical runrefuses to start instead of serving broken auth (the INT-S004 lockout mode). Each README now notes that/api/pingreturns 200 even when auth is broken, so health checks must not rely on it alone. The retireds004/(s004.ccc.bot) row inanythingllm-docker/sites/README.mdis marked superseded bys004.ccc.bot/. -
k8s/docs/CLUSTER_MIGRATION.mdβ forward note on secrets (2026-05-30) β added an Important-Notes callout that the K8s/K3s target is Infisical Secrets Operator (InfisicalSecretCRD) + Kubernetes-native auth +auto-reloadannotation, the same runtime-injection pattern as the Docker fleet (ADR-006), rather than the nativekubectlSecret backups this node-pool runbook currently uses. -
docs/WEOWN-APP-CLUSTER-RUNBOOK.md(2026-06-05, v4.1.1.1) β Phase 2 pivoted from chart-based tovelero installCLI after Day-4 execution surfaced thecluster-backup/Helm chart as broken across multiple resource types and flag formats (pattern matches codehelm template-validated but neverhelm install-tested against modern Velero). Bugs found include: CRDs not packaged inhelm/crds/; BSL helper uses pre-Velero-1.0 schema (spec.bucketinstead ofspec.objectStorage.bucket); Schedule helper wraps BackupSpec in Pod-styletemplate.metadata + template.specinstead of flattemplate; VSL template emits invaliddefault: truefield;--default-volume-snapshot-locationsflag passes location name without requiredprovider:prefix; missing AWS plugin init container (mandatory since Velero 1.10); deprecated--restic-timeoutand--restic-restore-timeoutflags; unknown--metrics-enabledflag; VeleroappVersionpinned at retired 1.12.2. Β§4 (Phase 2) rewritten to usevelero installCLI with--provider aws --plugins velero/velero-plugin-for-aws:v1.11.0 --bucket <SPACES_BUCKET> --backup-location-config region=atl1,s3Url=...,s3ForcePathStyle=true --secret-file <mktemp_creds> --use-volume-snapshots=false --use-node-agent --uploader-type=kopia(Velero v1.18 removed Restic uploader, Kopia is canonical), with--velero-pod-cpu-request=50m / --node-agent-pod-cpu-request=50moverrides tuned for the W23 cluster'ss-1vcpu-2gb-amdnodes (defaults at 500m/200m don't schedule on this size). Β§4.2 / Β§4.3 reordered so target install runs first (zero risk on empty cluster, validates bucket connectivity). Β§13 rollback Phase 2 entry updated fromhelm uninstalltovelero uninstall --force. Chart's preset Pattern A Schedules and ServiceMonitor are NOT installed byvelero installβ re-added as a thin "WeOwn add-ons" layer post-cutover (separate follow-up, out of W23 scope). Β§3.1 (cluster create) version pin updated to1.33.12-do.0β DOKS retired source's exact1.33.1-do.2slug for new cluster creation post-inventory; within-minor patch drift is API-compatible and Velero cross-restore is unaffected, but the explicit pin needed to reflect operational reality. Added--auto-upgrade=falseto the create flags to prevent DOKS from bumping the cluster's K8s version mid-migration. Β§1 (Approach) bullet 1 reframed from exact slug pin to "highest available1.33.X-do.Yslug to match source's minor". Source: W23 SOW Day-4 execution. Full chart-bug list tracked in operator-internal notes for the next@nciminosync; possible follow-ups are either a focusedfix(cluster-backup)PR or chart deprecation in favour ofvelero installCLI + a thin Helm wrapper for WeOwn-specific Schedules / ServiceMonitor (out of W23 scope). -
docs/WEOWN-APP-CLUSTER-RUNBOOK.md(2026-06-05, v4.1.1.1) β earlier same-day amend (now superseded by the chart-pivot entry above): Phase 2 + Phase 5 patterns amended after Day-4 validation of the secret-pull flow end-to-end. Phase 2 (Velero install): replaced interactivedeploy.shprompts with aninfisical run --projectId=<W23_INFISICAL_PROJECT_ID> --env=prod -- bash -c '...'wrapper that mapsSPACES_ACCESS_KEY/SPACES_SECRET_KEY(per@ncimino2026-06-05 directive on Infisical key naming) to the chart's expectedS3_ACCESS_KEY/S3_SECRET_KEYenv-var contract (seecluster-backup/deploy.shlines 111-122). Non-secret values (bucket, region, endpoint, tenant, cluster, environment) inlined as explicitexportlines. Bucket strategy switched from "shared bucket + prefix" to a dedicated per-cluster bucket (<SPACES_BUCKET>) β thecluster-backupchart'sBackupStorageLocationhelper does not emit aprefixfield, so per-cluster bucket matches the existing per-tenant-bucket precedent in the WeOwn DO Spaces fleet without requiring a chart change. Phase 5 (OTel): dropped the Machine Identity universal-auth pattern in favour of user-levelinfisical login+infisical secrets get OTEL_URL / OTEL_KEY --env=prod(theotelproject's dev env values are placeholders; only prod is operationally configured). Operator's CLI session is the auth boundary; secret values pass tokubectl create secret genericvia the existing$(mktemp)+trap+--from-env-filepattern, preserving the--from-literalban from.github/copilot-instructions.mdΒ§3.0 PR.DS. Infisical K8s Operator +InfisicalSecretCRD path tracked as a post-W23-soak follow-up so the migration itself stays scope-tight. Β§2.1 (Access) rewritten to surface the user-level Infisical access pattern (Viewer onotelprod env; owner on<W23_INFISICAL_PROJECT>prod env) and drop the MI bullets. Β§13 (Rollback) Phase 5 entry updated to point at Infisical prod env, not Pass vault. Date stamp bumped to 2026-06-05. Source: W23 SOW Day-4 execution +@nciminoSignal 2026-06-05 directive. -
Observability pivot to SigNoz Cloud (2026-05-23) β per CTO direction, WeOwn AI now sends telemetry to SigNoz Cloud (Yonks' managed account), not a self-hosted SigNoz droplet. The
signoz-docker/copier template is preserved as an optional/fallback path only β seesignoz-docker/README.mdfor the status banner.OTEL_URL+OTEL_KEYlive in the Infisicalotelproject and are injected at everydocker compose upviainfisical run(never stored on disk; container restarts pick up rotated values automatically). -
otel-agent/config.yamlrewritten for SigNoz Cloud (2026-05-23) β exporter switched from in-VPCotlp(insecure) tootlphttp/signozwith TLS +signoz-access-tokenheader reading${env:OTEL_KEY}; endpoint reads${env:OTEL_URL}. OTLP/HTTP chosen over gRPC so the Infisical-storedOTEL_URL(a fullhttps://ingest.us2.signoz.cloudURL) works as-is β the gRPC exporter requires barehost:portform. SigNoz Cloud destination region is us2 (not us1 or us). Health endpoint moved from0.0.0.0:13133β127.0.0.1:13133(loopback only) sonetwork_mode: hostdoes not expose it. Addedfilter/skip_selfprocessor to prevent the otel-agent's own logs creating a feedback loop. Addedsending_queuefor buffering during transient SigNoz Cloud unavailability. Stricter filesystem exclude list (avoids container/snap pseudo-filesystems). Compliance: NIST DE.CM, CIS 8.5, ISO A.8.15. -
otel-agent/compose.yamlrewritten for Infisical runtime injection (2026-05-23) βOTEL_URLandOTEL_KEYare now read from the environment with:?shell-fail-if-missing guards, so a misconfiguredinfisical runfails fast with a clear message instead of silently shipping no data. Health probe target updated to127.0.0.1:13133. JSON-file log driver pinned with rotation caps (10MB Γ 3 files). All host mounts remain read-only. -
otel-agent/deploy.ymlrewritten (2026-05-23) β removed broken Jinja escaping pattern ({{ '{{' }} β¦ {{ '}}' }}) that Gemini had introduced; these are plain Ansible playbooks, not copier templates. Now requires<otel_agent_dir>/.infisical-auth.envto exist (created bybootstrap-otel-agent.sh) and starts the container underinfisical runusing the bootstrapped Machine Identity. Path is configurable via-e otel_agent_dir=...(default/opt/otel-agent; burnedout-xyz uses/root/observability/otel-agent). Infisical env slug is configurable via-e infisical_env=...(defaultdevto match the existing "Development" environment in theotelproject). Added pre-flight checks (docker present, infisical CLI present, auth file present). Added wait-for-health task. -
scripts/bootstrap-otel-agent.shadds--dirand--env-slugflags (2026-05-23) β--dir <path>overrides the install directory (default/opt/otel-agent; burnedout-xyz uses/root/observability/otel-agent).--env-slug <slug>records the Infisical env to use at runtime in the auth file (defaultdev); the deploy script reads this back so the env-slug only needs to be specified once during bootstrap. -
scripts/deploy-otel-fleet.shrewritten +--dir/--env-slugflags (2026-05-23) β dropped the<signoz-private-ip>positional argument (no longer applicable with SigNoz Cloud). Now supports--droplet <name>,--tag <tag>, and--host <user@ip>selectors, plus--dir,--env-slug,--environment,--namespace,--dry-runoptions.--env-slugis optional and overrides the value recorded by bootstrap. Refuses to deploy to a host that has not been bootstrapped at the specified--dir. Wrapsdocker compose upininfisical runfor runtime secret injection. Includes inline health wait (up to 60s).
signoz-docker/(self-hosted SigNoz copier template) β superseded by SigNoz Cloud +otel-agent/. Files retained, deployment paused. Reactivation requires an ADR; seesignoz-docker/README.mdfor the procedure.
-
anythingllm-docker β the deploy refuses an image on a registry host the box does not log in to (2026-10-02, WeOwnDev/weown-fleet#112). The box logs in to one registry host, the one from
image_registry. IfANYTHINGLLM_IMAGE(read from the store) names another registry host, its pull would run unauthenticated and fail atcompose up. Each compose-up task now checks it first and stops with both hosts named. A Docker Hub ref (no dot or colon in its first part) needs no login and still passes. Verification:scripts/test-image-host-check.shcuts the check out of both compose-up tasks of four renders (infisical/openbao Γ default/mirror) and runs 28 cases. Without the host comparison, 8 fail. -
anythingllm-docker (openbao) β the deploy stops with the reason when the platform store is unreachable (2026-10-02, WeOwnDev/weown-fleet#36). A new droplet's private /32 is not in the store firewall's 8200 allowlist until an operator adds it. The host unwrap then failed with an opaque non-zero rc (a first openbao deploy, three runs, 2026-09-07). Every new tenant has defaulted to openbao since 09-04. A pre-check now curls
<bao_addr>/v1/sys/healthwith the instance CA before the unwrap and fails naming the /32 allowlist (platform_api_source_cidrs). The firewall change itself stays with the operator. Verification:scripts/test-bao-reach-check.shruns the two tasks from the rendered playbook against a refused port, a reachable HTTPS host and an unusable CA (3/3); with the stop condition disabled, 2 of the 3 fail. -
Tombstoned two dead renders (2026-10-02, WeOwnDev/weown-fleet#112).
wordpress-docker/sites/burnedout-xyz(droplet off) andanythingllm-docker/sites/dev-weown-anythingllm(no droplet) getRETIRED-DO-NOT-DEPLOY.md, and theirscripts/deploy.shexits 1. They are not rolled over to the post-Minimus registry. -
wordpress-docker β the template renders again (2026-10-02).
copier copyfailed onmainfor two reasons.scripts/backup.sh.jinjahad Go-template braces (docker inspect --format '{{range ...}}') outside{% raw %}. Thedomain_styleanddroplet_sizechoices were written valueβlabel, where copier expects labelβvalue, so the defaults failed validation. Stored answers keep the same values (apex,s-2vcpu-2gb-amd). Committed site renders are unaffected. -
anythingllm-docker β dashboard review fixes (2026-09-30): booking button ink now picks the higher-contrast of dark/white, or black for mid-tones where neither reaches 4.5:1 (white on Harbor Gold was 2.42:1; now 7.38:1), and the preview matches; the Appearance and Booking cards track unsaved changes separately (including edits made while a save is in flight) so Copy stays blocked until both are saved;
locationVerifiedis true only for a folder-qualified exact match; an oversize request body now gets a 413 with the limit instead of a dropped connection or a misleading 400. Tests:dashboard/test-embed-appearance.mjs, newdashboard/test-http.mjs,embed-filter/test.js. -
Operator scripts and CI β failures that were reported as success (2026-09-30). From the same review sweep; each has a test run against
mainas the negative control.dev/up.sh: only a 2xx counts as ready (a dashboard answering 503 was announced as up); an unexpectedenable-multi-userreply now stops the run;--destroyalso clears the savedALLM_ADMIN_API_KEY, whose database it just deleted, so the next run mints a new one; an existing credentials file is re-tightened to 0600 before any mode runs,--downand--destroyincluded.scripts/offboard-account.sh: a failed Keycloak disable printedKC_DONEand was reported as done; a failed session logout is now a manual step. Both Gitea paths refuse to PATCH unless the user GET returned that user, so an error body can no longer reset an SSO account'slogin_name/source_idto local.anythingllm-docker/scripts/allm-prompt-sync.sh: no mode ever ran (${1:?β¦{discover|pull|diff|push}β¦}ended at the first}, so every mode string had the usage text appended); a failedpulltruncated the committed prompt to empty; it now writes a temp file and moves it into place on success.anythingllm-docker/scripts/allm-admin-account.sh,allm-set-role.sh,allm-user-rename.sh:mktemp -t namefails on GNU mktemp ("too few X's"); now an explicitXXXXXXtemplate.allm-admin-account.shstages its helper at a per-runmktemppath on the droplet instead of a fixed/tmp/allm-admin.js.scripts/provision-openrouter-key.sh: the store-failure message printedOpenBao weown/platform/xplatform/x; it now names the real destination..github/workflows/validation.yml: the pre-commit cache key now includes the Python version (setup-python, pinned) and a pinned pre-commit version, in both the key and the restore prefix. (The Django-comment guard fix from this sweep was superseded on merge by #269's stricter version, recorded in its billing-docker entry.)anythingllm-docker/sites/{ai.weown.agency,dev-weown-anythingllm,s004.ccc.bot}: added the.python-versionpin (3.12.12) that onlybeta-weown-chathad, soansible-playbookresolves under pyenv with asystemglobal.
-
Docs (2026-09-30): runbook receipt no longer prints the operator's Infisical token; affiliate runbook says
create_affiliatenever setsactive; gitea-docker README and help text say admin SSH isssh_source_cidrsonadmin_ssh_port(default open to all); anythingllm README uses an<openbao-checkout>placeholder;dev/README.mdgives the dashboard login email; the client-intake design doc states the embedding routes and provider-routing control for R1/R2, the event hand-off, token transport, scan order, quarantine key model and client binding, andCUSTOMER_INSTANCE_PROVISIONING.mdmarks its ZDR claim pending verification. -
billing-docker β sweep of review findings on merged PRs #236, #239, #240, #242 and #252 (2026-09-30).
prune_demo_data --restoreno longer re-activates a code that has referred customers, and its "(none)" line now prints. The template givesprovisioning_watchstate its own volume (<project>_ops_state) instead of the staticfiles one, and the app image creates/app/stateowned byappuser, so a fresh volume is writable. The provisioning-watch cron log, whose ALERT lines name customer e-mails, gets a logrotate rule (daily, 14 kept), is created or corrected asroot:root 0640on every deploy, and is written underumask 027..github/workflows/validation.yml: the Django template-comment check now tests every comment opener on a line (PCRE lookahead), and a failed scan (grep exit >1) fails the job instead of reading as clean. ADR-007 gets its#WeOwnVerversion line. The volume split is not applied to the livesites/billingrender yet: there both keys resolve to the same volume, so renaming needs a planned migration (seebilling-docker/sites/billing/CHANGELOG.md). -
anythingllm-docker β smoke test check 3.2 now tests AnythingLLM's real health endpoint (2026-09-30):
/api/pinginside the container, PASS only on"online":true, SSH failure reported as NOT CHECKED. It used to pass on any body from a SigNoz-only path. In-container probes usedocker execby compose label (a baredocker compose execcan't parse the compose file outsideinfisical run). -
anythingllm-docker β backups skipped the dashboard's state volume (2026-09-29). Embed allowlist domains, document delete-locks and embed appearance/logos live in
<project>_dashboard_state, whichbackup.sh/restore.shnever archived. Both now include it (existence-guarded on backup, archive-guarded on restore); template +sites/beta-weown-chatre-synced. Follow-up 2026-09-30: restore never touches the dashboard volume it can't stop (and still attempts to restart it), always restarts AnythingLLM, and exits 1 on any dashboard error; the backup quiesces the dashboard aroundtarso the volume is one consistent snapshot. A failed stop no longer aborts the backup before the dashboard restart. -
billing-docker/sites/billingβ the committed render was 3 migrations behind the template, and PRODUCTION was running it (2026-08-26) β verified against the live service:billing.weown.dev/agreement/returned 404 while/instances/new/returned 302, so the deployed app was the stale render. Absent from production as a result: the customer-agreement gate on checkout (the template had closed the second door with "a gate on only one door is not a gate"), the free-trial lifecycle (TRIALING,trial_end), transactional lifecycle email (core/mail.py+ templates),customer_agreement.html,tests.py, and migrations0008β0010. The stale render also still hardcoded$1,000/monthon the checkout button β which the template had deliberately removed so one image can serve both the $5 real-money drill product and the $1,000 live product viaSTRIPE_PRICE_IDβ meaning the $5 pilot could not have worked on what was deployed.sites/billing/app/is now byte-identical totemplate/app/.docker/,site.confandterraform/were deliberately not touched: they carry the render'sapp_dirand volume-name identity, and a mismatch there brings the app up on fresh empty volumes. Verified against the site copy: 59 tests pass,makemigrations --checkreports no changes, and a from-scratchmigrateapplies0001β0011cleanly.β οΈ Code change only β the droplet keeps running the old image until a deploy. -
buzz-docker β Windows Desktop DM/@mention toasts (2026-08-18) β Buzz on Windows false-denies notification permission at every start (block/buzz#2445), then auto-disables Desktop alerts so real DMs and channel tags never toast. A prior local CDP βrepairβ fired 3β4 fake startup toasts and still left the mention pipeline off. Standard per-machine workaround:
buzz-docker/scripts/windows-fix-desktop-notifications.ps1(silent Notification interceptor, one reload, no test toasts, rewires Start Menu/Desktop/Startup shortcuts). Runbook:docs/PRJ-432.2-buzz-deployment.md. -
buzz-docker β Desktop Mobile pairing 404 (NIP-AB pairing relay) (2026-08-12) β membership relays advertise NIP-43 but, without
pairing_relay_url, Desktop v0.5.9 falls back towss://<host>/pairon the main relay (no handler β HTTP 404). Mirror now ships apairing-relaysidecar (buzz-pair-relay), Caddyhandle_path /pair*, requiredBUZZ_PAIRING_RELAY_URL, plusscripts/enable-pairing-relay.shfor live hosts (recreates only pairing/caddy/relay; leaves Postgres/Redis/MinIO up). Runbook:docs/PRJ-432.2-buzz-deployment.md. -
searxng-docker β image pin + Valkey cap_drop (2026-08-11) β bump static
docker-compose.yaml/ copiersearxng_imagedefault tosearxng/searxng:2026.8.10-0a118066d(prior2026.5.13-eb12c9b404s on Docker Hub). Dropcap_drop: ALLon the Valkey service β its entrypointsetpriv/setresuidexits 127 under a full capability drop. -
auto-pr-to-main.yml β present-but-dead WEOWN_BOT_PAT no longer reds every push (2026-08-11) β empty-PAT skip was already on
main, but a present expired token still failedactions/checkoutwithcould not read Username. Gate now probesGET /userand skips (exit 0) unless HTTP 200. RotateWEOWN_BOT_PATto restore auto-PR asweown-bot. -
auto-pr-to-main.yml β invalid job-level secrets gate (2026-08-11) β
if: ${{ secrets.WEOWN_BOT_PAT != '' }}is not legal onjobs.*.if(secrets context unavailable there), so every feature-branch push failed at workflow setup with "Invalid workflow file β¦#L1" / "No jobs were run". Gate moved into a step (env:+GITHUB_OUTPUT), remaining steps skipped, job exits 0 when the PAT is absent. RotateWEOWN_BOT_PATto restore auto-PR authorship asweown-bot. -
buzz-docker β Copilot review: pin image + harden compose (2026-08-11) β require immutable
BUZZ_IMAGE(digest/tag; reject:main/:latest) in compose,.env.example, bootstrap, and runbook; pin upstream git SHA in docs; addcap_drop: ALL+no-new-privileges(Caddy getsNET_BIND_SERVICE); bootstrap uses a local venv instead ofpip --break-system-packages. -
anythingllm-docker dashboard β assistant replies rendered raw markdown syntax instead of formatted text, and new conversations could silently overwrite an existing thread instead of stacking (2026-08-01) β two customer-reported dashboard bugs. (1)
buildMsg()'s message bubble usedtextContentfor every reply, so assistant answers containing**bold**, lists, or code fences showed the literal markdown source in the chat instead of formatted text. Added a markdown-lite renderer (mdBlocks/mdInline/renderMarkdowninpublic/index.html) covering bold/italic, inline code, fenced code blocks, headings, bullet/numbered lists, blockquotes, tables, links and rules β built entirely withcreateElement/textContent(neverinnerHTML), preserving the redesign's safe-DOM invariant; user-sent messages are unchanged (still plain text). Mobile type was also bumped and given real breakpoints (860px/480px/360px) instead of one fixed size, and every text input'sfont-sizeis pinned to 16px under 860px so iOS Safari no longer auto-zooms the page on input focus. (2)POST /api/threads(server.js) let AnythingLLM derive the new thread'sslugfromname, which is just the first message's text viaderiveTitle(); two conversations opening with the same or a similarly-worded first message could then generate the same slug, so "New conversation" would resolve to the SAME existing thread instead of creating a new one β silently merging one conversation's history into another rather than stacking a new sidebar entry. Fixed by generating an explicit unique slug (t-<timestamp36>-<8 hex chars>) server-side for every new thread, independent of the message text. Verification:node --checkonserver.js+ the extracted inline script; the markdown renderer was exercised in a real browser against bold/list/table/blockquote/code-block/link input at 800px, 375px and 320px viewports, confirming correct parsing and no page-body horizontal scroll at any width. No live AnythingLLM instance was available to reproduce the thread-slug collision directly, so item (2) is a code-level hardening fix for the exact mechanism that produces the reported symptom, not a live-confirmed root-cause fix β worth a live re-test before closing this out. -
Template consolidation from the Keycloak/Gitea production rebuild (2026-07-24) β every defect found while driving
keycloak-dockerandgitea-dockerto production ported back to template level so future renders deploy first-try: Caddyfile emptytls {}(does not parse β Caddy never listened on 80/443) + dead admin vhost removed; host CA bundle mounted for the bind-mounted Infisical CLI (both app containers crash-looped without it) with a neutral mount path so image truststores are never shadowed; Caddy log-dir bind mount; Keycloak 26 migration (image 26.7.0,KC_PROXY_HEADERS, health on the :9000 management port, tmpfs resource cache, container healthcheck removed); Giteacommand:s6 path (/usr/bin/s6-svscan); headlessinfisical login --plain </dev/nulleverywhere a droplet logs in (bare login hangs on keyring); backup/restorePOSTGRES_*names + canonicalweown-prod-backupsbucket; health-probe retries 20β40 for first-boot schema migration; entrypoint wrapper 0755 for non-root hardened images; gitea smoke test probes git-over-SSH via hostname to catch proxied DNS. Fleet-wide: executable bit restored on 44*.sh.jinja/init.sh.jinjaacross 10 further templates (copier renders the source file's mode β a 644 template renders an unrunnable script);anythingllm-dockerdeliberately untouched (owned by its own workstream). Verification: both templates re-rendered; keycloak render is byte-identical (excluding comments) to the livesso.weown.idsite files; compose YAML parses;bash -nclean. -
anythingllm-docker dashboard β
/app/healthzanswered 401, so every deploy reported a scary false warning (2026-07-22) β the dashboard's public health route was checked before the base path was stripped, so it only ever matched the container-internal/healthz; Caddy routes only/app*to the dashboard, which meant the public probe fell through to the auth gate. Added the health check after base-path stripping too, leaving the container healthcheck's/healthzworking unchanged. -
anythingllm-dockerterraform βssh_key_fingerprint(s)name/type regression broke every fresh provision (2026-07-15) β the 12-PR consolidation (e8496d1) pluralized the template var tossh_key_fingerprints/list(string), butweown-tofuprovides the singularTF_VAR_ssh_key_fingerprint(one shared DO bootstrap key; team access is post-boot viaOPS_AUTHORIZED_KEYS), and the live sites (s004) use the singularstring. Result:tofu planfailed withNo value for required variable ssh_key_fingerprints. Reverted the template (variables.tf,main.tfβssh_keys = [var.ssh_key_fingerprint],terraform.tfvars.example) to the singular convention that matches live +weown-tofu. openclaw-docker intentionally keeps the plural (different folder-basedweown-tofustructure; its var is designed as a dev-key list). Another consolidation regression never live-tested until the firstweown-fleetprovisioning run. Verification: rendered terraform parses (tofu fmt); nossh_key_fingerprintsrefs remain in the template. -
itofu.shβ M2 regression:plan/apply/outputfailed on the S3 backend (2026-07-15) β closes handoff H1 (the M2 argvβenv change was static-verified only;copierwasn't installed in the agent env, so the tofu path was never live-tested). The M2 change addedexport AWS_ACCESS_KEY_ID/SECRET(fromTF_VAR_spaces_access_key/secret) to theinitcase so the DO Spaces S3 backend could authenticate β but not toplan,apply, or the passthrough (output/destroy) cases. Those creds are deliberately not persisted in.terraform, so every state op needs them in env;tofu plantherefore failed withNo valid credential sources found ... no EC2 IMDS role. Fixed by routing all non-init tofu calls through atrun()helper that exports the AWS_* creds (sse_customer_keystays init-only β it is persisted). Applied to both templates carrying this wrapper:anythingllm-dockerandopenclaw-docker. Surfaced by the first live provisioning run of the fleet orchestrator (weown-fleet). Verification: renderedbash -n+shellcheck -S errorclean on both. -
deploy-new-site.shβ automated AnythingLLM deploys now bootable (2026-07-15) β the script pushedJWT_SECRET/ADMIN_EMAIL/Spaces creds but none of the other fail-loud-required AnythingLLM values, so an automated deploy produced a container that refused to boot (the manualDEPLOYMENT_GUIDE.mdflow was unaffected). Newanythingllm-dockerbranch in Phase 2 pushesANYTHINGLLM_IMAGE(from env β the private registry namespace is never committed),EMBEDDING_ENGINE(defaultopenrouter) + optionalEMBEDDING_MODEL_PREF, and mints the per-customer cappedOPENROUTER_API_KEYviascripts/provision-openrouter-key.sh(cap overrideOPENROUTER_LIMIT_USD, default $50); loud warnings when a value can't be set automatically. Also adds a "Compliance & data handling" facts table todocs/CUSTOMER_INSTANCE_PROVISIONING.md(tenancy, locality, encryption, retention, subprocessors, framework mapping) backing the customer-facing terms. Verification:bash -n+shellcheckclean. -
otel-agent/config.yamlβ syslog parser matched zero Ubuntu 24.04 lines; agent flooded SigNoz with its own error spam (2026-06-10, #WeOwnVer v4.1.2.2) β thefilelog/syslogreceiver'sregex_parserassumed RFC3164 timestamps (Jun 10 07:00:33), but Ubuntu 24.04 rsyslog writes RFC3339 (2026-06-10T07:00:33.179378+00:00 host unit[pid]: msg), so every syslog/auth.log line failed to parse and the collector logged 2 ERROR lines + Go stack traces per input line (84β504/min on int-s004; ~24% of that host's log rows in the 2026-06-10 incident export were the agent's own stanza v0.114.0 stack traces βtransformer.go:100/reader.go:214). Reproduced and fixed againstotelcol-contrib:0.114.0(the deployed version) using real incident log lines: new regex parses the three real shapes (unit[pid]:,kernel:no-pid,(udev-worker)[pid]:) with gotime RFC3339 timestamp parsing (microsecond-accurate; repro: 9/9 lines parsed + 2 garbage lines passed through, 0 error lines, was 18), pluson_error: send_quietso any future unmatched line passes through as raw body logged at DEBUG β the agent can never ERROR-spam per line again. Also hardened the previously-ineffectivefilter/skip_self(itscontainer.name/log.file.nameconditions never matched docker's<container-id>-json.logpaths, which is how the self-spam reached SigNoz): added a body-signature condition matching the collector's own tab-separated zap console format, starving the feedback loop even during future agent error bursts (e.g. exporter outages).otelcol validatepasses on 0.114.0. Rollout:scripts/deploy-otel-fleet.sh(canary int-s004-anythingllm β--tag weown-ai). Compliance: NIST CSF 2.0 DE.CM-1 (signal fidelity); CIS v8 8.5, 8.11. -
Propagated the INT-S004 /
anythingllm-dockerdeploy-bug battery across all*-dockercopier templates (2026-06-03) β the AnythingLLM rebuild surfaced a set of latent first-boot/deploy bugs (the entries below) that the other templates inherited from the same authoring patterns. Scanned every template (and rendered site) and applied only the bug classes actually present in each. cloud-init + deploy + scripts were re-validated per template:grep -cP '[^\x00-\x7F]'= 0 on every cloud-init, copier render (5/6;wordpress-dockerhas a pre-existing copier-choicesrender incompatibility, andsearxng-dockerneeded a one-line{% raw %}wrap on adocker ps --format '{{.Names}}'Go-template that previously blocked rendering β same patternsignoz-dockeralready used), simulated-templatefile()YAML parse,bash -non all extracted/rendered shell, andpre-commit(gitleaks/yamllint/shellcheck/markdownlint) all green.keycloak-dockerβ B1 non-ASCII scrub of cloud-init; B3 kebab-caseinfisical login --client-id/--client-secret(cloud-initinfisical-auth.sh+ cron + the inline-SSH remote restore inscripts/restore.sh); B4export INFISICAL_TOKEN="$(infisical login β¦ --plain --silent)"headless capture (auth helper now sourced into the compose-up shell; the redundant standalone authruncmddropped); B5 droppedawsclifrom aptpackages:+ addedunzipand the official AWS CLI v2 installer inruncmd; B6 pre-create/var/log/caddy(0777) inruncmdbefore compose-up. (B2 N/A β its embeddedbackup.shalready used single-$; the only$$is the legitimate Compose$$?healthcheck escape. B7/B8 N/A.)sandbox-dockerβ B1 ASCII scrub; B2$$VAR/$$(β¦)β$VAR/$(β¦)de-escape in the embeddedrotate-bootstrap-secret.sh(kept$${β¦}); B3+B4 inansible/deploy.yml(Γ3 login sites) +scripts/backup.sh+scripts/restore.sh; B5 awscliβAWS CLI v2; B6/var/log/caddyansiblefile:task before compose-up; B8 published127.0.0.1:8080:8080(loopback-only) on thesandboxservice so the deploy's host-sideuri:health probe (http://127.0.0.1:8080/v1/docs) can connect β caddy still proxies the public over the internal network. (B7 N/A βsandbox_imageis the publicghcr.io/agent-infra/sandbox.)searxng-dockerβ B1 ASCII scrub; B2 de-escape in the embeddedbackup.sh(kept$${β¦}for Compose/volume refs); B3+B4 in cloud-initinfisical-auth.sh+ cron +scripts/restore.sh; B5 awscliβAWS CLI v2; B6/var/log/caddy(0777) inruncmdbefore compose-up. (B7/B8 N/A β publicsearxng/valkeyimages; in-container healthcheck only.)signoz-dockerβ B1 ASCII scrub; B2 de-escape in the embeddedbackup.sh; B3+B4 in cloud-initinfisical-auth.sh+ cron; B5 awscliβAWS CLI v2; B6/var/log/caddy(0777) inruncmdbefore compose-up. (B7/B8 N/A β publicsignoz/*images.)wordpress-dockerβ B1 ASCII scrub (comments + the twoMYSQL_*:?β¦fail-loud messages); B3+B4 in the cloud-initinfisical-auth.shheredoc (already sourced by the boot + croninfisical run). (B2 N/A β the only$$are the legitimate Compose escapes$$_SERVER/$${WP_IMAGE}etc. B5/B6 N/A β local-only backups, noaws; caddy does no file logging. B7 N/A β thereg.mini.dev/1923/wordpress-fluentsmtpref is the already-accepted, repo-wide pattern, not introduced here. B8 N/A.)openclaw-docker(+sites/claw-weown-tools) β B1 ASCII scrub; B2 de-escape in the embeddedrotate-bootstrap-secret.sh; B3+B4 inansible/deploy.yml(Γ3) +scripts/backup.sh+scripts/restore.sh; B5 awscliβAWS CLI v2; B6/var/log/caddyansiblefile:task before compose-up. The same fixes were applied to the renderedclaw-weown-toolssite so the committed artifact matches the hardened template; this is a repo edit only β the live droplet would need a redeploy (ansible-playbook deploy.yml) and, for the cloud-init changes, atofu apply -replaceto pick them up. No droplet was recreated, replaced, or SSH'd. (B7 N/A βopenclaw_imagedefault is top-levelreg.mini.dev/openclaw, same category as the retainedreg.mini.dev/caddy:2copier-var pattern; B8 N/A β the host probe targets caddy's published:80.)- Bug 7 (private-registry image hardcoded) applied to NO target template β none of the six hardcode a private-namespaced app image with an invalid pin the way
anythingllm-dockerdid; app images are either public (ghcr.io,docker.io,signoz/*) or top-levelreg.mini.dev/<image>(the same shape ascaddy_image, which the canonical fix deliberately kept as a copier var).wordpress-docker's namespacedreg.mini.dev/1923/β¦ref is already committed repo-wide and out of scope for this hardening pass.devbox-dockeris not present in this repo (no such template orsites/weown-devbox), so it could not be assessed.
-
Post-deploy follow-ups: caddy log-dir perms +
ANYTHINGLLM_IMAGEbootstrap (2026-06-02) β (1) the hardened (non-root)caddy:2crash-looped withpermission deniedwriting/var/log/caddy/<site>.logbecause nothing created that host bind-mount dir writable;ansible/deploy.ymlnow ensures/var/log/caddy(mode 0777) beforedocker compose up(template +s004.ccc.bot+ai.weown.agency). (2)scripts/bootstrap-s004-infisical.shnow prompts for the now-requiredANYTHINGLLM_IMAGE, and thes004.ccc.botREADME documents it, so a rebuild repopulates the image ref in Infisical. -
AnythingLLM published no host port β deploy health probe + runbook tunnel could never connect (2026-06-02) β the
anythingllmservice had noports:(caddy proxies it over the internal Docker network), yetansible/deploy.yml's health task pollshttp://127.0.0.1:3001/api/pingon the host and the runbook's Phase-4 validation usesssh -L 3001:127.0.0.1:3001β both assume 3001 is published on the droplet loopback. The container was healthy the whole time (docker inspecthealth=healthy, internal ping{"online":true}); the probe was checking an unpublished port and failed withConnection refused. Addedports: ["127.0.0.1:3001:3001"](loopback-only β not internet-exposed; caddy still serves the public via the internal network) to the template +s004.ccc.bot+ai.weown.agency. -
AnythingLLM image pin
1.7.2was invalid + hardcoded a private registry path (2026-06-02) β the compose pinnedreg.mini.dev/anythingllm:1.7.2, but that tag was never in the Minimus registry (the source box actually ran:latest, and Minimus rotates tags + had moved the image to a private namespaced path), so the deploy 404'd on image pull. The image ref is now injected from Infisical (ANYTHINGLLM_IMAGE, e.g.reg.mini.dev/<ns>/anythingllm:v1.12.1): compose reads${ANYTHINGLLM_IMAGE:?β¦}(fail-loud) anddocker compose uppulls it underinfisical run, so the private namespace never enters this public repo and each site pins its own version. The deploy's image-pull task now pulls only the staticcaddyref. Applied to the template +s004.ccc.bot+ai.weown.agency; render parity + compose interpolation verified. -
Infisical machine-identity auth hung on headless boxes β
infisical runfell back to an interactive login prompt (2026-06-02) βansible/deploy.yml,scripts/backup.sh, andscripts/restore.shraninfisical login --method=universal-auth β¦followed by a bareinfisical run β¦, but a universal-auth (machine identity) login does not leave a session thatinfisical runreuses.runtherefore saw no auth and dropped to the interactiveSelect hosting option / Email / Passwordprompt, which hangs forever on a no-TTY ansible/cron run (Login via browser failed. inappropriate ioctl for device). Fixed by capturing the token βexport INFISICAL_TOKEN="$(infisical login β¦ --plain --silent)"β whichinfisical runreads directly (the documented headless/CI pattern). Applied to the template +s004.ccc.bot+ai.weown.agency; render parity andbash -nverified. -
Infisical CLI
loginused camelCase flags the current CLI rejects (2026-06-02) βansible/deploy.yml,scripts/backup.sh, andscripts/restore.sh(template +s004.ccc.bot+ai.weown.agency) calledinfisical login --method=universal-auth --clientId β¦ --clientSecret β¦, but the CLI installed fromartifacts-cli.infisical.comrequires kebab-case and fails withunknown flag: --clientIdβ breaking the deploy'sdocker compose uptask, the daily backup cron, and remote restore. Renamed to--client-id/--client-secret. (infisical run --projectId/--envand the cloud-init rotation's API JSONclientId/clientSecretkeys are correct as-is and unchanged.) -
cloud-init
$$over-escaping broke Layer-2 rotation;awscliapt-install errored the bootstrap (2026-06-02) β two moreanythingllm-dockercloud-init bugs found bootstrapping INT-S004. (1) The embeddedrotate-bootstrap-secret.shused$$VAR/$$(β¦), assuming Terraformtemplatefile()collapses$$β$; it does not (only$${β${), so the literal$$became the bash PID and the rotation silently failed on every deploy (it is best-effort/exit-0, so it went unnoticed) β the v1 bootstrap secret was never rotated. Fixed$$VARβ$VAR/$$(β¦)β$(β¦), keeping the legitimate$${β¦}escapes. (2)awscliin the aptpackages:list fails to install on current Ubuntu, which setcloud-init status: errorand skipped the rest ofruncmd; replaced with the official AWS CLI v2 installer inruncmd(best-effort, non-fatal) and addedunzip. Applied to the template + both sites (s004.ccc.bot,ai.weown.agency); rendered YAML validated and the rotation script passesbash -nafter render. Also corrected the now-known-wrong$$guidance inCLAUDE.md. -
cloud-init silently skipped its entire bootstrap on non-ASCII characters (2026-06-02) β the
anythingllm-dockercloud-init template and both rendered sites (s004.ccc.bot,ai.weown.agency) had em-dashes (β), arrows (β), and a section sign (Β§) in comments and log strings. The em-dash's UTF-8 middle byte is0x80, a C1 control char that DigitalOcean's ConfigDrive β cloud-init YAML 1.1 parser rejects (unacceptable character #x0080), so the whole cloud-config was discarded as "empty cloud config" β Docker, the Infisical CLI,.infisical-auth.env, and the Layer-2 secret rotation never ran (cloud-init status=degraded,/opt/<project>/absent). Scrubbed all three cloud-init files to pure ASCII (ββ-,ββ->,Β§βsection); the template re-renders byte-identical to the site. cloud-init user-data must be ASCII-only. Caught bootstrapping the INT-S004 rebuild. -
itofu.sh+restore.shheader inconsistencies (2026-06-02) βitofu.sh(template +s004.ccc.bot) now listsTF_VAR_alert_email(a DO-verified email) in its "Required Infisical secrets" header, with theTF_VAR_enable_monitoring=falseopt-out, matching the runbook/tfvars and closing theemail is not verifiedapply footgun.restore.sh(template + site) no longer advertises ans3://β¦path argument it would actually reject β the header now documents that the input is the backup name only (validated^[A-Za-z0-9._-]+$) and that the tarball is auto-fetched from DO Spaces when absent locally. Both scripts were also scrubbed to pure ASCII. Caught by Copilot review on PR #39. -
OPS_AUTHORIZED_KEYSsync could revoke all team SSH access on a blank value (2026-06-02) β the ansible SSH-key sync task guarded only against an unset/emptyOPS_AUTHORIZED_KEYS([ -z ]), but a whitespace-only value passed that check and then had every line stripped by the blank-line filter, producing an empty temp file thatinstallwould write overauthorized_keys.weown-opsβ wiping all team keys and locking ops out (the DO break-glass key, managed separately, would survive). Reordered to filter first and treat "no non-blank lines" the same as unset (warn + exit without touching the destination). Fixed in the template (ansible/deploy.yml.jinja) and thes004.ccc.botsite; verified with a harness (unset/empty/whitespace-only all preserve the existing file; real keys still sync). Caught by Copilot review on PR #39. -
TF_VAR_alert_emailmissing from theweown-tofusetup caused atofu applyfailure (2026-06-02) β thes004.ccc.botrunbook'sweown-tofuTF_VAR_*table omittedalert_email, sovar.alert_emailfell back to its placeholder defaultalerts@example.comand DigitalOcean rejected the CPU/mem/disk monitor-alert creation with400 β¦ email is not verified(the rest of the apply β droplet, reserved IP, firewall β succeeded). AddedTF_VAR_alert_emailto the runbook table with a note that it must be a DO-verified email (plus aTF_VAR_enable_monitoring=falseopt-out), and added the sameβ οΈ warning toterraform.tfvars.examplein both thes004.ccc.botsite and the template.jinja. -
OPENROUTER_API_KEYnever injected into the AnythingLLM container acrossanythingllm-docker(2026-06-02) β the compose files documentedOPENROUTER_API_KEYas a required Infisical secret, but theenvironment:block only injectedJWT_SECRETandADMIN_EMAIL(the original Helm chart mounted all three). A redeploy/restore therefore came up with no OpenRouter credential in the container env β and for the INT-S004 restore specifically, AnythingLLM would fall back to the expired key persisted in the restoredstorage/.env, breaking inference with an opaque OpenRouter 401. Added a fail-loud${OPENROUTER_API_KEY:?...}injection (same pattern as theJWT_SECRETguard) to the template (compose.prod.yaml.jinja) and both live sites (s004.ccc.bot,ai.weown.agency); a value injected viainfisical runnow correctly overrides the stale persisted key. Also gaveOPENROUTER_MODEL_PREFa default (${OPENROUTER_MODEL_PREF:-anthropic/claude-opus-4.5}in the sites,{{ openrouter_model_pref }}in the template) to silence the Compose "variable is not set" warning. Caught by Copilot review on PR #39. -
otel-agent/deploy.ymlbroken Jinja escaping (2026-05-23) β the file is a plain Ansible playbook (not a copier template), but it had been written with{{ '{{' }} otel_agent_dir {{ '}}' }}patterns that would render as the literal string{{ otel_agent_dir }}and prevent Ansible variable substitution. Replaced with plain{{ otel_agent_dir }}throughout.
-
signoz-docker/copier template (2026-05-14, Added) β new copier template for self-hosted SigNoz observability platform (logs + metrics + traces via ClickHouse). 19 files: Docker Compose (6-service stack: ZooKeeper, ClickHouse, SigNoz, OTel Gateway, Caddy), OpenTofu IaC (droplet, reserved IP, firewall with OTLP ports VPC-only), cloud-init bootstrap, Ansible deploy playbook, skinny backup/restore scripts, monitoring alerts. Compliance: NIST DE.CM, CIS 8.2, ISO A.8.15. Status as of 2026-05-23: see Deprecated section above β kept as optional fallback only. -
searxng-docker/copier template (2026-05-14, Added) β restructured SearXNG deployment (previously plain Ansible onfeature/searxng-deploymentbranch) into full copier template matchingkeycloak-dockerpatterns. 20 files: Docker Compose (SearXNG + Valkey + Caddy), OpenTofu IaC, Infisical secrets management, skinny backups, monitoring alerts, browser-search integration playbook (Chrome/Edge/Firefox on Linux + Windows). Resolves PR #20 alignment gaps. -
otel-agent/fleet deployment (2026-05-14, Added) β OpenTelemetry Collector agent deployed per-host to/opt/otel-agent/. Collects: container metrics (docker_stats), host metrics (CPU, memory, disk I/O, filesystem, load), container logs, Caddy access logs, syslog/auth.log. Exports via OTLP gRPC. Original 2026-05-14 design targeted an in-VPC SigNoz gateway; superseded 2026-05-23 by SigNoz Cloud destination β see Changed section above. -
scripts/deploy-otel-fleet.sh(2026-05-14, Added) β fleet deployment script. Original 2026-05-14 design took<signoz-private-ip>arg; superseded 2026-05-23 by Infisical-driven flagged interface β see Changed section above. -
scripts/enable-do-agent.sh(2026-05-14, Added) β idempotent script to install free DigitalOcean extended metrics agent (do-agent) on all tagged droplets. Provides memory, disk, and load metrics in DO dashboard at zero cost. -
anythingllm-docker/ansible/configure-allm.yml(2026-05-14, Added; moved 2026-05-25) β idempotent Ansible playbook for Docker-based AnythingLLM instances. Configures MCP servers (SearXNG atsearxng.weown.app) viadocker exec+jqdeep merge. Originally landed underanythingllm/ansible/but moved alongside the other Docker-stack assets inanythingllm-docker/sinceanythingllm/is reserved for the helm/k8s deployment path. Gemini-style broken Jinja escapes were fixed in PR #26 cleanup. -
CLAUDE.md(2026-05-14, Added) β project-level Claude Code guide. References.github/copilot-instructions.mdfor compliance standards, documents copier template patterns, branch naming convention, Infisical secrets model, and fleet management scripts. -
.github/CODEOWNERScontributor handoff (2026-05-13, Changed) β@romandidomizioremoved from all path assignments (left 2026-05-15);@dhruvmalik007references removed (no longer with team).@nciminois now the sole assigned reviewer on every path.@iamwaseem18(Mohammed) and@mshahid538(Shahid) noted as available at@ncimino's discretion. AllTODO(2026-05-15)handoff comments removed. Header updated to reflect 1-approval requirement and Roman as previous steward. -
Required reviewers changed from 2 β 1 (2026-05-13) β updated across
.github/CODEOWNERSheader,CONTRIBUTING.mdΒ§6.4 merge requirements,README.mdCode Review Model,.github/ADR-004-copilot-auto-review-ruleset.mdCIS Controls v8 18.3 compliance row + Layer 1 pruning criteria reviewer requirement,.github/workflows/auto-pr-to-main.ymlPR body "Required Reviewers" text,.github/workflows/README.mdΒ§11 troubleshooting row. -
CONTRIBUTING.mdΒ§1 prerequisites + Β§10 Questions updated (2026-05-13) β removed@romandidomizioand@dhruvmalik007references; org-access contact updated to@ncimino; process/governance contact updated to@ncimino+ CODEOWNERS link. -
.github/workflows/auto-pr-to-main.ymlreviewer assignment updated (2026-05-13) β--add-reviewer ncimino,romandidomizioβ--add-reviewer ncimino(both existing PR update + new PR create paths).TODO(2026-05-15)comments removed. -
.github/ADR-004-copilot-auto-review-ruleset.mdruleset structure amended (2026-05-01) βdeletionrule removed from~ALLbranches in both Layer 1 (repo-level) and Layer 2 (enterprise-level) rulesets. Retained 2 rules:non_fast_forward+copilot_code_review. Rationale: post-merge branch deletion was permanently blocked bydeletionon~ALLwith emptybypass_actorsβ no one (including org admins) could delete any branch.mainbranch deletion protection remains via ADR-003 rule #11. Decision Log appended. Compliance mappings adjusted (SOC 2 CC6.1, ISO 27001 A.8.13). -
CONTRIBUTING.mdΒ§8 troubleshooting expanded (2026-05-01) β added "My PR is blocked β commits are unsigned" scenario. Removed Option 1 (rebase + force-push) becausenon_fast_forwardruleset on~ALLbranches blocks force-push. Retained Option 2: close PR, cherry-pick to fresh branch withgit cherry-pick -S, open new PR. Also added "Copilot review didn't start on my auto-created PR" troubleshooting entry documenting the PR-creation-time caching behavior: Copilot evaluates auto-review at PR-creation time, not push time, so the first commit on a new branch won't trigger review until a subsequent push. All subsequent pushes on an open PR are reviewed automatically. Replaces the older single "I need to sign commits I already pushed unsigned" subsection. -
Repo Settings β General β Pull Requests (2026-05-01): enabled "Automatically delete head branches" (merged branches auto-delete, replacing the retired
deletionrule on~ALL); enabled "Always suggest updating pull request branches" (keeps PRs current withmainfor cleaner merges). -
runcmds:βruncmd:typo in all cloud-init templates (2026-05-14, Fixed) β cloud-init only recognizesruncmd:(singular). The typoruncmds:silently caused boot commands to never execute. Fixed in:keycloak-docker/template/terraform/templates/cloud-init.yaml.jinja,anythingllm-docker/template/terraform/templates/cloud-init.yaml.jinja,keycloak-docker/sites/sso.weown.dev/terraform/templates/cloud-init.yaml. New templates (signoz-docker, searxng-docker) use the correct form. -
scripts/manage-droplets.shSSH key rotation parsing bug (2026-05-14, Fixed) βrotate-authorized-keysusedtr ' ' '\t'which mangled SSH key names containing spaces. Replaced with per-key-ID loop that fetches Name and PublicKey individually viadoctl compute ssh-key get. -
Copilot PR #17 round-2 comment fixes (2026-05-13, Fixed) β addressed remaining Copilot comments from the 2026-05-13 push:
workflows/README.mdstale "2 reviewer" references (comment 3236477797): updated 5 remaining "2 reviewers/approvals" β "1 reviewer/approval" at Β§6 PAT rotation step 13, Β§8.1 rule #1 table row, Β§8.1 "Interaction with workflows", Β§9 step 4, Β§10 item #7.workflows/README.mdΒ§9 Reviewer Rotation: updated step 1 to reflect handoff complete (2026-05-15, PR #17); removed@dhruvmalik007from the done-placeholder list;--add-reviewer nciminoonly.workflows/README.mdΒ§10 Transition Checklist: marked items 1 (PAT stewardship), 4 (CODEOWNERS), 5 (workflow reviewer) as β done; updated owners to reflect@nciminoand@YonksTEAMpost-transition; removed@romandidomiziofrom action-owner column.workflows/README.mdΒ§11 line 587 duplicate link (comment 3236447687): removed redundant second[ADR-004]link in the "No Copilot review on first commit" row; kept single[ADR-004 Β§ Empirical Validation Results]link with correct anchor.CODEOWNERSrisk-acceptance note (comment 3236477912): added explicitRISK ACCEPTANCEblock in the CODEOWNERS header documenting the deliberate 2-reviewer β 1-reviewer posture change, compensating controls (Copilot AI review + CodeQL), and trigger for revisiting.
-
Copilot PR #17 round-1 comment fixes (2026-05-13, Fixed) β addressed all open Copilot review comments on branch
feature/roman-ruleset-cleanup-and-docs:CONTRIBUTING.mdΒ§8 Cause B force-push (comments 3175275840 + 3175470086): removedgit push --force-with-leasestep; replaced with note that force-push is blocked bynon_fast_forwardand directs users to the close+recreate path already documented in the same Β§8 section.ADR-004title β removed "Deletion Protection" from title since thedeletionrule was removed from~ALL; title now accurately reflects the 2 active rules.ADR-004Verification Procedure expected output (comment 3175470115): updated Layer 1 expectedrulesarray from["deletion", "non_fast_forward", "copilot_code_review"]β["non_fast_forward", "copilot_code_review"]; updated Layer 2 expected count from "identical 3 rules" β "identical 2 rules".ADR-004Decision Log (comment 3175275917): removed reference to gitignoredPR7_HANDOFF_CHECKLIST.mdin the round-7 close-out entry.auto-pr-to-main.ymlNOTE (comment 3175470133): rewrote NOTE to clarify the push-before-PR pattern specific to this workflow β commits are pushed before the PR exists, soreview_on_push: truehas no new push delta at PR-creation time; first Copilot review fires on the next push to the open PR. Reconciled with ADR-004 Β§ Empirical Validation Results.workflows/README.mdΒ§11 Copilot row (comment 3175470144): updated "No Copilot review on first commit of auto-created PR" row to clarify this is specific to theauto-pr-to-main.ymlpush-before-PR pattern (not all PRs), and that manually-created PRs do get Copilot at PR-creation time.
Continuation iteration of the in-flight PR #13 (originally tagged [v3.3.4.2] below). Bumped to v3.3.5.1 because this section captures the portion of the PR that moved into the 5th week of April under the active #WeOwnVer interpretation (vSEASON.MONTH.WEEK.ITERATION β Season 3, April, Week 5, Iteration 1). The original 2026-04-23 work + Copilot rounds 1β3 (2026-04-24) remain under [v3.3.4.2] below; the later continuation rounds documented here span 2026-04-27 (rounds 4β11) through 2026-04-28 (rounds 12β13). The PR itself remains a single squash-merge unit; the CHANGELOG split is purely about #WeOwnVer fidelity per the active versioning calendar. Version stays at v3.3.5.1 across the day boundary because the PR is a single semantic unit (no fresh iteration started post-merge).
.github/ADR-004-copilot-auto-review-ruleset.mdβ new ADR (added 2026-04-27 in round-6 close-out, refined in round-7) documenting the two-layer defense-in-depth ruleset that targets~ALLbranches with three rules:deletion,non_fast_forward, andcopilot_code_review. Layer 1: repo-level "Copilot auto-review" ruleset (id12131972, configured 2026-04-23). Layer 2: enterprise-level ruleset configured 2026-04-27. Both layers enforce identical rules; the union-of-rules semantics provides defense-in-depth that survives enterprise migrations / SKU changes / individual misconfigurations and mirrors WeOwn's branch-name regex defense-in-depth pattern. Compliance mappings: SOC 2 CC6.1, CC7.1, CC7.2, CC8.1; ISO/IEC 27001:2022 A.8.32, A.8.13; ISO/IEC 42001:2023 A.6.2.7, A.6.2.8; NIST CSF 2.0 PR.IP-3, DE.CM-1; CIS Controls v8 18.3. Includes explicit pruning criteria for Layer 1 (do not delete unless ALL of: 5+ consecutive bot-authored PRs auto-trigger Copilot via Layer 2 alone + no enterprise migration planned + Layer 2 deletion+non_fast_forward verified by deliberate test + reviewer sign-off). Includes end-to-end auto-trigger validation procedure pointing at the ADR's own Decision Log table for in-repo logging (replaces a transient reference to a gitignored operational checklist; resolves Copilot R7 #5). Includes an Empirical Validation Results section documenting the round-7 controlled experiment finding: Layer 2 + Copilot Business entitlement DO work for newly-created PRs but Copilot auto-trigger is evaluated at PR-creation time, not push time β pre-existing PRs (created before the entitlement was provisioned) will never auto-trigger and the only remediation is close+re-open (usually not worth the cost)..github/workflows/README.mdΒ§2A "Whatauto-pr-to-main.ymlDoes (Step-by-Step)" β new authoritative narrative walkthrough of the auto-PR workflow, with three subsections: Trigger matrix (push + workflow_dispatch + concurrency); Step-by-step walkthrough (banners 1β9 with what-it-does + failure-modes columns); Why the team benefits (SOC 2 CC8.1 evidence narrative, zero-friction onboarding, Copilot AI review quality, defense-in-depth pattern). Inserted between Β§2 and Β§3 to give workflow-level context before the attribution model details. TOC updated..github/workflows/README.mdΒ§11 "Troubleshooting β Symptom β Cause β Verification" β consolidated symptom/cause/verification table covering all three workflows (auto-pr-to-main.yml,branch-name-check.yml,pat-health-check.yml) plus Copilot auto-review, branch protection / rulesets, and Infisical sync drift. Old Β§11 ("Related Documents") renumbered to Β§12. TOC updated. Closes the long-standing v3.3.4.3 queue item to add troubleshooting documentation; accelerated into this PR per user direction.
.github/workflows/auto-pr-to-main.ymlstep 7 contributor aggregation β replaced thewhile read -r count handlepipeline withawk '{count=$1; $1=""; sub(/^[[:space:]]+/, "", $0); ...}'. Theread-based approach split on whitespace and silently truncated multi-word names in the fallback path (e.g.,Jane Doerendered as- Jane (3 commits)instead of- Jane Doe (3 commits)). Theawk-based approach preserves the entire remainder of the line after the count. Includes an inline comment explaining the rationale so future maintainers don't regress. Resolves Copilot R7 comment #8. (The fallback path triggers whengh api /commits/{sha}returns a null login β typical for unlinked external commits.)auto-pr-to-main.ymlstep 6 attribution comment β softened the immutability claim from "first commit is immutable under thenon_fast_forwardruleset" (under-specified) to "stable across pushes because the 'Copilot auto-review' ruleset (id 12131972, see ADR-004) enforcesnon_fast_forwardon~ALLbranches in this repo, blocking the rebase / force-push that would change the first-commit identity". Resolves Copilot R6 comment A by making the enforcement chain auditable. Identical clarifications applied toworkflows/README.mdΒ§3 Opened-by table row (Copilot R6 D) andCONTRIBUTING.mdΒ§4 Opened-by bullet (Copilot R6 C).auto-pr-to-main.ymlstep 7 contributors fallback PII strip β changedgit log -1 --format='%an <%ae>'togit log -1 --format='%an'. Names are already public on GitHub commit pages; emails are PII per GDPR/CCPA + WeOwn's existing PII-minimization posture (already applied toCONTRIBUTING.mdhandle table +CODEOWNERS). Resolves Copilot R6 comment E.auto-pr-to-main.ymlstep 8 Recent Commits PII strip β changedgit log --format='### %h %s%n%n**Author:** %an <%ae>%n**Date:** %ad...'to'**Author:** %an'. Same principle as step 7 fallback. Reviewers can still cross-reference via the commit SHA + GitHub web UI. Closes the only remaining email exposure path in the PR body. User-directed extension of round-6 PII work.auto-pr-to-main.ymlstep 8 PR body parenthetical descriptions removed (user-directed UX polish): theOpened by:,Last pushed by:, andContributors on this branch:lines previously included italic explanatory parentheticals that duplicated information already visible in CONTRIBUTING.md Β§4 + workflows/README.md Β§3. Trims visual noise without information loss.auto-pr-to-main.ymlworkflow_dispatch:trigger added β enables manual re-run for debugging / PR-body refresh without needing an empty commit. The defense-in-depth branch-name regex (step 1) makes dispatch onmainor unconventional branches a safe no-op (exit 0). Resolves the 2026-04-23 PAT-rotation scenario that required commitba6d7ee(empty re-trigger commit) β going forward this is one click in the Actions tab.auto-pr-to-main.ymlconcurrency:block added βgroup: auto-pr-${{ github.ref }}, cancel-in-progress: true. Prevents race conditions when developers rapid-push: the older in-flight run is cancelled and only the latest run creates / updates the PR. Per-branch isolation; pushes on different branches don't interfere with each other.- PR-body attribution upgrade β three-tier model: replaced the prior single
**Triggered by:** @<user>line (semantically ambiguous β implied "opener" but actually showed most recent pusher) with three distinct, semantically-precise fields:Opened by:β GitHub @handle of the FIRST commit's author on the branch. Resolved viagit rev-list --reverse "${GIT_RANGE[@]}" | head -n 1βgh api /repos/$GITHUB_REPOSITORY/commits/$FIRST_SHA --jq '.author.login'. Stable across pushes (see ADR-004 for the enforcement chain). Two fallbacks: (1).committer.loginifauthor.loginis null, (2)LAST_PUSHED_BYif both fail.Last pushed by:β${{ github.triggering_actor || github.actor }}. Updates every push.Contributors on this branch:β list now uses GitHub @handles with commit counts (- @romandidomizio (4 commits)). Per-commitgh api /repos/.../commits/{sha} --jq '.author.login // .committer.login // ""'resolution; fallback to commit-author NAME ONLY (no email) for unlinked external commits.
workflows/README.mdΒ§3 rewritten β (a) intro sentence corrected to clarify attribution comes from GitHub event context + commits API, not branch-name parsing (resolves Copilot R5 #1); (b) Parsing Rules retitled "Parsing Rules (branch name β regex validation only)" with 3 simplified steps; (c) new "PR body attribution β three-tier model (by design)" subsection with Field/Value/Source/Updates table; (d) "Branch name vs. PR body" table expanded from 2 rows to 4 (branch<dev>+Opened by:+Last pushed by:+Contributors on this branch:).CONTRIBUTING.mdΒ§4 rewritten β (a)<dev>paragraph updated to clarify attribution uses commits API + event context, not branch parsing; (b) blockquote callout table expanded to 4 rows (matches workflows/README.md); (c) new explanatory bullets for each of the three PR-body fields with their resolution mechanism; (d)feature/add-thingtrailing note updated; (e) round-5 Copilot comment #2 fix: attribution paragraph rewritten to drop misleading phrasing; (f) round-7 alignment fix:Contributors:label updated toContributors on this branch:in both the table cell and the explanatory bullet to match the exact PR-body wording (resolves Copilot R7 #6).workflows/README.mdheader version + Last updated:**Version**: v3.3.4.1 β v3.3.5.1,**Last updated**: 2026-04-23 β 2026-04-27. The original 2026-04-23 bump was tov3.3.4.2(documented in[v3.3.4.2]below); this iteration's continuation work bumps it again tov3.3.5.1per the user's #WeOwnVer week-5 interpretation. Resolves Copilot R7 comments #1 + #2 (workflows/README.md header inconsistency) and #7 (CONTRIBUTING.md header inconsistency); same bump applied toCONTRIBUTING.md(Last updated 2026-04-23 β 2026-04-27),.github/ADR-003-main-branch-ruleset.md(Date "2026-04-23" β "2026-04-23 (ruleset configured) / 2026-04-27 (ADR last revised)" β resolves Copilot R7 #3), and.github/ADR-004-copilot-auto-review-ruleset.mdDecision Log + Empirical Validation Results section (resolves Copilot R7 #4)..github/ADR-003-main-branch-ruleset.mdRelated section β added cross-reference to ADR-004 (under Related), making the two-ADR structure discoverable..github/ADR-002-infisical-github-sync.mdnaming convention revised (2026-04-28) β the original convention usedWEOWN_BOT_PAT__<ORG>_<REPO>as the Infisical secret name with the assumption that the GitHub Sync UI exposed a per-secret rename feature at sync-config time. Empirical finding while configuring the sync on 2026-04-28: Infisical's "Key Schema" can ADD prefixes/suffixes around the{{secretKey}}template but cannot STRIP them, so the original convention is unworkable at sync-creation time. Revised convention: Infisical secret name =WEOWN_BOT_PAT(identity-mapped, no suffix) + namespace across repos via separate Infisical projects (weown-bot/<org>-<repo>), not secret-name suffixing. Updates: (a) Architecture diagram in ADR-002 redrawn to show project-per-scope; (b) Naming Convention section rewritten with explanation of why the original convention fails; (c) Implementation Notes "Initial setup" steps revised; (d) NEW Decision Log section appended with 2026-04-23 + 2026-04-28 rows documenting the convention evolution; (e) ADR-002 headerVersionbumped fromv3.3.4.1tov3.3.5.1,Dateupdated to2026-04-23 (initial) / 2026-04-28 (naming convention revised β see Decision Log). Status remains "Accepted" β this is an implementation-detail revision, not a decision reversal. Cross-doc updates:workflows/README.mdΒ§2.4 Usage Table + Β§5.1 Onboarding steps 2 + 4 + Β§6 Rotation step 6 + new Β§6.1 Sync Options Configuration sub-section. The Sync Options table documents the four configuration parameters (Initial Sync Behavior = Overwrite [forced]; Key Schema ={{secretKey}}identity; Disable Secret Deletion = Yes; Auto-Sync Enabled = Yes) with rationale per row..github/workflows/README.mdΒ§6.1 NEW: Sync Options Configuration β step-by-step guidance for configuring an Infisical β GitHub Secret Sync. Covers the four UI options (Initial Sync Behavior, Key Schema, Disable Secret Deletion, Auto-Sync Enabled) plus a Migration Steps sub-section for the 2026-04-28 transition from the legacyWEOWN_BOT_PAT__WEOWNNETWORK_AIsecret to the revisedWEOWN_BOT_PATidentity-mapped secret in the newweown-bot/weownnetwork-aiInfisical project. Cross-references ADR-002 Decision Log..github/workflows/README.mdΒ§2.4 Usage Table refreshed β active row updated to reflect the 2026-04-28 PAT rotation:Expiration: 2026-07-22 β 2026-07-27(90 calendar days from 2026-04-28);Last Rotated: 2026-04-23 β 2026-04-28;PAT Secret (Infisical) column: WEOWN_BOT_PAT__WEOWNNETWORK_AI β WEOWN_BOT_PAT (Infisical project: weown-bot/weownnetwork-ai). Placeholder rows for future repos updated to match the new project-per-scope naming..github/workflows/pat-health-check.ymlexample date refreshed β line 133 expected-format example string updated from'2026-07-22 17:13:00 UTC'to'2026-07-27 17:13:00 UTC'for currency. Behavior unchanged (the value is just an example in the diagnostic message; parsing logic is generic).
-
.github/ADR-004-copilot-auto-review-ruleset.mdvalidation step β the auto-trigger validation procedure previously instructed "record the run number + timestamp inPR7_HANDOFF_CHECKLIST.md" but that file is gitignored (operational notes; not in repo). Updated to point at this ADR's own Decision Log table for in-repo logging. Resolves Copilot R7 comment #5. -
CHANGELOG drift on three-tier attribution wording β the v3.3.4.2 "Added" bullet for platform-sourced attribution (originally written round-1) still described the workflow as producing
**Triggered by:** @<real-github-username>. Round-6 rewrote it to describe the three independent fields (Opened by:,Last pushed by:,Contributors on this branch:) currently emitted, with a parenthetical noting the round-5 evolution. Resolves Copilot R6 comment B. -
Round-5 auto-trigger claim REVERTED (carried forward from round-6, refined in round-7): prior session incorrectly inferred from the
2026-04-27T21:59:30ZCopilot review timestamp on commit34f28bfthat auto-trigger had succeeded. User clarified the round-5 review was MANUALLY triggered. Memory + checklist + this changelog corrected. Round-7 controlled-experiment finding: a fresh PR opened on 2026-04-27 from an old branch DID auto-trigger Copilot review (Copilot AI review requested due to automatic review settings), validating the Layer 2 ruleset + Copilot Business entitlement chain, but PR #13 (created 2026-04-23) does NOT auto-trigger because Copilot's auto-review decision is made at PR-creation time. See ADR-004 Β§ Empirical Validation Results. -
Round-20 Copilot review fixes (2026-04-28, 2 Copilot R20 fixes β 1 correctness fix + 1 doc-vs-UI display canonicalization with cross-doc cascade):
- R20 #1 /
.github/workflows/README.mdΒ§11 Troubleshooting "Merge blocked with 'requires signed commits'" row β guidance was incorrect. Old text: "Configure commit signing per CONTRIBUTING.md Β§3; retroactive signing is impossible (would force-push) β add a new signed commit to the branch." This was wrong: adding a new signed commit does NOT fix earlier unsigned commits β the merge will still be blocked because themainbranch protection requires ALL commits in the PR be signed (not just the most recent). Copilot R20 #1 caught the correctness bug. Fix (per Copilot's literal suggested change): rewrote the row to explain that adding a new signed commit does not fix earlier unsigned commits, and that because retroactive signing would rewrite history andnon-fast-forwardis blocked on~ALLbranches by Layer 1 + Layer 2 rulesets, the contributor must recreate the branch/PR with all commits signed (or otherwise ensure every commit in the PR is signed). Compliance basis: SOC 2 CC8.1 (incident-response runbooks must give correct remediation; misleading guidance during a merge-blocked incident wastes contributor time and may cause repeated failed remediation attempts); ISO/IEC 27001:2022 A.5.24 (runbook accuracy). Pattern: this is a runbook-correctness bug that survived 19 prior rounds of Copilot review because the row's narrative sounded plausible (the "force-push is blocked" framing was correct; the conclusion "add a new signed commit" was wrong). Lesson: troubleshooting tables describing remediation steps need explicit logic-validation against the underlying enforcement chain (here: branch-protection + ruleset rules), not just internal narrative consistency. - R20 #2 / Folder-path canonical display form across all docs β Copilot R20 #2 flagged that Infisical folder paths were shown with a trailing slash in several places (e.g.,
/WeOwnNetwork-ai/) but the Sync "Source secret path" was documented without it (/WeOwnNetwork-ai). This creates real-world copy/paste configuration errors during incident response and new-repo onboarding when stewards transcribe paths from docs into the Infisical UI. Copilot suggested standardizing β gave a literal "add trailing slashes everywhere" suggestion. User explicit preference: NO trailing slash (opposite of Copilot's literal suggestion). Rationale: the Infisical UI's "Source secret path" field accepts the path without trailing slash and mirrors the path verbatim, so the no-slash form is the form that survives the round-trip from docs β UI β docs without transformation. Applied trailing-slash β no-trailing-slash canonicalization across:.github/workflows/README.mdβ Β§2 Scope item 3, Β§2 Scope item 4 (Γ2 path examples), Β§2.4 Usage Table active row + 2 placeholder rows, Β§4.4 Step B step 3 + step 4, Β§5.1 Common steps step 2 + step 4, Β§6 Rotation step 6, Β§6.1 Sync Options Configuration explanatory paragraph + Migration Steps step 1 + step 4. Total: ~14 path normalizations within a single doc..github/INCIDENT_RESPONSE.mdβ Scenario 6 step 6 single instance (steward-handoff Infisical access verification)..github/ADR-002-infisical-github-sync.mdβ Architecture diagram lines 40 + 42 (with right-edge whitespace adjustment to maintain box-border alignment); Naming Convention bullet (line 64) with explicit "canonical form: NO trailing slash, per R20 close-out 2026-04-28" parenthetical added for future authors; Implementation Notes step 2 (line 138).
.github/ADR-002-infisical-github-sync.mdDecision Log appended with R20 row (in correct chronological position after R17 + R18, NOT as a replacement). The R20 row documents: (a) the canonical form decision; (b) the explicit PRESERVATION of R17 + R18 historical rows with their original trailing-slash forms per the R19-codified "Historical-narrative preservation in audit trails" rule (modifying the historical text would corrupt the audit trail of the substantive R17 + R18 convention decisions); (c) Status remains "Accepted" β this is a display canonicalization, NOT a convention change (both forms refer to the same Infisical folder); (d) NEW operational rule codified (R20 close-out): when documenting a path-like identifier that users will copy into a vendor UI text field, pick the canonical form that the vendor UI itself uses (or the simpler form when the UI accepts both) and use it consistently across all docs.- Header date bumps for traceability:
.github/ADR-002-infisical-github-sync.mdDate header:2026-04-23 (initial) / 2026-04-28 (naming convention revised twice β see Decision Log)β2026-04-23 (initial) / 2026-04-28 (naming convention revised twice + canonical no-trailing-slash form for folder paths β see Decision Log)..github/INCIDENT_RESPONSE.mdDate header: appendedR20 trailing-slash canonicalization on Scenario 6 step 6to the existing R13 + R18 list.
workflows/README.mdLast updated header (line 6) was already simply2026-04-28β no parenthetical detail to extend, no bump needed.- CHANGELOG
[v3.3.5.1]Β§ Changed historical bullets PRESERVED β entries documenting the R17 (project-per-repoweown-bot/weownnetwork-aireference) + R18 (folder-per-repo with trailing slashes) close-outs in their historical narrative form; modifying the original-state references would corrupt the close-out audit trail per the R19 rule. - Files touched (4):
.github/workflows/README.md,.github/ADR-002-infisical-github-sync.md,.github/INCIDENT_RESPONSE.md,CHANGELOG.md(this file). PR7_HANDOFF_CHECKLIST.md (gitignored) updated separately. - Pattern continuation: R20 #1 surfaced a runbook-correctness bug (different drift class from R19's label-citation drift); R20 #2 surfaced doc-vs-UI display drift (also distinct from prior rounds). Two new sub-classes of accuracy drift identified within a single round, both caught by Copilot late in the v3.3.5.1 review cycle. Cumulative pattern across rounds 7-20: late-round Copilot reviews continue surfacing new sub-classes of accuracy drift even after 19 prior rounds. Going-forward implication: the v3.3.5.1 review cycle is establishing a stable taxonomy of doc-accuracy-drift classes that future PRs can pre-validate against.
- R20 #1 /
-
Round-19 Copilot review fixes (2026-04-28, 1 Copilot R19 label-canonicalization fix + 3 same-doc + cross-doc cascades per the R18 #1 "Same-Document Consistency Sweep" rule):
- R19 #1 /
.github/workflows/README.mdΒ§3 "Branch name vs. PR body" table row label β the table row at line 170 was labeled**PR body \Contributors:` list**while the workflow itself emitsContributors on this branch:(auto-pr-to-main.ymlstep 8 line 320), and the SAME doc's earlier Β§3 three-tier table at line 161 already used the canonicalContributors on this branchlabel. Copilot R19 #1 flagged the inconsistency: readers cross-referencing the table against the actual PR body would see different labels and might think the doc is describing a different field than the one rendered. **Fix**: renamed the row label toPR body `Contributors on this branch:` listper Copilot's literal suggested change. **This fix is the canonical example of why the R18 #1 "Same-Document Consistency Sweep" rule matters** β the table row at line 170 saidContributors:while line 161 in the same doc already usedContributors on this branch`; R18's new rule mandated sweeping all earlier sections for the same convention; R19 #1 demonstrates the rule's value by catching exactly the kind of intra-doc drift it was designed to surface. Compliance basis: SOC 2 CC8.1 (documented operating procedures must be internally consistent across the same document); ISO/IEC 27001:2022 A.5.37 (no contradictory label citations within an authoritative reference doc); ISO/IEC 42001:2023 A.6.2.7 (AI system control descriptions must accurately match what the system emits β here, the actual workflow output line). - R19 same-doc cascade /
.github/workflows/README.mdΒ§11 Troubleshooting table row β applied the same fix to the Β§11 row "PR body shows wrong attribution" which usedContributors:in the resolution column when listing the three field-label semantics. Updated to canonicalContributors on this branch:for cross-section consistency. (Not Copilot-flagged; caught proactively per the R18 #1 same-doc-sweep rule.) - R19 cross-doc cascade #1 /
CONTRIBUTING.mdΒ§4 explanatory bullet β line 334 bullet**\Contributors:`β`Contributors on this branch:`**` to match the canonical workflow output. The same doc's earlier inline-quoted table at lines 289 + 294 already used the canonical form; the bullet at line 334 was the only inconsistent label-citation in the doc. - R19 cross-doc cascade #2 /
CONTRIBUTING.mdΒ§4 parenthetical at line 328 β within the "regex-valid but convention-violating" example paragraph, the parenthetical "(The PR body'sOpened by:,Last pushed by:, andContributors:fields will still attribute correctly regardless β see next paragraph.)" listed the three field names but used the shortened form for the third one while the other two used the canonical form. Updated to canonicalContributors on this branch:for full intra-paragraph consistency. CONTRIBUTING.mdLast updated header bump β "2026-04-28 (R12 Β§4 attribution-fallback fix + R13 header date sync)" β "2026-04-28 (R12 Β§4 attribution-fallback fix + R13 header date sync + R19 Β§4Contributors on this branch:label canonicalization)" for traceability of the R19 cross-doc cascade.- CHANGELOG
[v3.3.5.1]line 56 PRESERVED unchanged β the v3.3.5.1 Β§ Changed entry documenting the R7 fix that renamedContributors:βContributors on this branch:quotes the OLD label as the "before" value in a historical close-out narrative ("Contributors:label updated toContributors on this branch:"). Modifying that quoted historical value would corrupt the audit trail of when + why the rename happened. The R10 PII-recursive-quote lesson applies in reverse here: when a CHANGELOG entry documents a label rename, the original-value reference must remain verbatim for the audit-trail entry to retain its meaning. Compliance basis: SOC 2 CC7.2 (historical audit trail integrity); ISO/IEC 27001:2022 A.5.28 (records of revision history must accurately reflect the original state being revised). - Files touched (4):
.github/workflows/README.md,CONTRIBUTING.md,CHANGELOG.md,PR7_HANDOFF_CHECKLIST.md(gitignored). Single commit; squash-merge target preserved.
- R19 #1 /
-
Round-18 Copilot review fixes + Infisical naming convention 2nd revision (folder-per-repo) (2026-04-28, 4 Copilot R18 doc-consistency fixes + user-driven operational simplification of the R17 naming convention):
- R18 #1 /
.github/workflows/README.mdΒ§2 Scope & Principles β convention contradiction with Β§2.4 Usage Table β Copilot R18 #1 flagged that Β§2.4 Usage Table reflected the revised Infisical convention (WEOWN_BOT_PATin per-repo containers) but Β§2 "Scope & Principles" earlier in the doc still stated the old convention (single project +WEOWN_BOT_PAT__<ORG>_<REPO>naming), creating contradictory onboarding guidance. Fix: rewrote Β§2 item 3 ("Centralized storage") to describe the single shared projectweown-bot GitHub PATswith one folder per target repo (e.g.,/WeOwnNetwork-ai/), and rewrote item 4 ("Consistent naming") to match the revised convention β Infisical secret per folder =WEOWN_BOT_PAT(identity-mapped), namespace via folder path, GitHub Actions secret =WEOWN_BOT_PAT. Item 4 also notes the 2026-04-28 ADR-002 Decision Log revision and points at Β§6.1 Sync Options for the full rationale. Compliance basis: SOC 2 CC8.1 (documented operating procedures must be internally consistent across the same document); ISO/IEC 27001:2022 A.5.37 (no contradictory onboarding guidance β onboarding contributors must read one canonical convention). - R18 #2 /
.github/INCIDENT_RESPONSE.mdScenario 6 step 6 β legacy project-name reference β Copilot R18 #2 flagged that Scenario 6 (Stewardship Gap) step 6 still referenced the legacy Infisical project name (weown-bot GitHub PATsonly, no folder qualifier) but this PR's revised convention namespaces per-repo via folders inside that project. The runbook needed to match the new structure so a new steward verifying access during a SEV-4 hand-off would know which folder to check. Fix: per Copilot's suggested change, updated step 6 to "Verify new steward has Infisical access (projectweown-bot GitHub PATs, folder/WeOwnNetwork-ai/)". Also bumped the file's Date header to include the R18 update for traceability. Compliance basis: NIST CSF 2.0 RS.MA-1 + ISO/IEC 27001:2022 A.5.24 (incident-response runbooks must reference current resource identifiers under stress; legacy references slow steward onboarding during an active incident). - R18 #3 /
.github/ADR-004-copilot-auto-review-ruleset.mdLayer 1 bullet #3 β copilot_code_review claim qualification β Copilot R18 #3 flagged that the Layer 1 rules bullet #3 stated "auto-request Copilot review on every PR (draft + ready) and on every push to an open PR", which contradicts the same ADR's later Β§ Empirical Validation Results section documenting PR-creation-time eligibility caching (pre-existing PRs do not auto-request on subsequent push). Fix: rewritten per Copilot's suggested change to "auto-request Copilot review for newly-created, eligible PRs (draft + ready), and re-request review on later pushes only for open PRs where Copilot was auto-requested at creation"; appended a Note that explicitly cross-references Β§ Empirical Validation Results and documents the close+reopen / merge+open-fresh remediation paths for pre-existing PRs. Also bumped the file's Date header to include R18 for traceability. Compliance basis: SOC 2 CC7.2 (control-evidence must be internally consistent across rule descriptions and validation results); ISO/IEC 42001:2023 A.6.2.7 (AI system enforcement claims must accurately describe the population they apply to β here, newly-created PRs only). - R18 #4 /
.github/workflows/README.mdheaderLast updatedβ stale date β Copilot R18 #4 flagged that the file's headerLast updated: 2026-04-27was stale because this PR's R17 close-out added 2026-04-28 changes (PAT rotation dates + new Β§6.1 Sync Options Configuration sub-section). Fix: per Copilot's suggested change, bumped toLast updated: 2026-04-28. Compliance basis: SOC 2 CC8.1 + ISO/IEC 27001:2022 A.5.37 (versioned-doc headers must reflect the most recent edit so auditors can locate the audit-trail entry covering the change). - Folder-per-repo namespacing β second revision of the 2026-04-28 naming convention (user-directed operational simplification): the R17 first-iteration revision used project-per-repo (one Infisical project per target repo, e.g.,
weown-bot/weownnetwork-ai). User feedback during the actual sync configuration on 2026-04-28: a single shared Infisical project with one folder per target repo is operationally cleaner β (a) one project-level RBAC boundary instead of N; (b) one expiration-reminder convention; (c) sibling folders discoverable from the same project landing page; (d) the existingweown-bot GitHub PATsproject absorbs new repos without new project creation. Cascade: ADR-002 architecture diagram redrawn to show project-with-folders; Naming Convention section rewritten with the folder-per-repo model + a "why folder-per-repo, not project-per-repo" comparison; Implementation Notes initial setup steps revised (5 steps β 7 steps to add folder creation); Decision Log appended with a SECOND 2026-04-28 row tagged(R18)documenting the projectβfolder shift while preserving the R17 row for audit trail..github/workflows/README.mdupdates: Β§2 Scope items 3+4 rewritten (R18 #1 above); Β§2.4 Usage Table rows updated to showInfisical project: weown-bot GitHub PATs, folder: /WeOwnNetwork-ai/; Β§4.4 Step B + Β§4.5 Step C rewritten for folder-based initial setup with explicit Source Path =/WeOwnNetwork-ai; Β§5.1 replication steps 2 + 4 rewritten for folder-based; Β§6 Rotation step 6 references folder; Β§6.1 Sync Options Configuration updated with new "Why folder-per-repo, not project-per-repo" sub-section + revised Migration Steps..github/INCIDENT_RESPONSE.mdScenario 6 step 6 updated (R18 #2 above). Compliance basis: same as ADR-002 baseline (Infisical-primary-with-GitHub-Sync; Status remains "Accepted") β this is a second implementation-detail revision documenting the operationally-cleaner namespacing axis. NEW operational rule (R18 close-out): when a vendor-feature-driven convention is documented in an ADR, validate the convention against actual UI/API configuration before broad cascade β first-cut conventions may need a second-cut revision once the namespacing primitives are exercised in practice (R17 β R18 here is the canonical example; the R17 project-per-repo convention was correct in principle but operationally suboptimal; R18 folder-per-repo arrived from actual sync-creation feedback within the same day).
- R18 #1 /
-
Round-17 Copilot review fixes + PAT rotation + Infisical sync configuration revision (2026-04-28, 1 Copilot R17 doc-accuracy fix + ecosystem-shaping naming-convention revision + audited PAT rotation event):
- R17 #1 /
auto-pr-to-main.ymlstep 4 trap β unconditionalrm -f "$CONTRIB_RAW"is NOT a silent no-op β the R14 #1 trap consolidation comment block claimed"rm -f \"\""was a "silent no-op until step 7's mktemp populates the variable", but on GNU coreutils (the Ubuntu-based GitHub-hosted runner)rm -f ""emitsrm: cannot remove '': No such file or directoryand exits 1. If the script aborts before step 7's mktemp populatesCONTRIB_RAW, the trap'srm -fwould alter the trap's own exit status and could mask the real script-failure exit code. Copilot R17 #1 flagged the empty-operand failure mode. Fix: split the trap into an unconditional cleanup for the always-populated paths + a conditional cleanup branch for the optional placeholder βtrap 'rm -f "$PR_BODY" "$PR_TITLE" "$CONTRIBUTORS_FILE"; if [ -n "$CONTRIB_RAW" ]; then rm -f "$CONTRIB_RAW"; fi' EXIT. Comment block above the trap rewritten to (a) describe the actual GNU coreutils empty-operand behavior, correcting the R14 #1 "silent no-op" claim that was incorrect, (b) explicitly direct future contributors to add new optional temp files to the conditional branch (not the unconditional list) when their mktemp assignment happens after the trap is set. Compliance basis: SOC 2 CC7.2 (cleanup procedures must not alter the exit status of monitored job steps); ISO/IEC 27001:2022 A.5.37 (documented operating procedures must reflect actual runtime behavior, not assumed behavior); ISO/IEC 42001:2023 A.6.2.7 (correctness of code-comment claims is part of the AI-system documentation surface).
- R17 #1 /
-
Round-16 Copilot review fixes (2026-04-28, 4 items β 2 workflow improvements on
auto-pr-to-main.yml+ 1 ADR-004 heading rename for stable anchor + 1 CHANGELOG cross-doc link fix):- R16 #1 /
auto-pr-to-main.ymlstep 4 β RUNNER_TEMP fail-fast (policy-code alignment) β the comment block above the temp-dir setup explicitly stated "$RUNNER_TEMP β the GitHub-runner-scoped temp directory that is isolated from the shared /tmp", but the code didTEMP_DIR="${RUNNER_TEMP:-/tmp}"β silently falling back to/tmpifRUNNER_TEMPwas unset, which contradicts the documented isolation policy. Copilot R16 #1 flagged the policy-code drift. Fix: replaced the silent fallback with an explicit fail-fast check (if [ -z "${RUNNER_TEMP:-}" ]; then echo "::error::..." >&2; exit 1; fi) that aborts the workflow with a clear error message ifRUNNER_TEMPis unset. The error message explains: GitHub-hosted runners always setRUNNER_TEMP; an unset value means the workflow is being executed in an unsupported environment (e.g.,act, local emulation without env shimming) where the isolation guarantee cannot be honored. Comments above the check were preserved unchanged because they now accurately describe the runtime behavior. Compliance basis: SOC 2 CC7.2 + CC8.1 (controls must enforce the documented policy, not silently degrade to a weaker stance); ISO/IEC 27001:2022 A.5.37 (documented operating procedures must be enforced by the system, not just described); ISO/IEC 42001:2023 A.6.2.7-A.6.2.8 (AI system controls must fail-closed, not fail-open). - R16 #2 /
auto-pr-to-main.ymlstep 7 β email-keyed cache prevents per-commit API call storm on large branches β the contributors-list builder did onegh api repos/.../commits/$shacall per commit in the branch range. On a long-running branch with 50+ commits this could trigger GitHub API rate-limits, degrading attribution to the name-only fallback for the rest of the run. Copilot R16 #2 flagged the rate-limit risk. Fix: added a bash associative arrayEMAIL_LOGIN_CACHEkeyed by author email. Before each API call, check if the email already has a memoized login (cache hit β reuse). On miss, do the API call and memoize the result (including empty-string "unresolved" outcomes so we don't retry the same email twice within one run). On a typical PR (1β5 unique authors over 5β50 commits), this reduces API calls by 50β95%. Cache is correct because GitHub's commits API resolves the same email to the same login deterministically. Cache scope is the workflow run (rebuilt fresh perauto-pr-to-mainexecution); no persistence needed. Compliance basis: NIST CSF 2.0 PR.IP-1 (efficient resource use); NIST CSF 2.0 PR.AC-4 (rate-limit resilience prevents control degradation under load); SOC 2 A1.2 (system processing integrity β attribution remains accurate even on large PRs). - R16 #3 /
ADR-004Β§ Empirical Validation Results β heading rename for stable anchor β the section heading was## Empirical Validation Results (round-7, 2026-04-27). GitHub auto-generates Markdown anchors via lowercase + space-to-hyphen + non-alphanumeric drop, producing#empirical-validation-results-round-7-2026-04-27(with the parenthetical date suffix baked into the anchor). Internal links written as[Empirical Validation Results](#empirical-validation-results)(the human-intuition-friendly form) DO NOT resolve. Copilot R16 #3 flagged the broken intra-doc anchor. Fix: renamed the heading to## Empirical Validation Results(clean anchor#empirical-validation-results) and moved the round/date provenance to a*Source: round-7 controlled experiment, 2026-04-27 (sharpened in round-13 #1, 2026-04-28).*italic sub-line right under the heading. Audit trail of when the section was added + revised is preserved without polluting the anchor. Lesson: section headings that need to be cross-referenced should NEVER include parenthetical dates / round-numbers / version qualifiers β those go in a sub-line. Codified as a NEW operational rule (see Meta below). - R16 #4 /
CHANGELOG.mdline 66 β broken intra-CHANGELOG anchor for cross-doc reference β the R15 #1 narrative saidCross-references [Empirical Validation Results](#empirical-validation-results) from the procedure intro, but#empirical-validation-resultsresolves WITHIN THE CHANGELOG (where there is no such heading), not in ADR-004 where the section actually lives. Copilot R16 #4 flagged the implicit-anchor failure mode (Markdown(#anchor)always resolves within the current doc, not the intended target doc). Fix: rewritten as an explicit cross-doc link(.github/ADR-004-copilot-auto-review-ruleset.md#empirical-validation-results)per Copilot's suggestion. Combined with the R16 #3 heading rename, the link now resolves correctly from the CHANGELOG to the renamed clean-anchor section in ADR-004. Lesson: when a CHANGELOG narrative paraphrases or summarizes a cross-doc link, ALWAYS render the link as an explicit relative path ((./relative/path.md#anchor)), never as a bare(#anchor). The latter form silently resolves to a current-doc anchor that doesn't exist. Codified as a NEW operational rule (see Meta below).
- R16 #1 /
-
Round-15 Copilot review fixes (2026-04-28, 3 items β 1 ADR-004 verification-procedure alignment + 2 workflow improvements on
auto-pr-to-main.yml):- R15 #1 /
ADR-004Β§ End-to-end auto-trigger validation β procedure conflicted with Empirical Validation Results β the validation steps instructed "the next bot-authored push to any open PR is the live test" + "push a commit authored viaweown-bot(i.e., the auto-PR workflow runs and updates / creates a PR)", but the same ADR's Β§ Empirical Validation Results (added in R7, sharpened in R13 #1) documents that Copilot auto-review eligibility is evaluated at PR-creation / reopen time, NOT at push time. Running the procedure against a pre-existing PR (like PR #13 itself) produces a false negative because the PR-creation-time cache was set BEFORE ruleset enablement. Copilot R15 #1 flagged the internal contradiction. Fix: rewritten to explicitly require ONE of two trigger paths: (a) New PR path β pushweown-bot-authored commits to a fresh branch soauto-pr-to-main.ymlopens a brand-new PR, OR (b) Close+reopen path βgh pr close <N>thengh pr reopen <N>on an existing PR (Copilot re-evaluates eligibility on the reopen event). Added bold Important callout at top of section + explicit warning in step 4 ("Do not retry against a pre-existing PR via plain push β that path is known-unreliable per Β§ Empirical Validation Results"). Cross-references Empirical Validation Results from the procedure intro. Compliance basis: SOC 2 CC7.2 (validation procedures must be executable end-to-end without producing false negatives); ISO/IEC 27001:2022 A.5.37 (documented operating procedures must correctly describe the system behavior, including timing-sensitive enforcement semantics); ISO/IEC 42001:2023 A.6.2.7-A.6.2.8 (AI validation procedures must account for the population they apply to β here, newly-created PRs only). - R15 #2 /
auto-pr-to-main.ymlstep 6 β two API calls collapsed into one with jq fallback chain β theOPENED_BYresolution did twogh api repos/$REPO/commits/$FIRST_SHAcalls on the same SHA: first with--jq '.author.login // ""', then on empty fall through, with--jq '.committer.login // ""'. Both calls hit the same endpoint with the same SHA, wasting one API request per PR build + doubling rate-limit exposure. Copilot R15 #2 suggested consolidating via jq's//operator. Fix: rewritten to a single callgh api "repos/$GITHUB_REPOSITORY/commits/$FIRST_SHA" --jq '.author.login // .committer.login // ""'β jq evaluates the fallback chain in-process, returning the first truthy value (or empty string on total miss). Halves per-PR API requests, reducing rate-limit exposure especially under burst conditions (e.g., multiple pushes within theconcurrency:cancel-in-progress window). Added comment block explaining the consolidation rationale + rate-limit justification. Compliance basis: operational cost reduction + defense-in-depth against API rate-limits (NIST CSF 2.0 PR.IP-1 β configuration baselines include efficient resource use). - R15 #3 /
auto-pr-to-main.ymlstep 8 β@unknownsentinel leakage in PR body β linesecho "**Opened by:** @${OPENED_BY}"+echo "**Last pushed by:** @${LAST_PUSHED_BY_RESOLVED}"always prefixed@even when the resolved value was the fallback sentinelunknown(e.g., whenLAST_PUSHED_BYenv var was empty AND the API fallback chain exhausted). Output would read@unknownwhich is misleading β@unknownis not a valid GitHub login and looks like a broken mention. Copilot R15 #3 flagged the UX degradation. Fix: addedrender_handle()helper function before the step 8{block that uses a bashcaseto conditionally prefix@only when the value is non-empty AND not the literal stringunknown; otherwise renders plainunknown. Call sites changed from@${OPENED_BY}/@${LAST_PUSHED_BY_RESOLVED}to$(render_handle "$OPENED_BY")/$(render_handle "$LAST_PUSHED_BY_RESOLVED"). Output under degraded resolution now reads**Opened by:** unknown(no broken mention) instead of**Opened by:** @unknown. Helper is reusable for any future handle-rendering needs in the same workflow. Compliance basis: SOC 2 CC8.1 (user-facing output must accurately reflect system state); ISO/IEC 42001:2023 A.6.2.7 (AI-adjacent outputs must not produce misleading artifacts).
- R15 #1 /
-
Round-14 Copilot review fixes (2026-04-28, 6 items β 1 workflow refactor + 1 ADR-004 control-evidence wording + 4 anchor-link sweeps after R13 Β§ header date-range change):
- R14 #1 /
auto-pr-to-main.ymlstep 4 + step 7 trap consolidation β the workflow settrap '...' EXITtwice in the same shell (line 133 covering 3 temp files, line 213 overwriting it to cover 4). Functionally correct (each trap covered exactly the files extant at that point) but Copilot flagged the override pattern as regression-risky: any future temp file added between steps would silently leak if the dev forgot to update both trap calls. Fix: declaredCONTRIB_RAW=""placeholder in step 4 alongside the other mktemp calls; the single trap on line 141 now references all 4 paths upfront. The trap body single-quotes$CONTRIB_RAW, so variable expansion happens at fire time β empty string expands to a silentrm -f ""no-op until step 7 mktemp populates the variable. Step 7 retains only theCONTRIB_RAW="$(mktemp ...)"assignment; the duplicate trap call is removed. Added in-line comment block explaining the deliberate single-trap-with-deferred-mktemp pattern + regression-safety rationale (any future temp file just adds itself to the trap body once, with empty placeholder upfront). Compliance basis: SOC 2 CC8.1 (controls must be unambiguous to engineers) + ISO/IEC 27001:2022 A.5.37 (documented operating procedures must accurately describe enforcement mechanism) + WeOwnmktemp + trapdefense-in-depth pattern from Β§ 6 of.github/workflows/README.md. - R14 #2 / ADR-004 Β§ "Defense-in-depth" bullet β inaccurate control-evidence claim β the bullet read "Ruleset
non_fast_forwardANDauto-pr-to-main.yml's commit-signing requirement" butauto-pr-to-main.ymldoes NOT enforce commit signing β that's themainruleset (per ADR-003 Β§ 8.1 row 5:required_signatures: true). The auto-PR workflow signs ITS OWN commits (the bot-authored one creating the PR) but doesn't gate human commits on the branch. Copilot R14 #2 flagged the example as inaccurate control-evidence. Fix: rewritten to "Rulesetnon_fast_forwardAND ADR-003 /mainruleset signed-commit enforcement" β attributes the signing-enforcement to the actual control mechanism (themainbranch ruleset) instead of the workflow. The defense-in-depth point still holds:non_fast_forward(Layer 1,~ALLbranches) +required_signatures(mainruleset only) are independent enforcement layers. Compliance basis: SOC 2 CC7.2 (audit-trail accuracy: control-evidence statements must point to the actual enforcement mechanism, not an implementer that doesn't enforce); ISO/IEC 42001:2023 A.6.2.7-A.6.2.8 (AI control attribution must be precise to support compliance audit). - R14 #3-#6 /
CHANGELOG.md4 broken intra-doc anchor links β R13 #5 changed the[v3.3.5.1]section header fromβ 2026-04-27toβ 2026-04-27 to 2026-04-28. GitHub auto-generates Markdown anchors by lowercasing + replacing spaces with hyphens + dropping non-alphanumerics; the new anchor is#v3351--2026-04-27-to-2026-04-28, breaking the previous#v3351--2026-04-27references. Copilot R14 #3-#6 flagged 4 occurrences across the[v3.3.4.2]section: (3) Changed bullet about workflows/README.md header version bump cross-reference; (4) Changed bullet about Continuation rounds 4β7 cross-reference; (5) Added bullet about Platform-sourced developer attribution three-tier upgrade cross-reference; (6) Meta bullet about Version cadence cross-reference. Fix: singlesed-equivalent sweep β all 4 occurrences updated from(#v3351--2026-04-27)to(#v3351--2026-04-27-to-2026-04-28)viareplace_allon the exact pattern. Verified zero false positives (no other#v3351--2026-04-27substring exists). Lesson: when a## [vN.N.N.N]section header is modified mid-PR (e.g., date-range expansion), sweep ALL intra-doc links in the same commit β GitHub-generated anchors regenerate automatically and silently break references. Codified as a NEW operational rule in this round's close-out.
- R14 #1 /
-
Round-13 Copilot review fixes (2026-04-28, 5 items β 4 doc edits committed in this round + 1 deferred manual PR title alignment):
- R13 #1 /
ADR-004line 24 βcopilot_code_reviewclaim qualified β the rule description claimed "every PR (regardless of base branch) gets Copilot AI review automatically" but the same ADR's Β§ Empirical Validation Results section documents that auto-trigger is evaluated at PR-creation time and does NOT apply to pre-existing PRs (e.g., PR #13). Copilot flagged the internal contradiction. Fix: rewritten to "every newly-created PR (regardless of base branch) gets Copilot AI review automatically after ruleset enablement" + added explicit Note paragraph: "Copilot evaluates auto-review eligibility at PR-creation time, so PRs that already existed before the ruleset was applied (e.g., PR #13) do not retroactively gain auto-review β they must be triggered manually for the duration of their open lifecycle. See Β§ Empirical Validation Results below for the controlled experiment confirming this PR-creation-time caching behavior." Also bumped Date header to add2026-04-28 (R13 clarification on auto-trigger timing)for audit traceability. Resolves the contradiction without softening the forward-looking claim (all NEW PRs auto-trigger; only PR #13 is grandfathered to manual-only for its remaining lifecycle). - R13 #2 /
INCIDENT_RESPONSE.mdScenario 6 step 2 β awkward "per ... per ..." phrasing rephrased β the line read "Assign new primary steward ... per CODEOWNERS.github/CODEOWNERSper-path TODO comments (decision pending 2026-05-15 handoff)" which had repeated "per" and ambiguous parsing during a live incident. Fix: rewritten per Copilot's suggestion to "based on.github/CODEOWNERS; use the handoff TODO comments there to resolve the pending 2026-05-15 decision" β single canonical reference to.github/CODEOWNERS, semicolon-separated decision-procedure clarification, no duplicate "per" keywords. Also bumped headerVersionfromv3.3.4.1tov3.3.5.1andDateto2026-04-23 (initial) / 2026-04-28 (R13 phrasing fix on Scenario 6 step 2)so the header reflects the actual last-edit state and matches the rest of the v3.3.5.1 PR scope. Compliance basis: incident-response runbooks must be unambiguous under stress (NIST CSF 2.0 RS.MA-1, ISO/IEC 27001:2022 A.5.24); awkward phrasing is a documented incident-response failure mode. - R13 #3 / PR title vs. CHANGELOG version mismatch β deferred to manual
gh pr editafter this commit lands β PR #13 title was set at PR-creation time (2026-04-23) toAuto-PR: docs(v3.3.4.2): sync main ruleset docs + ADR-003 + confirmed contributor handles. The auto-PR workflow intentionally preserves PR titles across pushes (see R11 fix inworkflows/README.mdΒ§2A step 9 + ADR-001), so the title naturally drifts from the effective in-flight version (v3.3.5.1). Copilot R13 #3 flagged the divergence between the PR title and the[v3.3.5.1]CHANGELOG section. Fix: cannot be applied via a regular commit (workflow ignores--titleon existing PRs by design). Action plan: after this commit lands, manually rungh pr edit 13 --title "docs(v3.3.5.1): main ruleset docs + ADR-003/004 + auto-PR workflow attribution + Copilot review hardening"to align the PR title with the current effective version. This is a one-time manual override of the workflow's title-preservation contract, justified by audit-trail clarity at squash-merge time. The workflow's title-preservation rule remains correct for normal use; this is the documented exception when the in-flight version bumps mid-PR. - R13 #4 /
CONTRIBUTING.mdheaderLast updated: 2026-04-27β stale date refreshed β the file was modified in R12 (2026-04-28) but the header date wasn't updated. Copilot R13 #4 suggested bumping both Version (v3.3.5.1 β v3.3.5.2) and Last updated (2026-04-27 β 2026-04-28). Fix applied:Last updated: 2026-04-28 (R12 Β§4 attribution-fallback fix + R13 header date sync). Fix REJECTED: keepingVersion: v3.3.5.1rather than bumping tov3.3.5.2. Rationale for rejection: per the explicit decision in this[v3.3.5.1]section's intro paragraph ("Version stays atv3.3.5.1across the day boundary because the PR is a single semantic unit (no fresh iteration started post-merge)"), the in-flight PR keeps the same #WeOwnVer iteration number throughout. Bumping to v3.3.5.2 mid-PR would create cascade churn (CONTRIBUTING.md, workflows/README.md, ADR-003, ADR-004, CHANGELOG section header all need to match) without changing the PR's semantic scope. Documenting the rejection in theLast updatedparenthetical communicates intent to future auditors. (A future v3.3.5.2 will be the FIRST PR opened on 2026-04-28 or later β a fresh iteration boundary.) - R13 #5 /
CHANGELOG.md[v3.3.5.1]header date + intro paragraph β stale single-day date refreshed to span β the section header read## [v3.3.5.1] β 2026-04-27and the intro claimed "all changes in this section were made on 2026-04-27", but R12 added a 2026-04-28-dated entry. Copilot R13 #5 flagged the inconsistency. Fix: header rewritten to## [v3.3.5.1] β 2026-04-27 to 2026-04-28per Copilot's suggested rewording; intro paragraph rewritten to "this section captures the portion of the PR that moved into the 5th week of April under the active #WeOwnVer interpretation ... the later continuation rounds documented here span 2026-04-27 (rounds 4β11) through 2026-04-28 (rounds 12β13)" + added explicit "Version stays atv3.3.5.1across the day boundary because the PR is a single semantic unit (no fresh iteration started post-merge)" sentence so the v3.3.5.1-not-v3.3.5.2 decision is auditable from the section header alone (cross-references R13 #4 above).
- R13 #1 /
-
Round-12 Copilot review fixes (2026-04-28, 1 item β in
CONTRIBUTING.mdΒ§4 attribution paragraph):- R12 #1 / Β§4 post-attribution closing sentence β the trailing sentence claimed "The PR body always shows real GitHub usernames (
@ncimino,@romandidomizio, etc.), regardless of what<dev>segment was chosen" which contradicted the documented step-7 aggregation fallback behavior (perworkflows/README.mdΒ§2A step 7 +auto-pr-to-main.ymlstep 7:gh apireturns.author.login // .committer.login // ""; on empty, falls through togit log -1 --format=%anproducing a plain-text author name, no@prefix, no GitHub link). Copilot flagged "always" as overly absolute. Fix: rewritten to "shows real GitHub usernames (@ncimino,@romandidomizio, etc.) when available and otherwise falls back to commit-author names (for commits where the commits API doesn't return a linked GitHub login β e.g., unlinked email addresses), regardless of what<dev>segment was chosen for the branch name." Matches Copilot's suggested rewording and adds a parenthetical explaining the specific failure mode (unlinked email) so maintainers understand why the fallback exists. Compliance alignment: SOC 2 CC8.1 + ISO/IEC 27001:2022 A.5.37 (documented operating procedures must be accurate) β the "always" claim would fail an audit where the auditor traces the code path and finds the fallback branch.
- R12 #1 / Β§4 post-attribution closing sentence β the trailing sentence claimed "The PR body always shows real GitHub usernames (
-
Round-11 Copilot review fixes (2026-04-27, 1 item β in
workflows/README.mdΒ§2A walkthrough table):- R11 #1 / Β§2A step 9 row β the "What it does" column claimed the existing-PR idempotent-update path runs
gh pr edit $N --title --body --add-reviewer ncimino,romandidomizio(single combined call, updates title). In reality,auto-pr-to-main.ymllines 353β357 intentionally preserve the existing PR title and make two separategh pr editcalls:gh pr edit $N --body-file $PR_BODY(body only β no--titleflag passed), followed bygh pr edit $N --add-reviewer ncimino,romandidomizio(reviewers only). Title is set exactly once at PR creation time from$PR_TITLE(step 5). Row rewritten to accurately reflect the implementation: "If found βgh pr edit $N --body-file $PR_BODY(preserves the existing title β PR titles are set once at creation, not refreshed on subsequent pushes), followed by a separategh pr edit $N --add-reviewer ncimino,romandidomizio. If not βgh pr create --base main --head $BRANCH_NAME --title $(cat $PR_TITLE) --body-file $PR_BODYfollowed by the same--add-reviewercall." Rationale for title preservation (now explicit in doc): PR titles tend to carry human semantic context (feature scope, PR tracking nicknames); refreshing them on every push based on the latest commit subject would create churn in PR lists + notifications. Body is always refreshed to reflect latest commits + attribution. This matches GitHub best-practice for long-lived PRs with iterative review rounds (exactly the scenario PR #13 is in β title stable for 11 rounds, body refreshed each push). Compliance alignment: SOC 2 CC8.1 change-management β accurate developer documentation is part of the audit-trail evidence chain (the walkthrough table IS the onboarding doc for new contributors); inaccurate documentation of a security-sensitive workflow step would be flagged in an audit.
- R11 #1 / Β§2A step 9 row β the "What it does" column claimed the existing-PR idempotent-update path runs
-
Round-10 Copilot review fixes (2026-04-27, 2 items β both in this
CHANGELOG.md):- R10 #1 /
### Fixedround-8 close-out entry + R10 #2 /### Security / CompliancePII audit bullet β the round-8 entry quoted the literal personal email 2x ('Replace temp<actual-email>with permanent bot email' + 'stale<actual-email>references in Β§2 + Β§7') as part of describing the round-8 fix, and the PII-audit bullet quoted it 1x ('all 3<actual-email>references inworkflows/README.mdrewritten...'). All 3 occurrences flagged by Copilot R10 as still exposing PII despite the fix being correctly described. Fix: all 3 occurrences replaced with<redacted-personal-email>placeholder β same convention used elsewhere in the codebase for placeholder identifiers (e.g.,<dev>,<real-github-username>in v3.3.4.2 entry). Rationale and round-8 audit history fully preserved; only the literal email string is gone. Anticipated in round-8 close-out commit message: that commit explicitly noted "the email appears 3x in the round-8 commit body documenting Before: state of the fix" and identified squash-merge as the long-term sanitization mechanism. Round-10 brings forward that sanitization to the CHANGELOG narrative now (rather than waiting for squash-merge) so the in-flight CHANGELOG no longer carries the email. Same redaction approach should be applied to the round-10 close-out commit message itself β i.e., this entry must NOT quote the literal email it just redacted.
- R10 #1 /
-
Round-9 Copilot review fixes (2026-04-27, 3 items + 1 plain-text-handle defense-in-depth extension β all in
workflows/README.md):- R9 #1 / Β§2 Account Security Requirements line 64 β "β 2FA mandatory (TOTP + recovery codes held by Yonks as enterprise admin)" disclosed specific 2FA credential custody on a public repo. Round-3 had already removed this pattern from Β§10 Transition Checklist row 2 ("Transfer 2FA (TOTP seed) + recovery codes..." β "Transfer 2FA administration per internal runbook...") but missed the Β§2 line. Fix: rewritten to "β 2FA mandatory (administration, recovery, and custody tracked per internal runbook β no credential details in this public repo)". Aligns with the round-3/7/8 internal-runbook posture. Reduces social-engineering surface area (no specific credential types named, no custodian named in this section). Compliance basis: ISO/IEC 27001:2022 A.5.34 (PII protection) + A.8.1 (user endpoint devices) + NIST CSF 2.0 PR.AC-3 (access management without revealing custody chain).
- R9 #2 / Β§11 Troubleshooting Copilot auto-review row β manual-trigger snippet used
reviewers[]=Copilotbut ADR-004 Β§ Validation (line 169) references the canonical logincopilot-pull-request-reviewer. Inconsistency could cause API call to fail (Copilotis not a valid GitHub login) or confuse maintainers cross-referencing the two docs. Fix: snippet rewritten to usereviewers[]=copilot-pull-request-reviewerwith explicit "canonical GitHub Copilot reviewer login β same value referenced by ADR-004 Β§ Validation" cross-reference. Single canonical identifier across the docs reduces divergence risk. - R9 #3 / Β§10 Transition Checklist rows 3 + 6 β Owner column had plain-text "Yonks" (row 3 sole owner; row 6 second owner) while adjacent rows used
@YonksTEAMhandle. Inconsistency makes unambiguous paging during transition impossible (audit trail can't@-mention plain-text names). Fix: both rows updated to@YonksTEAMhandle. All 6 rows in the Transition Checklist now use GitHub handles consistently for the Owner column. - PII / handle-consistency defense-in-depth note: a separate sweep found that
SECURITY_ASSESSMENT.mdline 14 ("Yonks-held recovery codes") andINCIDENT_RESPONSE.mdlines 61, 149, 152 (similar credential-custody disclosures + plain-text "Yonks" handles) carry the same anti-patterns. These files are OUT OF SCOPE for PR #13 (which is the main-branch-ruleset + auto-PR + Copilot-review docs sweep) and intentionally NOT modified to avoid scope creep. Tracked: a follow-up PR (v3.3.5.2 or v3.3.6.1) will apply the same internal-runbook + handle-consistency rewrites acrossSECURITY_ASSESSMENT.md+INCIDENT_RESPONSE.md. Documented inPR7_HANDOFF_CHECKLIST.mdqueue.
-
Round-8 Copilot review fixes (2026-04-27, 3 items β all in
workflows/README.md):- R8 #1 / Β§10 Transition Checklist row 3 β "Replace temp
<redacted-personal-email>with permanent bot email" exposed a real personal email on a public repo. Fix: rewritten to "Update the service account's email to the permanent bot email (details tracked per internal runbook)". Defense-in-depth extension (same principle): stale<redacted-personal-email>references in Β§2 Account Security Requirements (line 65) and Β§7 Layer 1 email alert recipient (line 390) also rewritten to generic internal-runbook language to prevent Copilot from flagging them in a future round and to complete the PII audit. Aligns with the round-6/7 PII-minimization posture already applied toCONTRIBUTING.mdhandle table +CODEOWNERS+ step 7 contributors fallback + step 8 Recent Commits. Personal emails are PII per GDPR Art. 5(1)(c) data minimization + ISO/IEC 27001:2022 A.5.34 even when the individual is publicly associated with the repo. - R8 #2 / Β§11 Troubleshooting 403 row β the suggested PAT scopes (
contents:write, pull_requests:write, issues:write, metadata:read) contradicted the least-privilege guidance at Β§2.4 + ADR-001 (Contents: Read, Pull requests: Read/Write, Metadata: Read;Issues: Writeintentionally NOT on the PAT). Fix: row rewritten to specify the 3 correct least-privilege scopes, explicit "Do NOT addIssues: Writeto the PAT" warning, cross-reference to Β§2.4 + ADR-001, and explanation thatpermissions: issues: writeon the workflow job (using the ephemeralGITHUB_TOKEN) is the correct mechanism for issue actions β not a broader PAT scope. Prevents a future maintainer following the troubleshooting table from over-provisioning the PAT against the repo's own security posture. - R8 #3 / Β§2A step 6 failure mode β column said
gh apirate-limited "uses$GITHUB_TOKEN", butauto-pr-to-main.ymlactually runs everyghcall underGH_TOKEN=${{ secrets.WEOWN_BOT_PAT }}(the PAT is what makes PRs showweown-botas author, which is what makes Copilot auto-review trigger β see inline comment in the workflow). Fix: failure mode now reads "checkWEOWN_BOT_PATrate-limit status / scopes / rotation state (the workflow runs allghcalls underGH_TOKEN=${{ secrets.WEOWN_BOT_PAT }}, not the ephemeralGITHUB_TOKEN)". Correct diagnostic reference for on-call debugging.
- R8 #1 / Β§10 Transition Checklist row 3 β "Replace temp
- Copilot Business entitlement validated end-to-end on new PRs (2026-04-27) β controlled experiment confirmed the Layer 2 enterprise-level ruleset triggers
Copilot AI review requested due to automatic review settingsfor newly-createdweown-bot-authored PRs without manual intervention. PR #13 stays manual-review-only (PR-creation-time caching). All future bot-authored PRs (v3.3.5.2+ work) will auto-trigger. Compliance impact: ISO/IEC 42001:2023 A.6.2.7 (AI-aware technical controls) + A.6.2.8 (review of AI-generated output) are now actively enforced for all NEW bot-authored PRs in addition to human-authored ones. SOC 2 CC8.1 change-management evidence narrative now has a verified AI-assisted review layer. CIS Controls v8 18.3 (development security β code review) coverage upgraded from "manual on bot PRs" to "automatic for new PRs / manual for legacy". - PII minimization audit (round-6 + round-7 + round-8 + round-9): zero email addresses anywhere in PR body output paths. Step 7 contributors fallback (
%anonly), step 8 Recent Commits header (%anonly). Round-8 extension: all 3<redacted-personal-email>references inworkflows/README.mdrewritten to generic internal-runbook language. Round-9 extension: Β§2 Account Security Requirements line 64 rewritten to remove "TOTP + recovery codes held by Yonks as enterprise admin" credential-custody disclosure (last social-engineering-surface item in workflows/README.md Β§2 β the Β§10 row 2 equivalent was already fixed in round-3). Aligns with prior PII minimization onCONTRIBUTING.mdhandle table +CODEOWNERS. Per GDPR Art. 5(1)(c) data minimization, ISO/IEC 27001:2022 A.5.34 (privacy + protection of PII), and ISO/IEC 27001:2022 A.5.7 (threat intelligence β reduces public attacker recon surface). (Out-of-scope files NOT modified:vaultwarden/README.md,vaultwarden/CHANGELOG.md,anythingllm/helm/Chart.yamlfor emails;SECURITY_ASSESSMENT.mdline 14 +INCIDENT_RESPONSE.mdlines 61/149/152 for credential-custody + plain-text handles β follow-up PR queued.) - Defense-in-depth ruleset architecture documented in ADR-004 β see "Added" entry above. SOC 2 auditor narrative: WeOwnNetwork/ai now has THREE rulesets (
main rulesid 12119304 strict on~DEFAULT_BRANCH,Copilot auto-reviewid 12131972 baseline on~ALLrepo-scoped, plus enterprise-level layer baseline on all orgs/repos/branches). Bypass list empty on all three.
- Version cadence (revised under user's #WeOwnVer week-5 interpretation): 2026-04-23 work was Season 3, April Week 4 β
v3.3.4.x(original PR + rounds 1β3). 2026-04-27 work is Season 3, April Week 5 βv3.3.5.x(rounds 4β7). The exact ISO-week-to-WEEK mapping methodology is currently under leadership review perdocs/VERSIONING_WEOWNVER.md; this CHANGELOG follows the user's directive at PR #13 round-7 close-out time. Future iterations on 2026-04-27 itself would bev3.3.5.2,v3.3.5.3, etc. - Round 7 close-out scope = 8 Copilot R7 doc/PII/bug fixes + 2 user-directed README additions (Β§2A + Β§11) + new ADR-004 Empirical Validation Results section + version bump cascade across 4 files + CHANGELOG split. Single commit; squash-merge target preserved.
- Round 8 close-out scope = 3 Copilot R8 doc-accuracy fixes on
workflows/README.md(PII, least-privilege PAT scopes, correctGH_TOKENdiagnostic reference) + 2 PII defense-in-depth extensions (same stale email in Β§2 + Β§7 rewritten before Copilot flags them). Single-file change; single commit; squash-merge target preserved. Running total: 38 Copilot comments resolved across 8 rounds. - Round 9 close-out scope = 3 Copilot R9 fixes on
workflows/README.md(credential-custody PII removal in Β§2, canonical Copilot reviewer logincopilot-pull-request-revieweraligned across docs in Β§11, plain-text "Yonks" β@YonksTEAMhandle in Β§10 rows 3 + 6 for unambiguous paging). Single-file change; single commit; squash-merge target preserved. Running total: 41 Copilot comments resolved across 9 rounds. Out-of-scope follow-ups queued: cross-doc PII sweep onSECURITY_ASSESSMENT.md+INCIDENT_RESPONSE.mdfor the same credential-custody anti-pattern. - Round 10 close-out scope = 2 Copilot R10 fixes on
CHANGELOG.mditself (PII self-redaction β the round-8 close-out narrative + PII audit bullet quoted the literal personal email 3x while documenting the round-8 fix; replaced with<redacted-personal-email>placeholder, full audit rationale preserved). Single-file change; single commit; squash-merge target preserved. Running total: 43 Copilot comments resolved across 10 rounds. Lesson learned: when documenting a PII fix, the rationale entry itself must use a placeholder β quoting the original 'Before:' value defeats the purpose of the fix. This applies recursively to the round-10 commit message + this CHANGELOG entry. - Round 11 close-out scope = 1 Copilot R11 doc-accuracy fix on
workflows/README.mdΒ§2A walkthrough table step 9 (documented--title --body --add-reviewersingle combined call β rewritten to match actual implementation of two separategh pr editcalls with title preservation). Single-file change; single commit; squash-merge target preserved. Running total: 44 Copilot comments resolved across 11 rounds. Pattern: late-round reviews are surfacing walkthrough-table accuracy drift that earlier rounds missed βΒ§2Ais now the most heavily-reviewed section of the doc. - Round 12 close-out scope = 1 Copilot R12 doc-accuracy fix on
CONTRIBUTING.mdΒ§4 attribution paragraph (removed overly-absolute "always" claim; added fallback-to-name path that matches step-7 implementation). Single-file change; single commit; squash-merge target preserved. Running total: 45 Copilot comments resolved across 12 rounds. Pattern continuation: accuracy-drift fixes now extending beyondworkflows/README.mdΒ§2A into adjacent docs (CONTRIBUTING.mdΒ§4) that describe the same workflow behavior. Going-forward rule: when the attribution / step-7 code path changes, sweep BOTHworkflows/README.mdΒ§2A step 7 +CONTRIBUTING.mdΒ§4 for consistency in the same commit. - Round 13 close-out scope = 5 Copilot R13 fixes touching 4 files (
ADR-004auto-trigger qualification + Date bump;INCIDENT_RESPONSE.mdScenario 6 phrasing + version/date header sync;CONTRIBUTING.mdLast-updated date sync;CHANGELOG.md[v3.3.5.1]section header + intro paragraph spanning 2026-04-27β-28) + 1 deferred manualgh pr edit 13 --titleoverride (R13 #3). Multi-file change; single commit; squash-merge target preserved. Running total: 50 Copilot comments resolved across 13 rounds. Pattern continuation: accuracy-drift fixes are now sweeping ALL versioned-doc headers in the PR scope simultaneously (ADR-004, INCIDENT_RESPONSE.md, CONTRIBUTING.md, CHANGELOG section header) rather than waiting for Copilot to flag each one. Going-forward rule: when crossing the day boundary mid-PR, refreshLast updatedon every modified file in the same commit AND update the[vN.N.N.N]CHANGELOG section header to span the date range. R13 Β§ Empirical Validation Results consistency rule: when a forward-looking compliance claim (e.g., "every PR gets X automatically") is documented in an ADR, ensure ALL such claims explicitly reference the validation section that confirms or qualifies the claim β prevents internal contradictions like the one R13 #1 caught. - Round 14 close-out scope = 6 Copilot R14 fixes touching 3 files (
auto-pr-to-main.ymltrap consolidation;ADR-004defense-in-depth bullet control-evidence wording;CHANGELOG.md4 anchor-link sweeps). Multi-file change; single commit; squash-merge target preserved. Running total: 56 Copilot comments resolved across 14 rounds. Pattern continuation: R14 surfaced a NEW class of accuracy drift (intra-doc anchor breakage caused by R13 Β§ header date-range change), confirming that consistency sweeps must extend to all link-target derivations, not just text content. Three NEW operational rules codified: (a) Same-shell trap discipline β within a singlerun:block (one bash shell), use a SINGLEtrap '...' EXITset ONCE upfront, with all temp-file paths declared as variables (empty placeholder OK; bash expands at fire time). NEVER re-trapin the same shell to add a new file; just initialize the variable to""upfront and assign mktemp later. Override patterns are regression-risky and Copilot-flag-prone. (b) Post-anchor-change link sweep β when modifying a## [vN.N.N.N]section header in CHANGELOG (or any Markdown header that other parts of the same doc cross-reference), sweep ALL intra-doc(#anchor)references in the same commit. GitHub auto-generates anchors from header text via lowercasing + space-to-hyphen + non-alphanumeric drop; date-range expansions, version bumps, and any other header edit silently break links. (c) ADR control-evidence accuracy β when an ADR cites a control-evidence example (e.g., "X is enforced by mechanism Y"), verify Y is the ACTUAL enforcement mechanism (not just an implementer that interacts with Y). Cross-reference the relevant ADR's Β§ "Rules enabled" or Β§ "Configuration" section to confirm the control-evidence path is end-to-end accurate. - Round 15 close-out scope = 3 Copilot R15 fixes touching 2 files (
ADR-004validation procedure alignment with Empirical Validation Results;auto-pr-to-main.ymltwo code-quality improvements β API call consolidation via jq fallback chain +render_handlehelper preventing@unknownsentinel leakage). Multi-file change; single commit; squash-merge target preserved. Running total: 59 Copilot comments resolved across 15 rounds. Pattern continuation: R15 surfaced a NEW class of accuracy drift (validation-procedure semantic conflict with the same ADR's own empirical findings), confirming that consistency sweeps must extend to verification / troubleshooting / runbook sections of an ADR, not just the main body. ONE NEW operational rule codified: (a) Sentinel-value output discipline β when emitting user-visible strings that may include fallback sentinel values (e.g.,unknown,n/a,unresolved), NEVER prefix them with formatting that implies a valid reference (e.g.,@unknownimplies a GitHub mention,#unknownimplies an issue/PR). Use a small render helper function with acaseto conditionally apply the prefix only for real values, and render a plain sentinel otherwise. This pattern applies to GitHub handles, issue/PR numbers, commit SHAs, email addresses, and any other bracket/sigil-prefixed identifier. R15 also sharpened the ADR validation cross-reference rule (R13): whenever a validation / debug / runbook procedure is written IN an ADR, cross-reference the ADR's own Β§ Empirical Validation Results from the procedure intro β prevents the procedure from drifting out of sync with empirical findings when the latter are updated. This is an extension of the R13 rule (which covered main-body compliance claims) to cover procedural / instructional content as well. - Round 16 close-out scope = 4 Copilot R16 fixes touching 3 files (
auto-pr-to-main.ymltwo improvements: RUNNER_TEMP fail-fast for policy-code alignment + email-keyed cache for step 7 contributors-list builder;ADR-004heading rename for stable anchor;CHANGELOG.mdcross-doc link fix). Multi-file change; single commit; squash-merge target preserved. Running total: 63 Copilot comments resolved across 16 rounds. - Round 17 close-out scope = 1 Copilot R17 doc-accuracy fix (
auto-pr-to-main.ymltrap conditional cleanup, correcting the R14 #1 "silent no-op" claim that was incorrect on GNU coreutils) + ecosystem-shaping ADR-002 naming-convention revision (Infisical Sync Key Schema is identity-only, not strip-capable; revised to project-per-scope + identity-mapped secret name) + audited PAT rotation event (regenerated 2026-04-28; 90-day expiration 2026-07-27; stored in Infisical under the new convention). Multi-file change touching 5 files (auto-pr-to-main.yml,workflows/README.md,pat-health-check.yml,ADR-002,CHANGELOG.md). Single commit; squash-merge target preserved. Running total: 64 Copilot comments resolved across 17 rounds. - Round 18 close-out scope = 4 Copilot R18 doc-consistency fixes (R18 #1
workflows/README.mdΒ§2 Scope & Principles items 3+4 rewritten to match Β§2.4 Usage Table; R18 #2INCIDENT_RESPONSE.mdScenario 6 step 6 updated to reference per-repo folder; R18 #3ADR-004Layer 1 bullet #3 qualified to align with Β§ Empirical Validation Results PR-creation-time caching semantics + close+reopen remediation note; R18 #4workflows/README.mdheaderLast updated2026-04-27 β 2026-04-28) + user-driven 2nd-iteration revision of the 2026-04-28 Infisical naming convention (project-per-repo β folder-per-repo inside the single sharedweown-bot GitHub PATsproject; namespacing axis is now folder path scoped via Sync Source Path; cleaner operational footprint β one project-level RBAC boundary instead of N). Multi-file change touching 5 files (workflows/README.md,ADR-002,ADR-004,INCIDENT_RESPONSE.md,CHANGELOG.md). Single commit; squash-merge target preserved. Running total: 68 Copilot comments resolved across 18 rounds. Pattern continuation: R18 surfaced a NEW class of drift β same-day convention iteration drift (R17 introduced a vendor-feature-corrected naming convention that itself needed a second-cut revision once the operational namespacing primitives were exercised in practice; the R17 β R18 evolution happened within the same 24-hour window). TWO NEW operational rules codified: (a) Same-document consistency sweep on convention changes β when revising a convention in section X of a doc (e.g., Β§2.4 Usage Table), sweep ALL earlier sections of the same doc that introduce or summarize the convention (e.g., Β§2 Scope & Principles, Β§1 inventory) in the same commit. Documents that introduce a convention before describing the table that uses it must keep the introductory text in lockstep with the table; otherwise onboarding contributors read contradictory guidance. (b) Convention iteration discipline within the same close-out window β when a vendor-feature-driven convention is revised under empirical sync-configuration findings (R17), expect one to two same-day operational-simplification iterations (R18) before the convention stabilizes. Document each iteration as its own Decision Log row; preserve earlier rows for audit trail. The R17 first-iteration row (project-per-repo) was preserved verbatim in ADR-002 Decision Log even though the R18 second-iteration row supersedes it operationally β this preserves the empirical narrative for future ADR review cadences. R18 also extends the R17 rule ("vendor-feature verification before convention design"): the verification step must include exercising the namespacing primitive against the actual UI before publication, not just confirming the primitive exists. Pattern continuation: R17 surfaced a NEW class of drift β vendor-feature assumption drift (an ADR encoded a convention based on an assumed vendor capability that does not exist; the assumption was unchallenged for 5 days until empirical sync-configuration on 2026-04-28). ONE NEW operational rule codified: (a) Vendor-feature verification before convention design β when an architecture or convention relies on a specific vendor feature (e.g., "Infisical Sync supports per-secret rename", "GitHub Actions exposestriggering_actorinpull_requestevents"), validate the feature exists in the actual UI/API before encoding it in ADR conventions. Document the verification step (UI screenshot, API response capture, or documentation excerpt) in the ADR's Implementation Notes section. Future ADR review cadences should include a "verify cited vendor features still exist" step. R14 #1 "silent no-op" claim correction: the R14 #1 close-out narrative claimed"rm -f \"\""was a silent no-op; this was incorrect on GNU coreutils. The R17 fix replaces the unconditional cleanup with a conditional branch + corrects the comment block. The R14 #1 CHANGELOG entry is preserved as historical record (the trap consolidation itself was correct; only the empty-operand sub-claim was wrong). PAT rotation 2026-04-28 audit trail: PAT regenerated by@romandidomiziofromweown-botaccount; old PAT invalidated; new PAT 90-day fine-grained, single-repo scope (WeOwnNetwork/ai), permissions Contents: R / PRs: R/W / Metadata; expiration 2026-07-27. Health-check verification scheduled post-Infisical-sync-completion. Resolves the 2026-04-23 sync drift permanently via the revised naming convention. Pattern continuation: R16 surfaced TWO NEW classes of drift: (i) policy-code drift (comments declared a stricter policy than the code enforced β R16 #1) and (ii) anchor-derivation drift (heading text containing parenthetical date suffixes silently breaks intra-doc references; cross-doc links rendered as bare(#anchor)silently resolve to non-existent current-doc anchors β R16 #3 + #4). TWO NEW operational rules codified: (a) Stable-anchor heading discipline β section headings that need to be cross-referenced should NEVER include parenthetical dates / round-numbers / version qualifiers / any non-alphanumeric noise; provenance metadata goes in an italic sub-line right under the heading (*Source: ...*) instead. Keeps the auto-generated anchor stable across edits. (b) Cross-doc link explicitness β when a CHANGELOG narrative (or any document) paraphrases / summarizes / references content in another file, ALWAYS render the link as an explicit relative path ((./relative/path.md#anchor)), never as a bare(#anchor). The bare form silently resolves to a current-doc anchor that may not exist. R16 also sharpened the policy-code alignment rule: when a comment block declares a security / isolation / privacy policy (e.g., "isolated from /tmp", "emails are PII and not surfaced"), verify the code actually enforces it without silent fallbacks. Fail-fast on policy violations, never fall back to a weaker stance. This is the natural code-side counterpart of the R14 #2 ADR control-evidence accuracy rule (which covered ADR text claims about control mechanisms). - Round 19 close-out scope = 1 Copilot R19 label-canonicalization fix (
workflows/README.mdΒ§3 "Branch name vs. PR body" table row labelContributors:β canonicalContributors on this branch:to match the actual workflow output atauto-pr-to-main.ymlstep 8 line 320 + the same doc's earlier Β§3 three-tier table at line 161) + 3 same-doc + cross-doc cascades caught proactively per the R18 #1 "Same-Document Consistency Sweep" rule codified in the previous round (workflows/README.md Β§11 Troubleshooting row; CONTRIBUTING.md Β§4 explanatory bullet at line 334; CONTRIBUTING.md Β§4 parenthetical at line 328) + CONTRIBUTING.mdLast updatedheader bump for R19 traceability. CHANGELOG[v3.3.5.1]line 56 historical narrative was explicitly PRESERVED to retain the "before/after" audit trail of the original R7 rename event (modifying the historical "before" value would corrupt the rename narrative). Multi-file change touching 3 in-repo files (workflows/README.md,CONTRIBUTING.md,CHANGELOG.md) + gitignoredPR7_HANDOFF_CHECKLIST.md. Single commit; squash-merge target preserved. Running total: 69 Copilot comments resolved across 19 rounds. Pattern continuation: R19 #1 is the canonical demonstration of the R18 #1 "Same-Document Consistency Sweep" rule's value β Copilot caught a label inconsistency at line 170 that the SAME doc had already fixed at line 161 (same Β§3 section, just 9 lines earlier); the R18 rule mandates exactly this kind of intra-section sweep, and R19 #1 is the kind of drift the rule was designed to surface within 1 round of the rule being codified. TWO additional sub-rules added under the R18 rule's scope: (a) Workflow-output vs. doc-citation parity β when a doc cites the label of a workflow-emitted line (e.g., a PR body field name), the citation MUST match the actual emitted string verbatim, including any modifier phrases (e.g., "on this branch"). Search-grep before publication:grep '<label>' $WORKFLOW_FILEto confirm the canonical form. (b) Historical-narrative preservation in audit trails β when a CHANGELOG entry documents a rename or label change, the original-value reference in the close-out narrative MUST be preserved verbatim even if subsequent rounds touch the same area. Modifying the quoted historical value corrupts the audit trail (R10 PII-recursive-quote lesson applies in reverse). For R19, this meant explicitly NOT touching CHANGELOG line 56 even though it contains aContributors:reference, because that reference is part of the historical "before" narrative of the R7 rename event. - Round 20 close-out scope = 2 Copilot R20 fixes β (1) R20 #1 runbook-correctness fix on
workflows/README.mdΒ§11 Troubleshooting "Merge blocked with 'requires signed commits'" row: previous guidance said "add a new signed commit to the branch" which is incorrect (adding a new signed commit does NOT fix earlier unsigned commits β the merge will still be blocked because branch protection requires ALL commits be signed); rewritten per Copilot's literal suggested change to direct contributors to recreate the branch/PR with all commits signed sincenon-fast-forwardblocks history rewriting. (2) R20 #2 doc-vs-UI display canonicalization: Infisical folder paths were inconsistently shown with vs. without trailing slash; user explicitly chose the NO-trailing-slash form (opposite of Copilot's literal suggestion to add slashes everywhere) because the Infisical UI's "Source secret path" field accepts the path without trailing slash and mirrors it verbatim, so the no-slash form survives the docs β UI β docs round-trip without transformation. Cascade across 3 docs:workflows/README.md(~14 path normalizations across Β§2, Β§2.4, Β§4.4, Β§5.1, Β§6, Β§6.1);INCIDENT_RESPONSE.md(Scenario 6 step 6);ADR-002(Architecture diagram + Naming Convention + Implementation Notes). ADR-002 Decision Log appended with R20 row in correct chronological position after R17 + R18 (NOT a replacement); R17 + R18 historical rows PRESERVED with their original trailing-slash forms per the R19-codified historical-narrative-preservation rule. CHANGELOG[v3.3.5.1]Β§ Changed historical bullets (lines describing R17 + R18 work in their original narrative form) also PRESERVED for the same reason. ADR-002 + INCIDENT_RESPONSE.md Date headers bumped to include R20 references;workflows/README.mdLast updated header was already simply2026-04-28so no change needed. Multi-file change touching 4 in-repo files (workflows/README.md,ADR-002-infisical-github-sync.md,INCIDENT_RESPONSE.md,CHANGELOG.md) + gitignoredPR7_HANDOFF_CHECKLIST.md. Single commit; squash-merge target preserved. Running total: 71 Copilot comments resolved across 20 rounds. Pattern continuation: R20 surfaced TWO new drift classes β (a) runbook-correctness drift (R20 #1) where the narrative was internally consistent but the conclusion was wrong; survived 19 prior rounds because Copilot reviews tend to flag inconsistency rather than logical-correctness bugs in remediation steps. (b) doc-vs-UI display drift (R20 #2) where docs displayed paths in a form that differs from the form the vendor UI accepts/displays, creating real-world copy/paste configuration errors during incident response or new-repo onboarding. TWO NEW operational rules codified under R20: (1) Runbook remediation steps must validate against the underlying enforcement chain β when a troubleshooting row gives a remediation step, trace the step against the enforcement mechanism (here: branch-protection ruleset rule + signed-commits requirement + non_fast_forward enforcement) to verify the step actually unblocks the user. Internal narrative consistency is necessary but not sufficient. (2) Vendor-UI parity for path-like identifiers β when documenting a path-like identifier that users will copy into a vendor UI text field, pick the canonical form that the vendor UI itself uses (or the simpler form when the UI accepts both) and use it consistently across all docs. The cost of doc-vs-UI display mismatch is real-world copy/paste configuration errors during incident response or new-repo onboarding. Going-forward implication: the v3.3.5.1 review cycle (rounds 7-20, 14 rounds total) is establishing a stable taxonomy of doc-accuracy-drift classes that future PRs can pre-validate against β currently 12+ distinct sub-classes documented across all R-rounds.
Second #WeOwnVer iteration in the same week, following PR #7 (v3.3.4.1) merge. Ships documentation sync for the main branch ruleset (configured on the same day as the merge), a new ADR capturing ruleset decisions + compliance mappings, an inline contributors table + enforcement-posture matrix in CONTRIBUTING.md Β§4, and a small doc correction surfaced during workflow testing.
.github/ADR-003-main-branch-ruleset.mdβ new ADR documenting the 12 rules enabled on themainruleset (configured 2026-04-23), mapped to SOC 2 (CC6.1, CC6.3, CC7.1, CC7.2, CC8.1), ISO/IEC 27001:2022 (A.5.15, A.5.28, A.5.37, A.8.13, A.8.24, A.8.29, A.8.32), ISO/IEC 42001:2023 (A.6.2.7, A.6.2.8, A.7.2, A.8.3, A.9.3, A.9.4), NIST CSF 2.0 (PR.AC-4, PR.DS-6, PR.IP-1, PR.IP-3, DE.CM-1, DE.CM-7, DE.CM-8), and CIS Controls v8 (3.11, 11.2, 11.3, 16.1, 16.9, 16.11, 16.12, 18.3). Also documents rules intentionally NOT enabled (linear history, code scanning results, signed tags β latter two tracked as future items), rationale for empty bypass list under SOC 2 CC6.3, and a "Dev Attribution Enforcement Posture" section comparing three options (A=strict regex allowlist, B=reviewer-enforced convention [chosen], C=hybrid warning layer) with explicit numeric upgrade triggers (e.g., "escalate to Option C if team >15 or >10% of merged PRs use non-table handles in a quarter").- Platform-sourced developer attribution in
auto-pr-to-main.ymlstep 6 β the workflow reads${{ github.triggering_actor || github.actor }}to attribute PRs, producing**Triggered by:** @<real-github-username>in the PR body. Zero maintenance: no case-statement mapping to keep in sync, no onboarding / offboarding workflow edits required, no risk of stale mappings drifting fromCONTRIBUTING.md. The<dev>segment in branch names is preserved for human-readable naming only. Resolves 4 of 6 Copilot round-1 review comments on PR #13 that flagged@<short-handle>as potential misattribution. (An intermediate iteration added a case-statement mapping; subsequent iteration replaced it with${{ github.triggering_actor || github.actor }}for correctness-by-default. The singleTriggered by:line was later split into a three-tier model on 2026-04-27 β see the[v3.3.5.1]section above for the canonical record of that semantic upgrade.)
.github/workflows/README.mdΒ§8.1 rewritten to reflect the as-configured ruleset (12 enabled rules with compliance column) instead of an aspirational recommendation list. Removespat-health-check.ymlfrom "required status checks" (it isschedule:triggered, cannot be a PR-time gate). Clarifies that "Require code quality results at warning and higher" is satisfied by CodeQL Default Setup (distinct from "Require code scanning results" which requires SARIF via a different API). Adds explicit note on why linear history is intentionally NOT enabled and how "Restrict who can push" is effectively covered by "Require a PR" + empty bypass list.CONTRIBUTING.mdΒ§4 Branch Naming β (a) removed the false-invalid examplefeature/add-thing(which actually DOES pass the regex:add= 2+ char<dev>,thing= 3+ char description) and replaced with genuinely invalid examples (feature/ab-a,feature/roman--double-hyphen,random/roman-test); (b) added a "Convention beyond the regex" note clarifying that<dev>is a human-readable short handle (typically first name / alias), not used for PR attribution β attribution is sourced from${{ github.triggering_actor || github.actor }}directly; (c) added a "Known contributor handles" table with GitHub handles + branch<dev>segments only (extended context such as legal names, tenure, and roles lives in internal onboarding docs, not this public repo); Nik's branch<dev>segment changed fromnciminotonik(shorter, matches first-name convention); placeholder TODO handles replaced with real ones (@iamwaseem18,@mshahid538);@YonksTEAMadded as executive stakeholder reference; (d) added an "Enforcement posture" mini-matrix summarizing the three options documented in detail in ADR-003 so contributors understand why convention-not-mechanical is the current choice and what would trigger a change. Self-contained β no separate file..github/workflows/README.mdheader β version bumped fromv3.3.4.1tov3.3.4.2per #WeOwnVer pre-merge iteration policy (this is the second merge-event iteration in the same ISO week). A subsequent bump tov3.3.5.1was applied in the same PR on 2026-04-27 once the work crossed into ISO week 5 of April β see[v3.3.5.1]above..github/CODEOWNERSβ replaced placeholder handles@mohammed-TODO/@shahid-TODO/@dhruv-TODOwith confirmed real GitHub handles@iamwaseem18/@mshahid538/@dhruvmalik007(active per-path rules unchanged β still@ncimino @romandidomiziountil 2026-05-15 handoff decisions are finalized). Added@YonksTEAMas an executive stakeholder reference in the header (not a path reviewer β avoids notification noise). Header PII minimization: legal names, tenure descriptors, and recovery-credential custody details removed from the public header; extended operational context lives in internal onboarding / security docs.- Branch naming example sync β updated example branch
fix/ncimino-...βfix/nik-...across.github/workflows/branch-name-check.yml(error-message output),.github/workflows/README.mdΒ§3, and rootREADME.mdΒ§"Branch Strategy" to match Nik's confirmed branch<dev>segment inCONTRIBUTING.mdΒ§4.auto-pr-to-main.ymlreviewer assignment remains--add-reviewer ncimino,romandidomizioβ Nik's GitHub handle@nciminois unchanged; only his<dev>branch segment changed fromnciminotonik. .github/workflows/README.mdΒ§9 + Β§10 β Reviewer Rotation Procedure step 2 and Transition Checklist row 4 updated to reflect that placeholder-handle replacement is complete (~~strikethrough~~ + β), leaving only the per-path specialist-assignment decision as pending. Keeps the transition checklist an accurate audit artifact of what's done vs. still open..github/INCIDENT_RESPONSE.mdScenario 6 β Stewardship Gap response steps 2 and 3 updated to reference real GitHub handles (@iamwaseem18/@mshahid538/@dhruvmalik007) instead of@<name>-TODOplaceholders. Incident playbook accuracy matters for SEV-4 on-call response β no ambiguity about who to page.CONTRIBUTING.mdΒ§4 now explicitly documents the branch-name-vs-PR-body identifier split:<dev>is a human-readable short handle; PR**Triggered by:**attribution uses${{ github.triggering_actor || github.actor }}automatically. Added a blockquote callout table for quick reference. Resolves a user-facing ambiguity flagged in Copilot round-1 review of PR #13..github/workflows/README.mdΒ§3 Parsing Rules simplified from 5 steps (including a handle-to-username mapping step) down to 3 steps (regex validation + direct${{ github.triggering_actor || github.actor }}attribution). Added a "Branch name vs. PR bodyβtwo different identifiers (by design)" subsection with a clarity table..github/workflows/README.mdΒ§9 Reviewer Rotation Procedure β stripped contributor legal name in the "placeholder-handle replacement complete" note; kept only the GitHub handles (PII minimization in public runbook, addressing Copilot round-2 review feedback)..github/CODEOWNERSPII minimization β header re-written to remove contributor legal names, tenure descriptors, intern status, executive titles, and recovery-credential custody details. Kept only GitHub handles + minimal functional-area descriptors (IaC, Docker, Agentic AI). Reduces social-engineering surface area on a public repo, addressing Copilot round-2 review feedback.- Round-3 consistency + PII follow-ups (Copilot review, 2026-04-24):
.github/ADR-003-main-branch-ruleset.mdOption A regex example updated from(roman\|ncimino\|mohammed\|...)to(roman\|nik\|mohammed\|...)to match the current<dev>segment convention inCONTRIBUTING.mdΒ§4..github/ADR-003-main-branch-ruleset.mdOption B rationale bullet rewritten: previously described a now-retired inline<dev>β GitHub-username mapping + git-author-email fallback; now correctly describes the${{ github.triggering_actor || github.actor }}platform-sourced attribution. Keeps the ADR a truthful control-evidence artifact.CONTRIBUTING.mdΒ§4 β all three references (<dev>intro paragraph, blockquote callout table cell, blockquote explanatory paragraph, Known-contributor-handles onboarding paragraph) updated from shortformgithub.actorto the full${{ github.triggering_actor || github.actor }}expression to match the workflow env variable exactly..github/workflows/README.mdΒ§3 "Branch name vs. PR body" table cell updated to use the full${{ github.triggering_actor || github.actor }}expression to match the Parsing Rules step 3 above it..github/workflows/README.mdΒ§10 Transition Checklist row 2 softened:Transfer 2FA (TOTP seed) + recovery codes to enterprise admin (Yonks) + rotation leadβTransfer 2FA administration per internal runbook to enterprise admin + rotation lead. Removes specific credential-type details from a public runbook (social-engineering surface reduction).
- Continuation rounds 4β7 (all 2026-04-27) β see
[v3.3.5.1]above for the canonical record. Includes the round-5 three-tier PR-body attribution refactor +workflow_dispatch+concurrency, the round-6 PII strip + ADR-004 creation + auto-trigger claim revert, and the round-7 Copilot 8-comment close-out + Β§2A + Β§11.
.github/ADR-003-main-branch-ruleset.mdstale path reference β "Not enabled β Require signed tags" bullet previously pointed to.pr7-handoff-checklist(a local-only, gitignored file that does not exist in the repo tree). Replaced with/CHANGELOG.md+docs/VERSIONING_WEOWNVER.md, both of which are in-repo tracking locations. Resolves 1 of 6 Copilot review comments on PR #13..github/ADR-003-main-branch-ruleset.mddate arithmetic β "Next scheduled review" incorrectly read2026-07-23. 2026-04-23 + 90 calendar days = 2026-07-22 (April 30-23=7 remaining days in April, May=31, June=30 β 7+31+30=68 days, 90-68=22 β July 22). Corrected to2026-07-22and clarified "90 calendar days" to remove math ambiguity. Resolves 1 of 6 Copilot review comments on PR #13 about date inconsistency.
- SOC 2 CC6.3 evidence-ready: Β§8.1 table maps each of the 12 enabled rules to specific SOC 2 Trust Services Criteria. Auditors can cross-reference the GitHub ruleset config with this table for one-shot coverage proof.
- ISO 27001 A.5.15 + A.5.37: path-reviewer binding (CODEOWNERS #3) + change-of-state reset (stale approval dismissal #2) documented and enforced.
- ISO 42001 AI governance: rule #10 (auto-request Copilot review) now mapped to Annex A.6.2.7 (AI-aware technical controls) and A.6.2.8 (review + approval of AI-generated output).
WEOWN_BOT_PATrotation audit note β the fine-grained PAT scoped toWeOwnNetwork/aiwas regenerated on 2026-04-23 during this PR's cycle to restore theauto-pr-to-main.ymlworkflow (prior PAT was invalidated;PAT Health Checkworkflow run24870414632confirmed the post-rotation PAT authenticated asweown-botwith 89 days to next expiration on 2026-07-22). New PAT is 89-day fine-grained, single-repo scope, stored via Infisical β GitHub Secret sync. The rotation event itself is an auditable control (no sensitive credential material recorded here).
- Testing evidence: all three workflows validated via a 42-case solo regression matrix (branch-name regex coverage, defense-in-depth parity, developer attribution extraction, jq fallback behavior, YAML parse) β see
.pr7-test-workspace/solo-tests.sh(local-only, not committed). - Version cadence: this section captures the Season 3, April Week 4 work (original PR + Copilot rounds 1β3). Subsequent same-PR work on 2026-04-27 crossed into Week 5 of April and is documented under
[v3.3.5.1]above. Seedocs/VERSIONING_WEOWNVER.mdfor the full cadence rules.
First repository-level CHANGELOG entry (#WeOwnVer vSEASON.MONTH.WEEK.ITERATION β Season 3, April, Week 4 of April, Iteration 1). Establishes auto-PR workflow hardening, ecosystem-wide service account, Infisical GitHub Sync, branch naming enforcement, and the initial compliance roadmap.
weown-botecosystem-wide GitHub service account with per-repo fine-grained PATs, centralized in Infisical projectweown-bot GitHub PATs(see ADR-001)- Infisical β GitHub Sync integration for
WEOWN_BOT_PAT__WEOWNNETWORK_AI(see ADR-002) - New workflows:
.github/workflows/pat-health-check.ymlβ scheduled weekly PAT health check; opens issue at β€14 days; hard-fails at β€3 days.github/workflows/branch-name-check.ymlβ blocks non-conforming branch names (enforces<type>/<dev>-<description>convention)
- New documents:
.github/ADR-001-service-account-pat.md.github/ADR-002-infisical-github-sync.md.github/SECURITY_ASSESSMENT.mdβ threat model, risk register, compliance mapping.github/INCIDENT_RESPONSE.mdβ SEV-1..4 runbooks for PAT/account/Infisical/stewardship scenarios.github/CODEOWNERSβ path-based reviewer assignment with post-2026-05-15 handoff TODOs.github/workflows/README.mdβ authoritative ops reference (usage table, rotation procedure, alert stack, transition checklist, branch protection, replication steps)docs/COMPLIANCE_ROADMAP.mdβ detailed 5-phase compliance roadmap (NIST CSF β CIS β CSA CCM β ISO 27001 β SOC 2 β ISO 42001) with CI/CD integration per phase, success metrics, and forward-looking guardrailsCONTRIBUTING.mdβ first-time developer onboarding. Covers mandatory SSH-based commit signing (NIST PR.DS-6, SOC 2 CC7.1, ISO 27001 A.8.28, CIS 16.12) with 7-step setup includinggpg.ssh.allowedSignersFilefor local verification and verified-email alignment with GitHub account; the GitHub Flow branching model (explicitly named + lifecycle diagram + what we do NOT use + rationale table + branch lifetime expectations); branch naming convention; commit message conventions; full PR workflow with post-merge cleanup; review expectations; expanded troubleshooting (signature-present-vs-absent pre-check, 6-cause diagnostic for%G?=N, 2-cause diagnostic for GitHub "Unverified" including committer-email mismatch, retro-sign-and-force-push recipe, ssh-agent/keychain fixes); and compliance cross-referencesCHANGELOG.mdβ this file
- PR body enhancements (in
auto-pr-to-main.yml):- NIST CSF Function-aligned human review checklist (Govern, Identify, Protect, Detect, Respond, Recover)
- Full commit bodies visible to Copilot for context (
%bnot just%s) - Author identity (
%an <%ae>) and date (%ad) shown per commit - Developer attribution parsed from branch naming convention (
<type>/<dev>-<description>) - Deduplicated contributor list
- Auto-assignment of 2 reviewers (
@ncimino+@romandidomizio) viagh pr edit --add-reviewer
auto-pr-to-main.yml: token referenceROMAN_PATβWEOWN_BOT_PAT.github/copilot-instructions.md: full rewrite β removed command/test directives (Copilot is static-only), added phase-aware compliance, expanded Β§3 checklist to cover all six frameworks (NIST CSF 2.0, CIS v8 IG1, CSA CCM v4, ISO/IEC 27001:2022, SOC 2 TSC, ISO/IEC 42001:2023) plus seven ecosystem best-practice blocks (Kubernetes, Docker/Compose, IaC [OpenTofu for infrastructure + Ansible for software/config], Infisical, Observability, GitOps, Security/Supply Chain), added Β§3.0 explicit PUBLIC-repo precautions (never-commit list, placeholder patterns, git history hazards), moved checklist to top of document, added forward-looking guardrails, ecosystem awareness, anti-pattern reference, and cross-framework review output guidelines. Ansible integration documented throughout: Infisical secrets viacommunity.hashi_vault/infisical run -- ansible-playbook; idempotency / handlers / vault / inventory best practices; anti-patterns for plaintext secrets, non-idempotent shell, implicit root.github/CI_CD_WORKFLOWS.md: cross-referenced the new workflows README, ADRs, and compliance roadmap; documentedpat-health-check.ymldocs/VERSIONING_WEOWNVER.md: rewritten to the corrected calendar-driven methodology (L-094 REVISED):vSEASON.MONTH.WEEK.ITERATION, L-115 ISO-week-offset rule, finalized Season Calendar, Helm/OCI mapping, and calculation cheat sheetREADME.md(top-level): added "Compliance & Governance" section linking all new docs
.github/PAT_MIGRATION_GUIDE.mddeleted β content was superseded by ADR-001 (rationale), ADR-002 (Infisical sync), CHANGELOG (history), and.github/workflows/README.md(authoritative rotation procedure). Retaining the old guide created duplicate-source risk.maintenancebranch removed fromauto-pr-to-main.ymltriggers and all documentation. The repository now standardizes on GitHub Flow: short-livedfeature/*,fix/*,docs/*,hotfix/*branches offmain, merged back via reviewed PRs.
- Fine-grained PAT replaces broad-scoped tokens: minimally scoped to
Contents: Read+Pull requests: R/W+ metadata (auto) onWeOwnNetwork/ai. Issue creation inpat-health-check.ymlintentionally uses the ephemeral per-runGITHUB_TOKEN(with workflow-levelissues: write) rather than expanding the PAT β principle of least privilege (NIST PR.AC-3 / CIS 5.4) - Secret management centralized in Infisical with 90-day audit logs (SOC 2 evidence)
- Branch naming enforced by
branch-name-check.yml(blocks non-conforming branches via required status check). Description segment requires 3+ alphanumeric chars before any hyphen suffix (e.g.,feature/ab-anow rejected). Regex kept in sync with the defense-in-depth guard inauto-pr-to-main.yml - Branch protection to be configured: require 2 approvals + review from Code Owners + signed commits + no bypass (see
.github/workflows/README.mdΒ§8) - Workflow hardening (Copilot review rounds 3β5):
pat-health-check.ymlnow fails closed (exit 1) when thegithub-authentication-token-expirationheader is missing β previously silently exited 0, which defeated the workflow's safety-net purpose. A missing header indicates token-type misconfiguration (e.g., classic PAT instead of fine-grained) and must surface as a red-X in Actionspat-health-check.ymlnow fails closed (exit 1) when the header IS present but the timestamp cannot be parsed into an epoch β previously emitted a::warning::and silently exited 0, creating a second bypass path that would activate exactly when GitHub changed the header format. Error message includes the offending raw value for forensics (round 4)pat-health-check.ymlseparatesgh api /userexit-code check from header-grep so a transient API/network failure (previously swallowed by2>/dev/null | grep ... || true) is not misclassified as a PAT-configuration issue. Three distinct red-X paths now exist with targeted error messages: (1) API call itself failed, (2) call succeeded but header absent, (3) header present but unparseable. Stderr of the failinggh apiis surfaced in the Actions log (round 5)branch-name-check.ymlsetspermissions: {}β the workflow makes no API calls and does not check out the repo (only readsgithub.head_ref/github.refcontext and runs a local grep), so the ephemeralGITHUB_TOKENis stripped of every permission (round 5)pat-health-check.ymlremoved unusedISSUE_LABELSvariable β labels are passed directly togh issue createvia three explicit--labelflags; the unused variable falsely implied a single source of truth (round 4)- Temp files in both workflows now route through
$RUNNER_TEMP(GitHub-runner-scoped, auto-cleaned at job end) instead of the shared/tmpβ defense in depth beyond the existingmktemp+trapcleanup pattern pat-health-check.ymlissue-body links use${{ github.server_url }}instead of hardcodedhttps://github.comβ portable to GitHub Enterprise Server (matches theBLOB_BASEpattern already inauto-pr-to-main.yml)auto-pr-to-main.ymlPR-existence check uses jq.[0].number // emptyβ avoids jq's literal"null"string on empty arrays, which would previously cause the script to attemptgh pr edit nulldocs/VERSIONING_WEOWNVER.mdHelm chart mapping corrected for SemVer precedence: every iteration gets a-Nprerelease suffix (3.3.4-1 < 3.3.4-2 < 3.3.4-3 < 3.3.4), preventing the SemVer-downgrade pitfall where3.3.4-2would sort BELOW3.3.4in Helm/OCI tooling
- NIST CSF 2.0 β
Govern,Protect (Access Control, Data Security),Detect,Respondfunctions addressed for auto-PR workflow - CIS Controls v8 IG1 β Controls 3, 5, 6, 7, 8, 13, 16, 17 in scope
- ISO/IEC 27001:2022 β A.5.15 (access control), A.5.37 (documented operating procedures), A.8.2, A.8.24 (cryptographic/secret mgmt), A.8.32 (change mgmt)
- SOC 2 TSC β CC6.1βCC6.3 (logical access), CC7.1βCC7.2 (system operations), CC8.1 (change mgmt)
@romandidomizio departs 2026-05-15. PAT rotation responsibility transitions to one of Mohammed / Shahid / Dhruv. Full handoff checklist in .github/workflows/README.md Β§10.