Skip to content

fix(explorer): take entity ids as one percent-encoded path segment, so bundle ids resolve - #25

Merged
polakamtejas merged 2 commits into
mainfrom
tejas/explorer-encoded-entity-ids
Oct 1, 2026
Merged

polakamtejas merged 2 commits into
mainfrom
tejas/explorer-encoded-entity-ids

Conversation

@polakamtejas

@polakamtejas polakamtejas commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Bundle entities can't be opened in the explorer. Bundle ids are @<namespace>/… paths: @local/<package>/<name> for an installed bundle, or the bundle folder's whole path (@local/~/work/triage/tickets). A bundle run's instance ids look the same (@local/agentenv-framework/hello/hello-5xlcvxjq). The explorer's id routes took a single-segment {entity_id} / {task_id}, and uvicorn decodes %2F before routing. So every bundle entity 404'd, raw or encoded: the hello example can be listed but opening it shows "Failed to fetch (404)", and it can't be run or versioned.

The contract: an id in an explorer URL is one percent-encoded path segment, with / sent as %2F (encodeURIComponent). The published OpenAPI already implies this: OpenAPI 3.1 path parameter values must not contain an unescaped /. The UI already builds its own page URLs this way (/tasks/%40local%2F…). GitLab (projects/group%2Fproject) and npm (@scope%2fname) address names like these the same way.

Changes

  • explorer/entity_ids.py (new):
    • route_path decodes a raw path but keeps %2F and %25 escaped.
    • EncodedIdRouting is ASGI middleware that routes on route_path(raw_path), only under /api/v1/{artifacts,envs,tasks,agents,evals,task-instances}/. Plugin routes elsewhere see exactly what they see today.
    • EntityId is the path-parameter type that decodes the matched segment.
  • The id routes take EntityId:
    • get and versions for all five collections (routers/common.py)
    • every task_id in routers/runs.py (10 routes), plus instance_id on the instance and progress routes
    • task_instance_id on conversations
  • UI: 20 of the 27 places that put an id in an API path already encoded it; the other 7 now do (env-detail-page, run-step-progress, task-detail-page, universe-detail-page).
  • README (Explorer routes): ids can contain /, so a plugin route under the core prefixes receives the segment encoded and decodes it with urllib.parse.unquote.

Flat ids are unchanged: with no %2F in the path, the middleware changes nothing. The OpenAPI paths are byte-identical; the id parameters gain a description of the encoding.

Why not {id:path}: ids are opaque and built from user folder names, so a :path converter misroutes real ids, and it does so silently with a 200:

request {id:path} this PR
open @local/~/stuff/instances/foo "instance foo of task @local/~/stuff" the task
open a task named runs "list runs of @local/~/triage" the task
ids with spaces, ( ) & + , @, accents, a literal %2F or # ✅ ✅

Why not ?id=: also unambiguous, but it changes the public URL of every id route. /objects/content?object_url= already works this way for object URLs.

Notes

  • A raw-slash request (/tasks/@local/a/b) is a 404 by design: clients encode.
  • A reverse proxy in front of a hosted explorer must pass %2F through (Apache AllowEncodedSlashes NoDecode; nginx proxy_pass without a URI part). The local explorer has no proxy.
  • EntityId is not exported for plugins. Exporting it would add to the plugin surface, and no explorer plugin exists yet. check_plugin_api.py: no break.
  • Conflicts: feat(explorer): brand the UI as AgentEnv Explorer and link the framework docs #20 also edits routers/common.py and app.py (route summaries). Whichever lands second needs a small rebase.
  • Open question on the bundle/registry design: should this be the URL convention for every HTTP surface that addresses namespaced ids? That includes the hosted hub, whose task-id check (^[a-zA-Z0-9_\-][a-zA-Z0-9_.:@\-]*$) rejects / today, and future shared-registry namespaces.

Testing

  • New tst/unit/explorer/entity_ids_test.py: route_path round-trips 8 ids (flat, bundle, reserved-looking segments, spaces/&/+/,/@, unicode, a literal %2F, %25, #/?).
  • New tests in explorer_app_test.py:
    • 7 ids × 5 collections, including bare ids with a literal %2F or %25 (weird%2Fbare, 100%25done): get and versions return the id, and the unencoded form 404s.
    • A task whose id ends in runs (and one holding a literal %2F and %25) goes through every run route: start run, start runs, list runs, instances, instance, progress, run groups, run group, the stream reaching complete, and cancel.
    • A bundle run's namespaced instance id works on the instance, progress and conversations routes.
  • On main, 26 of the new tests fail; with this PR all 92 explorer tests pass. Decoding %25 in the router, the double-decoding bug, fails all 11 literal-percent HTTP cases. The full unit tier passes except 2 test_mcp_cli_builder tests that fail identically on main here: the generated CLI's interpreter has no click, which is environmental.
  • UI: typecheck, test:smoke and build:static pass.
  • In the browser against agent-env up with the real hello bundle: opened @local/agentenv-framework/hello/hello from the Tasks Hub (404 before), clicked Start 1 Run, watched it complete 3/3 steps, and opened the instance. The CLI run's namespaced instance serves its detail, progress and conversations.

🤖 Generated with Claude Code

RetriggerConfidence Score: 5/5

The PR appears safe to merge; the prior missing HTTP coverage is now present.

What we checked:

  • Literal-percent IDs open the wrong item: The new HTTP cases send each ID as an encoded segment and check that the response returns the original ID. This is test coverage, not a claim of a separately run verification.

Summary

The explorer now accepts bundle entity and instance IDs as one percent-encoded path segment, so those IDs can be looked up and used in run routes. The UI encodes IDs when it builds the affected API paths.

  • Routes decode IDs after matching the encoded path segment.
  • Entity, run, instance, progress, and conversation routes use the shared ID type.
  • Environment, task, universe, and progress requests encode IDs in the UI.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A["UI encodes the ID"] --> B["One URL path segment"]
  B --> C["Middleware keeps encoded slashes together"]
  C --> D["Route decodes the ID"]
  D --> E["Store looks up the original ID"]
Loading

Reviews (2) · Last reviewed commit: "test(explorer): cover ids with a literal..."

@polakamtejas
polakamtejas requested review from a team as code owners September 30, 2026 20:31
Comment thread tst/unit/explorer/explorer_app_test.py
@polakamtejas
polakamtejas requested a review from a team as a code owner September 30, 2026 21:26
polakamtejas and others added 2 commits September 30, 2026 17:28
…o bundle ids resolve

Bundle ids are @<namespace>/… paths (@local/<package>/<name>, or a bundle
folder's whole path), and a bundle run's instance ids are too. The explorer's
id routes took a single-segment {entity_id} / {task_id}, and the ASGI server
decodes %2F before routing, so every bundle entity 404'd: the hello example
could be listed but not opened, run or versioned.

An id in an explorer URL is now one percent-encoded path segment, '/' sent as
%2F, which is also what the published OpenAPI path parameters require.
EncodedIdRouting routes the entity and instance paths on the raw path with
%2F and %25 kept escaped, and the EntityId parameter type decodes the matched
segment. Flat ids are unchanged. A path converter was not used: it misroutes
ids whose trailing segments look like a route (a folder named "runs" or
"instances"). The UI now encodes the id at the 7 call sites that did not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
route_path keeps %25 escaped so an id is decoded exactly once; the HTTP cases
only covered slashes and unicode. Bare ids weird%2Fbare and 100%25done join
the get/versions matrix, and a task id holding both runs the run routes.
Decoding %25 in the router fails all of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@polakamtejas
polakamtejas force-pushed the tejas/explorer-encoded-entity-ids branch from b11d910 to 2c9d29a Compare September 30, 2026 21:29
@polakamtejas
polakamtejas merged commit 82772e5 into main Oct 1, 2026
13 checks passed
@polakamtejas
polakamtejas deleted the tejas/explorer-encoded-entity-ids branch October 1, 2026 05:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants