Skip to content

fix(runner): refresh the trace-export credential every turn - #6135

Closed
mmabrouk wants to merge 5 commits into
mainfrom
fix/otlp-per-turn-credential
Closed

mmabrouk wants to merge 5 commits into
mainfrom
fix/otlp-per-turn-credential

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Aug 20, 2026 •

Copy link
Copy Markdown
Member

The symptom

No agent traces were reaching observability. Every span batch from a warm agent
session was rejected with 401 Unauthorized, and had been for at least a week
before anyone noticed. The export log said Unauthorized and nothing else, so
there was no way to tell a stale credential from a wrong one.

Why it happened

The export bearer was captured once per session. The runner wrote it to the
read-once auth file during environment build only, and Pi's telemetry extension
read that file once at process start.

That bearer lives for minutes. A warm agent session lives for hours. So every
turn after the first few minutes exported with a dead credential.

What changes

The runner refreshes the auth file at turn dispatch, using the incoming
turn's credential, mirroring how the tool relay already starts each turn from the
incoming request rather than from a parked copy. The extension re-reads the file
at before_agent_start, so the read-once-then-delete handling is per turn
instead of per process. A turn whose file is missing keeps the previous bearer,
so a failed write is never worse than today's behavior.

/permissions/check mints a second, turn-sized telemetry token. Per-turn
refresh alone still leaves one turn longer than the credential's life exporting
with an expired bearer. Rather than lengthen the general credential, the endpoint
now issues a token with a trace-ingest scope claim and its own TTL, two hours
by default, overridable with AGENTA_OTLP_TOKEN_TTL_SECONDS. It comes back as
telemetry_credentials, and the SDK threads it to the trace exporter only. The
general 15 minute credential is untouched in lifetime, payload shape, and
behavior.

The longer TTL is safe only because verify_secret_token confines a scoped token
to its scope's allowed paths, trace-ingest to /otlp/v1/traces, and fails
closed on an unknown scope. That check lives inside verify_secret_token rather
than at any call site, so a future caller cannot bypass it.

The export credential gets its own wire field. The runner reads the
Authorization slot to authenticate session-coordination calls, so that slot has
to stay the caller's full-authority credential. Carrying the trace-ingest token
there broke those calls. export_authorization now carries the export-only
credential, and stays unset when no telemetry credential was issued so the export
falls back to the caller's credential.

Every minted token now carries an iat claim alongside exp, both derived
from one instant, so a token's age and its remaining life sum to the lifetime it
was minted with. Without it, export diagnostics could report that a credential
had expired but not how old it was, which is exactly the ambiguity that let a
week of 401s go undiagnosed.

Before and after

Before After
Auth file written once, at environment build at every turn dispatch
Extension reads it once, at process start at every before_agent_start
Export credential the caller's general 15 minute token a trace-ingest scoped token, 2h default
Wire field shared Authorization slot its own export_authorization
A failed export says Unauthorized status, endpoint, and credential age

Compatibility

Both directions degrade safely. An API that omits telemetry_credentials leaves
the SDK on today's fallback to the general credential. An SDK that ignores the
field is unaffected. The runner change ships inside the runner image, so it
deploys atomically.

Trade-off worth reviewing

The telemetry credential is delivered through /permissions/check rather than as
a second token on the /invoke wire, because that endpoint already hands the SDK
its credential and both sides cache them together. A single turn that outlives
even the two hour scoped TTL would still export with an expired bearer.

About the commit list

The bottom two commits, make trace export failures diagnosable and treat blank export credentials as missing, already merged as #6109 into release/v0.112.3.
That release branch has not reached main yet, so they still appear here. They
will deduplicate when the release lands.

Testing

  • Runner unit suite: 130 files, 2224 tests, all passing. Typecheck clean.
  • SDK agent suite: 896 passing. One unrelated failure,
    test_cli_stream_terminal_only_on_empty_request, also fails on plain main;
    this branch touches no streaming or CLI code.
  • API test_auth_secret_token.py: 20 passing.
  • New coverage in services/runner/tests/unit/otlp-auth-per-turn.test.ts and the
    runner and SDK wire-contract tests, plus SDK and API tests for the scoped token
    and the iat claim.

@dosubot dosubot Bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Aug 20, 2026
@vercel

vercel Bot commented Aug 20, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview Aug 21, 2026 10:15pm

Request Review

@dosubot dosubot Bot added backend python Pull requests that update Python code tests typescript Pull requests that update typescript code labels Aug 20, 2026
@coderabbitai

coderabbitai Bot commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added dedicated, scope-limited credentials for telemetry and trace uploads.
    • Traces can now be exported separately from general session authentication.
    • Added configurable telemetry-token expiration, defaulting to two hours.
    • Improved support for tool specifications in local and sandboxed agent runs.
  • Bug Fixes
    • Refreshed telemetry authorization on every run to prevent stale credentials.
    • Improved handling and diagnostics for failed or unauthorized trace exports.
    • Preserved credential behavior across cached and fallback authentication paths.

Walkthrough

The change adds scoped trace-ingest tokens, separates general and export credentials across the SDK and runner, refreshes OTLP credentials per turn, delivers Pi tool specifications through files, and adds OTLP export gating and diagnostics.

Changes

Telemetry authentication and export

Layer / File(s) Summary
Scoped telemetry token issuance and validation
api/oss/src/apis/fastapi/access/router.py, api/oss/src/middlewares/auth.py, api/oss/src/utils/env.py, api/oss/tests/pytest/unit/middlewares/test_auth_secret_token.py
The API issues scoped telemetry tokens with configurable TTL values. Token verification enforces scope paths, distinguishes expiry from malformed tokens, and records structured unauthorized reasons.
SDK credential propagation and tracing context
sdks/python/agenta/sdk/..., sdks/python/oss/tests/pytest/unit/agents/*, services/oss/tests/pytest/unit/agent/test_subscription_status.py
The SDK carries general and telemetry credentials separately through authentication, workflow execution, tracing context, and OTLP wire models. Tests cover fallback, caching, serialization, and secret representation.
Pi asset delivery and runtime setup
services/runner/src/engines/sandbox_agent/pi-assets.ts, services/runner/src/extensions/agenta.ts, services/runner/src/protocol.ts
Pi tool specifications can use a shared file path with local writes or Daytona uploads. Local agent-directory writability is probed, and file-based loading falls back to the legacy environment value.
Per-turn OTLP credential refresh
services/runner/src/engines/sandbox_agent/*, services/runner/src/environment/runtime-lifecycle.ts, services/runner/src/extensions/agenta.ts, services/runner/tests/unit/otlp-auth-per-turn.test.ts
The runner resolves export authorization and rewrites the OTLP auth file for each turn. The extension rereads the file before tracing turns and retains the previous bearer when reads fail.
OTLP export gating and diagnostics
services/runner/src/tracing/otel.ts
Exporter caching is keyed by endpoint and replaced when authorization changes. Credential-less Agenta ingest exports are skipped, and export failures and exceptions produce diagnostic logs.

Estimated code review effort: 4 (Complex) | ~60 minutes

Merge Risk: 🟡 Moderate · up to f20be

The change refreshes telemetry credentials per turn and adds scoped export tokens, but invalid TTL configuration can still make those tokens unusable, malformed credentials may return the wrong error class, and warm sessions may retain exporter resources longer than intended; a failed credential-file re-preparation can also proceed unexpectedly. These bounded correctness and runtime risks should be fixed or explicitly accepted before merge.

Sequence Diagram(s)

sequenceDiagram
  participant PermissionAPI
  participant SDKAuthMiddleware
  participant WorkflowTracing
  participant RunnerRuntime
  participant AgentaExtension
  participant OTLPExporter
  PermissionAPI->>PermissionAPI: mint trace-ingest telemetry credential
  PermissionAPI-->>SDKAuthMiddleware: return credentials and telemetry_credentials
  SDKAuthMiddleware->>WorkflowTracing: store separate credentials
  WorkflowTracing->>RunnerRuntime: serialize authorization and exportAuthorization
  RunnerRuntime->>RunnerRuntime: refresh OTLP auth file per turn
  AgentaExtension->>AgentaExtension: reread export credential before tracing
  AgentaExtension->>OTLPExporter: export spans with export authorization
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 60.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 72 functions across 22 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the primary runner change that refreshes the trace-export credential on every turn.
Description check ✅ Passed The description explains the trace-export failure, the credential refresh, scoped tokens, compatibility, and test coverage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/otlp-per-turn-credential

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mmabrouk

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Already reviewed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: e36ef66b-5939-44af-b60b-13c15bcb346a

📥 Commits

Reviewing files that changed from the base of the PR and between bc2481b and fdef52f.

📒 Files selected for processing (23)
  • api/oss/src/apis/fastapi/access/router.py
  • api/oss/src/middlewares/auth.py
  • api/oss/src/utils/env.py
  • api/oss/tests/pytest/unit/middlewares/test_auth_secret_token.py
  • sdks/python/agenta/sdk/agents/tracing.py
  • sdks/python/agenta/sdk/contexts/tracing.py
  • sdks/python/agenta/sdk/decorators/routing.py
  • sdks/python/agenta/sdk/decorators/running.py
  • sdks/python/agenta/sdk/middlewares/routing/auth.py
  • sdks/python/oss/tests/pytest/unit/agents/test_tracing.py
  • sdks/python/oss/tests/pytest/unit/test_auth_middleware_credentials.py
  • services/runner/src/engines/sandbox_agent/pi-assets.ts
  • services/runner/src/engines/sandbox_agent/run-turn.ts
  • services/runner/src/environment/runtime-lifecycle.ts
  • services/runner/src/extensions/agenta.ts
  • services/runner/src/tracing/export-diagnostics.ts
  • services/runner/src/tracing/otel.ts
  • services/runner/tests/setup/hermetic-env.ts
  • services/runner/tests/unit/otel-export-diagnostics.test.ts
  • services/runner/tests/unit/otel-flush-export.test.ts
  • services/runner/tests/unit/otel-trace-target-attribution.test.ts
  • services/runner/tests/unit/otlp-auth-per-turn.test.ts
  • services/runner/tests/utils/otel-export.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 3 remain after this review.

Comment on lines 963 to 979
except DecodeError as exc:
raise UnauthorizedException() from exc
log.debug(
"[auth] secret token unauthorized",
path=request.url.path,
method=request.method,
reason="invalid_token",
)

except ExpiredSignatureError as exc:
raise UnauthorizedException() from exc
raise UnauthorizedException(reason="invalid_token") from exc

except HTTPException as exc:
# Scope rejections (and any other deliberate 401/5xx) must reach the client
# with their status — never collapsed into a 500 by the catch-all below.
raise exc

except Exception as exc: # pylint: disable=bare-except
raise InternalServerErrorException() from exc

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 3 'from jwt.*exceptions|InvalidTokenError|DecodeError|ExpiredSignatureError' \
  api/oss/src/middlewares/auth.py

rg -n -C 4 'invalid_token|unsupported|algorithm|verify_secret_token' \
  api/oss/tests/pytest/unit/middlewares/test_auth_secret_token.py

Repository: Agenta-AI/agenta

Length of output: 4390


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- auth.py imports and verifier ---'
sed -n '1,25p;900,990p' api/oss/src/middlewares/auth.py

printf '%s\n' '--- secret-token tests ---'
sed -n '1,320p' api/oss/tests/pytest/unit/middlewares/test_auth_secret_token.py

printf '%s\n' '--- PyJWT dependency declarations ---'
rg -n -i 'pyjwt|jwt' api/oss pyproject.toml poetry.lock requirements*.txt 2>/dev/null || true

printf '%s\n' '--- exception definitions or vendored sources ---'
rg -n 'class (InvalidTokenError|DecodeError|InvalidAlgorithmError|InvalidIssuedAtError)|InvalidTokenError' . \
  -g '*.py' -g '!node_modules' -g '!dist' -g '!build' 2>/dev/null || true

Repository: Agenta-AI/agenta

Length of output: 15387


🌐 Web query:

PyJWT stable API exceptions InvalidTokenError DecodeError InvalidAlgorithmError InvalidIssuedAtError inheritance

💡 Result:

In the PyJWT library, InvalidTokenError is the base exception class for all errors related to a token being invalid during the decode process [1][2]. Both DecodeError and other specific validation errors, such as InvalidAlgorithmError and InvalidIssuedAtError, are subclasses of InvalidTokenError [1][3][4]. The exception hierarchy is structured as follows: - PyJWTError: The top-level base class for all exceptions in the library [1][5]. - InvalidTokenError: A subclass of PyJWTError and the specific base class for issues encountered when calling jwt.decode [1][2]. - DecodeError: A subclass of InvalidTokenError, typically raised when a token cannot be parsed or decoded (e.g., malformed token structure) [1][6][3]. - InvalidAlgorithmError: A subclass of InvalidTokenError, raised when the algorithm specified in the token header is not supported or does not match the expected algorithm [3][7]. - InvalidIssuedAtError: A subclass of InvalidTokenError, raised when the 'iat' (Issued At) claim validation fails (e.g., the claim is in the future) [3][4]. Because these are all subclasses of InvalidTokenError, catching this single exception is an effective way to handle various token validation failures [2]. For a broader catch-all that includes any library-related errors (including configuration or key-related issues), one may catch the parent PyJWTError [5].

Citations:


Map PyJWT validation failures to invalid_token.

InvalidAlgorithmError, InvalidIssuedAtError, and other InvalidTokenError subclasses bypass the DecodeError handler and become InternalServerErrorException. Import and catch InvalidTokenError after ExpiredSignatureError. Add a regression test for an unsupported JWT algorithm.

🧰 Tools
🪛 ast-grep (0.45.1)

[warning] 963-968: Logging request-derived input unsanitized allows log forging (CRLF injection); strip newlines / encode the value before logging.
Context: log.debug(
"[auth] secret token unauthorized",
path=request.url.path,
method=request.method,
reason="invalid_token",
)
Note: [CWE-117] Improper Output Neutralization for Logs. OWASP A09:2021 Security Logging and Monitoring Failures.

(log-injection-python)

Comment thread api/oss/src/utils/env.py
Comment on lines +364 to +366
token_ttl_seconds: int = int(
os.getenv("AGENTA_OTLP_TOKEN_TTL_SECONDS") or str(2 * 60 * 60)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject non-positive telemetry token lifetimes.

A value of 0 creates a token that expires at issuance. A negative value creates an already expired token. Both values cause every telemetry export to fail after configuration is loaded.

Validate AGENTA_OTLP_TOKEN_TTL_SECONDS as greater than zero before assigning token_ttl_seconds.

Comment on lines +352 to +382
const target =
(runId ? byRun?.get(runId) : undefined) ?? defaultTarget();
const problem = {
traceId,
endpoint: target.endpoint,
authorization: target.authorization,
spans: group.length,
redactors,
};
if (!target.authorization?.trim() && isAgentaIngest(target.endpoint)) {
// Agenta's ingest rejects an unauthenticated export, so sending one buys nothing and
// reports as a 401 that reads like a BAD credential. Say the credential is missing
// instead, and drop the batch here.
logExportProblem({ ...problem, outcome: "skipped" });
return Promise.resolve();
}
return new Promise<void>((resolve) => {
try {
getExporter(target).export(orderParentFirst(group), (result) => {
if (result.code === ExportResultCode.FAILED)
console.error(
"otel: trace export failed",
traceId,
result.error,
);
logExportProblem({
...problem,
outcome: "failed",
error: result.error,
});
resolve();
});
} catch (err) {
// A synchronous export throw (e.g. misconfigured exporter) must stay best-effort:
// flush() is awaited without a catch, so a reject here would break the run.
console.error("otel: trace export threw", traceId, err);
logExportProblem({ ...problem, outcome: "threw", error: err });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Bound the exporter cache by credential lifetime.

Line 370 calls getExporter(target). exporterCache keys entries with the complete authorization value. Each refreshed trace-ingest token creates a new exporter entry. The cache releases entries only during TraceBatchProcessor.shutdown().

A warm runner can retain expired bearer values and exporter instances for every credential refresh. Remove exporters after their final export, or add bounded eviction that calls shutdown() for evicted exporters.

@github-actions

github-actions Bot commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Status Destroyed (PR closed)

Updated at 2026-08-22T11:15:40.767Z

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: fc24dc2a-265f-446b-ba60-6523a50a19e4

📥 Commits

Reviewing files that changed from the base of the PR and between fdef52f and 00db686.

📒 Files selected for processing (11)
  • sdks/python/agenta/sdk/agents/dtos.py
  • sdks/python/agenta/sdk/agents/wire_models.py
  • sdks/python/oss/tests/pytest/unit/agents/golden/run_request.pi_core.json
  • sdks/python/oss/tests/pytest/unit/agents/test_dtos_capabilities_events.py
  • sdks/python/oss/tests/pytest/unit/agents/test_dtos_secret_repr.py
  • sdks/python/oss/tests/pytest/unit/agents/test_wire_contract.py
  • services/runner/src/engines/sandbox_agent/pi-assets.ts
  • services/runner/src/engines/sandbox_agent/runtime-policy.ts
  • services/runner/src/protocol.ts
  • services/runner/src/tracing/otel.ts
  • services/runner/tests/unit/wire-contract.test.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 1 remains after this review.

Comment on lines +197 to +199
export_authorization: Optional[str] = Field(
default=None, alias="exportAuthorization"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 \
  'class WireOtlpExporter|export_authorization|repr=False|WireOtlpExporter\(' \
  sdks/python/agenta/sdk/agents/wire_models.py \
  sdks/python/oss/tests/pytest/unit/agents/test_dtos_secret_repr.py

Repository: Agenta-AI/agenta

Length of output: 3928


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- wire model imports and base class ---'
sed -n '1,80p' sdks/python/agenta/sdk/agents/wire_models.py
printf '%s\n' '--- related secret fields and tests ---'
rg -n -C 5 'class TraceContext|class _WireModel|export_authorization|test_.*repr|WireOtlpExporter' \
  sdks/python/agenta/sdk/agents sdks/python/oss/tests/pytest/unit/agents
printf '%s\n' '--- Pydantic dependency declarations ---'
rg -n -C 2 'pydantic' sdks/python -g 'pyproject.toml' -g 'requirements*.txt' -g 'poetry.lock' -g 'uv.lock'

Repository: Agenta-AI/agenta

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pydantic import BaseModel, ConfigDict, Field

class WireOtlpExporter(BaseModel):
    model_config = ConfigDict(populate_by_name=True, extra="allow")
    export_authorization: str | None = Field(
        default=None, alias="exportAuthorization"
    )

class SafeWireOtlpExporter(BaseModel):
    model_config = ConfigDict(populate_by_name=True, extra="allow")
    export_authorization: str | None = Field(
        default=None, alias="exportAuthorization", repr=False
    )

secret = "Secret trace-ingest-token"
unsafe = WireOtlpExporter(exportAuthorization=secret)
safe = SafeWireOtlpExporter(exportAuthorization=secret)

print(f"pydantic={__import__('pydantic').VERSION}")
print(f"unsafe_repr={repr(unsafe)}")
print(f"unsafe_secret_in_repr={secret in repr(unsafe)}")
print(f"safe_repr={repr(safe)}")
print(f"safe_secret_in_repr={secret in repr(safe)}")
print(f"wire_value_preserved={unsafe.export_authorization == secret == safe.export_authorization}")
PY

Repository: Agenta-AI/agenta

Length of output: 275


🌐 Web query:

Pydantic 2 Field repr=False BaseModel __repr__ documentation

💡 Result:

In Pydantic V2, the repr parameter in the Field function controls whether a specific field is included in the model's generated repr output [1][2]. When you define a field using Field(repr=False), that field is excluded from the string representation of the model instance [1][3]. By default, repr is set to True for all fields [1][4]. Example usage: from pydantic import BaseModel, Field class User(BaseModel): name: str = Field(repr=True) age: int = Field(repr=False) user = User(name='John', age=42) print(repr(user)) # Output: User(name='John') For computed fields, you can similarly use the repr parameter within the @computed_field decorator [2][5]. For public computed fields, repr defaults to True, while for private computed fields, it defaults to False [2][5]. If you need more advanced customization of the model's representation beyond simply including or excluding fields, you can override the repr or str methods directly on your BaseModel class [6]. For further control over how specific complex types appear in the representation, wrapping them in a RootModel and defining a custom repr on that model is a recommended approach [7].

Citations:


Hide export_authorization from WireOtlpExporter representations. Set repr=False on the field and add a test that the export credential does not appear in repr(WireOtlpExporter(...)).

@mmabrouk
mmabrouk changed the base branch from main to release/v0.112.3 August 20, 2026 19:18
mmabrouk added a commit that referenced this pull request Aug 21, 2026
…call site

CodeRabbit review round on #6164:

- Webhook secret rotation and EE SSO provider updates built the parent SecretDTO for
  UpdateSecretDTO.secret, which pydantic rejects; both now build UpdateSecretPayloadDTO.
  A source walk pins every call site, and the webhook rotation path gets real coverage.
- The forged-token test asserts UnauthorizedException/401/invalid_token instead of any
  exception, and is renamed for what it forges (a foreign-signed token, not an unsigned one).
- The cache-key tenancy test no longer reads as if the truncated-id collision were an
  invariant: it records the tracked exception (issue #6166) that flipping the default needs
  invalidate_cache to carry the flag and a deploy plan for the evaluation lock keys.
- The vault route fake now models production invalidation faithfully: it reaches neither
  full-id namespace, so the stale-snapshot test passes on the generation, not on the fake.
- The design note names #6065 (frontend refactor, merged) and #6135 (stacking base)
  separately, and records the cache-tenancy gap.
mmabrouk added a commit that referenced this pull request Aug 21, 2026
…call site

CodeRabbit review round on #6164:

- Webhook secret rotation and EE SSO provider updates built the parent SecretDTO for
  UpdateSecretDTO.secret, which pydantic rejects; both now build UpdateSecretPayloadDTO.
  A source walk pins every call site, and the webhook rotation path gets real coverage.
- The forged-token test asserts UnauthorizedException/401/invalid_token instead of any
  exception, and is renamed for what it forges (a foreign-signed token, not an unsigned one).
- The cache-key tenancy test no longer reads as if the truncated-id collision were an
  invariant: it records the tracked exception (issue #6166) that flipping the default needs
  invalidate_cache to carry the flag and a deploy plan for the evaluation lock keys.
- The vault route fake now models production invalidation faithfully: it reaches neither
  full-id namespace, so the stale-snapshot test passes on the generation, not on the fake.
- The design note names #6065 (frontend refactor, merged) and #6135 (stacking base)
  separately, and records the cache-tenancy gap.
mmabrouk added a commit that referenced this pull request Aug 21, 2026
…call site

CodeRabbit review round on #6164:

- Webhook secret rotation and EE SSO provider updates built the parent SecretDTO for
  UpdateSecretDTO.secret, which pydantic rejects; both now build UpdateSecretPayloadDTO.
  A source walk pins every call site, and the webhook rotation path gets real coverage.
- The forged-token test asserts UnauthorizedException/401/invalid_token instead of any
  exception, and is renamed for what it forges (a foreign-signed token, not an unsigned one).
- The cache-key tenancy test no longer reads as if the truncated-id collision were an
  invariant: it records the tracked exception (issue #6166) that flipping the default needs
  invalidate_cache to carry the flag and a deploy plan for the evaluation lock keys.
- The vault route fake now models production invalidation faithfully: it reaches neither
  full-id namespace, so the stale-snapshot test passes on the generation, not on the fake.
- The design note names #6065 (frontend refactor, merged) and #6135 (stacking base)
  separately, and records the cache-tenancy gap.
@mmabrouk
mmabrouk changed the base branch from release/v0.112.3 to main August 21, 2026 21:02
mmabrouk added a commit that referenced this pull request Aug 21, 2026
…call site

CodeRabbit review round on #6164:

- Webhook secret rotation and EE SSO provider updates built the parent SecretDTO for
  UpdateSecretDTO.secret, which pydantic rejects; both now build UpdateSecretPayloadDTO.
  A source walk pins every call site, and the webhook rotation path gets real coverage.
- The forged-token test asserts UnauthorizedException/401/invalid_token instead of any
  exception, and is renamed for what it forges (a foreign-signed token, not an unsigned one).
- The cache-key tenancy test no longer reads as if the truncated-id collision were an
  invariant: it records the tracked exception (issue #6166) that flipping the default needs
  invalidate_cache to carry the flag and a deploy plan for the evaluation lock keys.
- The vault route fake now models production invalidation faithfully: it reaches neither
  full-id namespace, so the stale-snapshot test passes on the generation, not on the fake.
- The design note names #6065 (frontend refactor, merged) and #6135 (stacking base)
  separately, and records the cache-tenancy gap.
@mmabrouk

Copy link
Copy Markdown
Member Author

The services unit suite was red on every PR stacked above this one, from this lane's own change: get_credentials now returns (credentials, telemetry_credentials), but services/oss/tests/pytest/unit/agent/test_subscription_status.py's authed fixture still returned a single string, so the middleware's two-value unpack raised ValueError: too many values to unpack and every authenticated request in that suite became a 500. Five tests. Fixed here — the fixture returns the pair — and the suite goes from 52 passed / 5 failed to 158 passed. Lane tip 5004379e9c.

Separately, and worth knowing before this merges: git log --cherry-mark origin/main...fix/otlp-export-observability returns nothing, so #6109's content is already in main via release/v0.112.3 (main fc71b7cffe). This branch still carries those commits as ancestors, which is why GitHub reports CONFLICTING against main in three files — api/oss/src/middlewares/auth.py, api/oss/tests/pytest/unit/middlewares/test_auth_secret_token.py (add/add), and services/runner/src/environment/runtime-lifecycle.ts (against 089a2431c8). A rebase onto current main that drops the already-merged ancestors is queued.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: fb0894ec-9d4d-452e-b821-5751e9254b95

📥 Commits

Reviewing files that changed from the base of the PR and between 00db686 and 5004379.

📒 Files selected for processing (1)
  • services/oss/tests/pytest/unit/agent/test_subscription_status.py

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.

Comment on lines +432 to +433
# `get_credentials` returns the caller's credential and a separate trace-ingest
# one; a single value here unpacks as two characters in the middleware.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Describe the unpacking failure accurately.

Returning the old string would not unpack as two characters. Python would iterate the full string and raise ValueError: too many values to unpack (expected 2). Update the comment to document the actual failure mode.

Trace exports from warm agent sessions failed with 401 for every turn after the
first few minutes, so no agent traces reached observability at all. The export
bearer was captured ONCE per session: the runner wrote it to the read-once auth
file only during environment build, and Pi's telemetry extension read it once at
process start. The bearer lives minutes; a warm session lives hours.

Two changes, either of which alone leaves a gap.

Per-turn refresh. The runner now rewrites the auth file at TURN DISPATCH with the
incoming turn's credential, mirroring how the tool relay already starts each turn
from the incoming request's toolCallback rather than the parked copy. The
extension re-reads the file at before_agent_start, keeping the
read-once-then-delete handling per turn instead of per process. A turn whose file
is absent keeps the previous bearer, so a failed write is never worse than today.
This ships inside the runner image, so it deploys atomically.

Turn-sized telemetry credential. Per-turn refresh still leaves a single turn
longer than the credential's life exporting with an expired bearer. Rather than
lengthen the general credential, /permissions/check now mints a SECOND token
carrying a scope claim of trace-ingest with its own TTL (default 2h, override
AGENTA_OTLP_TOKEN_TTL_SECONDS) and returns it as telemetry_credentials. The SDK
threads it to the trace exporter only; the general 15-minute credential is
untouched in lifetime, payload shape, and behavior.

The long TTL is safe only because verify_secret_token confines a scoped token to
its scope's allowed paths (trace-ingest to /otlp/v1/traces) and fails closed on an
unknown scope. That check lives inside verify_secret_token, not at any call site,
so no future caller can bypass it.

Both directions degrade safely: an API that omits telemetry_credentials leaves the
SDK on today's fallback to the general credential, and an SDK that ignores the
field is unaffected.

Trade-off: the telemetry credential is delivered through /permissions/check rather
than as a second token on the /invoke wire, because that endpoint already hands the
SDK its credential and both sides cache it together. A single turn that outlives
even the 2h scoped TTL would still export with an expired bearer.
Every minted token now carries an `iat` claim alongside `exp`, both derived from
one instant so a token's age and its remaining life sum to the lifetime it was
minted with.

Without it, export-failure diagnostics can report that a credential expired but not
how OLD it is, which is the difference between a stale credential and a wrong one —
the ambiguity that let a week of 401s go undiagnosed.
…edential

The runner reads the Authorization slot to authenticate session-coordination
calls, so that slot has to stay the caller's full-authority credential. Carrying
the trace-ingest token there broke those calls. Give the export-only credential
its own export_authorization field, and leave it unset when no telemetry
credential was issued so the export falls back to the caller's credential.
Defines export_authorization across the wire: the runner protocol type, the
runner's exportAuthorizationOf accessor and its use in the OTLP exporter and
auth-file writer, and the SDK's TraceContext DTO and wire model, with the
contract tests and golden request updated to match.

Without this the export-only credential has nowhere to ride, and the callers
added in the preceding commits reference a field that does not exist.
…xture

This lane made get_credentials return the caller's credential and a separate trace-ingest one, but the fixture that stands in for it still returned a single string, so the middleware's two-value unpack turned every authenticated request in that suite into a 500. Five tests were failing on every PR stacked above this one.
@mmabrouk
mmabrouk force-pushed the fix/otlp-per-turn-credential branch from 5004379 to f20bede Compare August 21, 2026 22:14
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Aug 21, 2026
mmabrouk added a commit that referenced this pull request Aug 21, 2026
…call site

CodeRabbit review round on #6164:

- Webhook secret rotation and EE SSO provider updates built the parent SecretDTO for
  UpdateSecretDTO.secret, which pydantic rejects; both now build UpdateSecretPayloadDTO.
  A source walk pins every call site, and the webhook rotation path gets real coverage.
- The forged-token test asserts UnauthorizedException/401/invalid_token instead of any
  exception, and is renamed for what it forges (a foreign-signed token, not an unsigned one).
- The cache-key tenancy test no longer reads as if the truncated-id collision were an
  invariant: it records the tracked exception (issue #6166) that flipping the default needs
  invalidate_cache to carry the flag and a deploy plan for the evaluation lock keys.
- The vault route fake now models production invalidation faithfully: it reaches neither
  full-id namespace, so the stale-snapshot test passes on the generation, not on the fake.
- The design note names #6065 (frontend refactor, merged) and #6135 (stacking base)
  separately, and records the cache-tenancy gap.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
services/runner/src/engines/sandbox_agent/pi-assets.ts (1)

775-782: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Propagate agentDirWritable during re-preparation. reprepareLocalPiAssets returns only .dir, so a failed second write probe is discarded and the run can continue toward harness startup. Enforce PI_AGENT_DIR_UNWRITABLE_MESSAGE on this path.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 635716ad-a151-415e-82da-1d54629431b5

📥 Commits

Reviewing files that changed from the base of the PR and between 5004379 and f20bede.

📒 Files selected for processing (4)
  • api/oss/src/utils/env.py
  • services/runner/src/engines/sandbox_agent/pi-assets.ts
  • services/runner/src/environment/runtime-lifecycle.ts
  • services/runner/src/extensions/agenta.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.

@mmabrouk

Copy link
Copy Markdown
Member Author

Closed by owner decision: the per-turn credential refresh is a workaround. Trace export ownership moves to the runner; design in #6211.

@mmabrouk mmabrouk closed this Aug 22, 2026
mmabrouk added a commit that referenced this pull request Aug 22, 2026
…call site

CodeRabbit review round on #6164:

- Webhook secret rotation and EE SSO provider updates built the parent SecretDTO for
  UpdateSecretDTO.secret, which pydantic rejects; both now build UpdateSecretPayloadDTO.
  A source walk pins every call site, and the webhook rotation path gets real coverage.
- The forged-token test asserts UnauthorizedException/401/invalid_token instead of any
  exception, and is renamed for what it forges (a foreign-signed token, not an unsigned one).
- The cache-key tenancy test no longer reads as if the truncated-id collision were an
  invariant: it records the tracked exception (issue #6166) that flipping the default needs
  invalidate_cache to carry the flag and a deploy plan for the evaluation lock keys.
- The vault route fake now models production invalidation faithfully: it reaches neither
  full-id namespace, so the stale-snapshot test passes on the generation, not on the fake.
- The design note names #6065 (frontend refactor, merged) and #6135 (stacking base)
  separately, and records the cache-tenancy gap.
mmabrouk added a commit that referenced this pull request Aug 22, 2026
…call site

CodeRabbit review round on #6164:

- Webhook secret rotation and EE SSO provider updates built the parent SecretDTO for
  UpdateSecretDTO.secret, which pydantic rejects; both now build UpdateSecretPayloadDTO.
  A source walk pins every call site, and the webhook rotation path gets real coverage.
- The forged-token test asserts UnauthorizedException/401/invalid_token instead of any
  exception, and is renamed for what it forges (a foreign-signed token, not an unsigned one).
- The cache-key tenancy test no longer reads as if the truncated-id collision were an
  invariant: it records the tracked exception (issue #6166) that flipping the default needs
  invalidate_cache to carry the flag and a deploy plan for the evaluation lock keys.
- The vault route fake now models production invalidation faithfully: it reaches neither
  full-id namespace, so the stale-snapshot test passes on the generation, not on the fake.
- The design note names #6065 (frontend refactor, merged) and #6135 (stacking base)
  separately, and records the cache-tenancy gap.
mmabrouk added a commit that referenced this pull request Aug 23, 2026
…call site

CodeRabbit review round on #6164:

- Webhook secret rotation and EE SSO provider updates built the parent SecretDTO for
  UpdateSecretDTO.secret, which pydantic rejects; both now build UpdateSecretPayloadDTO.
  A source walk pins every call site, and the webhook rotation path gets real coverage.
- The forged-token test asserts UnauthorizedException/401/invalid_token instead of any
  exception, and is renamed for what it forges (a foreign-signed token, not an unsigned one).
- The cache-key tenancy test no longer reads as if the truncated-id collision were an
  invariant: it records the tracked exception (issue #6166) that flipping the default needs
  invalidate_cache to carry the flag and a deploy plan for the evaluation lock keys.
- The vault route fake now models production invalidation faithfully: it reaches neither
  full-id namespace, so the stale-snapshot test passes on the generation, not on the fake.
- The design note names #6065 (frontend refactor, merged) and #6135 (stacking base)
  separately, and records the cache-tenancy gap.

This branch was successfully deployed

1 active deployment
Preview — f20bede3 Deployed Aug 21, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend python Pull requests that update Python code size:L This PR changes 100-499 lines, ignoring generated files. tests typescript Pull requests that update typescript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant