Skip to content

Commit 2bd031e

Browse files
stephen-wang24claude
authored andcommitted
feat(obs): wire sgp-obs from the SDK for traces, metrics and logs (#523)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 966bb8a commit 2bd031e

61 files changed

Lines changed: 3662 additions & 54 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎adk/pyproject.toml‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ dependencies = [
6565
# agentex/lib/* uses `from typing import override` (3.12+) in 19 files.
6666
# The slim agentex-client keeps 3.11 support.
6767
requires-python = ">= 3.12,<4"
68+
6869
classifiers = [
6970
"Typing :: Typed",
7071
"Intended Audience :: Developers",
@@ -76,6 +77,23 @@ classifiers = [
7677
"License :: OSI Approved :: Apache Software License",
7778
]
7879

80+
# No `obs` extra, deliberately — do not add one for sgp-obs.
81+
#
82+
# sgp-obs is not on public PyPI (it is served from Scale's curated CodeArtifact
83+
# mirror), and declaring it in [project.optional-dependencies] makes THIS repo's uv
84+
# workspace unresolvable: `uv sync` re-locks, locking must resolve every declared
85+
# optional dependency of every workspace member, and there is no way to exempt one.
86+
# Measured: `uv lock --check`, `uv sync --all-extras`, plain `uv sync` with no extras,
87+
# and `uv sync --all-extras --no-extra obs` all fail (`--no-extra` filters what is
88+
# installed, not what is resolved); `uv lock` has no `--no-extra`; and
89+
# `[tool.uv] override-dependencies` does not exempt it either. Only `--frozen` works,
90+
# which would leave nobody able to re-lock this repo again.
91+
#
92+
# So the dependency is the AGENT's to declare — `sgp-obs[genai-auto,http,otlp]`
93+
# against the mirror — and the SDK wires it when it is importable. See
94+
# agentex/lib/core/observability/sgp_obs_setup.py; nothing imports sgp_obs outside a
95+
# try, so a plain `pip install agentex-sdk` is unaffected either way.
96+
7997
[project.urls]
8098
Homepage = "https://github.com/scaleapi/scale-agentex-python"
8199
Repository = "https://github.com/scaleapi/scale-agentex-python"
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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.

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,34 @@ 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 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.
40+
#
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.
3743
RUN --mount=type=cache,target=/root/.cache/uv \
44+
--mount=type=secret,id=codeartifact-pip-conf,required=false \
45+
if [ -s /run/secrets/codeartifact-pip-conf ]; then \
46+
export UV_INDEX="scale-pypi=$(sed -n 's#.*index-url = https://aws:[^@]*@\(.*\)#https://\1#p' /run/secrets/codeartifact-pip-conf | head -1)"; \
47+
export UV_INDEX_SCALE_PYPI_USERNAME=aws; \
48+
export UV_INDEX_SCALE_PYPI_PASSWORD="$(sed -n 's#.*index-url = https://aws:\([^@]*\)@.*#\1#p' /run/secrets/codeartifact-pip-conf \
49+
| python3 -c 'import sys,urllib.parse;print(urllib.parse.unquote(sys.stdin.read().strip()))')"; \
50+
fi; \
3851
uv sync --no-install-project --no-dev
3952

4053
# Copy the project code
4154
COPY {{ project_path_from_build_root }}/project ./project
4255

4356
# Install the project
4457
RUN --mount=type=cache,target=/root/.cache/uv \
58+
--mount=type=secret,id=codeartifact-pip-conf,required=false \
59+
if [ -s /run/secrets/codeartifact-pip-conf ]; then \
60+
export UV_INDEX="scale-pypi=$(sed -n 's#.*index-url = https://aws:[^@]*@\(.*\)#https://\1#p' /run/secrets/codeartifact-pip-conf | head -1)"; \
61+
export UV_INDEX_SCALE_PYPI_USERNAME=aws; \
62+
export UV_INDEX_SCALE_PYPI_PASSWORD="$(sed -n 's#.*index-url = https://aws:\([^@]*\)@.*#\1#p' /run/secrets/codeartifact-pip-conf \
63+
| python3 -c 'import sys,urllib.parse;print(urllib.parse.unquote(sys.stdin.read().strip()))')"; \
64+
fi; \
4565
uv sync --no-dev
4666

4767
ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH"

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,19 @@ 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 Scale-internal packages such as sgp-obs, injected by the
37+
# control-plane broker (SGPINF-1568). Inert unless the secret is present.
38+
#
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.
42+
#
3643
# Install the required Python packages
37-
RUN uv pip install --system -r requirements.txt
44+
RUN --mount=type=secret,id=codeartifact-pip-conf,required=false \
45+
if [ -s /run/secrets/codeartifact-pip-conf ]; then \
46+
export UV_DEFAULT_INDEX="$(sed -n 's#^[[:space:]]*index-url[[:space:]]*=[[:space:]]*##p' /run/secrets/codeartifact-pip-conf | head -1)"; \
47+
fi; \
48+
uv pip install --system -r requirements.txt
3849

3950
# Copy the project code
4051
COPY {{ project_path_from_build_root }}/project /app/{{ project_path_from_build_root }}/project

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,34 @@ 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 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.
40+
#
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.
3743
RUN --mount=type=cache,target=/root/.cache/uv \
44+
--mount=type=secret,id=codeartifact-pip-conf,required=false \
45+
if [ -s /run/secrets/codeartifact-pip-conf ]; then \
46+
export UV_INDEX="scale-pypi=$(sed -n 's#.*index-url = https://aws:[^@]*@\(.*\)#https://\1#p' /run/secrets/codeartifact-pip-conf | head -1)"; \
47+
export UV_INDEX_SCALE_PYPI_USERNAME=aws; \
48+
export UV_INDEX_SCALE_PYPI_PASSWORD="$(sed -n 's#.*index-url = https://aws:\([^@]*\)@.*#\1#p' /run/secrets/codeartifact-pip-conf \
49+
| python3 -c 'import sys,urllib.parse;print(urllib.parse.unquote(sys.stdin.read().strip()))')"; \
50+
fi; \
3851
uv sync --no-install-project --no-dev
3952

4053
# Copy the project code
4154
COPY {{ project_path_from_build_root }}/project ./project
4255

4356
# Install the project
4457
RUN --mount=type=cache,target=/root/.cache/uv \
58+
--mount=type=secret,id=codeartifact-pip-conf,required=false \
59+
if [ -s /run/secrets/codeartifact-pip-conf ]; then \
60+
export UV_INDEX="scale-pypi=$(sed -n 's#.*index-url = https://aws:[^@]*@\(.*\)#https://\1#p' /run/secrets/codeartifact-pip-conf | head -1)"; \
61+
export UV_INDEX_SCALE_PYPI_USERNAME=aws; \
62+
export UV_INDEX_SCALE_PYPI_PASSWORD="$(sed -n 's#.*index-url = https://aws:\([^@]*\)@.*#\1#p' /run/secrets/codeartifact-pip-conf \
63+
| python3 -c 'import sys,urllib.parse;print(urllib.parse.unquote(sys.stdin.read().strip()))')"; \
64+
fi; \
4565
uv sync --no-dev
4666

4767
ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH"

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,19 @@ 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 Scale-internal packages such as sgp-obs, injected by the
37+
# control-plane broker (SGPINF-1568). Inert unless the secret is present.
38+
#
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.
42+
#
3643
# Install the required Python packages
37-
RUN uv pip install --system -r requirements.txt
44+
RUN --mount=type=secret,id=codeartifact-pip-conf,required=false \
45+
if [ -s /run/secrets/codeartifact-pip-conf ]; then \
46+
export UV_DEFAULT_INDEX="$(sed -n 's#^[[:space:]]*index-url[[:space:]]*=[[:space:]]*##p' /run/secrets/codeartifact-pip-conf | head -1)"; \
47+
fi; \
48+
uv pip install --system -r requirements.txt
3849

3950
# Copy the project code
4051
COPY {{ project_path_from_build_root }}/project /app/{{ project_path_from_build_root }}/project

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,34 @@ 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 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.
36+
#
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.
3339
RUN --mount=type=cache,target=/root/.cache/uv \
40+
--mount=type=secret,id=codeartifact-pip-conf,required=false \
41+
if [ -s /run/secrets/codeartifact-pip-conf ]; then \
42+
export UV_INDEX="scale-pypi=$(sed -n 's#.*index-url = https://aws:[^@]*@\(.*\)#https://\1#p' /run/secrets/codeartifact-pip-conf | head -1)"; \
43+
export UV_INDEX_SCALE_PYPI_USERNAME=aws; \
44+
export UV_INDEX_SCALE_PYPI_PASSWORD="$(sed -n 's#.*index-url = https://aws:\([^@]*\)@.*#\1#p' /run/secrets/codeartifact-pip-conf \
45+
| python3 -c 'import sys,urllib.parse;print(urllib.parse.unquote(sys.stdin.read().strip()))')"; \
46+
fi; \
3447
uv sync --no-install-project --no-dev
3548

3649
# Copy the project code
3750
COPY {{ project_path_from_build_root }}/project ./project
3851

3952
# Install the project
4053
RUN --mount=type=cache,target=/root/.cache/uv \
54+
--mount=type=secret,id=codeartifact-pip-conf,required=false \
55+
if [ -s /run/secrets/codeartifact-pip-conf ]; then \
56+
export UV_INDEX="scale-pypi=$(sed -n 's#.*index-url = https://aws:[^@]*@\(.*\)#https://\1#p' /run/secrets/codeartifact-pip-conf | head -1)"; \
57+
export UV_INDEX_SCALE_PYPI_USERNAME=aws; \
58+
export UV_INDEX_SCALE_PYPI_PASSWORD="$(sed -n 's#.*index-url = https://aws:\([^@]*\)@.*#\1#p' /run/secrets/codeartifact-pip-conf \
59+
| python3 -c 'import sys,urllib.parse;print(urllib.parse.unquote(sys.stdin.read().strip()))')"; \
60+
fi; \
4161
uv sync --no-dev
4262

4363
ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH"

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,19 @@ 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 Scale-internal packages such as sgp-obs, injected by the
33+
# control-plane broker (SGPINF-1568). Inert unless the secret is present.
34+
#
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.
38+
#
3239
# Install the required Python packages
33-
RUN uv pip install --system -r requirements.txt
40+
RUN --mount=type=secret,id=codeartifact-pip-conf,required=false \
41+
if [ -s /run/secrets/codeartifact-pip-conf ]; then \
42+
export UV_DEFAULT_INDEX="$(sed -n 's#^[[:space:]]*index-url[[:space:]]*=[[:space:]]*##p' /run/secrets/codeartifact-pip-conf | head -1)"; \
43+
fi; \
44+
uv pip install --system -r requirements.txt
3445

3546
# Copy the project code
3647
COPY {{ project_path_from_build_root }}/project /app/{{ project_path_from_build_root }}/project

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,34 @@ 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 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.
36+
#
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.
3339
RUN --mount=type=cache,target=/root/.cache/uv \
40+
--mount=type=secret,id=codeartifact-pip-conf,required=false \
41+
if [ -s /run/secrets/codeartifact-pip-conf ]; then \
42+
export UV_INDEX="scale-pypi=$(sed -n 's#.*index-url = https://aws:[^@]*@\(.*\)#https://\1#p' /run/secrets/codeartifact-pip-conf | head -1)"; \
43+
export UV_INDEX_SCALE_PYPI_USERNAME=aws; \
44+
export UV_INDEX_SCALE_PYPI_PASSWORD="$(sed -n 's#.*index-url = https://aws:\([^@]*\)@.*#\1#p' /run/secrets/codeartifact-pip-conf \
45+
| python3 -c 'import sys,urllib.parse;print(urllib.parse.unquote(sys.stdin.read().strip()))')"; \
46+
fi; \
3447
uv sync --no-install-project --no-dev
3548

3649
# Copy the project code
3750
COPY {{ project_path_from_build_root }}/project ./project
3851

3952
# Install the project
4053
RUN --mount=type=cache,target=/root/.cache/uv \
54+
--mount=type=secret,id=codeartifact-pip-conf,required=false \
55+
if [ -s /run/secrets/codeartifact-pip-conf ]; then \
56+
export UV_INDEX="scale-pypi=$(sed -n 's#.*index-url = https://aws:[^@]*@\(.*\)#https://\1#p' /run/secrets/codeartifact-pip-conf | head -1)"; \
57+
export UV_INDEX_SCALE_PYPI_USERNAME=aws; \
58+
export UV_INDEX_SCALE_PYPI_PASSWORD="$(sed -n 's#.*index-url = https://aws:\([^@]*\)@.*#\1#p' /run/secrets/codeartifact-pip-conf \
59+
| python3 -c 'import sys,urllib.parse;print(urllib.parse.unquote(sys.stdin.read().strip()))')"; \
60+
fi; \
4161
uv sync --no-dev
4262

4363
ENV PATH="/app/{{ project_path_from_build_root }}/.venv/bin:$PATH"

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,19 @@ 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 Scale-internal packages such as sgp-obs, injected by the
33+
# control-plane broker (SGPINF-1568). Inert unless the secret is present.
34+
#
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.
38+
#
3239
# Install the required Python packages
33-
RUN uv pip install --system -r requirements.txt
40+
RUN --mount=type=secret,id=codeartifact-pip-conf,required=false \
41+
if [ -s /run/secrets/codeartifact-pip-conf ]; then \
42+
export UV_DEFAULT_INDEX="$(sed -n 's#^[[:space:]]*index-url[[:space:]]*=[[:space:]]*##p' /run/secrets/codeartifact-pip-conf | head -1)"; \
43+
fi; \
44+
uv pip install --system -r requirements.txt
3445

3546
# Copy the project code
3647
COPY {{ project_path_from_build_root }}/project /app/{{ project_path_from_build_root }}/project

0 commit comments

Comments
 (0)