Skip to content

Workstream B/a: quota middleware + workspace lifecycle + egress templates#4

Merged
MikeBengtson merged 4 commits into
mainfrom
wsB/quota-lifecycle-egress
May 13, 2026
Merged

Workstream B/a: quota middleware + workspace lifecycle + egress templates#4
MikeBengtson merged 4 commits into
mainfrom
wsB/quota-lifecycle-egress

Conversation

@MikeBengtson
Copy link
Copy Markdown
Collaborator

Summary

Workstream B slice (a) of gemba-remote M4 — Productization.

  • gm-o9t8.4.1 Per-tenant token-bucket quota middleware (`internal/quota/`, `internal/server/quota_mw.go`). 429 + Retry-After on deny; passthrough when no limiter / no tenant context.
  • gm-o9t8.4.2 Workspace lifecycle API (`internal/server/workspaces_lifecycle.go`). POST/DELETE/restore/archive for `/api/v1/tenants/{tid}/workspaces`. Archive integrates vault key shredding + S3 upload (fake stub for tests) + per-phase audit events. New `EventWorkspaceArchived` audit Kind.
  • gm-o9t8.4.3 Egress policy templates (`internal/egress/templates.go`, `internal/server/egress_templates.go`). Named bundles: `github-only`, `pypi+npm`, `wide-open`, plus `default` (alias). Seeded at workspace-create via the new lifecycle handler.

Test plan

  • `go test ./internal/quota/... ./internal/server/... ./internal/egress/...` — 678 passed across 13 packages.
  • `go vet ./...` clean.
  • Quota: 6 bucket tests + 4 middleware tests covering allow/deny/refill/burst/independence/thread-safety/reconfig/Retry-After.
  • Lifecycle: 6 tests — create / template aliases / soft-delete+restore / archive happy / cross-tenant rejection 403 / unconfigured 503.
  • Egress: 8 tests — bundles, default alias, unknown fallback, case-insensitivity, TemplateNames.
  • Staleness merge against origin/main ran clean at start and pre-push.

Branched off local `main` at `684227c`.

🤖 Generated with Claude Code

MikeBengtson and others added 4 commits May 13, 2026 04:30
New `internal/quota/` package implements a thread-safe per-tenant
token-bucket Limiter (rate + burst). The `internal/server/quota_mw.go`
middleware consumes one token per request, reading the bearer-bound
tenant via tenant.FromContext (mirroring tenants_admin.go); on deny it
returns 429 with a Retry-After header and the standard httperr
"rate_limited" envelope.

Tests cover allow/deny/refill/burst (bucket_test.go) and middleware
integration with httptest including the 429 + Retry-After contract
(quota_mw_test.go).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds named egress bundles (github-only, pypi+npm, wide-open, default-
aliasing-github-only) in `internal/egress/templates.go`. Each bundle
carries a list of allowed FQDNs and a default network stance
(deny | allow). Unknown / empty names fall back to "default".

`internal/server/egress_templates.go` defines the EgressTemplateProvider
interface the future workspace-create handler consumes, plus a
defaultEgressTemplateProvider that materializes a bundle as
positive-priority egress.Rule rows (1000+i, descending) so seeded
template rules outrank the hard-coded baseline allow-list.

Tests cover template lookup, fallback to default, case-insensitivity,
and the canonical bundle set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ive)

Adds `internal/server/workspaces_lifecycle.go` with four endpoints
under /api/v1/tenants/{tid}/workspaces:

  POST   /                — create (seeds egress allowlist from the
                            request body's `egress_template`, defaulting
                            to "default" which aliases "github-only")
  DELETE /{wid}           — soft-delete (sets DeletedAt; 30-day window)
  POST   /{wid}:restore   — restore (rejected once retention expires)
  POST   /{wid}:archive   — vault-shred, S3 manifest upload, fs remove,
                            finalize ArchivedAt

Tenant resolution mirrors tenants_admin.go (bearer-bound id via
tenant.FromContext; path {tid} must match the bearer or be the
DefaultTenant). Soft-delete and restore audit via EventTenantUpdate
with an "event" payload discriminator; archive emits the new
EventWorkspaceArchived Kind (added to internal/server/audit/audit.go)
for each phase (vault_shred → s3_upload → fs_remove → finalize).

S3Uploader is an interface with a FakeS3Uploader test double; real
client wiring is out of scope. VaultDeleter narrows the local
secrets.Vault surface to List+Delete so the archive path can shred
keys without depending on the full vault.

Tests cover create / soft-delete / restore / archive happy paths and
the unauthorized tenant rejection (cross-tenant DELETE → 403).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves conflict in internal/server/router.go between this branch
(quotaLimiter, egressTemplates, workspaceLifecycle) and wsB-b's
already-merged additions (usageAggregator, orgGates, adminEmails,
adminSessionLister, adminAuditPubKey, adminAuditRecords). Both sides
are additive struct fields; union resolve preserves both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@MikeBengtson MikeBengtson merged commit 3d88612 into main May 13, 2026
3 of 6 checks passed
@MikeBengtson MikeBengtson deleted the wsB/quota-lifecycle-egress branch May 13, 2026 09:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant