Commit graph

144 commits

Author SHA1 Message Date
ralyodio
3bfbd7059f ci(vu1nz-scan): don't fail the scan when the PR comment can't be posted
Dependabot PRs run with a read-only GITHUB_TOKEN, and GitHub was returning
503 (the HTML "Unicorn" page) for the comment write; the step's catch only
handled 403 and re-threw everything else, failing the whole scan even though
the security scan itself passed.

- skip the comment step for github.actor == 'dependabot[bot]'
- continue-on-error: true
- warn-and-continue on any status instead of only 403

Mirrors sh1pt pack vu1nz-scan@1.0.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 01:56:01 +00:00
RissRIce
24269b6799
fix(files): honor forwarded HTTPS proxy chains (#92)
Some checks failed
CI / build (push) Has been cancelled
deploy / deploy (push) Has been cancelled
test / test (push) Has been cancelled
Co-authored-by: rissrice2105-agent <rissrice2105-agent@users.noreply.github.com>
2026-07-15 06:21:31 -07:00
2048229cbb fix(setup): robustly move admin sshd off :22 on Ubuntu 24.04
Some checks failed
CI / build (push) Has been cancelled
deploy / deploy (push) Has been cancelled
test / test (push) Has been cancelled
The provision step aborted at "admin sshd is NOT listening on 2202"
because it decided ssh was socket-activated purely from `systemctl cat
ssh.socket` succeeding — but that unit file exists on every modern box,
including DigitalOcean images where the real listener is the standalone
ssh.service. It then restarted only ssh.socket, so nothing ended up
bound on the admin port and the safety check killed the deploy.

- Detect socket vs. standalone mode via `is-active`/`is-enabled`, not
  mere unit-file existence.
- In socket mode, stop the standalone ssh.service first so it can't
  fight the socket for the port.
- Fall back to the other restart path if the first doesn't bind.
- Dump listener + unit diagnostics before aborting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 07:09:07 +00:00
RissRIce
b777bd72e8
fix(files): encode download filenames (#91)
Co-authored-by: rissrice2105-agent <rissrice2105-agent@users.noreply.github.com>
2026-07-13 23:54:18 -07:00
SabFabDev
21630c2d17
fix(news): stop IHAVE when duplicate lookup fails (#89)
Some checks failed
CI / build (push) Has been cancelled
deploy / deploy (push) Has been cancelled
test / test (push) Has been cancelled
Co-authored-by: SabFabDev <SabFabDev@users.noreply.github.com>
2026-07-12 00:43:24 -07:00
RissRIce
c2a06f1cdf
fix(gopher): block symlink escapes (#85)
Some checks failed
CI / build (push) Has been cancelled
deploy / deploy (push) Has been cancelled
test / test (push) Has been cancelled
Co-authored-by: rissrice2105-agent <rissrice2105-agent@users.noreply.github.com>
2026-07-10 02:33:34 -07:00
dependabot[bot]
e38c0935f4
chore(deps): bump the go-modules group with 3 updates (#87)
Bumps the go-modules group with 3 updates: [github.com/livekit/server-sdk-go/v2](https://github.com/livekit/server-sdk-go), [golang.org/x/crypto](https://github.com/golang/crypto) and [golang.org/x/term](https://github.com/golang/term).


Updates `github.com/livekit/server-sdk-go/v2` from 2.16.7 to 2.17.0
- [Commits](https://github.com/livekit/server-sdk-go/compare/v2.16.7...v2.17.0)

Updates `golang.org/x/crypto` from 0.53.0 to 0.54.0
- [Commits](https://github.com/golang/crypto/compare/v0.53.0...v0.54.0)

Updates `golang.org/x/term` from 0.44.0 to 0.45.0
- [Commits](https://github.com/golang/term/compare/v0.44.0...v0.45.0)

---
updated-dependencies:
- dependency-name: github.com/livekit/server-sdk-go/v2
  dependency-version: 2.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules
- dependency-name: golang.org/x/crypto
  dependency-version: 0.54.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules
- dependency-name: golang.org/x/term
  dependency-version: 0.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-10 02:33:00 -07:00
b51fa5c0e6
Add himalaya + meli mail clients and Shedding Snake arcade game (#88)
Mail clients (internal/mailclients): launch himalaya and meli as
alternatives to the built-in AgentMail reader, pointed at the member's
mailbox with the same master IMAP / loopback-SMTP creds. They run
host-side (secrets never enter a pod), from a throwaway per-session
config that is deleted on exit; operators can override the config
template and argv via env. Wired into the hub ("Mail · Himalaya/Meli",
locked when the binary is absent) and the mail@ route
(ssh -t mail@host himalaya|meli).

Shedding Snake (plugins/arcade): a molting twist on Snake inspired by
cha.rlie.co/shedding-snake. The snake barely grows — each apple sheds
its whole body as a permanent field of scales you must not bite. Walls
wrap, speed ramps up, scales age fresh-teal → dusty-gray, and molt
counts feed a global "shedsnake" leaderboard. Grace period lets you
slither off a fresh molt.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 02:30:12 -07:00
Luna Ops
ff9eef907d
Require POST for file web mutations (#82)
Some checks failed
CI / build (push) Has been cancelled
deploy / deploy (push) Has been cancelled
test / test (push) Has been cancelled
2026-07-07 05:41:16 -07:00
RissRIce
ee06082ce1
fix(mailbox): reject invalid bot toggle values (#80)
Co-authored-by: rissrice2105-agent <rissrice2105-agent@users.noreply.github.com>
2026-07-07 05:40:35 -07:00
ad76e7fa16
feat(gopher): serve BBS content over Gopher + SSH-authenticated "hedgehog" (#83)
Add Gopher (RFC 1436) as a co-located protocol service, following the
internal/news pattern. Two surfaces share one read-only Resolve engine:

  - Public Gopher on :70 (RFC 1436) for any gopher client (lynx, Lagrange).
    Classic gopher is stateless with no auth verb, so this surface serves
    only public content.
  - `ssh gopher@` = "hedgehog": the same gopher wire semantics carried over
    the authenticated SSH channel (the member's key is the credential), so it
    additionally reaches members-only selectors. Gopher where gopher can,
    our own gopher-like thing over SSH where it can't.

Menus surface the member directory + homepages (public_html), an About page
(brand + MOTD), public newsgroups (allowlisted on :70, all groups on
hedgehog), and members' public files. Selectors are confined to each member's
area (path-traversal guarded). New AGENTBBS_GOPHER* env vars; docs/gopher.md
and README updated (incl. the setcap note for binding privileged :70).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 05:39:35 -07:00
9d0c0a0193
Fix hangman: lowercase r swallowed by restart binding during play (#79)
Some checks failed
CI / build (push) Has been cancelled
deploy / deploy (push) Has been cancelled
test / test (push) Has been cancelled
The Update key switch bound "r" to restart, but returned early even when
the game was still active, so lowercase r could never be entered as a
letter guess (Shift+R worked because "R" didn't match the case). Now r
only restarts when dead; otherwise it falls through to the guess logic.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 06:21:06 -07:00
47a88b2fc5
fix: point web client link to chat.<root> not chat.<full-host> (#78)
Some checks are pending
CI / build (push) Waiting to run
deploy / deploy (push) Waiting to run
test / test (push) Waiting to run
The setup help page hardcoded chat.${DOMAIN}, producing
chat.bbs.profullstack.com — a host the Caddy route never serves. The
The Lounge web IRC client is fronted at ${CHAT_DOMAIN}
(chat.${DOMAIN#*.} = chat.profullstack.com). Use ${CHAT_DOMAIN} so the
"web client" link and label match the actual host.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 18:27:29 -07:00
RissRIce
3a4d7ed8d8
fix(mailbox): validate bot list limits (#77) 2026-07-04 17:39:31 -07:00
forgou37
6a086eb7cc
fix(nntp): handle open-ended OVER range like '5-' (#75)
Some checks failed
CI / build (push) Has been cancelled
deploy / deploy (push) Has been cancelled
test / test (push) Has been cancelled
2026-07-02 17:20:39 -07:00
dependabot[bot]
11f7b8ea2e
chore(deps): bump the go-modules group with 3 updates (#76)
Bumps the go-modules group with 3 updates: [github.com/livekit/protocol](https://github.com/livekit/protocol), [golang.org/x/crypto](https://github.com/golang/crypto) and [golang.org/x/term](https://github.com/golang/term).


Updates `github.com/livekit/protocol` from 1.48.2 to 1.49.0
- [Release notes](https://github.com/livekit/protocol/releases)
- [Changelog](https://github.com/livekit/protocol/blob/main/CHANGELOG.md)
- [Commits](https://github.com/livekit/protocol/compare/v1.48.2...v1.49.0)

Updates `golang.org/x/crypto` from 0.52.0 to 0.53.0
- [Commits](https://github.com/golang/crypto/compare/v0.52.0...v0.53.0)

Updates `golang.org/x/term` from 0.43.0 to 0.44.0
- [Commits](https://github.com/golang/term/compare/v0.43.0...v0.44.0)

---
updated-dependencies:
- dependency-name: github.com/livekit/protocol
  dependency-version: 1.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules
- dependency-name: golang.org/x/crypto
  dependency-version: 0.53.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules
- dependency-name: golang.org/x/term
  dependency-version: 0.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-02 17:19:36 -07:00
43dbdf0e06
ci(deploy): preempt self-update timer so it can't starve the deploy lock (#72)
Some checks failed
CI / build (push) Has been cancelled
deploy / deploy (push) Has been cancelled
test / test (push) Has been cancelled
* ci(deploy): preempt self-update timer so it can't starve the deploy lock

The self-update systemd timer redeploys from source (no SKIP_BUILD) and
can hold setup.sh's flock for >5min while compiling on a tiny droplet.
When it fires close to a CI push it starves the deploy, which waits the
full 5min on the lock and then fails with 'another setup.sh run is in
progress (lock held >5m)'.

The CI push is authoritative (ships prebuilt binaries + resets to the
exact commit), so stop any in-flight timer run to release the lock and
pause the timer before taking it. setup.sh re-enables the timer at the
end of its run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(files): isolate over-quota test from seeded-README baseline

TestWebSaveOverQuotaPreservesExistingFile set quota=5 but left sess.used
at the newSession baseline, which already counts the README.txt that
ensureUserPub seeds into /public (added in d19c5c4). That baseline alone
exceeds 5 bytes, so the initial 2-byte save was rejected with
'quota exceeded' before the test could exercise the over-quota replace.

Reset sess.used to 0 after setting the tiny quota, mirroring
TestQuotaEnforced, so the writer starts from a clean gauge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 01:32:10 -07:00
Kyle Zengo
c8718f25ea
fix: canonicalize root in safeJoin to handle symlinked storage roots (#64)
When the configured storage root (or a system temp dir on macOS where
/var → /private/var) is reached through a symlink, filepath.EvalSymlinks
on a child path resolves to the canonical form, but within() was comparing
against the lexical root — causing valid paths to be rejected with
"files: path escapes its area".

Fix: resolve the root once with EvalSymlinks before the symlink guard
loop, and compare resolved paths against the canonical root. The initial
lexical containment check (line 108) still uses the original root so
that the returned path keeps the caller's expected prefix.

Adds two regression tests:
  - TestSafeJoinSymlinkedRoot: valid file under a symlinked root is accepted
  - TestSafeJoinChildSymlinkEscapeStillBlocked: escaping child symlink is still rejected

Fixes #62

Co-authored-by: Kyle Paul Zengo <kylezengo@mac2012kylezengo.tail2f018b.ts.net>
2026-07-01 01:10:20 -07:00
RissRIce
3e28a57b5d
fix(files): preserve existing file on failed quota upload (#68)
Co-authored-by: rissrice2105-agent <289161642+rissrice2105-agent@users.noreply.github.com>
2026-07-01 01:09:01 -07:00
dependabot[bot]
51273e9a4a
chore(deps): bump the github-actions group with 4 updates (#70)
Bumps the github-actions group with 4 updates: [actions/checkout](https://github.com/actions/checkout), [actions/setup-go](https://github.com/actions/setup-go), [actions/setup-python](https://github.com/actions/setup-python) and [actions/github-script](https://github.com/actions/github-script).


Updates `actions/checkout` from 4 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v7)

Updates `actions/setup-go` from 5 to 6
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

Updates `actions/setup-python` from 5 to 6
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

Updates `actions/github-script` from 7 to 9
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v7...v9)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/github-script
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-01 01:08:44 -07:00
dependabot[bot]
83a02f6d6c
chore(deps): bump the go-modules group with 6 updates (#71)
Bumps the go-modules group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/livekit/protocol](https://github.com/livekit/protocol) | `1.46.0` | `1.48.2` |
| [github.com/livekit/server-sdk-go/v2](https://github.com/livekit/server-sdk-go) | `2.16.6` | `2.16.7` |
| [github.com/pion/webrtc/v4](https://github.com/pion/webrtc) | `4.2.15` | `4.2.16` |
| [golang.org/x/crypto](https://github.com/golang/crypto) | `0.50.0` | `0.52.0` |
| [golang.org/x/term](https://github.com/golang/term) | `0.42.0` | `0.43.0` |
| [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) | `1.52.0` | `1.53.0` |


Updates `github.com/livekit/protocol` from 1.46.0 to 1.48.2
- [Release notes](https://github.com/livekit/protocol/releases)
- [Changelog](https://github.com/livekit/protocol/blob/main/CHANGELOG.md)
- [Commits](https://github.com/livekit/protocol/compare/v1.46.0...v1.48.2)

Updates `github.com/livekit/server-sdk-go/v2` from 2.16.6 to 2.16.7
- [Commits](https://github.com/livekit/server-sdk-go/compare/v2.16.6...v2.16.7)

Updates `github.com/pion/webrtc/v4` from 4.2.15 to 4.2.16
- [Release notes](https://github.com/pion/webrtc/releases)
- [Commits](https://github.com/pion/webrtc/compare/v4.2.15...v4.2.16)

Updates `golang.org/x/crypto` from 0.50.0 to 0.52.0
- [Commits](https://github.com/golang/crypto/compare/v0.50.0...v0.52.0)

Updates `golang.org/x/term` from 0.42.0 to 0.43.0
- [Commits](https://github.com/golang/term/compare/v0.42.0...v0.43.0)

Updates `modernc.org/sqlite` from 1.52.0 to 1.53.0
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.52.0...v1.53.0)

---
updated-dependencies:
- dependency-name: github.com/livekit/protocol
  dependency-version: 1.48.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules
- dependency-name: github.com/livekit/server-sdk-go/v2
  dependency-version: 2.16.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-modules
- dependency-name: github.com/pion/webrtc/v4
  dependency-version: 4.2.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-modules
- dependency-name: golang.org/x/crypto
  dependency-version: 0.52.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules
- dependency-name: golang.org/x/term
  dependency-version: 0.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules
- dependency-name: modernc.org/sqlite
  dependency-version: 1.53.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-modules
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-01 01:08:25 -07:00
807ecf2530
chore: keep all agentbbs services on latest software (#69)
Some checks are pending
CI / build (push) Waiting to run
deploy / deploy (push) Waiting to run
test / test (push) Waiting to run
* ci: add mailu-update workflow to keep the mail stack current

The deploy/mailu compose stack pins the floating series tags
(ghcr.io/mailu/*:2024.06); patch releases within the series only land when
someone runs `docker compose pull`, so the box drifts behind on security fixes.

Add a scheduled (weekly) + on-demand workflow that SSHes to the droplet
(reusing deploy.yml's DEPLOY_* secrets), backs up DKIM keys + the admin DB,
pulls the latest images for the pinned series, recreates the containers, and
health-checks the Mailu front on 127.0.0.1:8080. Shares deploy.yml's
concurrency group so it never races a code deploy. Stays within the pinned
series on purpose — crossing to a future series stays a deliberate PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: bump Forgejo to 11.0.15 + add Dependabot to keep deps current

Audit of every version pin in the repo: Ergo (2.18.0), Go (1.26 → latest
patch via setup-go), the Ubuntu pod base (24.04 LTS), and the GitHub Action
majors are all already current. Only Forgejo was stale — bump 11.0.1 →
11.0.15 (latest patch of the 11.x LTS line; a 15.x major stays a deliberate,
tested upgrade because of DB migrations).

Add .github/dependabot.yml so github-actions, Go modules, and the Docker
image tags (Mailu compose + pod Containerfile) get review-gated update PRs
weekly. Shell-string pins (FORGEJO_VERSION/ERGO_VERSION in setup.sh) can't be
watched by Dependabot; noted inline. Mailu runtime patch level is handled by
the mailu-update workflow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* mailu: document RECIPIENT_DELIMITER=+ in mailu.env.example

Plus-addressing (chovy+tag@ -> chovy@) is a hard prerequisite for qaaas.dev's
packages/mail but was missing from the example, so tagged mail bounces as an
unknown recipient until an operator sets it by hand. Add it with a note that it
governs DELIVERY only, not login (Mailu auths the exact address; base <name>@
is the single login and already receives all +tagged mail).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 20:06:06 -07:00
d19c5c4c3e files: seed a default README.txt into every member's public area
ensureUserPub only ran os.MkdirAll, so a freshly-provisioned /public
(and thus ~<name>/public on the web) came up empty — only ~chovy had a
README because it was uploaded by hand. Embed that help text as a
default and write it whenever the area has no README.txt.

ensureUserPub is hit on SFTP connect (fs.go) and when the web host
materializes ~<name>/public (AnonRoot), so this self-heals every
existing empty member the next time they connect or their page is
viewed — no manual backfill. A member's own README is never clobbered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 02:47:27 +00:00
042166b05f members: show joined date in admin + public lists; 'last active: X ago'
Admin Users view appends 'joined YYYY-MM-DD'. Public member directory
shows 'last active: N days ago' for offline members plus the joined date,
both sourced from store.User.CreatedAt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 02:40:24 +00:00
28ad36834e irc(ergo): allow chrome-extension:// + irc domain origins for WS
Some checks are pending
CI / build (push) Waiting to run
deploy / deploy (push) Waiting to run
test / test (push) Waiting to run
TronBrowser's in-extension IRC client connects to the Ergo WebSocket from a
chrome-extension:// page; the ID is path-derived (differs per install) so use a
wildcard. Safe — the network is members-only and every WS client still passes
SASL, so Origin isn't the access gate. Also add the irc domain itself.

Needs deploy (push to main → setup.sh re-renders /etc/ergo/ircd.yaml + restarts
ergo) for the web/extension client to connect.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 16:28:29 +00:00
105ff0ed8a
fix(passwd): also reset The Lounge web-login password (#61)
Some checks failed
CI / build (push) Has been cancelled
deploy / deploy (push) Has been cancelled
test / test (push) Has been cancelled
passwd@ reset the IRC SASL credential (Ergo store + The Lounge saslPassword) but
NOT The Lounge's own web-login password (the bcrypt field used to sign in to
chat.<domain> itself). So a member who reset their password could connect to IRC
but got "auth failed" at chat.profullstack.com/sign-in.

set-irc-password.sh now sets all THREE chat credentials to the new password:
Ergo SASL, the Lounge saslPassword, and the Lounge web-login password via
`thelounge reset <member>` (AGENTBBS_LOUNGE_RESET_CMD, default targets the
dockerized The Lounge). The password is piped on stdin to `thelounge reset`, so
it never lands on a command line. Best-effort: a Lounge web-reset failure warns
but doesn't fail the run (Ergo SASL stays the primary IRC secret).

Bumped ircpass SetPassword's context timeout 20s→60s since the helper now also
runs a `docker exec thelounge ...` step.

Verified live on bbs.profullstack.com: the modified helper sets all three for an
existing member (web bcrypt match + saslPassword + Ergo store), and the affected
member's web login was reconciled.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 09:40:28 -07:00
54ede811f9
Merge pull request #60 from profullstack/feat/group-broadcast-messaging
feat(messaging): group & broadcast messages + email-all
2026-06-27 07:03:41 -07:00
103ddeb7c9 feat(messaging): group & broadcast messages + email-all
Members can now message a group or everyone, and the operator can announce to
the whole membership by email.

Inbox channel (any member):
  - msg@ recipient spec accepts a comma list (alice,bob,carol) or all/*/everyone
    to broadcast: `ssh msg@host alice,bob hi`, `ssh msg@host all hi`.
  - Members hub TUI gains multi-select: space toggles, `a` selects all, `m`
    messages the selected group (header names the audience); selection clears
    after send.
  - store.SendMessageMulti delivers one body to many inboxes in a single
    transaction (dedupes, skips empties); resolveRecipients validates names,
    excludes the sender, and skips banned members on broadcast.

Email channel (operator, explicit):
  - new `agentbbs broadcast` subcommand sends an announcement to ALL members via
    inbox + email. Preview by default (like notify-creds); --send delivers;
    --no-inbox/--no-email pick a channel; --subject/--from/--user refine it.
    Email reaches only verified addresses and refuses --send without SMTP.

Tests: SendMessageMulti (dedupe/empty), resolveRecipients (list/unknown/all
tokens, banned + sender exclusion), TUI selection (toggle/select-all/group
compose). Docs: docs/messaging.md. build/vet/gofmt/`go test ./...` green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 12:18:41 +00:00
54da317f4e
feat(passwd): self-service password reset across git, mail & chat (#59)
Add a key-gated `ssh passwd@host` route (alias `password@`) that sets ONE
member-chosen password across every service with its own credential:

  - git  (Forgejo)        new forgejo.SetPassword (PATCH /admin/users, clears
                          must_change; EnsureUser first so the account exists)
  - mail (Mailu webmail)  existing mailu.SetPassword
  - chat (IRC/Ergo + The Lounge)  new internal/ircpass package

Because the route authenticates by the member's registered SSH key, it also
serves as the forgot-password path — no old password required.

The BBS runs as a non-root service user, but the Ergo password store and The
Lounge user files are root-owned. internal/ircpass bridges this by shelling out
to scripts/set-irc-password.sh through a narrow sudoers rule (installed by
setup.sh). The new password travels on stdin (a new `set-irc-password.sh
<member> -` form), so it never appears in the process table or sudo's log.

UX: masked entry typed twice (readSecret); no-PTY reads stdin; empty input
generates a strong password and shows it once. Each service leg is independent
and best-effort with a per-service ✓/✗ summary, plus a confirmation email that
never contains the password.

Tests: ircpass (stdin contract + member/password rejection), forgejo.SetPassword,
auth IsPasswdName + reservation. Docs: credentials.md (passwd@ section) + irc.md.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 03:46:23 -07:00
f2bcb7e063 files: /me and /public are two separate per-user areas
Per feedback: /me is PRIVATE and the public folder must be its own
top-level area, not nested under /me.

- A member now has two sibling areas over SFTP: /me (private,
  <root>/files/users/<name>) and /public (their own public files,
  <root>/files/public/<name>), served anonymously at ~<name>/public.
- Drop the global shared /public web route and the /me/public nesting.
  The anon surface only exposes ~name/public; /me has no anon route.
- Both owned areas count toward the quota gauge.
- Index publish hint, docs, and setup.sh updated to scp :/public/.

files.<host> stays a file server; member sites remain on the BBS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 01:36:43 +00:00
a2d4817a8e files: per-user public at ~name/public; home = member directory
Re-model the web file host as a file server (not a website host):

- Drop the misnamed /site area. A member's public files are now their
  /me/public subfolder (unix ~/public), served anonymously at
  ~<name>/public. The rest of /me stays private; only ~name/public is
  ever exposed. Bare /~name redirects to /~name/public/.
- The root / is now a directory of ALL members, each linked to their BBS
  site (https://<bbs-host>/~name via WebConfig.SiteBase) AND their public
  files here (~name/public). No longer hides empty members.
- Sites/homepages stay on the BBS — files.<host> only links to them.
- Usage gauge is just /me again (which includes /me/public).

setup.sh + docs updated; tests cover ~name/public browse/download, the
bare-~name redirect, empty-member empty-listing, /public-only exposure,
and traversal confinement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 01:21:00 +00:00
87fb2b4a1f files: make ~member browsable before first session
A valid member's ~name 404'd until they'd opened their first SFTP/web
session, because joining onto a not-yet-created site dir tripped the
safeJoin escape guard (it walks up to sites/, outside the per-user root).
AnonRoot now materializes the idempotent site dir for a known member, so
~name renders an empty listing the moment the account exists. Missing
sub-paths and unknown members still 404. Adds a regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 01:03:12 +00:00
192b117fc1 Merge feat/files-store-provisioning: per-user /site + anonymous web file surface
# Conflicts:
#	setup.sh
2026-06-26 00:53:38 +00:00
e478da905f files: add per-user public /site + anonymous web surface
Add a third storage area, /site — each member's own public root, served
unauthenticated on the web at ~<name> alongside the shared /public.

Web file host (files.<host>) is no longer a login wall:
- GET /            -> directory of members' ~user sites (+ sign-in link)
- GET /~<name>/... -> anon read-only browse + clean file URLs of /site
- GET /public/...  -> anon read-only browse + clean file URLs of shared
                      area (fixes bare /public requiring login: the old
                      Caddy `handle_path /public/*` never matched /public)
Login is now optional and gates only private /me + writes. The anon
surface has no route into anyone's /me and safeJoin rejects traversal.

Usage gauge now sums the member-owned areas (/me + /site) instead of
/me alone; shared /public stays operator-managed and unmetered.

Caddy: route all of files.<host> to the Go manager. Docs + tests updated
(anon download/browse, traversal confinement, /site metering).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 00:49:38 +00:00
eb8ef546cf
files: provision-user CLI + anonymous public HTTP serving (#58)
* feat(files): provision-user CLI + anonymous public HTTP serving

Lets external services (the TronBrowser extension store) host files on
files.profullstack.com without the interactive `ssh join@` onboarding.

- `agentbbs provision-user --name <h> --pubkey "<ssh key>"`: registers a member
  from an SSH *public* key (account = handle + key fingerprint). Reuses
  SanitizeUsername (same rules as join@) + EnsureUser; Files/SFTP access is free
  for members, so the account can immediately
  `scp … files@host:/public/extensions/<slug>/`. JSON output; refuses on key/
  handle collision. New auth.FingerprintAuthorizedKey() parses an
  authorized_keys line to the same SHA256 fp as a live session key (tested).
- setup.sh: the files.<host> Caddy site now serves the shared /public area as
  unauthenticated, read-only static files (handle_path /public/*), so .crx/.zip
  download links work for anyone — mapping 1:1 to the SFTP path. Non-/public
  paths still hit the auth'd web file manager.
- docs/files.md updated.

Note: not compiled here — repo go.mod requires go 1.26 and this sandbox has
1.22.2; changes pass gofmt parse/format checks. Reuses existing store/auth APIs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(vet): redundant newline in wish.Println premium-flow messages

`go test ./...` / `go vet ./...` fail on `wish.Println(… "…\n")` — Println
already appends a newline. Pre-existing on main (its CI is red for the same two
lines); surfaced here. Switched both to `wish.Print` with an explicit trailing
"\n\n" so output bytes are unchanged and vet is satisfied.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 11:37:45 -07:00
d1615ac817 fix(vet): redundant newline in wish.Println premium-flow messages
`go test ./...` / `go vet ./...` fail on `wish.Println(… "…\n")` — Println
already appends a newline. Pre-existing on main (its CI is red for the same two
lines); surfaced here. Switched both to `wish.Print` with an explicit trailing
"\n\n" so output bytes are unchanged and vet is satisfied.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 18:36:23 +00:00
e5e8e75553 feat(files): provision-user CLI + anonymous public HTTP serving
Lets external services (the TronBrowser extension store) host files on
files.profullstack.com without the interactive `ssh join@` onboarding.

- `agentbbs provision-user --name <h> --pubkey "<ssh key>"`: registers a member
  from an SSH *public* key (account = handle + key fingerprint). Reuses
  SanitizeUsername (same rules as join@) + EnsureUser; Files/SFTP access is free
  for members, so the account can immediately
  `scp … files@host:/public/extensions/<slug>/`. JSON output; refuses on key/
  handle collision. New auth.FingerprintAuthorizedKey() parses an
  authorized_keys line to the same SHA256 fp as a live session key (tested).
- setup.sh: the files.<host> Caddy site now serves the shared /public area as
  unauthenticated, read-only static files (handle_path /public/*), so .crx/.zip
  download links work for anyone — mapping 1:1 to the SFTP path. Non-/public
  paths still hit the auth'd web file manager.
- docs/files.md updated.

Note: not compiled here — repo go.mod requires go 1.26 and this sandbox has
1.22.2; changes pass gofmt parse/format checks. Reuses existing store/auth APIs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 18:19:08 +00:00
fb7722eacc Wire shared MOTD into BBS hub and IRC
Use profullstack.com/motd as the shared Message of the Day across both
the SSH BBS hub and the Ergo IRC server.

- internal/motd: fetch + in-memory cache with background refresh (stdlib
  only); Current() never blocks session start, keeps last value on error.
  Source overridable via AGENTBBS_MOTD_URL.
- hub: append the daily MOTD below the existing welcome/onboarding text.
- IRC: deploy/ergo/refresh-motd.sh pulls /motd into Ergo's ergo.motd and
  rehashes; setup.sh installs it + an ergo-motd.timer (hourly) mirroring
  the ergo-certs timer, with a seeded fallback if the source is offline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 11:51:06 +00:00
63548bcbbe fix(join): only mint the $99 CoinPay charge on explicit opt-in
offerPremium ran on every join@ and called CreatePremiumCharge just to
show the pitch, so CoinPay minted a $99 payment for everyone who
connected. Show the pitch with no charge, then create the payment only
when the member types "yes" at the prompt (the SSH equivalent of
clicking "Become a paid member").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 12:07:34 +00:00
94ac404725 fix(setup): rename MAIL toggle to MAIL_STACK — it collided with the reserved $MAIL env var
The mail Caddy route (mail.<host>) and the entire §9e Mailu provisioning block
were gated on $MAIL, but MAIL is a standard env var (the mail-spool path) that
PAM's pam_mail sets for the sudo session the CI deploy runs under. So deploys saw
MAIL=/var/mail/root (!= "1"), silently skipping: the mail.<host> Caddy block
(webmail went down with TLS handshake aborts), the AGENTBBS_MAIL_SMTP_* env
upserts (incl. the new SMTP_SERVERNAME), AND it ran the else-branch which disabled
the mail cert-refresh timer every deploy. Rename the toggle to MAIL_STACK (no
collision) so all of it actually runs. Matches the intermittent 'mail block
dropped after a deploy' seen before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 16:16:25 +00:00
185d3da2f9 fix(caddy): generate the chat.<host> site so deploys stop dropping The Lounge
The chat.profullstack.com Caddy block was only ever appended manually, so every
setup.sh deploy (which rewrites the whole Caddyfile) silently dropped it and
broke https://chat.profullstack.com (fell into the on-demand-TLS catch-all whose
ask endpoint denies it -> TLS handshake aborts). Generate it like the mail/news/
files sites (CHAT=1 toggle, CHAT_DOMAIN/CHAT_ADDR). The Lounge container is still
provisioned out-of-band; this only owns the route.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 16:07:32 +00:00
55d517feb4 fix(mailbox): verify SMTP STARTTLS against the mail host, not the dial IP
AgentMail compose/send failed with 'cannot validate certificate for 127.0.0.1
because it doesn't contain any IP SANs': the sender dialed the local relay at
127.0.0.1:25 and net/smtp pinned the TLS ServerName to the dial host, but the
relay's cert is for mail.<host>. Reimplement smtpSend (mirrors net/smtp.SendMail)
with an overridable IMAPConfig.SMTPServerName; default it to the mail host
(AGENTBBS_MAIL_SMTP_SERVERNAME). Now we dial the loopback for relay permission
yet verify the real hostname cert — no /etc/hosts hack. setup.sh upserts the new
var. Tested against a fake SMTP server (full MAIL/RCPT/DATA flow).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 15:54:30 +00:00
25266845e0 irc: enforce per-member SASL passwords + irssi setup on landing page
Auth model change: ergo-auth-member now verifies the SASL passphrase against a
per-member pbkdf2 hash (/var/lib/ergo/irc-passwd) in addition to BBS membership,
replacing the old "membership is the credential, passphrase ignored" gate that
let anyone who knew a member name connect as them. Rewrote deploy/ergo/auth-script.sh
in python3 (drops jq/curl dep); setup.sh already installs it as ergo-auth-member.

- scripts/set-irc-password.sh: set/rotate a member's IRC password (or --all to
  backfill); also syncs The Lounge saslPassword so the web client keeps working.
- setup.sh landing page: new "IRC from a desktop client" section with irssi/HexChat/
  WeeChat SASL setup (connect by network name ProfullstackBBS, not hostname).
- docs/irc.md: document password auth, the helper, the 6697 cloud-firewall note,
  and an irssi quick-start.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 14:11:29 +00:00
57daf894b2 Merge remote-tracking branch 'origin/main' into feat/notify-creds 2026-06-23 14:09:25 +00:00
cddd9819cc feat(files): web file browser at files.<host> with webmail-password login
Adds a browser-based file manager so members can use their files without an
SSH key. Served on a loopback HTTP server (AGENTBBS_FILES_WEB_ADDR, default
127.0.0.1:8092) that Caddy fronts at files.<host>. Members sign in with their
webmail username + password, verified against the Mailu IMAP backend
(mailbox.VerifyLogin), and browse the same virtual /me + /public areas as SFTP
— no home directory is ever exposed. Upload/download/mkdir/delete with the
private-area quota enforced; reuses internal/files confinement (fs.go).
setup.sh renders the files.<DOMAIN> Caddy site + env knob. Unit tests cover
the auth gate and an upload/list/download/delete round trip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 14:09:18 +00:00
1dbb2c70e2 feat(mailbox): compose & reply in the AgentMail TUI
The interactive reader (ssh mail@ / hub Mail) was read-only; the send path
existed only for agents (bot mode). Add compose (c), reply (r), reply-all (a)
to the TUI with a To/Cc/Subject/Body form (tab/arrows to move fields, ctrl+d
send, esc cancel). Reply prefills + quotes the original and threads via
In-Reply-To. Tests drive the model key-by-key through send.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 14:09:18 +00:00
045acc965a fix(ergo): use absolute lock-file path so it lands in the writable data dir
WorkingDirectory is /opt/ergo with ProtectSystem=strict (only /var/lib/ergo
and /etc/ergo are writable), but lock-file was relative ("ircd.lock"), so Ergo
tried to create /opt/ergo/ircd.lock on a read-only path and crash-looped with
"failed to acquire flock on ircd.lock: read-only file system" — nothing ever
listened on 6697/6667/8097 (clients got ECONNREFUSED). datastore.path is already
absolute (__DATA__/ircd.db); per Ergo's own config comment the lock-file should
match. Render it to __DATA__/ircd.lock (/var/lib/ergo, ergo-owned + in
ReadWritePaths).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 12:57:49 +00:00
11503ca3f0 docs: credentials.md for git/mailbox creds, notify-creds, and SMTP ports
New docs/credentials.md covers member git-account provisioning, the
notify-creds backfill command (flags + behavior), required env, and the
two SMTP paths — transactional AGENTBBS_SMTP_* (STARTTLS :587) vs. the
gateway loopback relay AGENTBBS_MAIL_SMTP_ADDR (:25 into Mailu),
clarifying why :25 is correct and that 465 (not "467") is implicit-TLS.

Cross-linked from README ops and docs/mail.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 10:48:36 +00:00
ce0758b0ec
Email members their git + mailbox credentials on provisioning (#57)
* feat(files): SFTP member storage — private workspaces + shared public area + mgmt TUI

Implements M4 (Files). A fully virtual Go SFTP server (pkg/sftp + crypto/ssh,
no OS users) wired as an "sftp" subsystem on the existing :22 wish listener, so
members reach their files with their login key:

    sftp files@bbs.profullstack.com     # scp/rsync ride the same endpoint

Identity is the SSH key (the username is conventional/ignored). Two areas per
session: a private, quota-limited /me workspace and a single shared public file
area /public (old-school BBS file area; world-read, members-only write by
default, operator-moderated). This reverses the old NG1 "no sharing" boundary in
favour of one sanctioned, inspectable sharing surface (PRD §9.3 amended).

internal/files:
- backend.go  service, layout, quota/usage, live-session registry, operator API
- fs.go       per-session virtual FS; resolve() is the single security
              chokepoint (area confinement + symlink-escape guard) + pkg/sftp
              request handlers
- server.go   subsystem handler: key auth -> member session -> request server,
              with byte metering and force-disconnect
- tui.go      in-BBS member browser (hub plugin "Files")
- admin.go    operator management TUI: sessions, workspaces/quotas, public area

Operator console: ssh sftp@<host> (allowlist-gated; sftpadmin@/filesadmin@
aliases) — list/disconnect sessions, set per-user quotas, revoke SFTP access,
toggle public write, moderate the public area.

store: files_access (per-user quota override + revoked) and files_settings
(public-write mode) tables + methods. main.go wiring guarded by AGENTBBS_FILES
(+ AGENTBBS_FILES_QUOTA_MB, default 1 GiB). Route names reserved.

Tests (incl -race): path traversal/confinement, symlink-escape rejection,
public-write ACL, quota enforcement, usage accounting, and an end-to-end run
against a real SFTP client. Docs: docs/files.md; PRD §5.3/§5.3.1/§9.3 + README
updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add notify-creds subcommand to (re)email members git + mailbox creds

`agentbbs notify-creds` backfills credential emails to verified members
who signed up before the git/mailbox welcome emails existed.

- git (all verified): forgejo.EnsureUserReset resets each account to a
  fresh one-time password (must-change) and emails the web login link,
  username, and password. New method since the original one-time
  password is not recoverable for existing accounts.
- mailbox (all verified): ensures the forwardemail alias and emails the
  address + webmail link.
- Preview by default; --send executes. --git/--mail/--user filters.
  Refuses --send without SMTP; warns+skips when Forgejo/forwardemail
  are unconfigured.

Also folds in the welcome-email functions (gitWelcomeEmailBody,
mailWelcomeEmailBody, EnsureUser password return, provisionGit/
ensurePremium sends) that this builds on. README ops + forgejo tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 03:48:11 -07:00
7c794dcf79
Merge pull request #56 from profullstack/feat/files-sftp
feat(files): SFTP member storage — private workspaces + shared public area + mgmt TUI
2026-06-23 03:47:19 -07:00