Skip to content

Latest commit

 

History

History
234 lines (185 loc) · 53.4 KB

TODO.md

File metadata and controls

234 lines (185 loc) · 53.4 KB

Overslash — TODO

Roadmap to public launch. Phases 1–4 are shipped (see STATUS.md); what remains is the commercial + trust surface around the product engine, plus the dashboard residuals that still ship visibly broken or missing.

Three blocks, in priority order:

  1. Launch Blockers — must ship before opening the public signup funnel.
  2. Launch +1 — polish within the first weeks of GA.
  3. Backlog — post-launch, not gating.

A PR can ladder up to a block by tagging its first line [launch], [launch+1], or [backlog]. When everything in §1 is checked, we cut GA.


1. Launch Blockers

1.1 Transactional email

No mailer exists today. Billing has no receipts and new accounts get no welcome / verification touch. Approvals and secret-requests are explicitly not email-driven — email is the wrong channel for real-time decisions (latency, deliverability, off-device), and the dashboard + webhook + oversla.sh link is the path of record.

  • Pick a provider and wire it (likely Resend — we already template a service for it). Config: EMAIL_PROVIDER, EMAIL_FROM, EMAIL_REPLY_TO; secret via the existing vault.
  • Templated emails — store templates in crates/overslash-core/templates/email/ with {var} interpolation matching the audit-description style.
  • Billing: receipt on invoice.payment_succeeded; dunning on invoice.payment_failed; subscription canceled / trial ending.
  • Welcome / first login for both root signups and corp-org JIT provisioning.
  • Webhook DLQ digest → daily digest to org admins listing webhook endpoints with terminal failures.
  • Per-user unsubscribe state for non-transactional (welcome) emails only — billing emails are exempt by policy.
  • (Optional, post-MVP) User-level opt-in email for newly remembered permissions — informational only, not a control surface.

1.2 Onboarding & trust domains

D12 keeps trust domains clean. Corp-org admins still need a way to onboard the first teammate before that teammate has logged in via the org's IdP. Slug squatting is intentionally not mitigated pre-launch — paid org creation is the natural gate; we'll deal with squatters reactively if any appear.

  • Corp-org invite path — email-gated against the org's org_idp_configs.allowed_email_domains. Invite resolves on first IdP login (binds the new identity to the invite's role). Does not bypass the IdP; only pre-authorizes the email.
  • Audit events on creator-admin add (POST /v1/orgs) and removal (DELETE /v1/account/memberships/{org_id} when the leaver is the original creator).
  • Login page on a corp subdomain renders a clear empty state when no IdP is configured + a "you've been invited, please log in via X" state when the visitor's email matches a pending invite.

1.3 Human-facing documentation site

SKILL.md covers agents. Humans (the people swiping the credit card) have nothing past www.overslash.com. The single biggest sales blocker after email.

  • [-] Docs site at docs.overslash.com (or /docs on the marketing site). MDX or VitePress; ship as static.
  • Concepts: identity hierarchy, permissions/approvals, secrets, services, groups, rate limits.
  • Quickstart: 10-minute "first authed call" against Resend or GitHub.
  • Per-template setup guides for the 9 shipped services (Eventbrite, GitHub, Gmail, Google Calendar, Google Drive, Resend, Slack, Stripe, X).
  • REST API reference auto-generated from the routes (consider serving /openapi.json from the API and rendering with Scalar/Redoc).
  • MCP setup guide (Claude Desktop, Cursor, Claude Code) — fold in dashboard/src/routes/docs/claude-code/ content.
  • Self-hosting guide (overslash web, OpenTofu module, single-org mode).

1.4 Operational readiness

Monitoring is deployed; paging and recovery procedures are not yet exercised.

  • Bind pagerduty_integration_key in infra/env/prod.tfvars (or a Slack channel via a custom notification channel) so P0 alerts actually page someone.
  • Public status page (Better Stack / Statuspage / Instatus) wired to the existing P0 uptime check + a manual override.
  • Master-key rotation runbook — documented procedure to rotate the AES-256-GCM master key with zero downtime (dual-key read, re-encrypt loop, drop old key). Run the drill end-to-end on dev.
  • Postgres PITR restore drill — document and execute a full restore-to-new-instance against the dev DB; record RTO/RPO observed.
  • Serve api.overslash.com over IPv6 — the LB's IPv6 address and its 443/80 forwarding rules are declared in infra/modules/api-lb/main.tf (api_lb_enable_ipv6 = true in prod tfvars, on the same proxies so the SSL policy and Cloud Armor apply). Not applied: run make tofu-apply ENV=prod (3 additions), then publish tofu output api_lb_ipv6 as AAAA for api and *.api at Namecheap per lb-hardening.md. Per-IP throttles already key v6 clients on their /64.
  • On-call runbook: how to roll back a Cloud Run revision, how to disable a webhook target, how to revoke a leaked API key, how to suspend an org.
  • Flip integrity_alert_enabled on in infra/env/dev.tfvars and then prod.tfvars once the exporter has written integrity_violations, then make tofu-plan/tofu-apply per env (runbook). Triage whatever the first sweep reports — the migration-133 Reveni leftovers should show as binding_foreign_user_vault.
  • Integrity sweep: pin ↔ template provider mismatch. The one stored-reference rule the sweep cannot express in SQL — the template's OAuth provider lives in YAML / layered DB templates. Needs an API-side sweep through template_resolve emitting the same invariant label.

1.5 Legal / compliance

  • security.txt + vuln disclosure policy page. RFC 9116 file served by the dashboard at https://app.overslash.com/.well-known/security.txt (dashboard/static/.well-known/; org subdomains redirect to it), pointing at SECURITY.md. scripts/check-security-txt.sh fails the daily Dependency audit a month before Expires. The marketing site (www.overslash.com) does not serve one yet.
  • (later) DPA template + signing flow (DocuSign / PandaDoc / countersigned PDF). Procurement asks for this on every B2B deal.
  • Subprocessor list page on the marketing site (Cloud Run, Cloud SQL, Stripe, Cloudflare, Resend, configured IdPs). On www.overslash.com/privacy
  • (later) GDPR request handling — document the manual process for data-export and hard-delete requests (intake → DPO ack → manual SQL → audit row). Automated endpoints are a post-launch backlog item; volume expected to be near zero at GA.

1.6 CASA readiness

The annual security assessment Google requires of apps holding restricted OAuth scopes — we ship gmail.*, drive and keep templates, so the system OAuth client needs it. Full assessment in docs/compliance/casa/; the gap list is gap-assessment.md. 2 of 55 requirements are gaps today.

P0 — live vulnerabilities, not compliance items. Decide on a security timeline.

  • SSRF on the action-execution path — services/http_caller.rs now builds its own pinned client from the URL via ssrf_guard::outbound_client instead of accepting state.http_client, so all five action-execution call sites are covered by construction; webhook_dispatcher::deliver does the same per attempt. Tests in tests/ssrf_guard.rs. (CASA 5.1.5, 7.3.1)
  • Decide the default Everyone → admin on http grant — org bootstrap (crates/overslash-db/src/repos/org_bootstrap.rs:143-153) grants it to preserve the old allow_raw_http=true default, and users skip Layer 2, so raw HTTP is available to every member on day one. Removing it makes raw HTTP a deliberate grant and lets dast-readiness.md tell a lab the default-configured product has no unbounded egress. Behaviour change for new orgs — a human call, not an agent's.
  • SSRF on OIDC issuer discovery — services/oidc_discovery.rs no longer takes state.http_client or runs its own string check: every hop, including each redirect, goes through ssrf_guard::outbound_client_validated (resolve, check every address, pin). https is required, with plain http accepted only to loopback, and that is re-checked after resolution on every hop, so http to an allow-listed private range is still refused. Any failure after the input check returns a generic OIDC discovery failed 400, and the detail is logged server-side, so neither handler echoes an upstream error or body any more. Tests in tests/ssrf_guard.rs. (CASA 5.1.5)
  • Cross-tenant API-key minting — POST /v1/api-keys no longer takes an org_id at all: it uses AdminAcl + the OrgScope extractor, so the org is the presented credential's currently-active org. identity_id resolves through that scope, and migration 121 adds a composite api_keys (org_id, identity_id) -> identities (org_id, id) FK. The unauthenticated bootstrap branch — the only reason a body org_id existed — is gone; POST /v1/orgs returns the first admin key instead. (CASA 3.1.2, 3.1.4 — both now pass)

P1 — hard CASA fails.

  • Secure on every auth cookie, plus __Host-/__Secure- prefixes — one builder in crates/overslash-api/src/cookies.rs; the unprefixed oss_session is no longer read and is cleared on login/logout. One-time logout of every user on deploy. (2.3.1 — now pass)
  • Server-side sessions — user_sessions (migration 130) with a jti claim checked per request by middleware/session_gate.rs, before any extractor or rate limiter reads the cookie, so a revoked cookie is anonymous and never spends its former owner's bucket. Keeps the 7-day UX. Logout, a new sign-in, an identity change (IdP-reported email change, admin email rewrite), admin removal and archive all revoke. Account → Sessions lists live sign-ins with End session and Sign out all other sessions. One primary-key lookup per request, cached in Valkey (30 s live, 1 h dead, DEL on revoke); without REDIS_URL it reads Postgres every time rather than cache per-replica. Session JWTs without a jti are accepted only under 24 h, which refuses every pre-change cookie once. Tests in tests/user_sessions.rs. (2.2.1, 2.2.2, 2.2.3)
  • Webhooks are HTTPS-only — registration parses the URL into HttpsUrl and refuses http:// with a 400; the dispatcher refuses to dial one and records the failed delivery; pre-existing http:// subscriptions were disabled by migration 124 (disabled_reason = needs_https, shown in the dashboard's org Webhooks card). Plain http survives only to loopback when OVERSLASH_SSRF_ALLOWED_CIDRS covers it (services/https_policy.rs, shared with OIDC discovery). (7.1.1 — now pass)
  • Webhook endpoint-ownership handshake — a new subscription is pending_verification until its endpoint echoes a signed webhook.verification challenge (2xx, bare or {"challenge"}, 10s); registration runs it inline and POST /v1/webhooks/{id}/verify re-runs it. Events for a pending subscription are recorded as held deliveries and released on verification; the handshake goes through ssrf_guard and the HTTPS rule like every delivery. Migration 125 grandfathered every existing subscription (verified, grandfathered = true). Dashboard org Webhooks card shows the state and a Verify button. Tests in tests/webhook_verification.rs. (7.1.2 — now pass)
  • Webhook signed timestamps — every attempt carries X-Overslash-Timestamp and X-Overslash-Signature-V1: v1=<hmac("<ts>.<body>")>, re-signed per attempt so a retry gets a fresh timestamp; consumers are told to reject a missing timestamp and anything outside ±5 min (SPEC §webhooks, sdk verifyWebhook). Not breaking: the legacy body-only X-Overslash-Signature: sha256=<hex> is still sent unchanged, so existing verifiers keep working — its removal is tracked in TECH_DEBT.md. Tests in tests/webhook_signing.rs. (7.2.3 — now pass)
  • Stop sending the legacy X-Overslash-Signature — the dispatcher sends only X-Overslash-Timestamp + X-Overslash-Signature-V1; verifyWebhookSignature removed from @overslash/sdk; SPEC, SDK README and the dashboard hint describe v1 only. Breaking for receivers still reading the body-only header (D125). Tests assert the header is absent (tests/webhook_signing.rs). (7.2.3 — compensating control no longer needed)
  • overslash-docs webhook guide: v1 verification section — the guide (separate overslash-docs repo) must document X-Overslash-Timestamp + X-Overslash-Signature-V1 over "<ts>.<raw body>", ±5 min tolerance, constant-time compare, verifyWebhook, and that X-Overslash-Signature is no longer sent.
  • Security-headers layer on the API + a headers block in dashboard/vercel.json. The API's outermost middleware (middleware/security_headers.rs) sets HSTS (1y, includeSubDomains), nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer, a Permissions-Policy and a CSP on every response, each only if the handler did not: default-src 'none' for JSON/SSE, a style-only policy for the HTML pages the API renders, and a per-page script hash for the two that run one. The dashboard's CSP is enforced through SvelteKit's kit.csp (a per-response nonce on Vercel, a hash in the static build), so script-src carries no 'unsafe-inline'; it includes frame-ancestors 'none'. The rest of the baseline is in vercel.json, which sets no CSP itself so the two never collide, and which vite preview also serves so e2e runs under it (scripts/screenshot-security-headers.mjs fails on any violation).
  • Dependency vulnerability scanning in CI — .github/workflows/deps-audit.yml runs cargo-deny (deny.toml), npm audit --audit-level=high (dashboard, sdk) and OSV (osv-scanner.toml) over every lockfile. ci.yml calls it on dependency changes, gated by ci-ok, and a daily schedule on dev + master files a needs-triage issue on failure. The four fixable advisories are cleared (h2, rustls, crossbeam-epoch, event-listener), plus OSV's devalue and three yanked crates. rsa and paste are time-boxed exceptions with review dates enforced by scripts/check-deny-reviews.sh. Policy: dependency-vulnerability-policy.md; D101. (6.1.1 — now pass)
  • Require TLS on outbound calls — services/outbound_tls.rs. The action transport refuses plain http on every hop (redirects included), checked against the address the SSRF guard pinned, and so does the MCP caller; Mode A is refused with or without secrets. The same rule runs as a string check at the boundary — instance url on create/update, an org layer's instance_defaults.url, a template's mcp.url (endpoint_requires_https in the validation report) — and again at resolve, so an endpoint stored before this fails with a 400 before any approval is filed. The only way past it is OVERSLASH_SSRF_ALLOWED_CIDRS: plain http is accepted to an address inside an operator-allowed range, which is how the suite's loopback fakes still work. The dashboard flags an http:// endpoint next to every base-URL input. Tests in tests/outbound_tls.rs. The Cloud SQL REQUIRE_SSL and LB SSL-policy halves of 4.1.1 are in code, pending apply (§1.6 GCP items). (4.1.1 — outbound)
  • TLS on the MCP OAuth upstream hops — routes/oauth_upstream.rs. Resource metadata, AS metadata, dynamic client registration and the token exchange (which sends the authorization code + PKCE verifier and gets back the access and refresh tokens) all go through outbound_tls::check_resolved against the SSRF-pinned address, on clients that never follow a redirect. The token_endpoint and authorization_endpoint the AS metadata advertises are string-checked before anything is registered or a flow minted, so initiate fails with a 400 naming the field; the callback re-checks a stored token_endpoint before consuming the flow. Same OVERSLASH_SSRF_ALLOWED_CIDRS escape hatch. Tests in tests/oauth_upstream_tls.rs. (Webhook delivery is HTTPS-only since #664, via services/https_policy.rs.) (4.1.1)
  • redirect_uri scheme allowlist + loopback-only rule + array cap on Dynamic Client Registration. https, loopback-only http, and reverse-DNS or named app schemes (cursor, vscode, windsurf); at most 10 URIs of 2048 bytes each (services/oauth_redirect_uri.rs). (3.2.2)
  • Per-IP registration cap on POST /oauth/register — middleware/ingress_rate_limit.rs, 20 per address per hour by default (OAUTH_REGISTER_RATE_LIMIT / _WINDOW_SECS), checked before the handshake bucket so a refused registration does not spend it. Keyed on ClientIp, so a forged X-Forwarded-For behind a trusted proxy buys no fresh bucket. Tests in tests/ingress_rate_limits.rs. (3.2.2 residual — closed; 3.1.5 anti-automation half)
  • SECURITY.md with a vulnerability disclosure policy and a security contact (security@overslash.com): response and fix SLAs matching the dependency policy, 90-day coordinated disclosure, scope, safe harbor. Pairs with the security.txt item in §1.5.

P2 — will be raised by a lab.

  • Admin step-up: admin-class mutations need a fresh IdP sign-in — user_sessions records how each session was signed in (migration 137: auth_method, auth_provider, authenticated_at, the ID token's amr/acr; a switch-org re-scope keeps them). AdminAcl and InstanceAdminAuth refuse a non-GET from a dashboard session in a shared org unless it is an IdP sign-in, through a provider the org signs in with, from the last ADMIN_STEP_UP_MAX_AGE_SECS (1 h): 403 step_up_required (services/admin_step_up.rs). That closes the magic-link path, where a link lands in the personal org and is re-scoped into an org it administers. The dashboard's Confirm it's you prompt (StepUpModal.svelte) re-runs the org's IdP login with OIDC max_age=0, and Account → Sessions shows each session's sign-in method and amr. MFA itself is delegated to the IdP and not enforced on amr (D136). A deployment with no IdP at all accepts a fresh magic link instead. Found and fixed on the way: /v1/org-idp-configs mutations had no admin check, so an agent key could add an org IdP; they now take AdminAcl. Tests in tests/admin_step_up.rs. In code, pending apply: infra/ pins ADMIN_STEP_UP_MAX_AGE_SECS (the app already defaults to the same value). Operator step still open: apply, verify, and capture the IdP-enforcement evidence per admin-step-up.md. (3.3.1 — now statement; 2.4.1 — statement, Low)
  • Step-up on secrets/reveal — reveal is session-cookie-only and audited but not freshness-gated, because it is a member's own read rather than an admin action. A lab probing 2.4.1 will try it. admin_step_up::require with a Target::Org and a non-method guard would cover it, at the price of a prompt for every member. (2.4.1 residual)
  • Boot interlocks — config/boot_policy.rs, enforced in load_config, over a parsed DeploymentEnv (OVERSLASH_ENV; the container image defaults it to prod). DEV_AUTH that is not a recognised boolean stops the boot everywhere, and DEV_AUTH on with OVERSLASH_ENV=prod stops it too. Weak SECRETS_ENCRYPTION_KEY / SIGNING_KEY (placeholder, one repeated byte, < 32 bytes, < 8 distinct bytes) stop the boot outside a local checkout and warn on one; .env.example ships no key values. A debug/trace RUST_LOG is clamped to info in production with a warning, and prod.tfvars now says info (not auto-applied — the clamp holds either way). /health and /ready drop db_error; the error is logged instead. (6.2.1, 1.2.1 — both now pass)
  • Rate limiting beyond osk_ keys — middleware/rate_limit.rs now charges whoever the extractors authenticate: a dashboard session against its own per-identity bucket (sized from the user budget, so a runaway agent cannot lock its owner out of the dashboard), an MCP access token against its owner's bucket plus the agent's identity cap, exactly like an agent key. The /oauth/* + /.well-known/oauth-* + /mcp subrouter has its own layer (middleware/ingress_rate_limit.rs, mounted via overslash_api::mcp_oauth_routes so the test harness runs it too): per client IP on the handshake (120/60s), per MCP client on /mcp (600/60s), falling back to the IP bucket for an anonymous /mcp probe. Instance-wide env knobs, 0 disables, a typo stops the boot; SPEC §13, D108. No dashboard surface: the new limits are instance settings with no org to hang them on, and there is no org rate-limit page. Tests in tests/ingress_rate_limits.rs. (1.1.1, 3.1.5)
  • Secret Manager Data Access audit logs + a log sink with locked retention + a documented secrets access-control policy. infra/modules/audit-logging/ (Data Access on Secret Manager, Cloud SQL, Cloud Run; every Cloud Audit Log sunk to overslash-<env>-audit, 400 days, locked in prod; [P1] Unexpected Secret Access alert), policy in secrets-access-policy.md; D112. Operator step still open: apply dev, verify, then prod unlocked, then lock — infra/README.md "Audit logging". 6.7.1 stays gap until then. (6.7.1)
  • Subdomain inventory + dangling-record check — subdomain-inventory.md: every record in overslash.com and oversla.sh (36 rows; the "ten hostnames" count was wrong — dev, docs, mail, acme do not resolve and are pinned absent), with owner and claim model per provider. scripts/check-subdomains.sh fails the daily Dependency audit on a record that differs from its row, a dangling CNAME/MX, a web host without a valid certificate for its own name (unclaimed target), any resolving name not in the table (random-label probe under every parent, common labels, Certificate Transparency), or a registration within 14 days of expiry (warning from 60). The dev *.api.dev wildcard CNAME is an accepted risk (only a Search Console owner can map a name under it; reasoning in the inventory). (6.4.1)
  • DNS hardening (optional, reasoning in the inventory): enable DNSSEC at Namecheap; CAA for Let's Encrypt + Google Trust Services; later, move both zones into Terraform via the existing infra/modules/dns/ (Cloud DNS) rather than Cloudflare.
  • Infra hardening: deletion_protection and ssl_mode on Cloud SQL — done in infra/modules/cloud-sql/main.tf (Terraform and API-level deletion protection, ENCRYPTED_ONLY), pending apply per cloud-sql-hardening.md; an explicit google_compute_ssl_policy and Cloud Armor on the LB — applied, re-measured 2026-10-22 (lb-hardening.md); INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER — applied in prod (*.run.app 404, prod plan clean, 2026-10-22); dev stays all because it has no LB and its hosts are domain mappings; least-privilege IAM in place of project-wide secretmanager.secretAccessor and cloudsql.admin — applied 2026-10-23 on both projects, verified per least-privilege-iam.md (still to capture: the Policy Troubleshooter checks, and a dev db-start/db-stop run after the apply — every scheduler run so far predates it): one service account per workload; the API's secretAccessor is conditioned on overslash-<env>-* minus an operator-only list; the shortener, overfwd and exporter are granted per secret; the scheduler gets a two-permission custom role on its one instance, only where night shutdown runs. Follow-ups left open: Cloud Build's project-wide iam.serviceAccountUser and the scheduler's project-wide run.invoker (CASA 3.1.1); Memorystore auth_enabled + transit_encryption_mode — done in infra/modules/memorystore/main.tf, pending apply per memorystore-auth-tls.md.

P2 — raised by Google's own Recommender. Measured on both overslash and overslash-dev; see gcp-posture.md. Remediating these closes them in Google's console, which is itself the evidence artifact.

  • P2 REQUIRE_SSL — production overslash-prod-db runs sslMode=ALLOW_UNENCRYPTED_AND_ENCRYPTED, behind a HIGH-severity insight. ssl_mode = "ENCRYPTED_ONLY" is now set in infra/modules/cloud-sql/main.tf, so both environments get it. Every client was checked first: the API (and its in-process migrations) and the metrics-exporter job use the /cloudsql Auth Proxy volume, BigQuery federation uses a cloud_sql connection, humans use bin/db-shell.sh — all TLS, all admitted under any mode. Not applied — the Recommender closes it only after a human runs cloud-sql-hardening.md. (CASA 4.1.1)
  • P2 unused roles/editor — on production, two GCP-default service accounts held Editor: the default Compute Engine SA (0 permissions used in 90 days → remove) and the cloudservices agent (12 used, all the VPC connector's instance group → roles/compute.editor). Dev had only the first. Checked first that nothing depends on the default Compute SA: every Cloud Run service and job, Cloud Build trigger and Scheduler job names its own account; there are no instances, keys or actAs grants; the only build ever run as it was the manual submit of 2026-04-29. Applied in prod 2026-10-27 (re-measured: only compute.editor on the cloudservices agent remains): infra/modules/project-guardrails/ removes both with google_project_iam_member_remove, so a re-added binding is drift that the next apply removes, and grants compute.editor first where use_private_vpc. Dev's binding was removed by hand on 2026-10-27 (overslash-dev-api /health still 200). Both projects now sit in the overspiral.com org, whose inherited policies already enforce iam.automaticIamGrantsForDefaultServiceAccounts. The module pins it at project level and adds iam.managed.preventPrivilegedBasicRolesForDefaultServiceAccounts, so the grants cannot come back by creation or by a later grant. Verify per project-guardrails.md; the Recommender closing both P2s is the remaining evidence. (CASA 3.1.1)
  • P3/P4 Cloud SQL policies + auditing — no instance or user password policy, and no database auditing. Added log_connections, log_disconnections, cloudsql.enable_pgaudit, pgaudit.log = ddl,role and cloudsql.pgaudit_mask_literals (so the API's boot-time ALTER ROLE bi … PASSWORD is not logged in clear) as database flags; the one-off CREATE EXTENSION pgaudit is in the runbook. pgAudit output lands in Data Access logs, so it is only readable once the audit-config item below is done. Password policies declined, with the reasoning in gcp-posture.md. Not applied yet — cloud-sql-hardening.md. (CASA 1.1.1, 6.7.1)
  • Audit logging is entirely off — the project IAM policy returns auditConfigs: NONE, so Secret Manager, Cloud SQL and Cloud Run Data Access logs are not written. google_project_iam_audit_config for all three, a sink to a locked bucket and a log-based alert on AccessSecretVersion are in infra/modules/audit-logging/. Live until applied — same operator step as the 6.7.1 item above. (CASA 6.7.1)
  • _Default log bucket is 30 days and unlocked — no export sink exists. Everything an assessor wants to see is in the bucket that is neither retained nor tamper-evident. Every Cloud Audit Log is now also sunk to overslash-<env>-audit (400 days, locked in prod). _Default itself stays 30 days on purpose: it holds application logs, and the app's own audit trail is the Postgres audit_log. Same operator step. (CASA 6.7.1)
  • <project>_cloudbuild bucket — auto-created, not in Terraform, holds repository source archives with uniform bucket-level access off and public-access prevention merely inherited. Measured on both projects: US multi-region, one archive each from a manual gcloud builds submit on 2026-04-29, and nothing written since. The archive carried a local-only .claude/settings.local.json, because gcloud honours .gitignore but not .git/info/exclude. None of the three triggers uses the bucket (they are GitHub-connected and clone from GitHub), so there is no trigger config to move. Applied 2026-10-23, both projects. infra/modules/cloud-build-source/ adopts overslash_cloudbuild and overslash-dev_cloudbuild through an import block in infra/main.tf, with uniform_bucket_level_access = true, public_access_prevention = "enforced" and a 30-day delete lifecycle rule. A root .gcloudignore stops a future manual submit from uploading local-only files. Re-measured after the apply: UBLA true, prevention enforced, the 30-day rule present, an empty acl, and no live objects (the archive is soft-deleted, so it is gone for good after 7 days). A targeted plan shows no changes on either workspace. Runbook: cloud-build-bucket.md. (CASA 3.1.1)
  • Enable container image vulnerability scanning (containerscanning.googleapis.com); none of Container Scanning, Container Analysis, Binary Authorization or Security Command Center is enabled. Pairs with the CI-side 6.1.1 work.
  • Secret Manager: 16 secrets, none with rotation, next_rotation_time or expire_time. Nothing schedules or alerts on rotation age. (CASA 6.7.1)
  • Memorystore has neither AUTH nor transit encryption — overslash-prod-valkey confirmed live: authEnabled unset, transitEncryptionMode: DISABLED, BASIC tier. In code, pending apply: infra/modules/memorystore/main.tf sets auth_enabled = true and transit_encryption_mode = "SERVER_AUTHENTICATION", and publishes the AUTH string and server CA to Secret Manager (overslash-<env>-valkey-auth, -valkey-server-ca), which reach the API (REDIS_PASSWORD, REDIS_CA_CERT) and oversla-sh (VALKEY_*) by secret_key_ref. Both clients connect rediss:// and verify the server against only that CA (services/valkey.rs, oversla-sh/src/storage.rs). Operator step still open: TLS forces a replacement of the instance (downtime, empty keyspace, oversla.sh links lost unless exported) — pre-flight, apply and verify per memorystore-auth-tls.md. (CASA 6.7.1)
  • Production has 0 uptime checks and no [P0] API Down alert — 8 alert policies were deployed (10 on 2026-10-27), which was exactly 12 declared minus 3 disabled in tfvars minus the API-down policy count-gated on api_domain != "". The only deployed P0 is API High 5xx Rate, which cannot fire when the service returns nothing at all. Root cause: infra/main.tf fed the gate var.domain, which prod leaves empty because the GCLB serves api_host_suffix instead of a domain mapping. It now falls back to api_host_suffix when the LB is on, which creates overslash-prod-api-health on api.overslash.com/health and [P0] API Down (PagerDuty + email). The 3 disabled gates (api_latency, oauth_refresh, upstream_error) are on in both envs: their descriptors were measured present on 2026-10-27. The OAuth, upstream and already-deployed webhook ratios clamped a per-second denominator to 1, so at prod volume they could never fire. They now use a 0.001 floor + a minimum-sample guard, like the latency alert. Better Stack (status.overslash.com) still probes the same URL, but it is console-managed and does not page. Applied 2026-10-27, both envs, verified per uptime-and-alerts.md: prod has 14 policies ([P0] API Down on PagerDuty + email) and overslash-prod-api-health passing from all 6 checker regions. Found on the way: dev's /health 500'd every night, because the HTTP liveness probe on /health (1 s default timeout against a 2 s DB check) killed the instance about 90 s after the Cloud SQL stop, and no replacement could boot. Cloud Run probes are now TCP, and /health returns 503 when Postgres is down (D141), so the prod page covers the database too. That part is in code, pending a Cloud Run apply + an API deploy.
  • No SSL policy and no Cloud Armor on the production LB — both gcloud compute ssl-policies list and security-policies list are empty. TLS 1.2/1.3-only is currently true but unpinned and uncitable. infra/modules/api-lb/main.tf now declares overslash-prod-api-ssl-policy (TLS 1.2 minimum, RESTRICTED) on the HTTPS proxy and overslash-prod-api-armor on the backend service — default allow, OWASP CRS rule sets in preview (logged, never blocked) until tuned. Not applied — in code, pending a human run of lb-hardening.md, which also lists the evidence commands. (CASA 4.1.1)
  • Set the org-policy baseline that would make the above guardrails rather than conventions. Applied 2026-10-27 in both projects. The overspiral.com org now enforces a baseline at org level (measured table in gcp-posture.md), and infra/modules/project-guardrails/ (project-guardrails.md) pins the Overslash-specific ones per project: sql.managed.restrictPublicIp + restrictAuthorizedNetworks (enforced in prod, explicitly off on dev's public-IP database), storage.publicAccessPrevention and iam.disableAuditLoggingExemption (both projects). iam.managed.disableServiceAccountKeyCreation is inherited from the org, and dev's hand-set opt-out for the Directory-sync key is adopted into Terraform. Org-wide policies belong to the org repo, not here.
  • run.allowedIngress — unset (allow all) on both projects, measured 2026-10-27. It applies project-wide, so it can only be set to internal-and-cloud-load-balancing once overfwd and the shortener stop running with INGRESS_TRAFFIC_ALL (prod API is already LB-only). (CASA 6.2.1)
  • Keyring::test() is compiled only under cfg(test) or overslash-core's test-support feature, which only overslash-api's [dev-dependencies] enable — the hardcoded [0xAB; 32] key is no longer in the production library.
  • Trusted-proxy configuration for X-Forwarded-For: services/client_ip.rs. ClientIp now takes the rightmost untrusted address instead of the leftmost header value, trusting hops by position (OVERSLASH_TRUSTED_PROXY_HOPS), by range (OVERSLASH_TRUSTED_PROXIES), and the Vercel rewrite hop by a shared secret its Routing Middleware stamps (OVERSLASH_TRUSTED_PROXY_SECRET, dashboard/middleware.ts). With nothing configured, the socket peer is the client. Parsed at boot, and a malformed value refuses to start. The two handlers that parsed XFF inline (/oauth/register, MCP upstream initiate) go through the extractor too. prod/dev values and the enable/rotate/verify steps are in infra/README.md. Tests in tests/client_ip.rs. Operator step still open: set the secret in GSM and Vercel per env, then flip enable_trusted_proxy_secret.
  • Cache-Control: no-store on secrets/reveal and other sensitive authenticated responses — the default on every API response that does not set its own (/icons/* keeps public, max-age=86400; streamed and deferred-download passthrough keeps the upstream's).
  • ci-ok is a real gate — .github/workflows/ci.yml. It used to accept cancelled as success, and its paths-filter left out services/**, scripts/**, .githooks/** and the other workflows, so a PR touching only those skipped every substantive job and the one required check went green. Now failure and cancelled fail it, and so does a job that was skipped while its paths filter said the PR needed it; a job missing from the needed-map counts as always required. The rust filter adds services/**, assets/**, SKILL.md, tests/mock-target/**, scripts/**, Makefile, .github/actions/** and codecov.yml. A new tooling filter and job (actionlint over every workflow, plus bash/python/JSON parses of hooks, scripts and release-please config) covers .github/**, .githooks/**, bin/**, docker/** and the release-please files. Only prose, the generated SCHEMA.sql, ignore files and agent/editor config sit in no filter, and no CI job reads any of them.
  • CODEOWNERS + a documented required-review count, so four-eyes is evidenceable from the repo.

P3 — document rather than fix. CMEK; SBOM, artifact signing and build provenance (--provenance=false in Cloud Build); db-f1-micro sizing; an org-policy baseline; the three production alert policies disabled in infra/env/prod.tfvars.

Assessment logistics.

  • Close the dev-environment deltas (public Cloud SQL IP with no authorized_networks; DEV_AUTH on) or stand up a dedicated scan environment. See dast-readiness.md.
  • Write the SSRF scoping memo before engaging a lab — the product looks exactly like SSRF to a DAST scanner, and the argument has to be supplied during scoping, not after the report.
  • Run the ADA Burp Audit Scan Configuration ourselves against the scan target and fix what it finds. At AL1 this is also the deliverable.
  • Assemble the evidence pack — evidence-index.md.
  • Engage an authorized lab.

2. Launch +1 (first weeks after GA)

2.1 Dashboard residuals

  • IdP config edit UI on /org — backend PUT /v1/org-idp-configs/{id} already supports it (TECH_DEBT.md §3).
  • Notification bell dropdown in the top bar (review card 504a7). Dashboard-side sibling of the agent inbox — the same "what's waiting on me?" question the overslash get_events action answers; reuse services::inbox classification rather than inventing a second one.
  • Archived sub-agents — list + restore button on /agents, plus per-org cleanup config form (subagent_idle_timeout_secs, subagent_archive_retention_days).
  • Per-agent permission management — rules, scopes, "Allow & Remember" review/edit on the agent detail page.
  • /account profile editing — name + avatar editable.
  • Org webhook management UI — list, create, rotate signing secret, disable.
  • Toggle Switch component (ToggleSwitch.svelte) adopted everywhere replacing checkboxes (review card 2e268).
  • OAuth Connections UX (review card c2575): stop creating phantom Identity Provider + UUID connection when an admin adds a Google OAuth Client ID; reuse connections across services sharing the same provider; show provider email instead of UUIDs; support incremental scopes auth.
  • Services view fixes (review card 73d90): show username/email for service owners, fix /users/{name} 404, correct the overslash meta-service "Needs Setup" copy, group pills as a column, services connectable to groups from the detail view.

2.2 Templates & coverage

  • Ship 11 more service templates to hit top 20 (priority order: Notion, Linear, Jira, Asana, HubSpot, Salesforce, Airtable, Discord, PagerDuty, Zendesk, Intercom).
  • Complete the OpenAPI bulk import UX at /services/templates/import — currently scaffolded; needs overlay-default suggestions and a diff preview before save.
  • User-to-org template sharing — propose / approve / deny flow (review card 7e5ee).
  • Declare a credential probe on every template that can have one. x-overslash-test (D83) is on 20 of the 23. The three without one are deliberate: deepwiki (every tool needs a repo name, and it authenticates with nothing), overslash itself (runtime: platform), and the hidden fixture test_email. Any template added from here should declare one, and shipped_test_actions_resolve_to_read_actions holds a floor so the practice cannot quietly lapse.
  • Hard pins on instance_defaults — a layer default is a preset an instance may override (D36, D38). Add an opt-in "not instance-changeable" flag so an org layer can mandate a value: the instance form renders it read-only and instance_config::validate_config rejects a key the layer has hard-pinned. Deferred deliberately — the preset is the useful case for per-instance values like a mailbox username, and a ceiling only matters once a layer wants to mandate an org-wide constant.

2.3 API surface gaps

  • Approval visibility scoping — GET /v1/approvals?scope=actionable vs ?scope=mine (Phase 3 carry-over).
  • Webhook payload: include gap_identity and can_be_handled_by on approval events (Phase 3 carry-over).
  • Live Map follow-ups (D58, dev-gated behind OVERSLASH_LIVE_MAP):
    • Structural agent→service edges from permission rules. Today they only appear once traffic reveals them, because GET /v1/permissions is per-identity and deriving them up front would cost one request per agent.
    • Resolve an approval from the map. The amber state is real (it comes off approval.pending); the design's click-a-packet Allow/Deny popover was dropped rather than built against a half-modelled in-flight approval.
    • The force layout's repulsion pass is O(n²) over every structural node, every frame — inherited from the design prototype. Fine for a few hundred nodes, and the reason the map is dev-gated is not this, but it is the first thing to fix if it ever ships wider. A spatial grid is the usual answer.
    • Decide whether activity can ever be on by default. It is the only topic whose volume scales with the gateway's hot path — one durable events row per call — so ungating it means answering that first.
  • A setup page that handles every slot at once. A template with two unbound per-instance credential slots gets two setup links today (D83), handed over in sequence; the page names the outstanding siblings but cannot link to them, because each link is a separate capability. No shipped template declares two, which is why this is a documented limit rather than a blocker.
  • Add a Deny button to the setup page. The provide page's Deny now records the refusal (secret_requests.declined_at) and ends a waiting URL-mode elicitation; /services/setup has no Deny at all, so a refused setup link stays pending until it expires.
  • Let an agent verify its own service. POST /v1/services/{id}/activate is owner-or-admin, and an agent is deliberately not an ancestor of its own owner-user, so the agent that created an instance cannot probe it — which is exactly why D86's gate defaults off for a create with no human in it. Exposing it as a platform action means synthesising an AuthContext and re-entering call_action_impl from inside a call; worth doing only if the re-entrancy (permission chain, audit rows) can be shown not to double.
  • Take the credential value on the wizard's configure step, not only on its failure step. D86 put a SecretValueField behind "Edit and retry" because a named-but-empty vault secret is otherwise a dead end under the gate. The first pass through the form still only binds a name (SecretNamePicker, whose hint tells you to go to the Secrets page), so the common case takes one extra round trip through a red verdict.
  • The setup-draft purge does not clean up orphaned connections. DELETE /v1/services/{name} calls cleanup_orphaned_connection; the sweeper cannot, because fire_connection_deleted wants an actor for its audit row and event. Identity-owned connections are reusable, so the cost is a row rather than a leak.
  • MCP Login Flow Fixes (review card 877cb) — assignment/consent page served from dashboard, default inherit_permissions=true for new MCP agents, reuse the existing agent on reauth, hide revoked MCP clients from the UI after 3s.

3. Backlog (post-launch)

  • Global service registry community contribution workflow (PR-based, with the validator endpoint as CI).
  • Multi-region / data residency (EU instance separate from US).
  • SOC 2 prep — separate workstream; controls audit, evidence collection, vendor (Vanta / Drata).
  • Bulk permission operations on the Org Settings view.
  • Directory sync follow-ups (D107). The OIDC-claim source ships; the rest of the shape is deliberately unbuilt. (a) Google Admin SDK pull — shipped (D110; instance service account + sign-in proof, D113); follow-ups: nested Google groups (the sweep reads direct membership only), secondary Workspace domains (only the hd primary domain is matched today), and a Cloud Identity variant for orgs without an Admin SDK-capable admin. (b) SCIM 2.0 push (source = 'scim'), which is also the natural home for the offboarding half of the docs/design/user-stories.md gap. (c) Periodic re-sync for the claim source — the Google source sweeps (leased worker in services/google_directory_worker.rs); the claim source still refreshes only at sign-in, since it has no stored directory credential. (d) General group-in-group nesting — the effective_identity_groups view is deliberately one hop; nesting makes it recursive and needs cycle detection. (e) ID-token signature verification — see TECH_DEBT.md.
  • Light mode + theme toggle on the dashboard.
  • More e2e: MCP approval-bubbling and elicitation full-chain (puppet + scaffold specs in; deterministic gap-trigger pending — likely via a seeded service template + a no-permissions sub-agent).
  • Increase integration coverage across all API routes; unit tests for permission resolution; OAuth refresh + BYOC fallback edge cases.
  • Priority-aware compact truncation. compact_response::shrink_to_budget applies uniform limits across the whole JSON tree, so a payload that ships column descriptors alongside rows (Metabase, BigQuery, Snowflake, most tabular APIs) spends the 8 KB budget on metadata before the truncator reaches the rows — a 254-row Metabase result renders as 10 rows plus …+244 more items. Make it priority-aware: detect the principal collection by shape, drop sibling metadata before it, and add a depth lever so nested descriptor subtrees (cols[i].fingerprint) collapse while their scalar leaves (cols[i].name) survive. Heuristic rather than a template x-overslash-* extension: the motivating traffic arrives over Mode A (service: "http"), which has no template action to annotate. D57 (advertised paging params, a reachable filter) and D61 (a cropped result now carries a URL to its own full bytes) both made this less urgent, but neither made it wrong. The key-cap half is now done — #537 made truncate rank survivors structurally (cursor keys, then arrays, then the rest) instead of keeping whatever sorted first, which is why metadata no longer beats rows to the cap. What remains here is the budget ladder itself: detecting the principal collection by shape, and the depth lever.
  • Priority-aware compact truncation. compact_response::shrink_to_budget applies uniform limits across the whole JSON tree, so a payload that ships column descriptors alongside rows (Metabase, BigQuery, Snowflake, most tabular APIs) spends the 8 KB budget on metadata before the truncator reaches the rows — a 254-row Metabase result renders as 10 rows plus …+244 more items. Make it priority-aware: detect the principal collection by shape, drop sibling metadata before it, and add a depth lever so nested descriptor subtrees (cols[i].fingerprint) collapse while their scalar leaves (cols[i].name) survive. Heuristic rather than a template x-overslash-* extension: the motivating traffic arrives over Mode A (service: "http"), which has no template action to annotate. D57 (advertised paging params, a reachable filter) and D61 (a cropped result now carries a URL to its own full bytes) both made this less urgent, but neither made it wrong.
  • Inbound media for MCP services. x-overslash-download (D61) lets an MCP tool result hand back a capability URL, so bytes leave a container without passing through a context window. There is no counterpart in the other direction. A tool that takes a reference to bytes the caller must upload first — send_file / send_audio_message on services/whatsapp.yaml, and the same shape on any MCP server that moves files, since MCP cannot carry bytes in either direction — can only be given a reference a previous read produced (download_media without deliver: "url"). That covers forwarding an attachment someone sent you, which is the common case; it does not cover originating one, because uploading needs the container's own URL and bearer token and the gateway deliberately hands out neither. Closing it means an upload capability URL: mint on a permission-checked action call, redeem with a PUT the gateway proxies upstream, credentials re-resolved at redeem time exactly as services::deferred_download already does — plus the Ext variant and token table to match. Deferred at the v0.7.0 WhatsApp template sync, where the forwarding path was judged enough to ship on.
  • Template async markers. Let an action declare that it runs async by default (x-overslash-async-default), resolved at template-resolution time rather than mid-flight, and surfaced in the MCP tool schema and /v1/search so a caller sees it before calling. This is the answerable half of D56's objection to auto-promotion: the response shape stays a function of the request plus the published contract. Worth its own decision record, since it revisits D56.
  • Hybrid mode (execution: "hybrid") — shipped, D68. The re-dial problem this entry said had to be solved first was dissolved rather than solved: hybrid is not sync-that-promotes but async that the connection waits on, so the request path never dials and there is no in-flight request to hand over. The job owns a durable, already-claimed row from before the first byte; the connection is a spectator with a deadline, and the handoff changes only who reports the result.
  • Do not auto-promote a long-running sync call to async. Considered and rejected: promotion either re-dials the upstream (duplicate side effects, and there are no idempotency keys) or spawns a detached task (no lease, dies on scale-in — a second async path with strictly weaker guarantees than the first). It also makes the response shape depend on runtime behaviour rather than on the request plus the published contract. The cheap fix for the underlying need is to have the 504 name execution: "async" in its hint.
  • Async execution follow-ups (D62/D66): (a) execution on the overslash_read MCP tool — the canonical async case, a slow analytics query, is read-class, but that tool has its own required-args schema and body-builder so it needs a second forwarder plus tests; (b) binary async results — currently a 400, because http_caller::call runs bodies through from_utf8_lossy before they reach the row; the real fix is the worker writing to object storage with the row keeping {result_url, mime, size_bytes}, which needs a bucket + IAM in Terraform; (c) NOTIFY-driven wake-up so a queued job starts immediately instead of within one 2s tick — the LISTEN bridge already exists in services::events::bus; (d) orgs.max_async_call_timeout_ms, if an org ever complains that the sync ceiling they set to bound connection-holding also bounds their background jobs; (e) a direct async row records no actions_executions_total at all — stored_call only records the upstream response — so since D66 a gated async call is strictly more observable than a direct one, which is backwards.

Done

Through 2026-05. Highlights below; full detail in STATUS.md.

  • Phases 1–4 backend: orgs, identities, secrets (versioned + encrypted), permission rules, approvals (with bubbling), webhooks, audit, API keys.
  • OAuth + Service Registry: native OAuth engine with three-tier BYOC, 9 OpenAPI 3.1 templates, three-tier template registry, template validation endpoint, per-service scopes, on_behalf_of.
  • Mode A/B collapse (SPEC §8): single Service + HTTP verb execution surface; typed reauth_required envelopes; dry-run /v1/actions/validate; stable webhook envelope.
  • Identity hierarchy: parent/child + inherit_permissions live pointer; approval bubbling; sub-agent idle archive + retention (backend).
  • Groups (Layer 1 ceiling): read/write/admin grants, auto_approve_level (a second ceiling on the same ladder), raw HTTP as the http singleton.
  • Rate limiting: two-tier (User bucket + identity caps), Redis/Valkey or in-memory, standard headers + 429.
  • Multi-provider OIDC + per-org IdP configs + GitHub social login + email-domain provisioning.
  • Multi-org auth: subdomain routing on *.app|api.overslash.com, switch-org, account memberships, corp-org creation with creator-admin.
  • MCP: Streamable HTTP + OAuth 2.1 AS endpoints, overslash mcp login CLI, annotated tools split into overslash_search / _read / _call / _auth, metaservice bridge for self-management actions, typed error envelopes.
  • Stripe billing: checkout-creates-org, customer portal, geo-priced EUR/USD, automatic tax, free_unlimited tier, full Stripe fake + Playwright Checkout e2e.
  • Monitoring: 5 GCP dashboards (overview / api-use / actions-and-oauth / cloudsql-use / business) + P0/P1/P2 alerts + uptime + OTel sidecar + JSON logs.
  • Dashboard: Agents tree, Services + templates, Secrets list + detail with reveal/restore, Audit Log with CSV export, Approval queue redesign, Members, Account, Billing flows, OAuth consent, API Explorer with Try-it, per-agent MCP Connection card, responsive shell.
  • Real-stack e2e: scenarios library, MCP puppet client (Rust + REST + TS), OAuth fake AS, Auth0/Okta IdP fakes, Mode-C e2e against connected GitHub.
  • CLI: single overslash binary with serve / web / mcp / mcp login subcommands.