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>
This commit is contained in:
Anthony Ettinger 2026-06-23 09:35:04 +00:00
parent 68899180a7
commit 6dc94bd784
18 changed files with 2223 additions and 27 deletions

View file

@ -30,7 +30,7 @@ supply inventory. The BBS hub is the funnel that builds that audience.
|---|---|
| `profullstack.com` | Primary BBS host — `ssh play@profullstack.com` (guest) and member access |
| `logicsrc.com` | Home of the AgentGames spec and developer/agent-facing docs |
| `cl1.tech` | Managed file-transfer service (SFTP product), surfaced in-BBS as a plugin |
| `bbs.profullstack.com` | Member file storage over SFTP — `sftp files@bbs.profullstack.com` (same SSH key as login) |
### 1.2 One-line pitch
@ -100,7 +100,7 @@ supply inventory. The BBS hub is the funnel that builds that audience.
▼ ▼ ▼ ▼
┌─────────┐ ┌───────────┐ ┌────────────┐ ┌──────────┐
│ Arcade │ │ AgentGames│ │ Files │ │ AgentAd │
│ plugin │ │ plugin │ │ (cl1.tech) │ │ plugin │
│ plugin │ │ plugin │ │ (SFTP) │ │ plugin │
└────┬────┘ └─────┬─────┘ └─────┬──────┘ └────┬─────┘
└─────────── sandbox runner ─────────┘ │
│ │
@ -193,20 +193,46 @@ Same backend, inverted player: **AI agents connect and compete**.
- **Spec home:** the protocol and SDK live on `logicsrc.com` for agent
developers.
### 5.3 Files (cl1.tech)
### 5.3 Files (SFTP)
A managed file workspace, surfaced in-BBS and as a standalone SFTP product on
`cl1.tech`.
Member file storage for bbs.profullstack.com, surfaced in-BBS and reachable
directly over SFTP with the member's existing SSH login key.
- **Model:** strictly **private, per-user** storage. Each account is chrooted to
its own directory tree with a disk quota.
- **Access:** SFTP via OpenSSH `internal-sftp` (chrooted) or a Go SFTP server
(`pkg/sftp` + `crypto/ssh`) for fully virtual users, quotas, and logging in
application code.
- **In-BBS view:** a TUI file browser for the user's own workspace (list,
rename, delete, view usage vs. quota).
- **Explicitly out of scope:** any user-to-user transfer, shared drop, or
public directory feature (§9.3, NG1).
- **Model:** two areas per server:
1. **Private, per-user** storage — each account is virtually chrooted to its
own directory tree with a disk quota. The default and primary surface.
2. **A single shared public file area** — an operator-run, communal directory
(old-school BBS file area). World-readable; write access is a tunable
(members-only by default). Operator-moderated; not encrypted or blind.
- **Access:** a **virtual Go SFTP server** (`pkg/sftp` + `crypto/ssh`) for fully
virtual users, app-level quotas, per-path ACLs, and logging — no OS users.
Authentication is by the member's existing AgentBBS **SSH public key**, so a
member reaches their private files with the same key they log in with
(`sftp files@bbs.profullstack.com`). `scp`/`rsync -e ssh` work over the same
endpoint.
- **In-BBS view:** a TUI file browser for the user's own workspace and the
shared area (list, rename, delete, up/download path, view usage vs. quota).
- **Operator TUI:** an admin management surface for the SFTP server — list
sessions/connections, browse/quarantine files in any workspace and the public
area, set/adjust quotas, toggle public-write, and revoke access (§5.3.1).
- **Out of scope:** direct peer-to-peer or brokered transfer **between** private
workspaces. Sharing happens only through the single moderated public area
(§9.3, NG1 as amended).
#### 5.3.1 Management TUI
A `bubbletea` admin console (gated to operators/admins), reachable both as an
in-BBS admin route and standalone. Panes:
- **Sessions:** live SFTP connections (user, key fingerprint, bytes, idle time),
with force-disconnect.
- **Workspaces:** per-user usage vs. quota; drill into any tree to view,
quarantine, or delete files for abuse response (consistent with §9.2 — the
operator can act on its own systems).
- **Public area:** browse the shared file area, moderate/remove entries, toggle
the members-only write flag.
- **Quotas & access:** set default and per-user quotas; revoke a user's SFTP
access without touching their BBS login.
### 5.4 AgentAd (marketplace)
@ -306,12 +332,18 @@ The platform does **not** adopt content-blind/zero-knowledge storage designed so
the operator cannot inspect hosted files. Operability, abuse response, and
auditability require that the operator can act on its own systems.
### 9.3 No user-to-user distribution
### 9.3 No peer-to-peer distribution (amended)
File workspaces are private and per-user. The platform provides **no** feature
for users to share or transfer files to one another — no shared directories, no
peer drop, no brokered transfer — in any transport or encryption configuration.
This is a hard product boundary, not a tunable.
Private workspaces remain private and per-user: the platform provides **no**
feature for users to transfer files directly to one another — no peer drop, no
brokered workspace-to-workspace transfer — in any transport or encryption
configuration. That direct path is a hard product boundary, not a tunable.
Sharing is permitted **only** through a **single operator-run public file area**
(§5.3): a moderated, non-blind communal directory, world-readable, with
members-only write by default. Because it is operator-run and inspectable
(§9.2), the operator can moderate and act on takedown notices (§9.4). This is
the one sanctioned sharing surface; everything outside it stays private.
### 9.4 Standard hosting compliance
@ -333,7 +365,7 @@ from the start.
| **M1 — Arcade** | doom-ascii + Freedoom/shareware, sandbox runner, guest play, member saves & leaderboards |
| **M2 — Admin** | plugin enable/disable, user moderation, session audit |
| **M3 — AgentGames** | game protocol, phase-1 catalog, agent auth, ladders, replays; spec published on logicsrc.com |
| **M4 — Files (cl1.tech)** | private per-user SFTP workspaces, quotas, in-BBS browser |
| **M4 — Files (SFTP)** | virtual Go SFTP server (key auth), private per-user workspaces + quotas, single shared public area, in-BBS file browser, operator management TUI |
| **M5 — AgentAd** | two-sided marketplace, buyer storefront, seller dashboard, creative review, revenue share |
| **M6 — Hardening & scale** | rate limits, fail2ban, metrics, Postgres migration path, web buyer dashboard |
@ -345,8 +377,10 @@ from the start.
websocket/API endpoint? (Affects how non-interactive agents authenticate.)
2. **Sandbox technology:** Docker/Podman per session vs. `systemd-run` transient
scopes — which fits the target VPS footprint best?
3. **Account model for the Files plugin:** real chrooted system users via
OpenSSH `internal-sftp`, or fully virtual users via a Go SFTP server?
3. ~~**Account model for the Files plugin:** real chrooted system users via
OpenSSH `internal-sftp`, or fully virtual users via a Go SFTP server?~~
**Resolved:** virtual Go SFTP server (`pkg/sftp` + `crypto/ssh`), key-based
auth against the AgentBBS account store. (§5.3)
4. **AgentAd inventory mix:** which surfaces ship first (interstitials vs. hub
banners vs. sponsored ladder slots)?
5. **Web footprint:** does the AgentAd buyer dashboard warrant a web app in v1,

95
docs/files.md Normal file
View file

@ -0,0 +1,95 @@
# Files (SFTP) — member storage
AgentBBS gives every verified member file storage over **SFTP**, reachable with
the same SSH key they log in with. It rides the existing `:22` listener as an
SSH *subsystem*, so there is no new port and no separate account:
```bash
# interactive
sftp files@bbs.profullstack.com
# one-shot copies (same endpoint, same key)
scp report.pdf files@bbs.profullstack.com:/me/
rsync -avz ./site/ -e ssh files@bbs.profullstack.com:/me/site/
```
The username (`files`) is conventional and ignored — **identity is your SSH
key** (one key = one account, like the rest of the BBS). `scp`/`rsync` work
because they tunnel over the same SSH transport.
## Two areas
When you connect you see a virtual root with two directories:
| Path | What it is | Access |
|---|---|---|
| `/me` | Your **private** per-user workspace | read/write, quota-limited |
| `/public` | The single **shared public file area** (old-school BBS file area) | world-read; members-only write by default |
There is **no** path from one member's `/me` to another's — the only sharing
surface is the one public area (PRD §9.3, amended). Both areas are confined: a
path that tries to escape its root (`../`, an absolute path, or a planted
symlink) is rejected.
## Quotas
Each private workspace has a byte quota (default **1 GiB**, set by
`AGENTBBS_FILES_QUOTA_MB`). Writes that would exceed it fail. Operators can set a
per-user override in the management TUI. The public area is operator-managed and
not metered per user.
## In-BBS browser
Inside the hub, the **Files** entry opens a TUI browser for your workspace and
the public area: navigate, view text files, make directories, rename, and
delete, with a live usage gauge. Actual transfers happen over SFTP/scp/rsync (a
PTY can't move file bytes).
## Operator management TUI
Operators (the `$AGENTBBS_ADMINS` allowlist) reach the SFTP management console
with:
```bash
ssh sftp@bbs.profullstack.com # aliases: sftpadmin@, filesadmin@
```
Panes (Tab to switch):
- **Sessions** — live SFTP connections (user, remote, rx/tx, idle); `x`
force-disconnects.
- **Workspaces** — every member's usage vs. quota; `Q` sets a per-user quota,
`x` revokes/restores SFTP access (the BBS login is unaffected).
- **Public area**`t` toggles members' write access; `x` removes an entry
(moderation).
Per PRD §9.2 the operator can inspect and act on hosted files; storage is not
content-blind.
## Configuration
| Var | Default | Meaning |
|---|---|---|
| `AGENTBBS_FILES` | `1` | enable the SFTP subsystem + Files plugin (`0` disables) |
| `AGENTBBS_FILES_QUOTA_MB` | `1024` | default per-user workspace quota (MB) |
| `AGENTBBS_DATA` | `./data` | storage lives under `<data>/files/{users,public}` |
## Implementation
- `internal/files` — a fully virtual Go SFTP server (`github.com/pkg/sftp` +
`crypto/ssh`); no OS users.
- `backend.go` — service, layout, quota/usage, live-session registry, operator
surface.
- `fs.go` — per-session virtual filesystem: `resolve()` is the single security
chokepoint (area confinement + symlink-escape guard) and the pkg/sftp
request handlers.
- `server.go` — the `wish.WithSubsystem("sftp", …)` handler: key auth → member
session → request server, with byte metering and force-disconnect.
- `tui.go` — the in-BBS member browser plugin.
- `admin.go` — the operator management TUI.
- Storage: `files_access` (per-user quota override + revoked flag) and
`files_settings` (e.g. public-write mode) in the shared SQLite store.
Security is covered by `internal/files/*_test.go`: path-traversal/confinement,
symlink-escape rejection, the public-write ACL, quota enforcement, and an
end-to-end run against a real SFTP client.