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:
- Launch Blockers — must ship before opening the public signup funnel.
- Launch +1 — polish within the first weeks of GA.
- 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.
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 oninvoice.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.
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.
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/docson 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.jsonfrom 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).
Monitoring is deployed; paging and recovery procedures are not yet exercised.
- Bind
pagerduty_integration_keyininfra/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.comover IPv6 — the LB's IPv6 address and its 443/80 forwarding rules are declared ininfra/modules/api-lb/main.tf(api_lb_enable_ipv6 = truein prod tfvars, on the same proxies so the SSL policy and Cloud Armor apply). Not applied: runmake tofu-apply ENV=prod(3 additions), then publishtofu output api_lb_ipv6as AAAA forapiand*.apiat 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_enabledon ininfra/env/dev.tfvarsand thenprod.tfvarsonce the exporter has writtenintegrity_violations, thenmake tofu-plan/tofu-applyper env (runbook). Triage whatever the first sweep reports — the migration-133 Reveni leftovers should show asbinding_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_resolveemitting the sameinvariantlabel.
-
security.txt+ vuln disclosure policy page. RFC 9116 file served by the dashboard athttps://app.overslash.com/.well-known/security.txt(dashboard/static/.well-known/; org subdomains redirect to it), pointing atSECURITY.md.scripts/check-security-txt.shfails the daily Dependency audit a month beforeExpires. 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.
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.rsnow builds its own pinned client from the URL viassrf_guard::outbound_clientinstead of acceptingstate.http_client, so all five action-execution call sites are covered by construction;webhook_dispatcher::deliverdoes the same per attempt. Tests intests/ssrf_guard.rs. (CASA 5.1.5, 7.3.1) - Decide the default
Everyone → admin on httpgrant — org bootstrap (crates/overslash-db/src/repos/org_bootstrap.rs:143-153) grants it to preserve the oldallow_raw_http=truedefault, 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.rsno longer takesstate.http_clientor runs its own string check: every hop, including each redirect, goes throughssrf_guard::outbound_client_validated(resolve, check every address, pin).httpsis required, with plainhttpaccepted 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 genericOIDC discovery failed400, and the detail is logged server-side, so neither handler echoes an upstream error or body any more. Tests intests/ssrf_guard.rs. (CASA 5.1.5) - Cross-tenant API-key minting —
POST /v1/api-keysno longer takes anorg_idat all: it usesAdminAcl+ theOrgScopeextractor, so the org is the presented credential's currently-active org.identity_idresolves through that scope, and migration 121 adds a compositeapi_keys (org_id, identity_id) -> identities (org_id, id)FK. The unauthenticated bootstrap branch — the only reason a bodyorg_idexisted — is gone;POST /v1/orgsreturns the first admin key instead. (CASA 3.1.2, 3.1.4 — both nowpass)
P1 — hard CASA fails.
-
Secureon every auth cookie, plus__Host-/__Secure-prefixes — one builder incrates/overslash-api/src/cookies.rs; the unprefixedoss_sessionis no longer read and is cleared on login/logout. One-time logout of every user on deploy. (2.3.1 — nowpass) - Server-side sessions —
user_sessions(migration 130) with ajticlaim checked per request bymiddleware/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,DELon revoke); withoutREDIS_URLit reads Postgres every time rather than cache per-replica. Session JWTs without ajtiare accepted only under 24 h, which refuses every pre-change cookie once. Tests intests/user_sessions.rs. (2.2.1, 2.2.2, 2.2.3) - Webhooks are HTTPS-only — registration parses the URL into
HttpsUrland refuseshttp://with a 400; the dispatcher refuses to dial one and records the failed delivery; pre-existinghttp://subscriptions were disabled by migration 124 (disabled_reason = needs_https, shown in the dashboard's org Webhooks card). Plainhttpsurvives only to loopback whenOVERSLASH_SSRF_ALLOWED_CIDRScovers it (services/https_policy.rs, shared with OIDC discovery). (7.1.1 — nowpass) - Webhook endpoint-ownership handshake — a new subscription is
pending_verificationuntil its endpoint echoes a signedwebhook.verificationchallenge (2xx, bare or{"challenge"}, 10s); registration runs it inline andPOST /v1/webhooks/{id}/verifyre-runs it. Events for a pending subscription are recorded as held deliveries and released on verification; the handshake goes throughssrf_guardand 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 intests/webhook_verification.rs. (7.1.2 — nowpass) - Webhook signed timestamps — every attempt carries
X-Overslash-TimestampandX-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,sdkverifyWebhook). Not breaking: the legacy body-onlyX-Overslash-Signature: sha256=<hex>is still sent unchanged, so existing verifiers keep working — its removal is tracked in TECH_DEBT.md. Tests intests/webhook_signing.rs. (7.2.3 — nowpass) - Stop sending the legacy
X-Overslash-Signature— the dispatcher sends onlyX-Overslash-Timestamp+X-Overslash-Signature-V1;verifyWebhookSignatureremoved from@overslash/sdk; SPEC, SDK README and the dashboard hint describev1only. 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:
v1verification section — the guide (separate overslash-docs repo) must documentX-Overslash-Timestamp+X-Overslash-Signature-V1over"<ts>.<raw body>", ±5 min tolerance, constant-time compare,verifyWebhook, and thatX-Overslash-Signatureis no longer sent. - Security-headers layer on the API + a
headersblock indashboard/vercel.json. The API's outermost middleware (middleware/security_headers.rs) sets HSTS (1y,includeSubDomains), nosniff,X-Frame-Options: DENY,Referrer-Policy: no-referrer, aPermissions-Policyand 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'skit.csp(a per-response nonce on Vercel, a hash in the static build), soscript-srccarries no'unsafe-inline'; it includesframe-ancestors 'none'. The rest of the baseline is invercel.json, which sets no CSP itself so the two never collide, and whichvite previewalso serves so e2e runs under it (scripts/screenshot-security-headers.mjsfails on any violation). - Dependency vulnerability scanning in CI —
.github/workflows/deps-audit.ymlrunscargo-deny(deny.toml),npm audit --audit-level=high(dashboard, sdk) and OSV (osv-scanner.toml) over every lockfile.ci.ymlcalls it on dependency changes, gated byci-ok, and a daily schedule ondev+masterfiles aneeds-triageissue on failure. The four fixable advisories are cleared (h2,rustls,crossbeam-epoch,event-listener), plus OSV'sdevalueand three yanked crates.rsaandpasteare time-boxed exceptions with review dates enforced byscripts/check-deny-reviews.sh. Policy: dependency-vulnerability-policy.md; D101. (6.1.1 — nowpass) - Require TLS on outbound calls —
services/outbound_tls.rs. The action transport refuses plainhttpon 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 — instanceurlon create/update, an org layer'sinstance_defaults.url, a template'smcp.url(endpoint_requires_httpsin 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 isOVERSLASH_SSRF_ALLOWED_CIDRS: plainhttpis accepted to an address inside an operator-allowed range, which is how the suite's loopback fakes still work. The dashboard flags anhttp://endpoint next to every base-URL input. Tests intests/outbound_tls.rs. The Cloud SQLREQUIRE_SSLand 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 throughoutbound_tls::check_resolvedagainst the SSRF-pinned address, on clients that never follow a redirect. Thetoken_endpointandauthorization_endpointthe AS metadata advertises are string-checked before anything is registered or a flow minted, soinitiatefails with a 400 naming the field; the callback re-checks a storedtoken_endpointbefore consuming the flow. SameOVERSLASH_SSRF_ALLOWED_CIDRSescape hatch. Tests intests/oauth_upstream_tls.rs. (Webhook delivery is HTTPS-only since #664, viaservices/https_policy.rs.) (4.1.1) -
redirect_urischeme 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 onClientIp, so a forgedX-Forwarded-Forbehind a trusted proxy buys no fresh bucket. Tests intests/ingress_rate_limits.rs. (3.2.2 residual — closed; 3.1.5 anti-automation half) -
SECURITY.mdwith 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 thesecurity.txtitem in §1.5.
P2 — will be raised by a lab.
- Admin step-up: admin-class mutations need a fresh IdP sign-in —
user_sessionsrecords how each session was signed in (migration 137:auth_method,auth_provider,authenticated_at, the ID token'samr/acr; a switch-org re-scope keeps them).AdminAclandInstanceAdminAuthrefuse 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 lastADMIN_STEP_UP_MAX_AGE_SECS(1 h): 403step_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 OIDCmax_age=0, and Account → Sessions shows each session's sign-in method andamr. MFA itself is delegated to the IdP and not enforced onamr(D136). A deployment with no IdP at all accepts a fresh magic link instead. Found and fixed on the way:/v1/org-idp-configsmutations had no admin check, so an agent key could add an org IdP; they now takeAdminAcl. Tests intests/admin_step_up.rs. In code, pending apply:infra/pinsADMIN_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 — nowstatement; 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::requirewith aTarget::Organd 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 inload_config, over a parsedDeploymentEnv(OVERSLASH_ENV; the container image defaults it toprod).DEV_AUTHthat is not a recognised boolean stops the boot everywhere, andDEV_AUTHon withOVERSLASH_ENV=prodstops it too. WeakSECRETS_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.exampleships no key values. Adebug/traceRUST_LOGis clamped toinfoin production with a warning, andprod.tfvarsnow saysinfo(not auto-applied — the clamp holds either way)./healthand/readydropdb_error; the error is logged instead. (6.2.1, 1.2.1 — both nowpass) - Rate limiting beyond
osk_keys —middleware/rate_limit.rsnow 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-*+/mcpsubrouter has its own layer (middleware/ingress_rate_limit.rs, mounted viaoverslash_api::mcp_oauth_routesso 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/mcpprobe. Instance-wide env knobs,0disables, 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 intests/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 tooverslash-<env>-audit, 400 days, locked in prod;[P1] Unexpected Secret Accessalert), 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 staysgapuntil then. (6.7.1) - Subdomain inventory + dangling-record check — subdomain-inventory.md: every record in
overslash.comandoversla.sh(36 rows; the "ten hostnames" count was wrong —dev,docs,mail,acmedo not resolve and are pinnedabsent), with owner and claim model per provider.scripts/check-subdomains.shfails 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.devwildcard 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:
— done indeletion_protectionandssl_modeon Cloud SQLinfra/modules/cloud-sql/main.tf(Terraform and API-level deletion protection,ENCRYPTED_ONLY), pending apply per cloud-sql-hardening.md;an explicit— applied, re-measured 2026-10-22 (lb-hardening.md);google_compute_ssl_policyand Cloud Armor on the LB— applied in prod (INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER*.run.app404, prod plan clean, 2026-10-22); dev staysallbecause it has no LB and its hosts are domain mappings;least-privilege IAM in place of project-wide— applied 2026-10-23 on both projects, verified per least-privilege-iam.md (still to capture: the Policy Troubleshooter checks, and a devsecretmanager.secretAccessorandcloudsql.admindb-start/db-stoprun after the apply — every scheduler run so far predates it): one service account per workload; the API'ssecretAccessoris conditioned onoverslash-<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-wideiam.serviceAccountUserand the scheduler's project-widerun.invoker(CASA 3.1.1);Memorystore— done inauth_enabled+transit_encryption_modeinfra/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— productionoverslash-prod-dbrunssslMode=ALLOW_UNENCRYPTED_AND_ENCRYPTED, behind a HIGH-severity insight.ssl_mode = "ENCRYPTED_ONLY"is now set ininfra/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/cloudsqlAuth Proxy volume, BigQuery federation uses acloud_sqlconnection, humans usebin/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 oractAsgrants; the only build ever run as it was the manual submit of 2026-04-29. Applied in prod 2026-10-27 (re-measured: onlycompute.editoron the cloudservices agent remains):infra/modules/project-guardrails/removes both withgoogle_project_iam_member_remove, so a re-added binding is drift that the next apply removes, and grantscompute.editorfirst whereuse_private_vpc. Dev's binding was removed by hand on 2026-10-27 (overslash-dev-api/healthstill 200). Both projects now sit in the overspiral.com org, whose inherited policies already enforceiam.automaticIamGrantsForDefaultServiceAccounts. The module pins it at project level and addsiam.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,roleandcloudsql.pgaudit_mask_literals(so the API's boot-timeALTER ROLE bi … PASSWORDis not logged in clear) as database flags; the one-offCREATE EXTENSION pgauditis 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_configfor all three, a sink to a locked bucket and a log-based alert onAccessSecretVersionare ininfra/modules/audit-logging/. Live until applied — same operator step as the 6.7.1 item above. (CASA 6.7.1) -
_Defaultlog 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 tooverslash-<env>-audit(400 days, locked in prod)._Defaultitself stays 30 days on purpose: it holds application logs, and the app's own audit trail is the Postgresaudit_log. Same operator step. (CASA 6.7.1) -
<project>_cloudbuildbucket — auto-created, not in Terraform, holds repository source archives with uniform bucket-level access off and public-access prevention merelyinherited. Measured on both projects:USmulti-region, one archive each from a manualgcloud builds submiton 2026-04-29, and nothing written since. The archive carried a local-only.claude/settings.local.json, because gcloud honours.gitignorebut 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/adoptsoverslash_cloudbuildandoverslash-dev_cloudbuildthrough animportblock ininfra/main.tf, withuniform_bucket_level_access = true,public_access_prevention = "enforced"and a 30-day delete lifecycle rule. A root.gcloudignorestops a future manual submit from uploading local-only files. Re-measured after the apply: UBLAtrue, preventionenforced, the 30-day rule present, an emptyacl, 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_timeorexpire_time. Nothing schedules or alerts on rotation age. (CASA 6.7.1) - Memorystore has neither AUTH nor transit encryption —
overslash-prod-valkeyconfirmed live:authEnabledunset,transitEncryptionMode: DISABLED,BASICtier. In code, pending apply:infra/modules/memorystore/main.tfsetsauth_enabled = trueandtransit_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_*) bysecret_key_ref. Both clients connectrediss://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 Downalert — 8 alert policies were deployed (10 on 2026-10-27), which was exactly 12 declared minus 3 disabled in tfvars minus the API-down policycount-gated onapi_domain != "". The only deployed P0 isAPI High 5xx Rate, which cannot fire when the service returns nothing at all. Root cause:infra/main.tffed the gatevar.domain, which prod leaves empty because the GCLB servesapi_host_suffixinstead of a domain mapping. It now falls back toapi_host_suffixwhen the LB is on, which createsoverslash-prod-api-healthonapi.overslash.com/healthand[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 a0.001floor + 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 Downon PagerDuty + email) andoverslash-prod-api-healthpassing from all 6 checker regions. Found on the way: dev's/health500'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/healthreturns 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 listandsecurity-policies listare empty. TLS 1.2/1.3-only is currently true but unpinned and uncitable.infra/modules/api-lb/main.tfnow declaresoverslash-prod-api-ssl-policy(TLS 1.2 minimum,RESTRICTED) on the HTTPS proxy andoverslash-prod-api-armoron 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.publicAccessPreventionandiam.disableAuditLoggingExemption(both projects).iam.managed.disableServiceAccountKeyCreationis 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 tointernal-and-cloud-load-balancingonce overfwd and the shortener stop running withINGRESS_TRAFFIC_ALL(prod API is already LB-only). (CASA 6.2.1) -
Keyring::test()is compiled only undercfg(test)or overslash-core'stest-supportfeature, which onlyoverslash-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.ClientIpnow 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 upstreaminitiate) go through the extractor too. prod/dev values and the enable/rotate/verify steps are ininfra/README.md. Tests intests/client_ip.rs. Operator step still open: set the secret in GSM and Vercel per env, then flipenable_trusted_proxy_secret. -
Cache-Control: no-storeonsecrets/revealand other sensitive authenticated responses — the default on every API response that does not set its own (/icons/*keepspublic, max-age=86400; streamed and deferred-download passthrough keeps the upstream's). -
ci-okis a real gate —.github/workflows/ci.yml. It used to acceptcancelledas success, and its paths-filter left outservices/**,scripts/**,.githooks/**and the other workflows, so a PR touching only those skipped every substantive job and the one required check went green. Nowfailureandcancelledfail 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. Therustfilter addsservices/**,assets/**,SKILL.md,tests/mock-target/**,scripts/**,Makefile,.github/actions/**andcodecov.yml. A newtoolingfilter 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 generatedSCHEMA.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_AUTHon) 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.
- IdP config edit UI on
/org— backendPUT /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 theoverslashget_eventsaction answers; reuseservices::inboxclassification 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.
-
/accountprofile editing — name + avatar editable. - Org webhook management UI — list, create, rotate signing secret, disable.
- Toggle Switch component (
ToggleSwitch.svelte) adopted everywhere replacing checkboxes (review card2e268). - 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 theoverslashmeta-service "Needs Setup" copy, group pills as a column, services connectable to groups from the detail view.
- 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),overslashitself (runtime: platform), and the hidden fixturetest_email. Any template added from here should declare one, andshipped_test_actions_resolve_to_read_actionsholds 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 andinstance_config::validate_configrejects 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.
- Approval visibility scoping —
GET /v1/approvals?scope=actionablevs?scope=mine(Phase 3 carry-over). - Webhook payload: include
gap_identityandcan_be_handled_byon 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/permissionsis 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
activitycan ever be on by default. It is the only topic whose volume scales with the gateway's hot path — one durableeventsrow per call — so ungating it means answering that first.
- Structural agent→service edges from permission rules. Today they only
appear once traffic reveals them, because
- 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/setuphas 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}/activateis 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 anAuthContextand re-enteringcall_action_implfrom 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
SecretValueFieldbehind "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}callscleanup_orphaned_connection; the sweeper cannot, becausefire_connection_deletedwants 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, defaultinherit_permissions=truefor new MCP agents, reuse the existing agent on reauth, hide revoked MCP clients from the UI after 3s.
- 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 thehdprimary 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 thedocs/design/user-stories.mdgap. (c) Periodic re-sync for the claim source — the Google source sweeps (leased worker inservices/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 — theeffective_identity_groupsview 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_budgetapplies 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 templatex-overslash-*extension: the motivating traffic arrives over Mode A (service: "http"), which has no template action to annotate. D57 (advertised paging params, a reachablefilter) 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 madetruncaterank survivors structurally (cursor keys, then arrays, then the rest) instead of keeping whatever sorted first, which is whymetadatano longer beatsrowsto 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_budgetapplies 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 templatex-overslash-*extension: the motivating traffic arrives over Mode A (service: "http"), which has no template action to annotate. D57 (advertised paging params, a reachablefilter) 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_messageonservices/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_mediawithoutdeliver: "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 aPUTthe gateway proxies upstream, credentials re-resolved at redeem time exactly asservices::deferred_downloadalready does — plus theExtvariant 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/searchso 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 (— 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.execution: "hybrid")- 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)
executionon theoverslash_readMCP 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, becausehttp_caller::callruns bodies throughfrom_utf8_lossybefore 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 — theLISTENbridge already exists inservices::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 noactions_executions_totalat all —stored_callonly records the upstream response — so since D66 a gated async call is strictly more observable than a direct one, which is backwards.
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_requiredenvelopes; dry-run/v1/actions/validate; stable webhook envelope. - Identity hierarchy: parent/child +
inherit_permissionslive 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 thehttpsingleton. - 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 loginCLI, annotated tools split intooverslash_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_unlimitedtier, 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
overslashbinary withserve/web/mcp/mcp loginsubcommands.