AGENTS.md@internal · git:20261020.8d47d7a · 2026-10-20 · sha256 2ee98ed5c6306bc2

AGENTS.md@internal git:20261020.8d47d7aA

Immutable. This exact content never changes and is served at /api/v1/blob/2ee98ed5c6306bc2.

# Agent Instructions — `internal/`

## Integration tests share one database

A Go integration test package (`internal/api/integration_test`,
`internal/storage/stmttest`) runs every test in one database, so rows a test
leaves behind slow every test after it. On the Spanner emulator the authz list
predicate's cost grows with the rows in its tables.

- Clean up what a test creates. Create fixtures under the test's own project:
  `helpers.Harness.EnsureProjectService` registers the delete, and the authz
  tables (`resource_scope_index`, `authz_assignments`,
  `authz_membership_edges`) cascade from `projects`.
- Clean up anything written outside the test's project (the platform
  project, the catalog tables) with the test's own `t.Cleanup` and
  `context.Background()`.
- A project created over HTTP goes through the cached handler's unwrapped
  service; remove it with `Harness.CleanupProject`.

## Errors

Services and storage return `domain.Error` values, which match by `Code`
through any wrapping.

- Ask "is this that error?" with `errors.Is`:
  `errors.Is(err, domain.ErrAuthAttemptNotFound())`. Use `errors.As` only to
  read a field, and switch on codes only where they map to statuses, as
  `errorResponse` (`internal/api/error_handler.go`) does.
- An error nobody classified is a server fault and ends as a 5xx; keep that
  in every layer that turns a service error into something else.
- A layer that turns errors into refusals (401, 403) refuses only the errors
  that mean "no" and passes every server fault through, so it keeps its own
  5xx (`domain.IsServerFault`, as `errorResponse` reads it):

  ```go
  if errors.Is(err, domain.ErrTokenRevoked()) { // an answer that means "no"
      return ErrDenied
  }
  if domain.IsServerFault(err) { // 500, 501 or 503, decided by errorResponse
      return err
  }
  return domain.ErrInternal(err) // anything else: we could not decide
  ```

- A refusal for an error that means "we could not decide" (the database
  failed, say) tells clients their session is dead, and the SDKs clear their
  cookie on a 401: an outage would sign every user out.

## Format before push

Run `moon run server:format` before pushing Go changes. It only checks
(`gofmt -l` on tracked `*.go`); on failure run `gofmt -w` on the listed paths
and re-run it. `server:format` is part of `ci / full-pr`.

## Redirect targets

A URL the server sends a browser to, or stores to send one to later, comes
from a caller until proven otherwise.

- Parse it with `httputil.ParseRedirectTarget` and nothing else, never
  `url.Parse` plus checks by hand: a browser and a Go parser read the same
  string differently (a backslash is a slash, `%2e` a dot, a tab is dropped,
  a leading `//` names another host), and an open redirect lives in that gap.
- The function decides whether the URL is safe for a browser; the caller
  still decides which hosts it accepts (for example, the request's origin).
- A new way around the check goes into
  `internal/httputil/testdata/redirect_targets.json`, and the fix into the
  function and into the API mock's port (`packages/api-mock/src/redirect-target.ts`),
  which reads the same file. Never add a second check at the call site.