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

5.6 KiB

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.