Skip to content

feat(envs): add read-only worlds and sandbox profiles - #323

Open
Wings-Of-Disaster wants to merge 2 commits into
modelscope:mainfrom
Wings-Of-Disaster:pr1-sandbox-env
Open

Wings-Of-Disaster wants to merge 2 commits into
modelscope:mainfrom
Wings-Of-Disaster:pr1-sandbox-env

Conversation

@Wings-Of-Disaster

@Wings-Of-Disaster Wings-Of-Disaster commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

PR type

  • Bug Fix
  • New Feature
  • Document Updates
  • More Models or Datasets Support

PR information

Add host-served read-only data, tool profiles and reproducible workspace seeds for agentic tasks.
Mixins compose these features with existing environments.

Use an existing world bundle containing manifest.json, ops.db and docs/:

from twinkle_agentic.envs import HostWorld, LocalEnv, WorldToolsMixin

class WorldEnv(WorldToolsMixin, LocalEnv):
    pass

world = HostWorld('/data/world')
world.load()
env = WorldEnv(workspace='/tmp/slot', world=world.fork(), release_world_on_close=True)
print(env.step('world_schema', {}).observation)
env.close()

Profiles filter dispatch and seed workspaces. Live search and PostgreSQL need deployment backends;
aliases to run_command grant that full command tool. World forks isolate receipts and database roles.

Experiment results

  • 21 world-tool tests passed, including two new state-isolation regressions.

Two problems, one commit, because the second exists to make the first safe to
hand out.

An agentic run needs tasks whose answers the policy cannot invent. A task that
says "report the failure rate in the ops database" is only a real task if the
database exists and the policy has to read it; otherwise the policy writes
plausible numbers and a check that compares strings passes either way. Measured on
one run's 256 generated tasks: every one of them verified with os.path.isfile(),
and 27% parsed a SQLite file the model had itself just written -- so a high score
meant the answer agreed with its own premise.

And every environment the run handed out was the same environment: the same tools,
an empty workspace, and a topic that had no bearing on either. A task about
querying an operations database and a task about office-chair ergonomics then
differ only in their prompt, and the policy learns that the fixture is something
it can invent -- because for most topics it was.

world.py -- HostWorld is a read-only SQLite database, a document set and a frozen
search corpus, served from the training host and exposed as seven tools
(world_sql, world_schema, world_doc, web_search, fetch_page, db_query, db_schema).
Host-side is the point, not an implementation detail: a tool call already
round-trips through Env.step_batch, so the host sees every call before and after
the sandbox does and needs no network path into it. That keeps a networkless
environment honestly networkless, adds no listening port to defend, and keeps the
data out of the sandbox where it could be edited or left behind in a snapshot.

Every retrieval answer is recorded as a receipt, with the urls taken from the
tool's own output rather than its arguments -- web_search is asked a query and
decides which urls to return, so what was actually served is only knowable from
the result. A fetch that failed records the call with no url, so "I fetched this"
has to be earned. This is what makes a citation gradeable rather than assertable.

WorldToolsMixin exposes it on any Env. A mixin rather than a change to AgentEnv,
so it applies to LocalEnv and OpenEnv too and the host half is testable without a
sandbox host -- which is how all 19 of its tests run. It strips the
{server}---{tool} suffix a harness adds, and tracks step_batch positions
explicitly so an observation cannot be paired with the wrong call. Tools are
advertised only when their backing store is present: offering one that always
answers "not configured" costs the policy a turn, and a difficulty measurement
built on that episode counts the wasted turn as task work.

profiles.py -- SandboxProfile is an immutable manifest: capabilities, task forms,
allowed tools, which verifier kinds can grade it, and what the workspace must
contain before the episode starts. Validated at construction rather than at use;
a profile granting raw_network is refused outright, because serving the world from
the host is what keeps the sandbox networkless and a profile could otherwise undo
that quietly. Each profile and the registry carry a content digest, so a run can
record which profile set it used and a reader can tell whether two runs are
comparable. Thirteen ship in default_profile_registry().

ProfiledEnvMixin narrows tools() to the profile and refuses anything else in
step()/step_batch(), because advertising is not enforcement -- a policy can name a
tool it was never shown. switch_profile runs the profile's setup manifest, and a
step that does not resolve, or a canary that fails, raises rather than being
recorded: a slot missing its fixture produces tasks that read as hard, not as
broken.

Order matters and is documented at both classes: ProfiledEnvMixin has to precede
WorldToolsMixin, so the world contributes its tools first and the profile then
decides which survive. Reversed, a profile could not restrict the world at all.

seeds.py -- the programs those manifests name. A seed is what makes a task
checkable against data the policy did not author: seed-sql-analytics builds a
shop.db with customers, products and orders; seed-csv-dirty writes a CSV with a
BOM, duplicate rows and empty cells; seed-broken-repo lays down a repository whose
tests fail; seed-log-corpus writes two logs that disagree; seed-docs-corpus plants
a document set with decoys in it. Programs rather than data files, so the fixture
is reproducible from the profile alone and a run ships no bytes.

Each seed is a template holding one {workspace!r} placeholder, substituted by
exact str.replace and never by str.format: the programs are real python and
contain set and dict comprehensions whose braces format() would read as
replacement fields.

The live search backend and the per-episode database role provisioning are
deployment-specific, so both name their module directory explicitly
(TWINKLE_WORLD_SEARCH_MODULE, TWINKLE_WORLD_DB_MODULE) and raise WorldUnavailable
when it is absent. Neither provider is named anywhere here. psycopg2 and the
search plug-in are imported lazily, so a corpus-only run needs no database driver
and no search dependency.

Only envs/__init__.py is touched among existing files, to export the new names. No
environment's behaviour changes unless a caller mixes one of these in.
@Wings-Of-Disaster Wings-Of-Disaster changed the title feat(envs): a host-served read-only world, and least-privilege profil… feat(envs): a host-served read-only world, and least-privilege profiles for it Oct 20, 2026
Give each slot a shallow fork of loaded world data with independent retrieval
receipts and database credentials. Owned worlds discard those mutable fields
when an environment is reset, preventing one episode from inheriting another's
retrieval evidence or role. The cookbook opts into this ownership explicitly.

Remove the unused split_calls helper; WorldToolsMixin already dispatches batches
while preserving indices. Keep manifest validation, all seven tool schemas,
13 profiles, all workspace seed programs and the tool-alias limitation intact.
Condense repeated design discussion into API and deployment guidance.

Validation: world/slot targeted run passed 39 cases, including world-fork and
reset regressions; no training or broad regression suite was rerun.
@Wings-Of-Disaster Wings-Of-Disaster changed the title feat(envs): a host-served read-only world, and least-privilege profiles for it feat(envs): add read-only worlds and sandbox profiles Oct 21, 2026
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.

1 participant