Files
member-console/scripts/agent-runner/CONTEXT.md
T
cgalo5758 5829091881 Track the agent runner and its method page
The contained runner that drove the 2026-09 security audit, the README
review and four rounds of design ideation lived only inside the ignored
notebook. It moves to scripts/agent-runner/: the prepare, tools, audit,
ideation and teardown scripts, the compose and container files, the
allowlist proxy and the prompt templates, with the paths that assumed
the notebook fixed and findings written to a caller-named directory. Run
outputs, transcripts and the round-specific sheet scripts stay behind.

docs/agent-runner.md states the method: the disposable git archive
copy and the fail-closed proxy, how a task is shaped, union rather than
intersection of findings across models, separate adjudication of every
finding against the source, and the evidence a finding must carry.
2026-09-19 19:47:15 -05:00

124 lines
5.6 KiB
Markdown

# member-console — system context for the auditor
Supplied because you cannot infer it from a slice of code. These are facts
about this system, with locations you can verify. Nothing here is a finding.
## What the application is
A Go web application for worker-cooperative membership: members buy plans,
which confer entitlements, which provision resources (federated wiki sites,
forum access, custom domains). Server-rendered `html/template` with HTMX;
PostgreSQL via sqlc/pgx; Valkey for sessions; Temporal for workflows;
Keycloak as the OIDC identity provider; Stripe for payments.
Module `git.coopcloud.tech/wiki-cafe/member-console`.
## Two surfaces, one role
- `internal/server/shell.go` defines the member and operator surfaces.
- **The only privileged role is `OperatorRole = "operator-member"`**,
a literal in `internal/server/operator_partials.go:33`. There is no admin
role yet.
- Roles originate in the OIDC ID token and are **stored in the session** at
login. `HasRole` reads that session copy, not a live token
(`internal/auth/auth.go:757-782`).
- There are only three non-test `HasRole` call sites:
`internal/server/server.go:723`, `:754`, and `internal/server/operator.go:165`.
## Request pipeline, in order
`internal/server/server.go:668-699` builds `preAuthStack`:
RequestID → Logging → Recovery → Timeout(32s) → MaxBodySize(1MB) →
SecureHeaders → CORS → CSRF → Compress. That wraps `routeAware`
(`:688`), which applies the authentication middleware **only to requests that
match a registered route pattern**.
Server timeouts: Read 4s, Write 8s, Idle 16s, MaxHeaderBytes 1MB.
## Authentication middleware
`internal/auth/auth.go:126+`. In order:
1. Public paths pass straight through: `/login`, `/callback`, `/logout`,
`/logout-callback`, `/register`, `/favicon.ico`, **plus every path passed
in as `selfAuthenticatedPaths`**.
2. Any path under `/static/` passes through.
3. Session must carry `authenticated`.
4. The session's person is looked up in the database **on every authenticated
request**; if the row is gone the session is ended and the request bounces
to `/login`. This is deliberate (a session can outlive its person after an
identity merge, purge, or demo-database reset).
## The exemption coupling — important and deliberate
One list, `csrfExemptPaths`, feeds **both** CSRF bypass and authentication
bypass:
- It is seeded with `/domains/ask` (`internal/server/server.go:404`), which is
unauthenticated by design: a TLS-terminating proxy calls it mid-handshake
and holds no session and no CSRF token.
- Each integration appends its own via `RouteMount.CSRFExemptPaths`
(`server.go:411`).
- It flows to `csrfConfig.Ignore` (`server.go:561-564`) **and** to
`authConfig.Middleware(csrfExemptPaths...)` (`server.go:688`).
So declaring a path CSRF-exempt also makes it unauthenticated. Current
declarations: Stripe `/webhooks/stripe`
(`internal/integrations/stripe/stripe.go:79,147`); Discourse `web.WebhookPath`
(`internal/integrations/discourse/discourse.go:300`); FedWiki declares none
(`internal/integrations/fedwiki/fedwiki.go:297`).
## Response headers
`internal/middleware/security.go` sets `Cache-Control: no-store`,
`X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`,
`Strict-Transport-Security: max-age=3600; includeSubDomains`,
`Referrer-Policy: no-referrer`, `X-XSS-Protection`, `Cross-Origin-Embedder-Policy:
require-corp`, `Cross-Origin-Opener-Policy: same-origin`,
`Cross-Origin-Resource-Policy: same-origin`, and this CSP:
```
default-src 'self'; script-src 'self' https://unpkg.com/htmx.org@*;
style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self';
object-src 'none'; frame-ancestors 'none'; form-action 'self'; base-uri 'self';
```
`upgrade-insecure-requests` is appended only when `env == "production"`.
Project intent, stated in its own docs, is a strict CSP with **no inline
scripts** and **self-hosted** front-end assets (see `NOTICE` and
`internal/embeds/static/`).
## CSRF
`internal/middleware/csrf.go` wraps a gorilla-style CSRF with a 32-byte secret
that must persist across restarts, per-path `Ignore` predicates, configurable
`TrustedOrigins`, and configurable cookie `Secure`/`HttpOnly`/`SameSite`.
Browser writes send the token in an `X-CSRF-Token` header from the page, because
native form POSTs were failing an Origin check under `Referrer-Policy: no-referrer`.
## Tenancy model
Person → organization → workspace. A person's session carries person, org, and
workspace ids (`auth.go:722-747`). Grants form an append-only ledger; what is
"currently delivering" lives on `pool_provisions.status`, not on `grants.status`.
Cross-tenant isolation is the property that matters: a member of org A must not
read or mutate anything belonging to org B.
## Trust boundaries
| Boundary | Direction | Authenticated by |
|---|---|---|
| Browser ↔ app | inbound | session cookie + CSRF token |
| Keycloak ↔ app | inbound identity | OIDC authorization code flow; ID token |
| Stripe ↔ app | inbound webhook | provider signature (no session, no CSRF) |
| Discourse ↔ app | inbound webhook | provider secret (no session, no CSRF) |
| TLS proxy ↔ app | inbound `/domains/ask` | nothing, by design |
| app → FedWiki farm | outbound | admin bearer token |
| app → Stripe API | outbound | restricted key from a file |
| app → Postgres / Valkey / Temporal | outbound | connection credentials |
## What is deliberately test-only
Everything under `test/` is fixtures. `test/mc-config.yaml` carries
`TEST-ONLY-*` secrets on purpose and `test/secrets/` is gitignored. Do not
report these as leaked credentials. They matter only if the same value or
pattern reaches a production code path.