Skip to content

[AGE-4000] fix(runner): rebuild agent mounts before STS credentials expire - #5520

Open
mmabrouk wants to merge 5 commits into
mainfrom
worktree-fix-sts-mount-expiry-5516
Open

[AGE-4000] fix(runner): rebuild agent mounts before STS credentials expire#5520
mmabrouk wants to merge 5 commits into
mainfrom
worktree-fix-sts-mount-expiry-5516

Conversation

@mmabrouk

Copy link
Copy Markdown
Member

Context

Fixes #5516. Any agent conversation that stayed active past 15 minutes hit a wall: every file access under the agent's mounts returned "Permission denied" for minutes at a time (16 windows in one day on the OSS deployment, the longest 14 minutes), interrupted writers left stale git lock files, and the live QA reproduction showed a write acknowledged at the boundary being silently lost.

Two defects combined to cause this, and neither is the one the issue title suggests:

  1. The API already requests 3600-second mount credentials, but the web-identity token it mints to authenticate the STS call was hardcoded to 15 minutes, and SeaweedFS caps every session at that token's expiry. So every credential really lived 900 seconds.
  2. The runner's session pool was built to evict environments before their credentials expire, but on every warm turn it overwrote its expiry bookkeeping with the expiry of a freshly signed credential that never reaches the running geesefs daemons. Active conversations therefore never tripped the eviction and kept running on dead mounts.

Changes

API (acaa86e288): the web-identity token is minted with the requested lifetime (capped at the 12-hour session ceiling), DurationSeconds is clamped into each backend's valid range, the TTL becomes AGENTA_MOUNTS_CREDENTIALS_TTL_SECONDS (default 3600, wired through all seven compose files), and signing now raises instead of returning never-expiring credentials when an STS response carries no usable Expiration.

Before: requested 3600s, effective lifetime 900s (token cap), and a malformed expiry silently meant "never expires".
After: requested 3600s, effective lifetime 3600s, and a missing expiry refuses the sign (the turn degrades to mount-less, same as any sign failure).

Runner (290cfc244b): the environment records the expiry of the credentials actually installed in each geesefs daemon at mount time (installedMountExpiries), both park paths derive the pool's expiry from that installed lease, and reuse requires the lease to cover a full worst-case turn plus 60 seconds of clock skew. When it cannot, the dispatch logs mismatch (credentials-expiring) and rebuilds cold at the turn boundary, before any writer can be interrupted.

Before: pool expiry = whatever the latest dispatch signed (never installed anywhere).
After: pool expiry = min expiry of the mounts actually running; no turn starts on credentials it could outlive.

No wire changes, no new daemons, no refresh machinery inside the sandbox. A TTL misconfigured below the turn budget warns (lease-short) and runs rather than failing; the worst case equals today's behavior.

Why this is safe on both self-hosted SeaweedFS and real AWS S3

The runner never inspects which backend signed its credentials; it keys off the STS-reported expires_at, which both backends report truthfully and enforce with 403. On SeaweedFS the token-lifetime fix is what makes the TTL real. On AWS there is no web-identity path, so that change is inert; the TTL passes through GetFederationToken with AWS's own [900, 129600] bounds. The full two-backend analysis, including the AWS constraint that signing requires long-term IAM-user credentials, is in docs/design/fix-sts-mount-expiry/research.md section 4.

Design docs

The planning workspace ships in this PR under docs/design/fix-sts-mount-expiry/: context.md (the problem), research.md (credential flow end to end, the geesefs v0.43.0 refresh-mechanism analysis, the SeaweedFS-vs-AWS section), plan.md (the fix and QA plan), status.md, and decisions.md, a register of every decision with its trade-off and how to back it out. Start with decisions.md if you want to challenge choices rather than read code.

Tests

  • Runner: pnpm test 1209 tests green, pnpm run typecheck clean. New vitest cases pin the bug deterministically with fake clocks: a warm repark keeps the installed lease (fails without the fix), approval resume preserves the live secrets hash and lease, the lease is the minimum across mounts, and boundary cases at exactly/past the required-validity window.
  • API: pytest unit suite 1403 green. New request-level tests capture the actual STS POST and assert DurationSeconds clamping and the token's exp - iat for TTLs 120/3600/90000, plus fail-closed on missing and unparsable Expiration.
  • Live QA on a dedicated stack deployed from this branch (SeaweedFS, TTL lowered to 120s so expiry happens in 2 minutes instead of 15): the pre-fix runner reproduced the bug on demand (11 consecutive denied turns, all warm-continuing onto the dead mount); the fixed runner ran the identical cadence with zero denials across 8 lease-cycle rebuilds and no data loss; a 22-minute soak at default settings showed exactly one cold rebuild at the 14-minute mark and no denial window. A screen recording of the reproduction and the fix follows as a PR comment.

What to QA

  • Self-host repro (optional, 5 minutes): on an OSS/EE dev stack from this branch, set AGENTA_MOUNTS_CREDENTIALS_TTL_SECONDS=120 on the api service and AGENTA_RUNNER_RUN_TOTAL_TIMEOUT_MS=30000 on the runner, chat with an agent that appends to a file in agent-files/ every ~20 seconds. Expect periodic mismatch (credentials-expiring) cold rebuilds in the runner log and never a "Permission denied".
  • Regression: normal agent sessions (no knobs set) behave as before; sessions idle past a minute still tear down and remount fresh; the ENOTCONN hard-death recovery is untouched.

Known trade-off to weigh in review

With default knobs, a continuously active conversation now rebuilds cold about every 14 minutes (TTL 3600 minus the 45-minute worst-case turn budget minus skew). That replaces the previous outcome, which was breaking outright at minute 15. If production shows the churn matters, the designated follow-up is in-place refresh via a container-credentials endpoint (decisions.md item 2), not a longer TTL.

https://claude.ai/code/session_01BKPLpy1rUTBj5XktkP5eKY

mmabrouk added 4 commits July 25, 2026 21:19
The web-identity JWT was hardcoded to 15 minutes, so SeaweedFS capped every
mount credential at 900s regardless of the requested 3600s. Mint the token
with the requested lifetime (capped at the 12h session ceiling), clamp
DurationSeconds into each backend's valid range, make the TTL configurable
via AGENTA_MOUNTS_CREDENTIALS_TTL_SECONDS, and raise instead of returning
never-expiring credentials when an STS response carries no usable Expiration.

Claude-Session: https://claude.ai/code/session_01BKPLpy1rUTBj5XktkP5eKY
…y die

The session pool re-armed its expiry bookkeeping from each dispatch's fresh
sign, which never reaches the running geesefs daemons, so active
conversations reused mounts whose credentials had died (EACCES on every
path, issue #5516). Record the expiry of the credentials actually installed
at mount time, park from that installed lease, and evict to a cold rebuild
when the lease cannot cover a worst-case turn plus clock skew.

Claude-Session: https://claude.ai/code/session_01BKPLpy1rUTBj5XktkP5eKY
@linear-code

linear-code Bot commented Jul 25, 2026

Copy link
Copy Markdown

AGE-4000

@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Jul 25, 2026
@vercel

vercel Bot commented Jul 25, 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, Comment Jul 25, 2026 8:48pm

Request Review

@dosubot dosubot Bot added the Backend label Jul 25, 2026
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d972b783-b30b-4a4c-8cf7-4450094d59dd

📥 Commits

Reviewing files that changed from the base of the PR and between 533eed5 and 9ee103e.

📒 Files selected for processing (1)
  • api/oss/src/utils/env.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/oss/src/utils/env.py

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Mount credential lifetime is now configurable, with a default of one hour.
    • Session reuse now considers the actual remaining lifetime of installed mounts and avoids reusing environments that may expire during a run.
  • Bug Fixes
    • Improved compatibility with SeaweedFS and remote S3 credential-duration limits.
    • Prevented credentials with missing or invalid expiration data from being used.
    • Reduced mount failures caused by credentials expiring during ongoing sessions.
  • Documentation
    • Added design, investigation, implementation, and rollout documentation for mount-expiration handling.

Walkthrough

The change makes mount STS credential lifetimes configurable and backend-valid, rejects unusable STS expirations, and tracks installed mount leases in the runner to control session reuse and remount decisions.

Changes

STS credential lifetime and parsing

Layer / File(s) Summary
Configurable STS signing and validation
api/oss/src/core/mounts/service.py, api/oss/src/core/store/storage.py, api/oss/src/utils/env.py, api/oss/tests/pytest/unit/test_mounts_injection.py, hosting/docker-compose/*
Mount credential TTLs use AGENTA_MOUNTS_CREDENTIALS_TTL_SECONDS; SeaweedFS and AWS STS durations are clamped; missing or invalid Expiration values reject signing; tests cover these behaviors.

Runner lease tracking and keep-alive enforcement

Layer / File(s) Summary
Installed mount lease model
services/runner/src/engines/sandbox_agent/environment-setup.ts, environment.ts, runtime-contracts.ts, session-identity.ts
Successful cwd and agent mounts record expiration timestamps, and lease helpers compute minimum expiry and validity checks.
Keep-alive lease enforcement
services/runner/src/server.ts, services/runner/tests/unit/*, services/runner/tests/utils/capture-stderr.ts
Parked environments use installed mount leases; reuse checks account for total run duration and clock skew; expiring leases trigger cold eviction, while short leases emit warnings and continue execution.

Design and deployment documentation

Layer / File(s) Summary
Expiry investigation and rollout records
docs/design/fix-sts-mount-expiry/*
Design, research, decisions, implementation plan, QA status, rollout settings, and deferred follow-ups are documented.

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

Possibly related PRs

  • Agenta-AI/agenta#5156 — Related session keep-alive credential-epoch invalidation based on mount expiry.
  • Agenta-AI/agenta#5158 — Related runner parking and approval-resume validation paths.
  • Agenta-AI/agenta#5369 — Introduces the sandbox-agent session identity structure modified by this lease implementation.

Sequence Diagram(s)

sequenceDiagram
  participant Runner
  participant MountsService
  participant STS
  participant SessionPool
  Runner->>MountsService: request mount credentials
  MountsService->>STS: request bounded session duration
  STS-->>MountsService: credentials and Expiration
  MountsService-->>Runner: mount credentials
  Runner->>SessionPool: record installed mount lease
  SessionPool-->>Runner: reuse environment or evict before required validity deadline
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: rebuilding agent mounts before STS credentials expire.
Description check ✅ Passed The description is clearly related to the changeset and matches the API, runner, docs, and test updates.
Linked Issues check ✅ Passed The PR addresses #5516 by making mount credentials effectively last the requested TTL and rebuilding mounts before installed credentials expire.
Out of Scope Changes check ✅ Passed The added docs, compose entries, and tests all support the mount-expiry fix and stay within the stated objectives.
Docstring Coverage ✅ Passed Docstring coverage is 72.22% which is sufficient. The required threshold is 60.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fix-sts-mount-expiry-5516

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 mmabrouk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Inline notes to speed up the review. Each one states the invariant or the reason behind the nearby change; decisions.md in the design folder has the full register with trade-offs and how to back each choice out.

Comment thread api/oss/src/utils/env.py

# Lifetime of signed mount credentials, in seconds. The store backends clamp it to their
# own STS bounds.
credentials_ttl_seconds: int = Field(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The default_factory makes the env var get read when the config object is built, not at module import, so tests can construct MountsConfig() under monkeypatch without reloading the module. Parsing goes through the file's positive-int helper: unset or empty falls back to 3600; garbage or a non-positive value raises at startup, same as the file's other knobs.

# min(DurationSeconds, web-identity token expiry, maxSessionLength). The token-expiry cap
# has no floor, so minting the token at the capped TTL is what lets a sub-900 TTL take
# effect (the QA lever); DurationSeconds only has to stay inside the validated range.
capped = min(duration_seconds, _SEAWEEDFS_MAX_SESSION_SECONDS)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The core of the SeaweedFS half: the token now carries the real requested lifetime (capped at the 12h session ceiling), while DurationSeconds only needs to pass SeaweedFS's [900, 43200] validation. The effective lifetime is min(DurationSeconds, token expiry, maxSessionLength), and the token-expiry cap has no floor, which is what lets QA run 120-second credentials that AWS's hard 900 floor would refuse.

except ValueError:
expiration = None
if not raw_exp:
raise MountStorageUnavailable("STS response carried no credential expiry.")

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fail closed on purpose: the runner reads a missing expiry as "never expires", so one malformed STS response used to disable the reuse bound silently for the lifetime of a pooled environment. A refused sign degrades the turn to mount-less (the existing handling for any sign failure); it does not fail the turn. Both signing branches are STS, so there is no legitimate no-expiry path.

* The Daytona harness session-dir mounts are deliberately excluded: they are signed after the
* cwd mount with the same TTL, so their expiry is never the minimum.
*/
installedMountExpiries: InstalledMountExpiries;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This field is the heart of the runner half: the expiry of the credentials the running daemons actually hold, stamped only where a mount succeeds. The Daytona harness session-dir mounts are deliberately excluded; they are signed after the cwd mount with the same TTL, so their expiry can never be the minimum. If that ordering ever changes, they need entries here (decisions.md item 7).

* exactly on `asOfMs` counts as spent. The one place the lease comparison lives, so "already dead"
* and "dead before the turn ends" can never drift apart.
*/
export function leaseExpiresBy(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

One primitive owns the boundary semantics (<=, and undefined lease means no bound). Both the warm-path eviction and the cold-path lease-short warning delegate here, so they cannot drift apart at the edge. credentials-expired vs credentials-expiring is purely a log-label distinction for operators reading evictions.

const incomingEpoch = computeCredentialEpoch(request);
// Resolved once per dispatch: the longest a turn started now could still be running.
const turnBudgetMs = resolveRunLimits().totalMs;
const requiredValidThroughMs =

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Resolved once per dispatch: the lease must cover the longest a turn started now could still be running (45 min default) plus 60s of fixed clock skew. Consequence worth knowing: at default knobs the warm-reuse window is TTL 3600 minus 2700 minus 60, about 14 minutes, so a continuously active conversation rebuilds cold roughly every 14 minutes. That replaces breaking outright at minute 15; the follow-up if churn hurts is in-place refresh, not a longer TTL (decisions.md item 2).


// A parked epoch's expiry comes from the credentials installed in the environment's mounts, not
// from whatever this dispatch signed to compute the pool key.
const parkedEpoch = (

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Both park sites go through this helper so the rule lives in one place: a parked epoch's expiry always comes from the mounts actually installed, never from the credentials this dispatch signed (those never reach the daemons on a warm turn, which was defect 2 of the bug).

configFingerprint: cfgFp,
historyFingerprint: nextHistoryFp(env),
credentialEpoch: incomingEpoch,
credentialEpoch: parkedEpoch(env, live.credentialEpoch.secretsHash),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Always the live environment's hash, unconditionally: on the idle path the mismatch gate a few lines up already proved live and incoming hashes equal, and the approval-resume path deliberately ignores the incoming credentials, so adopting the incoming hash there would relabel an environment running old secrets as if it ran new ones. An earlier draft made this a per-call-site flag; the unconditional form removes a default that would be wrong to forget.

if (leaseExpiresBy(leaseMs, requiredValidThroughMs)) {
// A misconfigured TTL must never fail a turn: warn once per acquisition and run it.
klog(
`lease-short key=${key} leaseExpiresAtMs=${leaseMs} ` +

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

A TTL below the turn budget plus skew is not an error by design: every dispatch rebuilds cold (correct, denial-free, churny), the log names both knobs, and QA lowers the TTL on purpose. A hard failure here would turn one mis-set number into a full outage; proceeding is never worse than the pre-fix behavior (decisions.md item 6).

//
// A parked environment runs on the credentials its geesefs daemons were handed at mount time, not
// on the ones each dispatch signs to compute the pool key. These cases pin that the pool parks the
// installed lease and evicts to a cold rebuild before a worst-case turn could outlive it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The test that pins the bug is "a warm repark keeps the installed lease": turn 1 mounts with a short expiry, turn 2 signs a far-future one and runs warm, then advancing the fake clock past the installed expiry must force turn 3 cold. On the pre-fix code turn 3 warm-continues onto the dead mount, which is exactly the field failure. Fake Date only; the pool's real timers stay live.

@mmabrouk

Copy link
Copy Markdown
Member Author

Live QA evidence (SeaweedFS, dedicated stack from this branch)

All timestamps UTC, 2026-07-25. Full logs and the wire captures are archived on the dev box under ~/.claude/jobs/04ff9081/tmp/qa5516/.

Phase A: bug reproduced on demand (pre-fix runner + this branch's API, TTL=120s). Mount signed 19:32:14 with expiresAt exactly +120s. First denial 20 seconds after expiry, then 11 consecutive denied turns over a 200+ second window, every one warm-continued onto the dead mount:

turn 8 (19:34:34): {"errorText": "/bin/bash: line 1: agent-files/qa-log.txt: Permission denied ... exited with code 1"}
runner log: 17x "[keepalive] hit-continue", zero evictions for the session
geesefs:    fuse.ERROR *fuseops.LookUpInodeOp error: permission denied  (every turn from 19:34:36)

Bonus finding: turn 7, started exactly at the 120s boundary, reported a successful append; that line is absent from the file afterward. The pre-fix behavior can silently lose an acknowledged write in the geesefs cache flush, not just deny access.

Phase B: fixed runner, identical cadence. At the exact wall-clock offset where phase A first denied (+143.8s), the fixed runner's turn succeeded. 18/18 turns clean, file grew monotonically 9 to 26 lines across every rebuild, grep -ci "permission denied" on the runner log: 0. Eight lease-cycle evictions logged as designed:

[keepalive] mismatch (credentials-expiring) ...; evict + cold   (8x, expiresAt marching 19:41:11 -> 19:45:11)

lease-short warning (TTL=60 < 30s budget + 60s skew): every acquisition logged the warning naming both knobs; turns still succeeded.

Defaults soak (knobs removed, 22 minutes, turns every 60s): mount signed 19:47:30 with expiresAt 20:47:30, exactly the 3600s the service always requested and never got. 22/22 turns clean, 20 warm continues, exactly one credentials-expiring cold rebuild at 20:01:30 (mount + 14:00 to the second), warm again after, zero denials. The pre-fix behavior broke at minute 15 of exactly this scenario.

A screen recording of the reproduction and the fix is coming as a follow-up comment.

@mmabrouk

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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.

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-production-60ad.up.railway.app/w
Project agenta-oss-pr-5520
Image tag pr-5520-2e80cd3
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-07-25T20:51:56.906Z

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3dfb3dfd-e7f7-4b8b-b308-d8fee5256a79

📥 Commits

Reviewing files that changed from the base of the PR and between cb095f7 and 533eed5.

📒 Files selected for processing (26)
  • api/oss/src/core/mounts/service.py
  • api/oss/src/core/store/storage.py
  • api/oss/src/utils/env.py
  • api/oss/tests/pytest/unit/test_mounts_injection.py
  • docs/design/fix-sts-mount-expiry/README.md
  • docs/design/fix-sts-mount-expiry/context.md
  • docs/design/fix-sts-mount-expiry/decisions.md
  • docs/design/fix-sts-mount-expiry/plan.md
  • docs/design/fix-sts-mount-expiry/research.md
  • docs/design/fix-sts-mount-expiry/status.md
  • hosting/docker-compose/ee/docker-compose.dev.yml
  • hosting/docker-compose/ee/docker-compose.gh.local.yml
  • hosting/docker-compose/ee/docker-compose.gh.yml
  • hosting/docker-compose/oss/docker-compose.dev.yml
  • hosting/docker-compose/oss/docker-compose.gh.local.yml
  • hosting/docker-compose/oss/docker-compose.gh.ssl.yml
  • hosting/docker-compose/oss/docker-compose.gh.yml
  • services/runner/src/engines/sandbox_agent/environment-setup.ts
  • services/runner/src/engines/sandbox_agent/environment.ts
  • services/runner/src/engines/sandbox_agent/runtime-contracts.ts
  • services/runner/src/engines/sandbox_agent/session-identity.ts
  • services/runner/src/server.ts
  • services/runner/tests/unit/session-keepalive-approval.test.ts
  • services/runner/tests/unit/session-keepalive-dispatch.test.ts
  • services/runner/tests/unit/session-pool.test.ts
  • services/runner/tests/utils/capture-stderr.ts

Comment on lines +92 to +103
## 7. The lease tracks the cwd and agent mounts only

`installedMountExpiries` records the two durable mounts. The Daytona harness
session-directory and transcript mounts are excluded: they are signed seconds
after the cwd mount with the same TTL, so their expiry is never the minimum and
tracking them would add entries that cannot change the outcome.

- Trade-off: if that signing order or TTL parity ever changes, the exclusion
argument breaks silently.
- Backtrack: add fields to `InstalledMountExpiries` and stamp the two remote
session-dir mount sites; `installedMountLease` already reduces over whatever
entries exist.

Copy link
Copy Markdown

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 tracked mounts accurately.

installedMountExpiries tracks the session cwd mount and the durable agent mount; cwd is not itself durable. Calling both “the two durable mounts” contradicts README.md and may confuse future lease changes.

Suggested wording
-`installedMountExpiries` records the two durable mounts.
+`installedMountExpiries` records the two tracked mounts: the session `cwd` and durable agent mounts.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## 7. The lease tracks the cwd and agent mounts only
`installedMountExpiries` records the two durable mounts. The Daytona harness
session-directory and transcript mounts are excluded: they are signed seconds
after the cwd mount with the same TTL, so their expiry is never the minimum and
tracking them would add entries that cannot change the outcome.
- Trade-off: if that signing order or TTL parity ever changes, the exclusion
argument breaks silently.
- Backtrack: add fields to `InstalledMountExpiries` and stamp the two remote
session-dir mount sites; `installedMountLease` already reduces over whatever
entries exist.
## 7. The lease tracks the cwd and agent mounts only
`installedMountExpiries` records the two tracked mounts: the session `cwd`
and durable agent mounts. The Daytona harness session-directory and transcript
mounts are excluded: they are signed seconds after the cwd mount with the same
TTL, so their expiry is never the minimum and tracking them would add entries
that cannot change the outcome.
- Trade-off: if that signing order or TTL parity ever changes, the exclusion
argument breaks silently.
- Backtrack: add fields to `InstalledMountExpiries` and stamp the two remote
session-dir mount sites; `installedMountLease` already reduces over whatever
entries exist.

Comment on lines +55 to +72
Add the TTL, following the existing `int(os.getenv(...) or "...")` pattern
(compare `catalog_cache_ttl_seconds`, env.py:622). The `or` also absorbs an empty
string, which is what an unset compose passthrough delivers:

```python
class MountsConfig(BaseModel):
"""Mounts-domain config. Store credentials live in StoreConfig."""

# Lifetime of signed mount credentials. The store backends clamp it to their own
# STS bounds (SeaweedFS: effective floor via the web-identity token, hard range
# 900-43200 for DurationSeconds; AWS GetFederationToken: 900-129600). QA lowers
# it to force fast expiry; only SeaweedFS honors values below 900.
credentials_ttl_seconds: int = int(
os.getenv("AGENTA_MOUNTS_CREDENTIALS_TTL_SECONDS") or "3600"
)

model_config = ConfigDict(extra="ignore")
```

Copy link
Copy Markdown

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

Synchronize the design records with the final implementation.

The plan and research documents mix historical pre-fix behavior with current-tree claims and stale code samples. Either update these sections to the implemented contracts or explicitly label them as pre-fix/historical.

  • docs/design/fix-sts-mount-expiry/plan.md#L55-L72: document the actual default_factory/positive-integer parser rather than the obsolete raw int(os.getenv(...)) implementation.
  • docs/design/fix-sts-mount-expiry/plan.md#L328-L360: remove the obsolete keepSecretsHash option; the final design keeps the live environment’s hash unconditionally.
  • docs/design/fix-sts-mount-expiry/plan.md#L454-L461: replace undefined requiredMs and totalMs placeholders with the actual names.
  • docs/design/fix-sts-mount-expiry/plan.md#L619-L626: close the transcript-mount question, since decisions.md and status.md record that it was resolved.
  • docs/design/fix-sts-mount-expiry/research.md#L3-L7: revise “current tree” or update the referenced locations.
  • docs/design/fix-sts-mount-expiry/research.md#L17-L53: mark the removed TTL constant, old duration clamp, and optional-expiry behavior as pre-fix.
  • docs/design/fix-sts-mount-expiry/research.md#L103-L131: mark the old dispatch-expiry bookkeeping as historical.
📍 Affects 2 files
  • docs/design/fix-sts-mount-expiry/plan.md#L55-L72 (this comment)
  • docs/design/fix-sts-mount-expiry/plan.md#L328-L360
  • docs/design/fix-sts-mount-expiry/plan.md#L454-L461
  • docs/design/fix-sts-mount-expiry/plan.md#L619-L626
  • docs/design/fix-sts-mount-expiry/research.md#L3-L7
  • docs/design/fix-sts-mount-expiry/research.md#L17-L53
  • docs/design/fix-sts-mount-expiry/research.md#L103-L131

empty string falls back to 3600 (construct `MountsConfig` under
`monkeypatch.setenv`).

Run: `cd api && uv run --no-sync pytest oss/tests/pytest/unit/test_mounts_injection.py`.

Copy link
Copy Markdown

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

Use the repository-mandated API test command.

The plan documents uv run --no-sync pytest, but the repository requires API tests to run with py-run-tests from the api directory. Update this command so the validation step matches project tooling.

As per coding guidelines, api/**/*.py: Run API tests with py-run-tests from the api directory.

Source: Coding guidelines

@mmabrouk

Copy link
Copy Markdown
Member Author

QA screen recording (2 min 40 s): the bug reproduced live with 120-second credentials (turns 7-11 all "Permission denied", first denial 3 seconds after expiry, the runner warm-reusing the dead mount), then the fixed runner on the identical cadence: 11/11 turns clean, 10 credentials-expiring rebuilds, nothing lost.

qa-5516-repro-and-fix.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(runner): agent mounts return EACCES for ~15 min after STS credentials expire (never refreshed)

1 participant