Request: #153
Problem
When neither --agent-id nor SIMLOCK_AGENT_ID is set, the requester id is the CLI's pid. Every invocation is then a new requester. The one-lease-per-agent rule constrains nothing, simlock status cannot say which agent holds what, and against a gateway simlock simctl and simlock adb with no --lease cannot find the lease an earlier --detach left behind.
Most agents never set SIMLOCK_AGENT_ID, because nothing told them to. The agent tools they run in already export a session id, and Simlock ignores it.
Who it is for
- A coding agent running under Claude Code or Codex that calls
simlock (CLI or simlock mcp) without any Simlock-specific setup.
- The operator reading
simlock status or simlock list --leases to see which agent holds which device.
Outcome
- Under a Claude Code or Codex session, with no
--agent-id and no SIMLOCK_AGENT_ID, every simlock invocation in that session is the same requester. A second simlock lease from the same session is refused with REQUESTER_ALREADY_LEASED; against a gateway, simlock simctl and simlock adb with no --lease find the session's lease.
- The detected id fills exactly the slot
SIMLOCK_AGENT_ID fills today, in the CLI and in simlock mcp alike. Nothing else about identity changes.
- The id names the tool it came from, so an operator reading
status can tell a Claude Code session from a Codex one: claude-code:<session id> or codex:<session id>.
- Resolution order, first match wins:
--agent-id, SIMLOCK_AGENT_ID, the agent tool's session id, the pid-derived value. An explicit id always wins, so every existing setup behaves as before.
- Sub-agents and parallel commands inside one agent session share that session's id, and therefore its one lease.
Non-goals
- The HTTP API. Identity there is the token, never client-declared.
- The programmatic client. Its caller names the principal.
- Gateway requester namespacing, which is unchanged.
- A config key or flag to teach Simlock about other tools. The supported tools are a fixed list in this feature.
- Warning when the pid-derived fallback is used.
- Changing what
--agent-id or SIMLOCK_AGENT_ID mean.
Completion conditions
- With only a Claude Code session id in the environment, two
simlock lease calls in that session name the same requester, and the second answers REQUESTER_ALREADY_LEASED naming the first lease. Same for a Codex session.
- In that session, against a gateway,
simlock lease --detach then simlock simctl with no --lease runs against that lease's device.
simlock status and simlock list --leases show the requester as the tool's name followed by the session id.
simlock mcp started in that session leases under the same id the CLI resolves there.
- With a session id and
SIMLOCK_AGENT_ID both set, the requester is SIMLOCK_AGENT_ID's value. With --agent-id also given, it is --agent-id's value.
- With no session id, no
SIMLOCK_AGENT_ID and no --agent-id, behaviour is unchanged: the pid-derived id.
- A session id that is set but empty is skipped, and resolution continues down the order.
- The "Agent identity" section of
docs/CLI.md, the simlock mcp section, and the README's MCP setup describe the new order and no longer say that a stable id needs setup under a supported tool.
Open questions
None.
Decisions
None. No ADR: the change is one shared function, a two-row table and docs. Adding a tool later is one row and one test.
Technical spec
Modules touched
-
New src/agent-identity/index.ts, its own module like src/lease-policy, imported by both frontends. It exports one function, resolveRequesterId(env, fallback), and the table it reads:
| tool |
variable |
claude-code |
CLAUDE_CODE_SESSION_ID |
codex |
CODEX_SESSION_ID |
Order: SIMLOCK_AGENT_ID when defined, exactly as today. Else the first table row whose variable is set and not the empty string, as <tool>:<value>. Else fallback. No other validation: the value is passed through as SIMLOCK_AGENT_ID is today. Claude Code sets its variable for shell commands and for stdio MCP servers it starts. The Codex app sets CODEX_SESSION_ID.
-
src/cli/index.ts: fallbackRequesterId becomes a call to resolveRequesterId(env, String(process.pid)), or is deleted and buildCliEnvironment calls the module directly. The comments on CliEnvironment.requesterId and on the old helper say "SIMLOCK_AGENT_ID else pid" and must be rewritten.
-
src/mcp/main.ts: startMcpStdio resolves environment.requesterId ?? resolveRequesterId(env, \mcp:${process.pid}`). The env` field's comment must be rewritten. The two pid fallbacks stay as they are; only the shared part moves.
-
Rendering: none. status and list --leases print requesterId as-is.
-
Docs: docs/CLI.md "Agent identity" (new step 3 in the order, the two variables, the <tool>:<id> shape, the sub-agent note) and "simlock mcp" (drop "set a distinct SIMLOCK_AGENT_ID per server process" as the only way); README.md "MCP integration" (same); the README's config snippet keeps SIMLOCK_AGENT_ID as the explicit-id example.
Contract and event changes
None. lease.request's requesterId is already a free string; lease.requested and lease.granted payloads are unchanged. No EVENTS.md entry.
Rules in play
architecture.md rule 10: the CLI and MCP fallbacks are two copies of one rule today. After this change there is one, and both frontends call it. Rule 8: resolution stays in the frontend; the daemon never reads the environment. Rule 13: the three comments named above become false and are fixed in the same commit.
safety.md rule 10: an environment value is a claim. Empty is skipped, and beyond that it gets exactly SIMLOCK_AGENT_ID's treatment, no more.
documentation.md rules 2 and 3: docs/CLI.md and README.md are end-user docs. Nothing printed by the tool names a repo path.
testing.md rules 1 to 3: the existing test "fallbackRequesterId prefers SIMLOCK_AGENT_ID over a pid-derived default" is replaced by the tests below. Each row of the table needs a test that goes red when the row is deleted.
Tests
resolveRequesterId returns SIMLOCK_AGENT_ID unchanged when it is defined, even with a Claude Code session id also set
resolveRequesterId returns claude-code:<id> from CLAUDE_CODE_SESSION_ID when SIMLOCK_AGENT_ID is unset
resolveRequesterId returns codex:<id> from CODEX_SESSION_ID when SIMLOCK_AGENT_ID is unset
resolveRequesterId skips a session variable set to the empty string and takes the next row
resolveRequesterId returns the caller's fallback when no variable is set
buildCliEnvironment resolves requesterId to the session-derived id when SIMLOCK_AGENT_ID is unset
simlock lease --agent-id overrides a session-derived id
startMcpStdio sources the connection principal as claude-code:<id> from CLAUDE_CODE_SESSION_ID when neither requesterId nor SIMLOCK_AGENT_ID is given
startMcpStdio prefers an explicit requesterId over a session-derived id
- the CLI and
simlock mcp resolve the same requester id from the same environment
Written by an agent.
Request: #153
Problem
When neither
--agent-idnorSIMLOCK_AGENT_IDis set, the requester id is the CLI's pid. Every invocation is then a new requester. The one-lease-per-agent rule constrains nothing,simlock statuscannot say which agent holds what, and against a gatewaysimlock simctlandsimlock adbwith no--leasecannot find the lease an earlier--detachleft behind.Most agents never set
SIMLOCK_AGENT_ID, because nothing told them to. The agent tools they run in already export a session id, and Simlock ignores it.Who it is for
simlock(CLI orsimlock mcp) without any Simlock-specific setup.simlock statusorsimlock list --leasesto see which agent holds which device.Outcome
--agent-idand noSIMLOCK_AGENT_ID, everysimlockinvocation in that session is the same requester. A secondsimlock leasefrom the same session is refused withREQUESTER_ALREADY_LEASED; against a gateway,simlock simctlandsimlock adbwith no--leasefind the session's lease.SIMLOCK_AGENT_IDfills today, in the CLI and insimlock mcpalike. Nothing else about identity changes.statuscan tell a Claude Code session from a Codex one:claude-code:<session id>orcodex:<session id>.--agent-id,SIMLOCK_AGENT_ID, the agent tool's session id, the pid-derived value. An explicit id always wins, so every existing setup behaves as before.Non-goals
--agent-idorSIMLOCK_AGENT_IDmean.Completion conditions
simlock leasecalls in that session name the same requester, and the second answersREQUESTER_ALREADY_LEASEDnaming the first lease. Same for a Codex session.simlock lease --detachthensimlock simctlwith no--leaseruns against that lease's device.simlock statusandsimlock list --leasesshow the requester as the tool's name followed by the session id.simlock mcpstarted in that session leases under the same id the CLI resolves there.SIMLOCK_AGENT_IDboth set, the requester isSIMLOCK_AGENT_ID's value. With--agent-idalso given, it is--agent-id's value.SIMLOCK_AGENT_IDand no--agent-id, behaviour is unchanged: the pid-derived id.docs/CLI.md, thesimlock mcpsection, and the README's MCP setup describe the new order and no longer say that a stable id needs setup under a supported tool.Open questions
None.
Decisions
None. No ADR: the change is one shared function, a two-row table and docs. Adding a tool later is one row and one test.
Technical spec
Modules touched
New
src/agent-identity/index.ts, its own module likesrc/lease-policy, imported by both frontends. It exports one function,resolveRequesterId(env, fallback), and the table it reads:claude-codeCLAUDE_CODE_SESSION_IDcodexCODEX_SESSION_IDOrder:
SIMLOCK_AGENT_IDwhen defined, exactly as today. Else the first table row whose variable is set and not the empty string, as<tool>:<value>. Elsefallback. No other validation: the value is passed through asSIMLOCK_AGENT_IDis today. Claude Code sets its variable for shell commands and for stdio MCP servers it starts. The Codex app setsCODEX_SESSION_ID.src/cli/index.ts:fallbackRequesterIdbecomes a call toresolveRequesterId(env, String(process.pid)), or is deleted andbuildCliEnvironmentcalls the module directly. The comments onCliEnvironment.requesterIdand on the old helper say "SIMLOCK_AGENT_IDelse pid" and must be rewritten.src/mcp/main.ts:startMcpStdioresolvesenvironment.requesterId ?? resolveRequesterId(env, \mcp:${process.pid}`). Theenv` field's comment must be rewritten. The two pid fallbacks stay as they are; only the shared part moves.Rendering: none.
statusandlist --leasesprintrequesterIdas-is.Docs:
docs/CLI.md"Agent identity" (new step 3 in the order, the two variables, the<tool>:<id>shape, the sub-agent note) and "simlock mcp" (drop "set a distinctSIMLOCK_AGENT_IDper server process" as the only way);README.md"MCP integration" (same); the README's config snippet keepsSIMLOCK_AGENT_IDas the explicit-id example.Contract and event changes
None.
lease.request'srequesterIdis already a free string;lease.requestedandlease.grantedpayloads are unchanged. NoEVENTS.mdentry.Rules in play
architecture.mdrule 10: the CLI and MCP fallbacks are two copies of one rule today. After this change there is one, and both frontends call it. Rule 8: resolution stays in the frontend; the daemon never reads the environment. Rule 13: the three comments named above become false and are fixed in the same commit.safety.mdrule 10: an environment value is a claim. Empty is skipped, and beyond that it gets exactlySIMLOCK_AGENT_ID's treatment, no more.documentation.mdrules 2 and 3:docs/CLI.mdandREADME.mdare end-user docs. Nothing printed by the tool names a repo path.testing.mdrules 1 to 3: the existing test "fallbackRequesterId prefers SIMLOCK_AGENT_ID over a pid-derived default" is replaced by the tests below. Each row of the table needs a test that goes red when the row is deleted.Tests
resolveRequesterIdreturnsSIMLOCK_AGENT_IDunchanged when it is defined, even with a Claude Code session id also setresolveRequesterIdreturnsclaude-code:<id>fromCLAUDE_CODE_SESSION_IDwhenSIMLOCK_AGENT_IDis unsetresolveRequesterIdreturnscodex:<id>fromCODEX_SESSION_IDwhenSIMLOCK_AGENT_IDis unsetresolveRequesterIdskips a session variable set to the empty string and takes the next rowresolveRequesterIdreturns the caller's fallback when no variable is setbuildCliEnvironmentresolvesrequesterIdto the session-derived id whenSIMLOCK_AGENT_IDis unsetsimlock lease --agent-idoverrides a session-derived idstartMcpStdiosources the connection principal asclaude-code:<id>fromCLAUDE_CODE_SESSION_IDwhen neitherrequesterIdnorSIMLOCK_AGENT_IDis givenstartMcpStdioprefers an explicitrequesterIdover a session-derived idsimlock mcpresolve the same requester id from the same environmentWritten by an agent.