TeireDocs
ChangesScanDashboardTry Teire
GuideEcosystems & APIsNemotron on Token FactoryBuild
ChangesScanDashboard
Try Teire
ArchitectureHow Teire updates dependency filesVerificationToken Factory SandboxesSecurity and trustContributing to the catalog

Tools

CLIMCP serverAgent Skill
Build

How Teire updates dependency files

The manifest and lockfile change comes from Dependabot's own updater, run in an isolated sandbox on Teire's server; Teire's loop of locating, fixing and verifying the code sits on top.

Where it runs

The updater service runs on Teire's server and is measured below. On the deployed worker it decides fix runs' dependency files in all six ecosystems. A self-hosted worker names its ecosystems in TEIRE_RESOLVER_ECOSYSTEMS; an ecosystem left out uses Teire's own transaction, and the verification is the same either way.

Changing a version in package.json is the easy part. Resolving the new lockfile, moving every declaration across workspaces, and knowing which other packages must move too is what package managers spend years getting right. Teire doesn't reimplement that. It runs dependabot-core's updater (MIT), through GitHub's dependabot CLI, and builds its own work on top: finding the lines that break, fixing them, and proving the fix.

One job per upgrade

For each upgrade Teire decides on, it runs one updater job:

JobWhat it asks for
Version updateExactly the target version Teire chose: only this dependency, nothing above the target, no extra cooldown (Teire already applied the repository's)
Security updateThe lowest version that fixes the advisories that have a published fix

Refresh and group-refresh jobs are built too. Until they are wired in, a base change re-runs the version or security job at the pull request's own version, which gives the same files. A group configured in dependabot.yml becomes a group job once group refresh is wired in; until then each package in it gets its own pull request.

A package declared in several lockfile roots runs as one job listing every root the same package manager covers (npm and Yarn roots together), so one pull request carries every root's change.

The job is bound to the commit Teire scanned, and the run returns the sha256 of the tarball it used.

What comes back

The updater reports what it did as events. Teire reads those events, never the exit code:

  • A change:
    • the dependency files, byte for byte, including binary lockfiles, deletions, symlinks, and the executable bit of files that already have one. Only dependency files are taken: the updater runs repository code, so any other file it returns (a workflow, a source file) stops the run instead of reaching a commit;
    • every package the updater moved alongside the upgrade, listed in the PR with its old and new version. A moved package your repository declares is diffed and located like the upgrade itself (the eight largest moves), so the fix covers its breaking changes too and the PR's "Your code" column says what each move means: breaking changes and their sites, none it uses, or why it wasn't diffed;
    • the version the updater reports the dependency landed on. A version below the target means "resolvable only up to here", and the PR says why. The install and checks then run on exactly these files. For Cargo, which the updater can't fully unlock, Teire first looks for the crate holding the target back: one your Cargo.toml pins in the same compatible line, or one that links a native library. It raises that crate in its own job, then moves the upgrade on top of those files, so the PR reaches the target instead of stopping below it.
  • Another dependency moved mid-run: when the fix needs a second package moved too (two versions of one crate meeting in your code, a peer requirement), the fix agent asks for it. The updater moves it on top of the run's own dependency files, under the same rules as the first change. Teire reinstalls, and the PR lists the move as asked for by the agent, with why. A request has ten minutes, at most two per run, and only for packages your repository already declares; if the reinstall fails, the files are put back.
  • A named stop, quoted in the PR or issue. Examples: no set of versions satisfies every requirement; every newer version is ignored by the repository's configuration; no version that fixes the advisory has been published.
  • Nothing to change: for a version update whose every newer version the repository ignores, the updater changes nothing, as Dependabot does. The run ends saying so.

When the updater says no

Teire's own change takes over from the updater in two cases only, and the run's timeline says so, quoting the updater: a transitive security fix the updater can't make (the rungs below), and a Ruby gem the updater resolved on the gemspec's lowest Ruby when that Ruby is the only conflict and the repository pins a newer one (Bundler then resolves on the repository's Ruby in the sandbox). Otherwise:

What came backWhat Teire does
A change at the targetApplies it, then runs the verification
A change below the targetMakes that version the target, compares its API again, and the PR opens with Why X quoting the reason
A security fix above the lowest published oneThe lowest published fix doesn't resolve in this repository, so the updater's lowest fix becomes the target, as Dependabot's own security update would; the PR says why
A named stopOpens one issue for the package, quoting the updater's words. The next pull request for that package closes it
Nothing to changeEnds the run with the reason
The updater is unreachableTries again after 15 minutes, then 30, 60 and so on, six times (about 16 hours), then stops with an issue

Every file path is checked before it is used. A path that would leave the repository, or touch .git, is refused.

Measured on real repositories by Teire's pilot (2026-10-28); the cases and the updater's results are in smoke/resolver:

RepositoryUpgradeFilesTime
teire-dev/serve-handler (Yarn 1)path-to-regexp 3.3.0 → 9.0.0package.json, yarn.lock1 min 46 s
teire-dev/serve-handler (Yarn 1)minimatch 3.1.5 → 10.2.6package.json, yarn.lock2 min 17 s
teire-dev/examples (npm)zod 3.25.76 → 4.6.7package.json, package-lock.json42 s
teire-dev/examples (uv, two roots)anthropic 0.125.0 → 1.17.0pyproject.toml, uv.lock1 min 46 s
teire-dev/devchat (Poetry)pydantic 1.10.16 → 2.10.6pyproject.toml, poetry.lock2 min 51 s
teire-dev/examples (Go)golang.org/x/text 0.3.5 → 0.43.0go.mod, go.sum1 min 16 s
teire-dev/diskonaut (Cargo)crossterm 0.17.7 → 0.18.0Cargo.toml, Cargo.lock44 s
teire-dev/examples (Bundler, three roots)faraday 1.10.6 → 2.15.0Gemfile, Gemfile.lock1 min 50 s
teire-dev/coal (gemspec)faraday 1.10.3 → 1.10.6Gemfile.lock, *.gemspec1 min
teire-dev/examples (Gradle, catalog and lockfile)okhttp 3.14.9 → 5.5.0libs.versions.toml, gradle.lockfile3 min 35 s
teire-dev/spring-boot-realworld-example-app (Gradle)jjwt-api 0.11.2 → 0.13.0build.gradle34 s
teire-dev/examples (Maven)guava 19.0 → 23.0, not 33.7.2-jre: the updater keeps the version's suffixpom.xml51 s
teire-dev/examples (Cargo, removed feature dropped first)rand 0.8.8 → 0.10.3Cargo.toml, Cargo.lock36 s
teire-dev/coal (Bundler, security)rexml 3.2.6 → 3.3.9Gemfile.lock56 s
teire-dev/ai-test-reporter (npm, transitive security)fast-uri 3.1.8 → 3.1.9package-lock.json1 min 27 s
psf/requests-html (Pipenv, read only)lxml 4.9.2 → 6.1.3Pipfile.lock2 min 49 s
Nekmo/dirhunt (pip-compile, two .in pairs, read only)click 7.1.2 → 8.5.0requirements.in, requirements.txt, requirements-dev.txt3 min 4 s
searx/searx (requirements.txt, read only)flask-babel 2.0.0 → 4.0.0requirements.txt1 min 9 s
hozana/next-translate-routes (Yarn Berry, read only)path-to-regexp 6.2.1 → 9.0.0package.json, yarn.lock3 min 21 s

Your dependabot.yml counts

If the repository has .github/dependabot.yml, the updater's jobs follow it:

  • Applied: ignores (names, version ranges, update types), the versioning strategy, vendor, exclude-paths, the commit message prefix, and insecure-external-code-execution.
  • Conflicts: where teire.yml sets the same thing (commit prefix, target branch, PR limit), teire.yml wins, and the run's timeline says which file decided.
  • Not applied: reviewers, assignees, milestones, labels, schedule, allow, cooldown, rebase-strategy, registries, target-branch, open-pull-requests-limit and multi-ecosystem groups. Teire opens its own pull requests on its own schedule and applies its own cooldown and limits from teire.yml; private registries come with S18.

Where it runs

The updater runs on Teire's own server, in a separate Docker daemon whose containers run under gVisor. gVisor is a kernel in user space: container system calls never reach the host's kernel directly.

BoundaryRule
NetworkThe updater sits on a network with no route out. Only Dependabot's proxy reaches the internet, and the host firewall stops it reaching Teire's apps, its database, cloud metadata and every host port except the job's own event API. Jobs run one at a time, so no job can reach another's
CredentialsA token that can only read this one repository, valid for an hour. The proxy holds it; the updater never does
ImagesDependabot's updater and proxy images, pinned by digest. The CLI checks their build attestations before use
Code executionOff only when the job carries private-registry credentials and the repository hasn't allowed it, as Dependabot does
ResourcesOne job at a time; capped CPU and memory; a 20-minute limit, then everything the job started is removed

The full threat model, and the probe that proves each point, is in the repository.

What Teire still does itself

Some upgrades Dependabot doesn't do, so Teire keeps its own changes for them:

  • Go majors, which move to a new module path (/v5);

  • Cargo crates renamed in Cargo.toml; features a new version removed are dropped from the updater's input first, so it can reach that version (rand 0.10 removed small_rng);

  • Gradle lockfiles: the updater rewrites every entry, so Teire keeps its build-file change and re-locks only the upgraded coordinate;

  • npm aliases (npm:);

  • pnpm lockfiles older than version 6 (pnpm 7), which the updater's pnpm reader fails on although pnpm reads them;

  • Bun's binary bun.lockb, since the updater reads only the text bun.lock (Bun 1.1.39 and later);

  • pylock.toml;

  • a vulnerable transitive package the updater can't move within its parents' ranges. Teire raises it the way each package manager allows, and reads the new lockfile back:

    Package managerHow Teire raises a transitive fix
    npm, pnpmoverrides (pnpm: pnpm.overrides)
    Yarn, Bunresolutions
    uvoverride-dependencies under [tool.uv] in the workspace root's pyproject.toml (uv ignores it in members), after an in-range uv lock
    Gogo get module@version: Go builds with the highest version any module requires
    Gradlea dependency constraint in each module whose lock holds it, then a targeted re-lock
    Mavena <dependencyManagement> pin in the root POM (Maven has no lockfile, so Teire lists transitive versions only where a scan saw them)

    Cargo, Bundler, Poetry, Pipenv and pip-tools have no such override (Cargo's [patch] must point to another source than crates.io), so the run stops, quoting the updater and saying why.

  • the toolchain a new version needs, whichever transaction made the change: when the target's own Gradle Module Metadata asks for a newer Java than the build files declare (Spring Boot 4 asks for 17), the same change raises sourceCompatibility, the toolchain or Maven's release; when its Requires-Python floor is above the project's requires-python, that floor is raised; when its rust_version is above the crate's rust-version, that too. Go needs nothing: go get raises the go line itself, and dependabot-core's Go updater runs go get too. uv and Poetry record requires-python in their lock and refuse a target that needs more, so the raise is for pip-tools, Pipenv and plain requirements. The PR says why for each. When a Gradle plugin refuses the build's Gradle and names the one it needs ("Spring Boot plugin requires Gradle 8.14 or later"), the wrapper's distributionUrl moves to that version, with Gradle's published checksum when the repo pins one, and the step runs again; the PR lists the wrapper change and quotes the refusal.

Then, for every upgrade, the verification runs your own install, typecheck and tests before and after.

Architecture

A Next.js app on Vercel, a worker and Postgres on Coolify, sandboxes on Cloudflare Containers, and Nemotron on Nebius Token Factory, joined by a Postgres outbox.

Verification

Two sandboxes from the same commit, your own checks with the network off, compared check by check and by test counts. Rules decide the evidence, never a model.

On this page

One job per upgradeWhat comes backWhen the updater says noYour dependabot.yml countsWhere it runsWhat Teire still does itself