[release] v0.108.1 - #5698
Merged
Merged
Conversation
Recut of #5277 onto current main. Behind AGENTA_DAYTONA_OPAQUE_SECRETS=process_local, the runner creates per-sandbox Daytona Secret records and sends only Secret placeholders in the sandbox create request instead of plaintext credential env vars. The SDK resolves managed model and HTTP MCP credentials into typed wire descriptors and fails closed when resolution is incomplete. Flag off, behavior matches main. Re-expressed over main's later reworks: typed run failures, model capability catalog, current-turn/attachment delivery, Pi built-ins rework, session-storage rework, mount-credential tracking, @daytona/sdk 0.198.
- SDK: build_resolved_connection wraps classify_environment failures in InvalidConnectionConfigurationError (422), matching the effective_endpoint handling; malformed bindings no longer surface as 500s. Test added. - runner: buildDaytonaSecretPlan rejects an opaque model credential whose binding name (case-insensitive) collides with a direct environment binding, so one name can never ride both envVars and secrets in a create request. local_use bindings now settle before opaque ones so wire order cannot evade the check. Tests added. - runner: extracted one shared isDaytonaNotFound helper (daytona-secrets.ts) recognizing both the typed DaytonaNotFoundError and 404-shaped errors; used by Secret cleanup and the process-local sandbox lifecycle wrapper. Test added. - hermetic-env AGENTA_DAYTONA_OPAQUE_SECRETS scrub: already present at HEAD (SCRUBBED list + per-test re-scrub); no change needed. - daytona-secret-provider proxy staleness claim: rebutted, intentionally unchanged (all lifecycle methods are declared on the facade; attachments only affect create, which always rebuilds the delegate). - configFingerprint environment-values claim: rebutted, intentionally unchanged (modelConnection.environment carries non-secret config only by contract; secret material rides typed credentials whose values are already stripped from the fingerprint).
Re-express the typed-wire delta on top of the Codex harness feature: - keep wire_harness_mode / harnessMode alongside modelConnection; drop the retired flat fields main's copies still carried (provider, secrets, credentialMode) from protocol.ts, wire_models.py, and the fingerprint - port main's new Codex tests (run-plan gates, wire golden) to the typed modelConnection shape and regenerate run_request.codex.json - runner: tsc clean, 99 files / 1534 unit tests green; SDK: 1920 passed
Four problems, all caught by suites the port never ran locally.
1. The author's connection choice stopped reaching the runner. The port dropped
`wire_model_ref()` because it also carried the retired flat `provider` field,
but that method was the only emitter of the top-level `connection` reference.
The runner still gates Pi's OpenAI-compatible models.json path on
`request.connection.mode === "agenta"` and names the provider from its slug
(`pi-model-config.ts`), so a named custom connection silently fell back to the
generic provider-override env. Restored as `wire_connection_ref()`, which emits
only `{mode, slug}` and stays empty for the project default so a plain run's
payload is unchanged.
2. `_fake_runner_backend.py` still passed `secrets=` to `request_to_wire`, which
the typed wire no longer accepts. It now mirrors the production sandbox-agent
backend: the parameter is accepted for interface parity and ignored, because
credentials ride inside `config` as `modelConnection`.
3. The services handler tests still built `ResolvedConnection(env=...)` and
expected a resolution failure to degrade into an empty `runtime_provided`
plan. Ported to typed credentials, and the degradation test became a
fail-closed test. The shared no-credential stub now reads its provider from
the harness capability table, because a fail-closed resolve must return a
connection the post-resolve gate accepts for whichever harness is running.
4. The custom-connection replay test asserted the retired flat wire.
Also: the credential epoch now uses an HMAC keyed with a per-process random key
instead of a bare sha256 of the secret values. The epoch is only ever compared
within one process, so the key costs nothing, and the digest stops being
brute-forceable against candidate API keys. This is what CodeQL flagged.
Verified: runner tsc clean, 99 files / 1534 tests; SDK 2067 passed (unit +
integration) with the 10 pre-existing litellm xfails; services 100 passed.
Review found the feature had no documentation and, more importantly, no way for an operator to turn it on: the environment variable was read straight from process.env in the runner and was never plumbed through docker-compose or the Helm chart, so setting it on the host did nothing. - Renamed the flag to AGENTA_RUNNER_DAYTONA_OPAQUE_SECRETS. Every other runner Daytona setting is AGENTA_RUNNER_DAYTONA_*, and the bare AGENTA_DAYTONA_ prefix collided visually with the DAYTONA_* variables that configure the unrelated code-evaluator sandbox. The feature is unreleased, so no operator has set the old name. - Plumbed it through all seven docker-compose runner services, the four env examples, the Helm deployment template, values.yaml, and values.schema.json. - Documented it in the configuration reference (what it does, the two caveats: the Daytona API key needs Secrets permission, and AWS keys cannot be hidden) and in the how-agents-run concept page (why an agent reading its own environment is the threat, and what the sandbox sees with the flag on). Also expanded the wire-contract comments the review asked about: what the usage values mean and why there are exactly two, what endpoint covers beyond OpenAI-compatible routes (Azure apiVersion, AWS/Vertex region), why a Daytona secret candidate is called a candidate, why allowedHost can never be a wildcard, and why the retired-field guard rejects rather than ignores while `connection` is deliberately not among the retired fields.
CodeQL reports the alert on the createHmac call itself, and a suppression comment only applies to alerts on its own line. The preceding-comment form did not take, so the call is split out and carries the marker inline.
CodeQL flagged the credential epoch as an insecurely hashed password, and GitHub code scanning does not honour inline suppression comments, so the check stayed red. Rather than argue with the scanner, the design changed, and the result is better than what it replaced. The epoch answers one question: did the credential material change since this session was parked? It never needed a digest to do that. It now holds the material behind a `CredentialMaterial` value object and compares it with `timingSafeEqual`. Why holding the values is not a step backwards. A digest of an API key was never much protection, because keys carry little enough entropy that a leaked digest can be attacked offline, and the parked environment already holds the plaintext anyway. The realistic risk is a value reaching a log line, and that is now structurally impossible rather than a convention: the material sits in a private field with no getter, and every route from an object to text (`String`, template literals, `JSON.stringify`, `util.inspect`, so `console.log`) returns a placeholder. This mirrors what the Python side already does, where `ResolvedCredential` masks its value on dump and hides it from `repr`. A test pins every one of those rendering routes, so a future refactor that drops an override fails loudly instead of leaking silently. No behavior change: the same rotations evict and the same re-minted per-turn bearers do not. Runner tsc clean, 99 files / 1535 tests.
The README still described `request.secrets`, which this change retired, and said nothing about hiding keys from the sandbox. It now describes the consumer-grouped shape, the reject-on-retired-field guard, and the Daytona Secrets path with its three caveats.
…able mount The durable session cwd is a geesefs mount over object storage, which has no symlinks: `<cwd>/.codex/auth.json` flushes to the store as a 0-byte object and comes back as an ordinary empty file. `symlinkCodexSubscriptionAuthFile` guarded with `existsSync`, read that degraded entry as "already linked", and returned early, so every turn after the first authenticated from an empty token file. Extract the lstat/readlink/replace logic `linkAgentFiles` already used for the same hazard into `ensureDurableSymlink`, use it for both links, and attach the Codex link to the durable-mount lifecycle so a mid-session remount re-materializes it. A subscription run that still fails auth now says the mounted login is empty or unreadable instead of asking for a project key. Closes #5692
…lease gate The Codex subscription bug in #5692 shipped through a gate that could not see it: the only multi-turn journey stayed on the warm daemon, and the cell labelled "Codex subscription" actually ran the Pi harness with an OpenAI-compatible subscription provider, so the code that broke had no coverage at all. Adds warm / cold1 / cold2 journeys (the tier vocabulary from reports/warm-approvals-qa.md), running in every cell, over a store-backed durable cwd: - cold1 forces an eviction from the client by changing the agent's instructions (a configFingerprint input), so the runner unmounts and remounts the cwd. - cold2 replaces the runner replica through an operator hook (SIGKILL + owner TTL wait); local sandboxes must refuse, remote ones resume cold. - The store is a precondition: the journeys resolve the session's durable mount over the API and SKIP (FAIL with --require-store) when there is none, so a green result can never mean "the store was not in play". - Evidence is store-side, not prose: the client reads the agent's token straight out of the object store and writes a store-only file the agent must read back, plus the turn ledger's agent_session_id/sandbox_id and any 0-byte objects in the cwd. Cells: S2 (codex + runtime_provided — the genuine subscription cell), X2 (codex on daytona, so a completed cold 2 is observable), S1 relabelled as what it is. Also folds in the QA-infrastructure fix left over from the v0.108.0 release run: P2 needs connection mode `agenta` plus the full `<slug>/custom/<model>` key under v0.107.x resolver semantics. Refs #5692
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…-flag feat(frontend): voice input as an experimental per-user settings flag
Contributor
Author
Railway Preview Environment
Updated at 2026-08-04T10:18:41.326Z |
…bare 403 A Daytona API key that can create sandboxes does not automatically have the separate permission to manage Secrets. When it does not, enabling AGENTA_RUNNER_DAYTONA_OPAQUE_SECRETS makes every run with a hideable credential fail at sandbox creation, and the raw provider error is a bare Forbidden that says nothing about the flag that caused it. This is the first thing an operator hits when turning the feature on, so the create path now recognizes a permission refusal and re-raises it as a message naming the variable to fix, the permission to grant, and the way to revert. The provider's own error is kept as the cause so the logs keep the detail. Every other failure keeps its original error untouched. The docs promote the same point from a bullet to a warning, and say plainly that the runner does not fall back to plaintext, because doing the unprotected thing silently would be worse than stopping. Runner tsc clean, 99 files / 1538 tests.
…he gap Review catch. `wire_connection_ref()` emits a top-level `connection` field for a self-managed or named Agenta connection, and the runner reads its slug to register a custom OpenAI-compatible Pi run in Pi's models.json. But `WireRunRequest` no longer declared the field, so the exported contract schema omitted something the implementation both produces and requires. A client generated from that schema would drop it, and those runs would silently fall back to the generic provider-override path. The field went missing because it sat next to the flat credential fields this change retired and got swept out with them. It is not a credential. It is the author's connection CHOICE, which is non-secret routing config. Restored `WireConnection` and the `connection` field on `WireRunRequest`. Two reasons nothing caught this, both now closed: - `KNOWN_REQUEST_KEYS` was stale in exactly the same way, and its guard is a subset check over three sample payloads, none of which names a connection. Added the key, and added a test asserting the set EQUALS the schema's declared aliases in both directions. A key the schema declares but the producer never emits is dead contract surface, so equality is the honest assertion. - A validation-based test could not have caught it either: `_WireModel` sets `extra="allow"`, so a payload carrying a field the schema forgot still validates cleanly with the field quietly demoted to an extra. The new test is structural rather than validation-based for that reason, and the second new test round-trips a named connection through `model_dump` to prove it survives as a modelled field rather than an extra. I verified both guards fail when the schema field is removed, so neither is a test that can never break. Also corrected the runner's own doc comment on the field. It claimed "the current SDK resolver does NOT send it", which described the regression rather than the contract, and would have led the next reader to delete the models.json path as dead code. Verified: SDK 2069 passed with the 10 pre-existing litellm xfails, services 100 passed, runner tsc clean and 99 files / 1538 tests, ruff clean.
`RunPlan` had grown to thirty fields on one flat interface, and every consumer took the whole thing regardless of how little it read. Raised in review of #5670. The fields are grouped into `credentials`, `workspace`, `tools`, and `prompt`. The five identity fields (`harness`, `acpAgent`, `sandboxId`, `isPi`, `isDaytona`) stay at the top level because almost every consumer branches on them, and `sandboxPermission` stays there too since the declared security boundary is its own concern. The payoff is not the tidier interface. It is that a consumer can now say what it actually touches. `prepareWorkspace` used to take a nine-key `Pick` of the flat plan; it now takes three named slices and the parameter type reads as a description of the function. Several other consumers narrowed the same way, which means a future change to, say, credential delivery has a compiler-checked list of what depends on it. The one rename is `plan.prompt` to `plan.prompt.text`, forced by the group taking the name. Nothing else was renamed, no logic changed, and no `any` cast or `@ts-expect-error` was added. Runner tsc clean, 99 files / 1535 tests, the same counts as before the change.
[refactor] Group RunPlan by concern instead of one flat bag
[feat] Deliver agent credentials through Daytona Secrets
The Daytona cells (C2, C4) already existed, so the matrix covered the sandbox.
What no journey covered was whether credential hiding actually works.
That gap matters more than it looks. A plaintext provider key works exactly as
well as a Daytona Secret placeholder does, so if hiding silently stopped
working tomorrow, every existing journey would still pass and the gate would
report green. The security property is invisible to a test that only asks
whether the product works. Something has to go into the sandbox and look.
The new `secret_opaque` journey runs bash inside the sandbox and reads the
first 11 characters of the provider key variable. It passes when that value
begins `dtn_secret_`.
Two deliberate details:
- It reads 11 characters, never the whole value. That is exactly enough to
tell `dtn_secret_` from a real `sk-` key, and it never asks the model to
print a credential. A safety-trained model may refuse to do that, which
would turn a real check into one that fails for the wrong reason.
- It requires the bash call to have genuinely executed, so it cannot pass on
an absence. A refused call, an empty variable, or a model that declined to
answer all read as FAIL. An absence-only check ("no key in the reply") would
have passed on every one of those while proving nothing.
Daytona only. It SKIPs on local cells, where the harness runs inside the
runner container and there is nothing to hide the key from.
Also noted in SKILL.md that C2 and C4 now need the runner's Daytona API key to
have permission to manage Secrets, since hiding is on by default as of the
change earlier in this release.
Committed straight to the release branch: this is release-gate tooling for a
feature already merged into it, and the gate is about to run.
Removed duplicate text and added a demo link image.
Update README with demo link and remove duplication
Every streaming agent turn ended with a `data-agent-error` and an `error` frame on the browser's wire, even though the answer itself arrived intact. The text was right and the finish reason was `stop`, so it looked like a working turn with an error stapled to it. `_stream_in_redaction_scope` wraps the event stream in `redaction_context`, whose `finally` resets the ambient redactor's ContextVar token. PEP 567 gives generators no context of their own, so the install lands in whichever caller's context ran the first `__anext__`, while the close runs wherever the stream is finally exhausted. In the real service those are different tasks, and `ContextVar.reset` rejects a token from another context. The resulting ValueError escaped into the stream adapter, which turned it into an error frame, and it took OpenTelemetry's own context detach down with it. `reset_active_redactor` now falls back to installing a fresh empty redactor when the token is foreign. That keeps the property the reset exists for: the finished run's deny-set stops being reachable, so a later run sharing the context can neither see nor accumulate it. The regression test closes a stream from a different task than the one that opened it and fails with the exact production ValueError without this change.
The credential-hiding journey asked the sandbox to print the first eleven characters of the provider variable and looked for the `dtn_secret_` placeholder. That had two problems. It could write key material into the results file. Eleven characters are harmless while hiding works, but on the exact failure this journey exists to catch, the reply would carry a slice of a real provider key straight into the transcript. It could not run on the codex harness. The check required seeing a bash tool call complete on the wire, and codex runs shell through native exec frames that the tool-call probe cannot read. Cell X2, codex on Daytona, therefore failed for a reason that had nothing to do with credential hiding. The sandbox now classifies its own variable with a POSIX `case` and echoes a verdict word carrying a nonce this run invented. Only the verdict crosses back, so no key material can reach the transcript, and the nonce keeps the journey unable to pass on an absence: a refused call, an unset variable or a model that declined all still read as FAIL. Dropping the tool-call requirement is what makes it work on every harness. Verified on staging: C2, C4 and X2 all pass.
The cold-2 journey demanded that a local-sandbox resume REFUSE with "is not the owner of session". It cannot, and the journey's own transition is why. `isLocalRunnerEligible` allows a run when the session owner is unknown or is this replica. The transition deliberately waits out the session-owner key, so that a resume does not fail on a stale owner from the replica it just killed. Once that key lapses, the ownership claim hands the session to whoever asks next, which is the replacement replica. The guard then sees itself as the owner and correctly does not refuse. Waiting for the refusal to become impossible and then asserting it is a test that can only fail. Refusing is right when the original owner is still ALIVE, and this journey never creates that: it needs two replicas at once, and the gate drives one deployment over HTTP. The runner's own session-ownership tests cover the pure function; a two-replica end-to-end journey is a follow-up. What is actually correct after a genuine replica loss is what the remote tiers already assert. The dead replica took its local sandbox with it, so the replacement has to rebuild the conversation from the durable working directory in the object store. Local cold 2 now asserts that same resume, and still fails loudly if a refusal appears, since that would mean the owner key outlived the wait and the run measured the wait rather than the product. Verified against a local stack on this release: a SIGKILLed replica resumes cold and the durable cwd token comes back.
…y rotation Two paths this release depends on had no coverage at all. The first is the resume users actually hit. The session pool parks a Daytona sandbox after two minutes idle, which STOPS it rather than deleting it, and the next turn reconnects to that same sandbox. The existing cold-1 journey forces a config change instead, which deletes the sandbox and builds a fresh one with freshly created Secrets, so it never touches the reconnect path. That matters now that credentials arrive as Daytona Secrets: a Secret that quietly stopped substituting after a stop and start would leave the entire matrix green. The new `park` tier changes nothing and simply waits, then proves the resume was a reconnect rather than a recreate by reading the turn ledger, where a reconnect shows exactly one sandbox id across the turns and more than one harness session. The second is rotating a key while a conversation is live. Credential values are deliberately kept out of the session config fingerprint and live only in a separate credential epoch, so that epoch check is the only thing standing between a rotated key and a warm sandbox that keeps using the old one. Nothing else in the gate would notice if it broke, because a stale key still answers. The new `rotate` journey replaces the vault key with a decoy no provider accepts and requires the next turn to FAIL, since a success there means the previous credential was still being served. It then restores the real key and requires the conversation to come back with its durable working directory intact. The vault is restored in a `finally`, because leaving a shared deployment holding a decoy would break every cell that ran afterwards. Verified on this release: `rotate` passes against a local stack, and the vault comes back to its original value.
…ontinuity The new park tier required the harness session id to CHANGE across a parked resume. It does not change, and it must not: preserving it across a rebuilt session is the entire job of the runner's session-continuity store. So the check asserted the opposite of the intended behaviour and failed all three Daytona cells on a product that was working. The real discriminator is the sandbox id. Reconnecting to a parked sandbox keeps it, while a rebuild creates a new one, and cold1 on the same deployment reports two sandbox ids for exactly that reason. That contrast is what makes one id here evidence rather than an assumption. The remaining ambiguity is honest and now written down: from the client alone, "parked and reconnected" and "still pooled and served warm" both keep the sandbox. The transition settles it by construction, since it idles well past the pool TTL, and the runner log line in the evidence remains the definitive witness. Verified on staging: C2, C4 and X2 all pass, each reconnecting to the same sandbox with the durable working directory and the store-only file intact.
[docs] Update API reference docs
|
|
v0.108.1 validates a credentialed connection's endpoint far more strictly on a remote sandbox than it used to. The host has to be plain HTTPS on the default port with a real fully-qualified name, because that host is exactly what the credential's Daytona Secret is pinned to. A self-hosted proxy on a non-default port, which is an ordinary setup, is now refused outright where it worked in v0.108.0. Nothing in the gate could see that. The existing custom-provider cell runs only on the local sandbox, and a local sandbox never builds a secret plan, so it never reaches the validator. P3 is that same cell on Daytona, which is the surface where the rejection happens and where a self-hoster would hit it. Verified on staging: chat, tool and secret_opaque all pass, so the custom provider works end to end on a remote sandbox with credential hiding on.
mmabrouk
approved these changes
Aug 4, 2026
Two releases shipped without an announcement, and seven documentation pages had drifted into saying things those releases made untrue. **The announcements.** "Run Your Agents on Codex" covers the third harness, its five OpenAI models, both sandbox types, tool approvals, MCP servers, image attachments, and the two ways to pay for the model. It opens with a demo video. "API Keys Are Hidden from Agent Sandboxes" covers credential hiding on Daytona: what it does, what it covers, what it deliberately does not cover, and a warning block telling self-hosters to grant their Daytona key permission to manage Secrets before they upgrade, since there is no fallback to plaintext by design. **The documentation corrections.** Two pages still said Agenta ships two harnesses. One said MCP servers were Claude Code only. One still answered "Yes" to whether the model key crosses into a Daytona sandbox, which is exactly what this release stopped. The configuration reference contradicted itself about the default. The Daytona page never mentioned the Secrets permission its own setup now requires. Publishing a changelog that its own documentation disagrees with is worse than publishing nothing, so all of it is fixed here. **The roadmap.** Durable sessions, the agent work folder, and batch tool approvals moved to shipped. Channels and mobile moved from planned to in progress. The two new features were added. Also fixes a runner error message that told users to pick "a non-Pi harness (e.g. claude)" for an MCP server. Codex accepts them too, so it now names both and stops sending Codex users looking for a harness they were already allowed to use.
The v0.107.0 entry was titled "File Attachments in Agent Chat" and described the mechanics: you can paste, drag, or upload, and the files belong to the session. That is accurate and it undersells the feature badly. The point is not that a chat box accepts an upload. The point is that you, your team, and your agents now share one folder in the cloud, instead of the material living on one person's machine where nobody else can reach it. Retitled to "Shared Workspace Files" and rewritten around that. The summary leads with the shared folder, then the two scopes and why the difference matters: a conversation's files are the desk for today's work, while each agent's own folder persists across every conversation, so pointing the instructions at it means the agent starts each session already knowing your material. Adds a demo video, and drops the two screenshot placeholders it replaces. The slug stays `file-attachments-in-agent-chat`. It is already live and the site has no redirect plugin, so changing it would break a published URL for a title fix. The matching roadmap entry is retitled and rewritten to agree with it.
Both videos in this entry sat in bare iframes. An iframe is inline by default, so it lands on the text baseline with no vertical margin and picks up a descender gap, which is why the video looked jammed against the paragraph above and below it. Every other changelog entry wraps its iframe in a centred flex block with 20px of margin top and bottom. These two now do the same, so the whole changelog spaces its videos identically. No other entry had the problem.
…ntial-hiding [docs] Announce Codex and Daytona credential hiding
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
New version v0.108.1 in