Skip to content

Commit 45e4ecc

Browse files
docs(templates): one private-index doc instead of the same comment 38 times
Two problems with how the private-index wiring was documented. TEAM-RUNBOOK is not public, so pointing generated agent Dockerfiles at it sends the reader somewhere they cannot go. Replaced with the two references that do resolve: SGPINF-1568, and the PRD, both linked from the new doc. And the explanation was pasted into all 38 templates — roughly twenty comment lines apiece, restating the named-index requirement, the percent-decode, and the UV_INDEX pinning. That is 38 copies to keep in sync, and it buried the four lines of shell that actually do something. Now: PRIVATE_INDEX.md carries it once, and each Dockerfile keeps a short pointer plus the ticket. Net 608 deletions against 190 insertions. The pointer names the doc rather than giving a relative path, deliberately. These Dockerfiles are copied into generated agent repos, where `templates/PRIVATE_INDEX.md` would dangle; SGPINF-1568 resolves from anywhere and the filename is findable. Also dropped stale `agentex-sdk[obs]` references from the template comments — that extra does not exist; sgp-obs is the agent's own dependency. No behaviour change: all 38 templates still render as jinja, all 138 RUN bodies still pass `sh -n`, every template still carries the mount and the ticket reference. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent c0d5fab commit 45e4ecc

39 files changed

Lines changed: 252 additions & 608 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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+
default = true
26+
```
27+
28+
The name must be exactly `scale-pypi`. uv applies `UV_INDEX_SCALE_PYPI_USERNAME` /
29+
`UV_INDEX_SCALE_PYPI_PASSWORD` to the index of that name, so renaming it makes the credentials
30+
silently stop applying. Setting `UV_INDEX_URL` instead does not authenticate a *named* index at
31+
all, and the resolve fails with a 401.
32+
33+
## Three things that are easy to get wrong
34+
35+
**The token arrives percent-encoded.** The buildspec URL-encodes it to embed it in the pip config's
36+
URL userinfo, so a token containing `+`, `/` or `=` arrives as `%2B`, `%2F`, `%3D`. The `uv sync`
37+
templates decode it before exporting it as a password. Passing it through still-encoded sends a
38+
different string and the resolve 401s.
39+
40+
**The credential must not follow project-controlled configuration.** uv binds credentials by index
41+
*name*, and the name-to-URL mapping would otherwise come from the agent's own `pyproject.toml` — so a
42+
project that pointed `scale-pypi` at another host would receive the token. Verified against a local
43+
server: the rogue host receives `Authorization: Basic aws:<token>` and the real index is never
44+
contacted. The templates therefore export `UV_INDEX` to re-bind the name to the URL the *broker*
45+
supplied, which overrides whatever the project declared. With that in place the rogue host is never
46+
contacted. The pinned URL carries no userinfo; the token still travels only in
47+
`UV_INDEX_SCALE_PYPI_PASSWORD`.
48+
49+
The case this defends is not a malicious agent author — they also write the Dockerfile and could read
50+
the mounted secret directly. It is a *contributed* change to a project file, where a one-line URL edit
51+
is far less conspicuous in review than an exfiltration command in a Dockerfile.
52+
53+
**The two template variants work differently, deliberately.**
54+
55+
| Template | Install step | How the credential is supplied |
56+
| --- | --- | --- |
57+
| `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` |
58+
| `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` |
59+
60+
The `requirements.txt` variant does **not** decode the token, and that is the point: it stays inside
61+
the URL, already encoded for exactly that use. Decoding it there would corrupt it. It is also not
62+
exposed to the redirection problem above, because the URL comes wholly from the injected secret.

‎src/agentex/lib/cli/templates/default-claude-code/Dockerfile-uv.j2‎

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,31 +34,12 @@ WORKDIR /app/{{ project_path_from_build_root }}
3434
COPY {{ project_path_from_build_root }}/pyproject.toml ./
3535

3636
# Install dependencies (without project itself, for layer caching)
37-
# Optional private index for `agentex-sdk[obs]` and other Scale-internal packages
38-
# (TEAM-RUNBOOK / SGPINF-1568). Inert unless the control-plane broker injects the
39-
# secret, so local builds, CI builds, and agents that never opt in are unaffected.
37+
# Optional private index for Scale-internal packages such as sgp-obs, injected by the
38+
# control-plane broker (SGPINF-1568). Inert unless the secret is present, so local
39+
# builds, CI builds, and agents that never opt in are unaffected.
4040
#
41-
# To opt in, add this to the agent's pyproject.toml. The index name must be exactly
42-
# `scale-pypi`, because that is what binds the credentials exported below; rename it
43-
# and they silently stop applying. Exporting UV_INDEX_URL instead does not
44-
# authenticate a named index at all, and the resolve 401s.
45-
#
46-
# [[tool.uv.index]]
47-
# name = "scale-pypi"
48-
# url = "<scale-customer-pypi URL from TEAM-RUNBOOK>"
49-
# default = true
50-
#
51-
# UV_INDEX re-binds that name to the URL the BROKER supplied, overriding whatever URL
52-
# the project declared for it. Without this the credential follows the name wherever
53-
# pyproject.toml points it: uv sends the token to any host declared under the name
54-
# `scale-pypi`, so a one-line edit to a project file — far less conspicuous in review
55-
# than a change to this Dockerfile — would exfiltrate it. Verified both ways against a
56-
# local server: without UV_INDEX the rogue host receives `Basic aws:<token>`; with it
57-
# the rogue host is never contacted. The URL carries no userinfo; the token travels
58-
# only in UV_INDEX_SCALE_PYPI_PASSWORD.
59-
#
60-
# The token is percent-decoded on the way out: the buildspec URL-encodes it into the
61-
# pip config's URL userinfo, so a token containing + / = arrives as %2B %2F %3D.
41+
# To opt in, and for why UV_INDEX is pinned to the broker's URL rather than trusting
42+
# the project's, see PRIVATE_INDEX.md in the agentex-sdk CLI templates directory.
6243
RUN --mount=type=cache,target=/root/.cache/uv \
6344
--mount=type=secret,id=codeartifact-pip-conf,required=false \
6445
if [ -s /run/secrets/codeartifact-pip-conf ]; then \

‎src/agentex/lib/cli/templates/default-claude-code/Dockerfile.j2‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,12 @@ COPY {{ project_path_from_build_root }}/requirements.txt /app/{{ project_path_fr
3333

3434
WORKDIR /app/{{ project_path_from_build_root }}
3535

36-
# Optional private index for `agentex-sdk[obs]` and other Scale-internal packages
37-
# (TEAM-RUNBOOK / SGPINF-1568). Inert unless the control-plane broker injects the
38-
# secret, so local builds, CI builds, and agents that never opt in are unaffected.
36+
# Optional private index for Scale-internal packages such as sgp-obs, injected by the
37+
# control-plane broker (SGPINF-1568). Inert unless the secret is present.
3938
#
40-
# This template installs from requirements.txt, so no pyproject.toml is present for
41-
# uv to read a named index out of — unlike Dockerfile-uv.j2, which uses the named
42-
# `scale-pypi` index. The credentialed URL is taken straight from the injected pip
43-
# config instead. That is also why nothing is percent-decoded here: the token stays
44-
# inside the URL, already encoded for exactly that use.
39+
# This variant installs from requirements.txt, so there is no pyproject.toml for uv to
40+
# read a named index out of; the credentialed URL is used directly and is deliberately
41+
# NOT decoded. See PRIVATE_INDEX.md in the agentex-sdk CLI templates directory.
4542
#
4643
# Install the required Python packages
4744
RUN --mount=type=secret,id=codeartifact-pip-conf,required=false \

‎src/agentex/lib/cli/templates/default-codex/Dockerfile-uv.j2‎

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,31 +34,12 @@ WORKDIR /app/{{ project_path_from_build_root }}
3434
COPY {{ project_path_from_build_root }}/pyproject.toml ./
3535

3636
# Install dependencies (without project itself, for layer caching)
37-
# Optional private index for `agentex-sdk[obs]` and other Scale-internal packages
38-
# (TEAM-RUNBOOK / SGPINF-1568). Inert unless the control-plane broker injects the
39-
# secret, so local builds, CI builds, and agents that never opt in are unaffected.
37+
# Optional private index for Scale-internal packages such as sgp-obs, injected by the
38+
# control-plane broker (SGPINF-1568). Inert unless the secret is present, so local
39+
# builds, CI builds, and agents that never opt in are unaffected.
4040
#
41-
# To opt in, add this to the agent's pyproject.toml. The index name must be exactly
42-
# `scale-pypi`, because that is what binds the credentials exported below; rename it
43-
# and they silently stop applying. Exporting UV_INDEX_URL instead does not
44-
# authenticate a named index at all, and the resolve 401s.
45-
#
46-
# [[tool.uv.index]]
47-
# name = "scale-pypi"
48-
# url = "<scale-customer-pypi URL from TEAM-RUNBOOK>"
49-
# default = true
50-
#
51-
# UV_INDEX re-binds that name to the URL the BROKER supplied, overriding whatever URL
52-
# the project declared for it. Without this the credential follows the name wherever
53-
# pyproject.toml points it: uv sends the token to any host declared under the name
54-
# `scale-pypi`, so a one-line edit to a project file — far less conspicuous in review
55-
# than a change to this Dockerfile — would exfiltrate it. Verified both ways against a
56-
# local server: without UV_INDEX the rogue host receives `Basic aws:<token>`; with it
57-
# the rogue host is never contacted. The URL carries no userinfo; the token travels
58-
# only in UV_INDEX_SCALE_PYPI_PASSWORD.
59-
#
60-
# The token is percent-decoded on the way out: the buildspec URL-encodes it into the
61-
# pip config's URL userinfo, so a token containing + / = arrives as %2B %2F %3D.
41+
# To opt in, and for why UV_INDEX is pinned to the broker's URL rather than trusting
42+
# the project's, see PRIVATE_INDEX.md in the agentex-sdk CLI templates directory.
6243
RUN --mount=type=cache,target=/root/.cache/uv \
6344
--mount=type=secret,id=codeartifact-pip-conf,required=false \
6445
if [ -s /run/secrets/codeartifact-pip-conf ]; then \

‎src/agentex/lib/cli/templates/default-codex/Dockerfile.j2‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,12 @@ COPY {{ project_path_from_build_root }}/requirements.txt /app/{{ project_path_fr
3333

3434
WORKDIR /app/{{ project_path_from_build_root }}
3535

36-
# Optional private index for `agentex-sdk[obs]` and other Scale-internal packages
37-
# (TEAM-RUNBOOK / SGPINF-1568). Inert unless the control-plane broker injects the
38-
# secret, so local builds, CI builds, and agents that never opt in are unaffected.
36+
# Optional private index for Scale-internal packages such as sgp-obs, injected by the
37+
# control-plane broker (SGPINF-1568). Inert unless the secret is present.
3938
#
40-
# This template installs from requirements.txt, so no pyproject.toml is present for
41-
# uv to read a named index out of — unlike Dockerfile-uv.j2, which uses the named
42-
# `scale-pypi` index. The credentialed URL is taken straight from the injected pip
43-
# config instead. That is also why nothing is percent-decoded here: the token stays
44-
# inside the URL, already encoded for exactly that use.
39+
# This variant installs from requirements.txt, so there is no pyproject.toml for uv to
40+
# read a named index out of; the credentialed URL is used directly and is deliberately
41+
# NOT decoded. See PRIVATE_INDEX.md in the agentex-sdk CLI templates directory.
4542
#
4643
# Install the required Python packages
4744
RUN --mount=type=secret,id=codeartifact-pip-conf,required=false \

‎src/agentex/lib/cli/templates/default-langgraph/Dockerfile-uv.j2‎

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,31 +30,12 @@ WORKDIR /app/{{ project_path_from_build_root }}
3030
COPY {{ project_path_from_build_root }}/pyproject.toml ./
3131

3232
# Install dependencies (without project itself, for layer caching)
33-
# Optional private index for `agentex-sdk[obs]` and other Scale-internal packages
34-
# (TEAM-RUNBOOK / SGPINF-1568). Inert unless the control-plane broker injects the
35-
# secret, so local builds, CI builds, and agents that never opt in are unaffected.
33+
# Optional private index for Scale-internal packages such as sgp-obs, injected by the
34+
# control-plane broker (SGPINF-1568). Inert unless the secret is present, so local
35+
# builds, CI builds, and agents that never opt in are unaffected.
3636
#
37-
# To opt in, add this to the agent's pyproject.toml. The index name must be exactly
38-
# `scale-pypi`, because that is what binds the credentials exported below; rename it
39-
# and they silently stop applying. Exporting UV_INDEX_URL instead does not
40-
# authenticate a named index at all, and the resolve 401s.
41-
#
42-
# [[tool.uv.index]]
43-
# name = "scale-pypi"
44-
# url = "<scale-customer-pypi URL from TEAM-RUNBOOK>"
45-
# default = true
46-
#
47-
# UV_INDEX re-binds that name to the URL the BROKER supplied, overriding whatever URL
48-
# the project declared for it. Without this the credential follows the name wherever
49-
# pyproject.toml points it: uv sends the token to any host declared under the name
50-
# `scale-pypi`, so a one-line edit to a project file — far less conspicuous in review
51-
# than a change to this Dockerfile — would exfiltrate it. Verified both ways against a
52-
# local server: without UV_INDEX the rogue host receives `Basic aws:<token>`; with it
53-
# the rogue host is never contacted. The URL carries no userinfo; the token travels
54-
# only in UV_INDEX_SCALE_PYPI_PASSWORD.
55-
#
56-
# The token is percent-decoded on the way out: the buildspec URL-encodes it into the
57-
# pip config's URL userinfo, so a token containing + / = arrives as %2B %2F %3D.
37+
# To opt in, and for why UV_INDEX is pinned to the broker's URL rather than trusting
38+
# the project's, see PRIVATE_INDEX.md in the agentex-sdk CLI templates directory.
5839
RUN --mount=type=cache,target=/root/.cache/uv \
5940
--mount=type=secret,id=codeartifact-pip-conf,required=false \
6041
if [ -s /run/secrets/codeartifact-pip-conf ]; then \

‎src/agentex/lib/cli/templates/default-langgraph/Dockerfile.j2‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,12 @@ COPY {{ project_path_from_build_root }}/requirements.txt /app/{{ project_path_fr
2929

3030
WORKDIR /app/{{ project_path_from_build_root }}
3131

32-
# Optional private index for `agentex-sdk[obs]` and other Scale-internal packages
33-
# (TEAM-RUNBOOK / SGPINF-1568). Inert unless the control-plane broker injects the
34-
# secret, so local builds, CI builds, and agents that never opt in are unaffected.
32+
# Optional private index for Scale-internal packages such as sgp-obs, injected by the
33+
# control-plane broker (SGPINF-1568). Inert unless the secret is present.
3534
#
36-
# This template installs from requirements.txt, so no pyproject.toml is present for
37-
# uv to read a named index out of — unlike Dockerfile-uv.j2, which uses the named
38-
# `scale-pypi` index. The credentialed URL is taken straight from the injected pip
39-
# config instead. That is also why nothing is percent-decoded here: the token stays
40-
# inside the URL, already encoded for exactly that use.
35+
# This variant installs from requirements.txt, so there is no pyproject.toml for uv to
36+
# read a named index out of; the credentialed URL is used directly and is deliberately
37+
# NOT decoded. See PRIVATE_INDEX.md in the agentex-sdk CLI templates directory.
4138
#
4239
# Install the required Python packages
4340
RUN --mount=type=secret,id=codeartifact-pip-conf,required=false \

‎src/agentex/lib/cli/templates/default-openai-agents/Dockerfile-uv.j2‎

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,31 +30,12 @@ WORKDIR /app/{{ project_path_from_build_root }}
3030
COPY {{ project_path_from_build_root }}/pyproject.toml ./
3131

3232
# Install dependencies (without project itself, for layer caching)
33-
# Optional private index for `agentex-sdk[obs]` and other Scale-internal packages
34-
# (TEAM-RUNBOOK / SGPINF-1568). Inert unless the control-plane broker injects the
35-
# secret, so local builds, CI builds, and agents that never opt in are unaffected.
33+
# Optional private index for Scale-internal packages such as sgp-obs, injected by the
34+
# control-plane broker (SGPINF-1568). Inert unless the secret is present, so local
35+
# builds, CI builds, and agents that never opt in are unaffected.
3636
#
37-
# To opt in, add this to the agent's pyproject.toml. The index name must be exactly
38-
# `scale-pypi`, because that is what binds the credentials exported below; rename it
39-
# and they silently stop applying. Exporting UV_INDEX_URL instead does not
40-
# authenticate a named index at all, and the resolve 401s.
41-
#
42-
# [[tool.uv.index]]
43-
# name = "scale-pypi"
44-
# url = "<scale-customer-pypi URL from TEAM-RUNBOOK>"
45-
# default = true
46-
#
47-
# UV_INDEX re-binds that name to the URL the BROKER supplied, overriding whatever URL
48-
# the project declared for it. Without this the credential follows the name wherever
49-
# pyproject.toml points it: uv sends the token to any host declared under the name
50-
# `scale-pypi`, so a one-line edit to a project file — far less conspicuous in review
51-
# than a change to this Dockerfile — would exfiltrate it. Verified both ways against a
52-
# local server: without UV_INDEX the rogue host receives `Basic aws:<token>`; with it
53-
# the rogue host is never contacted. The URL carries no userinfo; the token travels
54-
# only in UV_INDEX_SCALE_PYPI_PASSWORD.
55-
#
56-
# The token is percent-decoded on the way out: the buildspec URL-encodes it into the
57-
# pip config's URL userinfo, so a token containing + / = arrives as %2B %2F %3D.
37+
# To opt in, and for why UV_INDEX is pinned to the broker's URL rather than trusting
38+
# the project's, see PRIVATE_INDEX.md in the agentex-sdk CLI templates directory.
5839
RUN --mount=type=cache,target=/root/.cache/uv \
5940
--mount=type=secret,id=codeartifact-pip-conf,required=false \
6041
if [ -s /run/secrets/codeartifact-pip-conf ]; then \

‎src/agentex/lib/cli/templates/default-openai-agents/Dockerfile.j2‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,12 @@ COPY {{ project_path_from_build_root }}/requirements.txt /app/{{ project_path_fr
2929

3030
WORKDIR /app/{{ project_path_from_build_root }}
3131

32-
# Optional private index for `agentex-sdk[obs]` and other Scale-internal packages
33-
# (TEAM-RUNBOOK / SGPINF-1568). Inert unless the control-plane broker injects the
34-
# secret, so local builds, CI builds, and agents that never opt in are unaffected.
32+
# Optional private index for Scale-internal packages such as sgp-obs, injected by the
33+
# control-plane broker (SGPINF-1568). Inert unless the secret is present.
3534
#
36-
# This template installs from requirements.txt, so no pyproject.toml is present for
37-
# uv to read a named index out of — unlike Dockerfile-uv.j2, which uses the named
38-
# `scale-pypi` index. The credentialed URL is taken straight from the injected pip
39-
# config instead. That is also why nothing is percent-decoded here: the token stays
40-
# inside the URL, already encoded for exactly that use.
35+
# This variant installs from requirements.txt, so there is no pyproject.toml for uv to
36+
# read a named index out of; the credentialed URL is used directly and is deliberately
37+
# NOT decoded. See PRIVATE_INDEX.md in the agentex-sdk CLI templates directory.
4138
#
4239
# Install the required Python packages
4340
RUN --mount=type=secret,id=codeartifact-pip-conf,required=false \

0 commit comments

Comments
 (0)