|
| 1 | +# The private package index in scaffold Dockerfiles |
| 2 | + |
| 3 | +Every scaffold Dockerfile mounts a build secret named `codeartifact-pip-conf`. It lets an agent |
| 4 | +install Scale-internal packages — `sgp-obs`, for instance — that are not on public PyPI, without the |
| 5 | +build holding any registry credential of its own. The control-plane broker mints a short-lived |
| 6 | +CodeArtifact token per build and injects it as that secret. |
| 7 | + |
| 8 | +- Design: [Private Package Access for Customer Agents (PRD)](https://app.notion.com/p/Private-Package-Access-for-Customer-Agents-PRD-3ad904d6e6cb802cb091df1c25e230bc) |
| 9 | +- Tracking: [SGPINF-1568](https://linear.app/scale-epd/issue/SGPINF-1568/provide-scale-internal-packages-to-agentex-agents-in-customer) |
| 10 | + |
| 11 | +## It is inert by default |
| 12 | + |
| 13 | +The mount is `required=false` and guarded by `[ -s ... ]`, so with no secret injected the build is |
| 14 | +byte-identical to one without any of this. That covers every local build, every CI build, and every |
| 15 | +agent that never opts in. An empty secret file is skipped too. |
| 16 | + |
| 17 | +## Opting in |
| 18 | + |
| 19 | +Add the index to the agent's `pyproject.toml`: |
| 20 | + |
| 21 | +```toml |
| 22 | +[[tool.uv.index]] |
| 23 | +name = "scale-pypi" |
| 24 | +url = "<the scale-customer-pypi URL>" |
| 25 | +``` |
| 26 | + |
| 27 | +**No `default = true`, deliberately.** An earlier revision of this snippet had it, which was |
| 28 | +misleading in both directions. It would not survive the build — the Dockerfiles export |
| 29 | +`UV_INDEX`, which binds the mirror as a *named* index ahead of public PyPI rather than |
| 30 | +replacing it as the default, and a name rebound that way does not carry the project entry's |
| 31 | +default flag. And it is not the behaviour we want anyway: the mirror exists to supply the |
| 32 | +Scale-internal packages that are not on public PyPI, not to become the sole source for every |
| 33 | +dependency. |
| 34 | + |
| 35 | +So resolution is **mirror first, public PyPI as fallback**. `sgp-obs` can only come from the |
| 36 | +mirror, because it exists nowhere else. An ordinary dependency the mirror happens not to carry |
| 37 | +still resolves from PyPI instead of failing the build, which is what keeps a scaffolded agent |
| 38 | +building when the mirror is incomplete or unreachable. |
| 39 | + |
| 40 | +The name must be exactly `scale-pypi`. uv applies `UV_INDEX_SCALE_PYPI_USERNAME` / |
| 41 | +`UV_INDEX_SCALE_PYPI_PASSWORD` to the index of that name, so renaming it makes the credentials |
| 42 | +silently stop applying. Setting `UV_INDEX_URL` instead does not authenticate a *named* index at |
| 43 | +all, and the resolve fails with a 401. |
| 44 | + |
| 45 | +## Three things that are easy to get wrong |
| 46 | + |
| 47 | +**The token arrives percent-encoded.** The buildspec URL-encodes it to embed it in the pip config's |
| 48 | +URL userinfo, so a token containing `+`, `/` or `=` arrives as `%2B`, `%2F`, `%3D`. The `uv sync` |
| 49 | +templates decode it before exporting it as a password. Passing it through still-encoded sends a |
| 50 | +different string and the resolve 401s. |
| 51 | + |
| 52 | +**The credential must not follow project-controlled configuration.** uv binds credentials by index |
| 53 | +*name*, and the name-to-URL mapping would otherwise come from the agent's own `pyproject.toml` — so a |
| 54 | +project that pointed `scale-pypi` at another host would receive the token. Verified against a local |
| 55 | +server: the rogue host receives `Authorization: Basic aws:<token>` and the real index is never |
| 56 | +contacted. The templates therefore export `UV_INDEX` to re-bind the name to the URL the *broker* |
| 57 | +supplied, which overrides whatever the project declared. With that in place the rogue host is never |
| 58 | +contacted. The pinned URL carries no userinfo; the token still travels only in |
| 59 | +`UV_INDEX_SCALE_PYPI_PASSWORD`. |
| 60 | + |
| 61 | +The case this defends is not a malicious agent author — they also write the Dockerfile and could read |
| 62 | +the mounted secret directly. It is a *contributed* change to a project file, where a one-line URL edit |
| 63 | +is far less conspicuous in review than an exfiltration command in a Dockerfile. |
| 64 | + |
| 65 | +**The two template variants work differently, deliberately.** |
| 66 | + |
| 67 | +| Template | Install step | How the credential is supplied | |
| 68 | +| --- | --- | --- | |
| 69 | +| `Dockerfile-uv.j2` | `uv sync` against the agent's `pyproject.toml` | Named index `scale-pypi`, pinned via `UV_INDEX`, token decoded into `UV_INDEX_SCALE_PYPI_PASSWORD` | |
| 70 | +| `Dockerfile.j2` | `uv pip install -r requirements.txt` | No pyproject is present, so there is no named index to bind to. The credentialed URL is used directly via `UV_DEFAULT_INDEX` | |
| 71 | + |
| 72 | +The `requirements.txt` variant does **not** decode the token, and that is the point: it stays inside |
| 73 | +the URL, already encoded for exactly that use. Decoding it there would corrupt it. It is also not |
| 74 | +exposed to the redirection problem above, because the URL comes wholly from the injected secret. |
0 commit comments