← Ataski · Trust & security · DPA · Subprocessors

This is the canonical English version. Native translations for DE / ES / FR / PT / RU are pending counsel sign-off.

Approved 2026-10-08

Secure Development Policy

Owner: the founder. Approved: 2026-10-08. Review: annually.

Scope: every line of code that reaches production, and every tool that decides whether it may. Change process (branches, PRs, deploys, rollback) is change-management.md; this policy is about the code itself.

1. The standing rules

  1. Type hints are mandatory. mypy --strict runs on src and tests in the pre-push gate. It is deliberately not in the commit hook — about five minutes on a fresh worktree with no cache (measured 2026-10-08) — and can be run there by hand as a manual-stage hook. A function without types does not get pushed.
  2. One linter, with the security ruleset on. ruff runs with S (flake8-bandit) selected, so the checks a standalone Bandit run would make — subprocess without a fixed argv, assert in shipped code, hardcoded credential literals, unsafe deserialisation, weak hashes — fail the build instead of being reported in a file nobody opens. T20 bans print(): nothing reaches a customer's log line except the structured logger.
  3. Input is validated at the boundary. Every HTTP request body and every external payload is a Pydantic v2 model. A field the model does not declare does not reach a query.
  4. Queries are parameterised. Data access goes through SQLAlchemy 2.0; raw SQL exists only in migrations and in the admin introspection paths, and is bound, never formatted.
  5. Output is escaped by default. Templates are Jinja2 with autoescape on. The | safe filter is used only on copy we author, never on customer text.
  6. No provider SDK in business logic. Model calls go through the LiteLLM gateway and external data through MCP tools, so budget caps, the audit row and the tenant scope cannot be bypassed by a new caller. ruff's TID banned-imports rule enforces it.
  7. No secret in the tree. See docs/security/secrets.md; the enforcement is two scanners, below.
  8. A dependency is added at a pinned version in pyproject.toml and locked in uv.lock. Adding a vendor (a service, not a library) is the founder's decision, not the implementer's.

2. What gates a merge

Gate Where What it fails on
file hygiene (whitespace, end of file, merge markers, YAML/TOML syntax, files over 500 KB, line endings) commit hook the staged files
ruff check src tests commit hook + pre-push gate (lint) style, bugbear, security (S), banned imports
mypy --strict src tests pre-push gate (types) any untyped or wrongly typed symbol
pytest pre-push gate (unit) any failing test, including the guard tests below
gitleaks commit hook (staged diff) + pre-push gate (secrets, the branch's commits) a credential in the diff
detect-private-key commit hook a PEM block in the diff
gitleaks (full history) pre-push gate (history) a credential anywhere in the history
trivy fs pre-push gate (dependencies) a CRITICAL advisory in a dependency or a CRITICAL Dockerfile misconfiguration (HIGH is reported, not gated)
api fuzz pre-push gate (api-fuzz) a 5xx or an unhandled exception on a fuzzed public surface
prompt-injection probe pre-push gate (unit) a known injection string reaching a tool call (hermetic corpus lint; the paid live run is manual)
migration chain pre-push gate (database) a migration that will not upgrade, downgrade and re-upgrade — and a final step that proves RLS still bites as the runtime role, not as the owner

The commit hook is scripts/git-hooks/pre-commit, which runs .pre-commit-config.yaml on the staged files in seconds. The pre-push gate is scripts/check.py, run by the git pre-push hook; it refuses the push unless every stage is green, and a pull request is merged only after the gate is green on its head (ADR-0119). Both are installed once per clone by scripts/install_git_hooks.sh, and tests/test_git_hooks.py fails if a hook these documents name is missing from scripts/git-hooks.

What these gates are not: an independent witness. They run on the machine the change was written on, --no-verify skips them, nothing on GitHub records that they ran, and there are no required status checks — GitHub branch protection is not available for a private repository on the free plan. "Merged only after the gate is green" is the founder's discipline, not a setting.

3. The invariants tests hold

These are the architectural promises this product is sold on. Each has a test that turns the suite red when the promise is broken, which is why they are controls and not slogans:

4. Third-party code

Dependencies are pinned and locked; trivy fs reads the lock file before every push. Upgrades inside a semver-compatible range are made autonomously; a major-version upgrade is a founder decision. There is no automated dependency- update bot configured today — see §6.

5. Vulnerability handling

A CRITICAL advisory in a dependency gates the push, so it is fixed or explicitly waived in .trivyignore with a reason and a date (tests/test_trivy_ignore_policy.py refuses an entry without either). A HIGH advisory is reported on the run and swept in batches. A vulnerability found in our code is handled as an incident when it is exploitable in production, and as an ordinary fix when it is not — the deciding question is whether customer data could have been reached, and it is answered in writing either way.

6. Known gaps