Session-scoped cwd: AgentLoop.create accepts one, the SDK wire has no way to send it #5516
DataNovarCo
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Context
We run dsh as the agent runtime behind an internal control plane: one shared dsh process serves
many authenticated users, each with their own conversations. Identity, permissions and quotas
live in the control plane; dsh is the execution engine.
This is the same deployment shape as #3389. In that thread a maintainer noted that per-user
sandboxing and quotas are a separate, larger topic — this is an attempt at one narrow, concrete
piece of it. It is a different axis from #4443, which scopes MCP servers and skills per workspace
(one operator, several projects); this is about per-session filesystem location.
Measured on
0.1.1-rc.2.What the types say
Three things are pinned at the process-wide handshake:
@deepseek-ai/dsh-sdk-protocol/lib/types/types.d.tsA turn carries no workspace dimension, and creates the session lazily:
But the session model itself does carry a per-session cwd:
@deepseek-ai/dsh-sessionAnd the in-process loop already accepts it:
@deepseek-ai/dsh-agent-loopThe gap, in one sentence
The session model has a per-session
cwdandAgentLoop.createalready takes it — but the SDKJSON-RPC surface has no way to send it, so every SDK-created session inherits the single
process-wide value from
initialize.What this costs an integrator
Because
cwd,providerandmodelare all pinned at the same handshake, anyone wantingper-user working directories has to key their process pool by user as well as by model. For us
that means one dsh child process per (user × model). Each process loads the full plugin tree; we
budget roughly 0.8 GB apiece.
That turns "give each user their own directory" from a path decision into a memory-capacity
decision — so we did not do it.
What we do instead today
We intercept at the plugin seams and do the mapping in our own layer:
tools/pre-execute→{ kind: 'deny', reason }for calls we refuse. The model receives thereason verbatim, which works well — it explains itself to the user instead of just failing.
tools/execute→ dropnext(), perform the call ourselves against a per-user directory,return the result.
This works, but it only covers tools that take a path argument. On our production ledger
run_codeaccounts for 427 of 493 file-writing tool calls, so most writes bypass anypath-level rewrite. I'll add those numbers to #3245 rather than expand on it here.
What we are not asking for
We are not asking the fs sandbox to become a security boundary.
@deepseek-ai/dsh-fs-sandboxalready states the stance plainly:
We agree with that stance and are not proposing to change it.
Asks, cheapest first
1. Expose what already exists. Let the SDK wire pass
meta.cwdat session creation, sinceAgentLoop.createalready accepts it. Either an explicitsession.createrequest, or an optionalcwdonSessionPromptParamshonoured only when the id is unknown — which matches the documentedlazy-create behaviour and stays backward compatible.
2. Same question for
provider/model. They are pinned identically, and per-sessionselection would remove the other multiplier in the pool arithmetic above. Separate ask; mentioned
only because the cause is the same line of code.
3. Docs, not code. The "containment, not a security boundary" sentence lives in the module doc
of
dsh-fs-sandbox. The names an integrator actually configures —read-only,workspace-write— read like a boundary. We built a deployment plan on the assumption that
workspace-writebounded reads too, and found otherwise only by testing: with
workspaceRootset, an agent read afile above the root successfully. One line in the user-facing sandbox docs saying reads are
unfenced would have saved us that. Happy to send that PR.
Happy to prototype (1) if the shape is agreed.
All reactions