// Security

Security model

Who runs what

Upseam never runs your code or the model's answer. Your CI does.

The model's answer becomes a commit on a branch upseam/* in your repository. That branch triggers your push and pull_request workflows, and because it is a branch of the same repository, not a fork, those workflows get your repository's secrets before anyone reviews the pull request. So the model's answer is untrusted code with access to your CI secrets, and the defenses below are built around that.

The untrusted input is the vendors' changelog and spec text, the text of your internal contracts and your own code: all of them go into the model's brief. An instruction planted in any of them could ask the model to add a network call or read environment variables.

Defenses, strongest first:

  1. Deterministic patch gates decide what may be pushed. See below.
  2. Vendor text is data. Changelogs and specs are quoted in the brief as untrusted data, and the model is told not to follow them. This lowers the risk but does not remove it, which is why the gates exist.
  3. Only brief files, never .github/workflows, never the default branch. The App has no Workflows permission and pushes only to upseam/*.
  4. ask pushes nothing before approval, so your CI sees no patch until a person decides. See Delivery modes.
  5. Dependabot and Renovate bumps always go through ask.
  6. Your side. Keep secrets away from jobs that run on upseam/* before review, and protect the default branch.

Patch gates

The gates are an allow-list, not a list of forbidden things. A patch that fails any of them is not pushed, and the finding goes to Needs you.

  • Files and place. Only files in the brief change. Each edit sits inside a statement that matched the change, within two lines of the match, and touches the changed symbol or its replacement. Each edit has a size budget of twice the matched place plus 40 characters. Whitespace-only edits and invisible or bidirectional characters are refused.
  • Parsing. Every changed file must still parse: JavaScript and TypeScript with the TypeScript compiler, Python and Go with a lexer. Other file types and Go test files are refused.
  • Control flow. In the whole file, the number of return, throw or raise, if, loops, switch, try, break and continue, and of comparison and logical operators, must not change. Conditions of if, loops and ? : must stay exactly as they were. Words like auth, permission, role, token or session at the edit stay.
  • Same tokens, one rename. At the edit, the sequence of names, keywords, strings, numbers and punctuation must stay the same, except that a matched symbol becomes its replacement or the new name the vendor's change gives. A replaced string or number may sit anywhere; a replaced name only after . or ?., as an object key, or as a Python keyword argument, and never as a global, a forbidden name or a name the file already declares. Boolean literals do not change, values of properties whose names look like URLs, hosts, keys, tokens, secrets, auth or certificates do not change, and a value may only be assigned to a property the matched place already assigned.
  • Simple syntax. In JavaScript and TypeScript, changed lines may use only names, member access, literals, objects and arrays with spread, calls, new of a class the file already constructs, await, arithmetic, comparisons, ? :, arrow functions, const and let declarations with destructuring, return, type annotations, as, satisfies and !. Computed indexes, template strings with substitutions, dynamic import() and similar constructs are refused. In Python, the only keywords a patch may add are return, await, None, True and False; dunder names, new f-strings, calling the result of an expression and computed indexes are refused. In all languages, non-ASCII names are refused. In Go, a patch may not change the package clause, imports, top-level declarations, compiler directives (//go:, //line, #cgo) or build constraints, may add no keyword or semicolon, and may not change code outside the edited lines.
  • Statement ends. A line break, ; or comment may not start or end a statement in a new place: each edit keeps where its statements end, and in Python at which indentation the next statement starts. A line break after return that would empty it is refused.
  • Forbidden names and addresses. Names such as process, fetch, eval, require, os, environ, subprocess, exec, open and __import__, and any URL, host or IP address, are refused in added lines unless the same line or URL was there before.
  • Type changes only. Conversion calls from a small list (for example String, Number, JSON.parse in JavaScript and TypeScript; str, int, len in Python) are allowed only for a vendor change of type "type change", and only inline where the value is used. Any other new call must be the SDK method the vendor's change names, on a receiver the removed lines already called. In Go, the SDK pointer helpers (stripe.String, stripe.Int64, …) count as conversions when the file imports the official SDK.
  • No new locals. A patch may not declare any new local name in any language: no const, let or var, no Python assignment to a new name, no Go :=. Such a patch goes to a person.
  • New names and strings come only from the file, from the symbols and replacements of the vendor's change.
  • Re-scan. After the edit, Upseam searches the patched files again for the group's changes. If any matched place is left unchanged, the patch is refused.

What remains. A patch that passes the gates can still change how data flows within two lines of the matched code, for example by renaming to a name from the vendor's data. In auto, your CI runs it with your secrets before review. That is why ask is the default. Legitimate edits the gates refuse, such as editing a condition or a line with process.env, go to a person too.

What Upseam stores

Stored Not stored
Installation, repositories, settings Your code, code lines, diffs, pull request and issue bodies
Surfaces: provider, SDK, versions, pins as file and line Repository archives: only in a temporary directory during a step
Matches: path, line, symbol The model's answer after the step
Change events from your internal contracts: changed operations, types and fields
Proposals: branch, commit, pull request number, CI status GitHub installation tokens: in memory only, for up to an hour
Approvals: channel, decision, who decided Your model key when you generate in your GitHub Actions
Encrypted: Slack bot token, model key (when stored)
Product events, see below, for 13 months

Uninstalling the App deletes the installation's repositories, surfaces, findings, settings and encrypted secrets. A removal marker with only the installation id and the time of removal stays. Its product events are deleted, Upseam asks PostHog to delete the pseudonymous person, and a marker with the installation id is kept for up to 13 months so that late events are not exported. Records of queued work and received webhook deliveries are not deleted, and the Slack bot token is not revoked with Slack: see Uninstall. Every row is scoped to its installation by the database itself, with row-level security.

Who else receives your data

  • GitHub, where your code already lives.
  • Your Slack workspace, if connected: file paths and line numbers, no code.
  • The model vendor you choose: the change data and the files with matches, sent with your key.
  • PostHog: the pseudonymous product events above, no code.
  • Nobody else gets your code. Change events are labelled with Jev (TypeSafe) once, when Upseam ingests a vendor change; your code is never sent to TypeSafe.

Product analytics

Upseam records product events such as: the App was installed, a vendor change was ingested, a dashboard issue was created or updated, a model or Slack was connected, and a pull request was opened, got a CI result, was merged or closed. Events carry only ids, flags and fixed values such as the mode, never code, repository names or user logins. They are kept for 13 months.

Upseam exports these events to PostHog. The installation id is replaced with a keyed hash (HMAC), and PostHog's IP geolocation is switched off. How long PostHog keeps them is set in PostHog, not by Upseam.

Accounts and sessions

  • The setup and settings pages use Sign in with GitHub. An installation is shown only to users GitHub lists for it, and every change is checked against a fresh answer from GitHub, with no cached permissions.
  • The session lives in an encrypted, HttpOnly, Secure cookie for up to seven days. Sign out revokes the GitHub token.
  • Every form is protected against cross-site requests, and the pages send a strict Content Security Policy.