Per-User Pub Groups — Final Design (supersedes prior tier-based proposals)
Per-user publish groups (<host-user>-pub). No tiers, no AccessTier enum, no PermissionManager interface, no POSIX ACLs, no new directories. Keep Site.Public bool. Delete makeWorldReadable(). Caddy joins each user's pub group to read published files.
Public access is controlled by one Unix group per host user.
<host-user>-pubSite.Public bool — no new schema fields<sites>/<shortid>-<slug>/public<user>-pub group to read only published sitesmakeWorldReadable() — no world-readable bits everchgrp to agent-lee-pub)beast host create auto-provisions the pub groupinternal/serve/service.gomakeWorldReadable()Remove entirely. No replacement should use chmod o+r or world-readable modes.
applySitePermissions(siteDir string, public bool) error currentHostUser() (string, error) pubGroupForUser(user string) string // returns "<user>-pub" privateGroupForUser(user string) string runPermissionCommand(name string, args ...string) error
Shell out to standard Unix tools (chgrp, chmod, find). No interfaces.
Service.DeployAfter staging + saving, call applySitePermissions(siteDir, site.Public). Apply regardless of Config.Public.Enabled — permissions are always correct, even before public Caddy starts.
Service.SetVisibilityBoth publish and unpublish call applySitePermissions(). Unpublish revokes group access.
internal/serve/site.goNo change — keep Public bool, keep PublicSites()
internal/serve/caddy.goNo change — keep existing RenderCaddyfile filter for !s.Public
cmd/serve/publish.go / cmd/serve/deploy.goNo change — behavior changes through Service layer only
internal/host/host.goManager.Creategroupadd <name>-pub (treat "already exists" as OK)usermod -aG <name>-pub caddysystemctl restart caddy (pick up new supplementary group)Manager.Deletegroupdel <name>-pub — ignore "does not exist" errors
beast host create <name> provisions the user's public namespace automatically:
<name>groupadd <name>-pubuseradd ...install -d ...usermod -aG <name>-pub caddysystemctl restart caddyThe pub group is not stored — always derived as <name>-pub.
beast serve publish report (as user agent-kai)
site.Public = true, save manifest + site.jsonchgrp -R agent-kai-pub <site-dir>find <site-dir> -type d -exec chmod g+rx {} +find <site-dir> -type f -exec chmod g+r {} +chmod -R o-rwx <site-dir>beast serve unpublish report
site.Public = false, save manifest + site.jsonchgrp -R agents <site-dir>chmod -R g-rwx,o-rwx <site-dir>find <site-dir> -type d -exec chmod u+rwx {} +find <site-dir> -type f -exec chmod u+rw {} +beast serve deploy ./dist --slug report → stage files, Public=false, apply private perms (owner-only), regen Caddy
beast serve deploy ./dist --slug report --public → stage files, Public=true, apply pub-group perms, regen Caddy
Re-deploying a public site re-applies pub-group permissions after staging.
<owner>-pub group exists<owner>-pubcaddy user is in each needed <owner>-pub/etc/group)makeWorldReadable remnantsConvert existing world-readable public sites to pub-group permissions:
groupadd <owner>-pubusermod -aG <owner>-pub caddychgrp -R <owner>-pub, set group-readable, remove world bitschgrp -R <private-group>, remove group+world bitsbeast serve doctorNo manifest schema migration needed.
makeWorldReadable with pub-group permission helpers in service.go. Wire into Deploy and SetVisibility. Add unit tests.beast host create to provision <name>-pub group and add Caddy. Update Delete to remove group.AccessTier enum (private/team/client/public) — over-engineered for our needsPermissionManager interface — unnecessary abstractionbeast-public group — per-user is more isolated