mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-07 11:14:28 +02:00
Adds handler.RequireHumanActor — a chi middleware that 403s any
request carrying X-Actor-Source=task_token — and applies it to the
cloud-billing route group.
Why this matters (the bug it fixes):
The general Auth middleware (server/internal/middleware/auth.go)
turns every recognized bearer format into the same shape: a stamped
X-User-ID header. JWT cookie, mul_ PAT, AND mat_ task token all
produce a non-empty X-User-ID. That uniformity is correct for issue
/ comment / chat scopes — agents acting as their owner inside a
bounded (agent, task) job is exactly what mat_ is designed for.
It is NOT correct for account-level scopes:
* Reading balance / transactions / batches / topups list lets an
agent see its owner's wallet state without the owner approving
the query.
* Creating checkout / portal sessions can move money or leak
payment-method state. A compromised agent (prompt-injected, bad
MCP tool, escaped quote in scratch data) could spin up a checkout
bound to an attacker-controlled email or open a Billing Portal
session.
Before this commit, /api/cloud-billing/* sat outside the workspace
group with only the generic Auth chain, so an mat_ task token's
stamped X-User-ID was indistinguishable from an mul_ PAT's. The
proxy happily forwarded everything to multica-cloud, where the
upstream's owner-only contract was bypassed entirely.
The fix:
* RequireHumanActor middleware checks the SERVER-SET (not client-
settable) X-Actor-Source header. The Auth middleware deletes any
client-supplied value before stamping its own, so a non-empty
'task_token' value here is authoritative — a member-token caller
cannot forge it, and an mat_-token caller cannot strip it.
* Wired in via r.Use(handler.RequireHumanActor) on the
/api/cloud-billing route group. Applying it as middleware (not
inline in each handler) means a developer adding a new billing
endpoint cannot accidentally skip the gate.
* Deliberately denylist-shaped, not allowlist. Future actor kinds
added to auth.go (e.g. service-account tokens) get a conscious
decision-point at the gate rather than being pre-emptively shut
out by an over-broad rule today.
The Stripe webhook is untouched — it sits outside the entire Auth
group, so X-Actor-Source is never stamped on it (the auth middleware
doesn't run). The guard is irrelevant on that path.
Tests:
* TestRequireHumanActor_AllowsHumanRequest — empty actor source
passes through (JWT / mul_ PAT path).
* TestRequireHumanActor_BlocksTaskToken — 'task_token' value
triggers 403 and inner handler is not called.
* TestRequireHumanActor_IgnoresUnknownActorSource — pins the
denylist shape with explicit comment so the next person adding
an actor kind knows where to revisit.
* TestRequireHumanActor_AppliedViaChiRouterUse — small but
important: builds a real chi router with r.Use(...) and proves
the guard fires on every endpoint in the group, which is the
contract router.go relies on.
go build / go vet / go test ./... clean.