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:
| Job | What it asks for |
|---|---|
| Version update | Exactly the target version Teire chose: only this dependency, nothing above the target, no extra cooldown (Teire already applied the repository's) |
| Security update | The 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.tomlpins 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 back | What Teire does |
|---|---|
| A change at the target | Applies it, then runs the verification |
| A change below the target | Makes 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 one | The 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 stop | Opens one issue for the package, quoting the updater's words. The next pull request for that package closes it |
| Nothing to change | Ends the run with the reason |
| The updater is unreachable | Tries 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:
| Repository | Upgrade | Files | Time |
|---|---|---|---|
teire-dev/serve-handler (Yarn 1) | path-to-regexp 3.3.0 → 9.0.0 | package.json, yarn.lock | 1 min 46 s |
teire-dev/serve-handler (Yarn 1) | minimatch 3.1.5 → 10.2.6 | package.json, yarn.lock | 2 min 17 s |
teire-dev/examples (npm) | zod 3.25.76 → 4.6.7 | package.json, package-lock.json | 42 s |
teire-dev/examples (uv, two roots) | anthropic 0.125.0 → 1.17.0 | pyproject.toml, uv.lock | 1 min 46 s |
teire-dev/devchat (Poetry) | pydantic 1.10.16 → 2.10.6 | pyproject.toml, poetry.lock | 2 min 51 s |
teire-dev/examples (Go) | golang.org/x/text 0.3.5 → 0.43.0 | go.mod, go.sum | 1 min 16 s |
teire-dev/diskonaut (Cargo) | crossterm 0.17.7 → 0.18.0 | Cargo.toml, Cargo.lock | 44 s |
teire-dev/examples (Bundler, three roots) | faraday 1.10.6 → 2.15.0 | Gemfile, Gemfile.lock | 1 min 50 s |
teire-dev/coal (gemspec) | faraday 1.10.3 → 1.10.6 | Gemfile.lock, *.gemspec | 1 min |
teire-dev/examples (Gradle, catalog and lockfile) | okhttp 3.14.9 → 5.5.0 | libs.versions.toml, gradle.lockfile | 3 min 35 s |
teire-dev/spring-boot-realworld-example-app (Gradle) | jjwt-api 0.11.2 → 0.13.0 | build.gradle | 34 s |
teire-dev/examples (Maven) | guava 19.0 → 23.0, not 33.7.2-jre: the updater keeps the version's suffix | pom.xml | 51 s |
teire-dev/examples (Cargo, removed feature dropped first) | rand 0.8.8 → 0.10.3 | Cargo.toml, Cargo.lock | 36 s |
teire-dev/coal (Bundler, security) | rexml 3.2.6 → 3.3.9 | Gemfile.lock | 56 s |
teire-dev/ai-test-reporter (npm, transitive security) | fast-uri 3.1.8 → 3.1.9 | package-lock.json | 1 min 27 s |
psf/requests-html (Pipenv, read only) | lxml 4.9.2 → 6.1.3 | Pipfile.lock | 2 min 49 s |
Nekmo/dirhunt (pip-compile, two .in pairs, read only) | click 7.1.2 → 8.5.0 | requirements.in, requirements.txt, requirements-dev.txt | 3 min 4 s |
searx/searx (requirements.txt, read only) | flask-babel 2.0.0 → 4.0.0 | requirements.txt | 1 min 9 s |
hozana/next-translate-routes (Yarn Berry, read only) | path-to-regexp 6.2.1 → 9.0.0 | package.json, yarn.lock | 3 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, andinsecure-external-code-execution. - Conflicts: where
teire.ymlsets the same thing (commit prefix, target branch, PR limit),teire.ymlwins, 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-limitand multi-ecosystem groups. Teire opens its own pull requests on its own schedule and applies its own cooldown and limits fromteire.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.
| Boundary | Rule |
|---|---|
| Network | The 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 |
| Credentials | A token that can only read this one repository, valid for an hour. The proxy holds it; the updater never does |
| Images | Dependabot's updater and proxy images, pinned by digest. The CLI checks their build attestations before use |
| Code execution | Off only when the job carries private-registry credentials and the repository hasn't allowed it, as Dependabot does |
| Resources | One 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 removedsmall_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 textbun.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 manager How Teire raises a transitive fix npm, pnpm overrides(pnpm:pnpm.overrides)Yarn, Bun resolutionsuv override-dependenciesunder[tool.uv]in the workspace root'spyproject.toml(uv ignores it in members), after an in-rangeuv lockGo go get module@version: Go builds with the highest version any module requiresGradle a dependency constraint in each module whose lock holds it, then a targeted re-lock Maven a <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 itsRequires-Pythonfloor is above the project'srequires-python, that floor is raised; when itsrust_versionis above the crate'srust-version, that too. Go needs nothing:go getraises thegoline itself, and dependabot-core's Go updater runsgo gettoo. uv and Poetry recordrequires-pythonin 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'sdistributionUrlmoves 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.