Skip to content

Ask Simlock who you are: identity.get, GET /v1/identity, simlock whoami #713

Closed

Description

@V3RON

Part of #701.

Scope

Anyone can ask Simlock who it thinks they are: the contract operation identity.get, served over HTTP as GET /v1/identity, on the CLI as simlock whoami, and in the typed client. It answers for a token, a socket session and a gateway uplink. The proxy answer comes in #714, which only fills the fields this task defines.

In short

A new question any caller may ask: "who am I?". It answers with the principal Simlock acts for, a name to show, the role, and how the caller signed in (via).

flowchart LR
  C[CLI / HTTP / client] --> D["Dispatcher: identity.get (new)"]
  D --> S[the caller's session]
Loading
$ simlock whoami                     # on the daemon's host, where the CLI can read admin.token
Principal: claude-1
Name:      claude-1
Role:      admin
Via:       socket

$ simlock whoami --json
{"principal":"claude-1","name":"claude-1","role":"admin","via":"socket"}

$ curl -s -H "Authorization: Bearer slk_…" http://127.0.0.1:4700/v1/identity
{"principal":"tok_9f2c","name":"ci-runner","role":"agent","via":"token"}
Caller principal name role via
socket, no credential, declared claude-1 claude-1 claude-1 agent socket
socket, admin secret the declared principal same admin socket
socket, operator token (--token slk_…) the declared principal same admin socket
HTTP, agent token tok_9f2c labelled ci-runner tok_9f2c ci-runner agent token
HTTP, operator token tok_1a2b, no label tok_1a2b tok_1a2b admin token
a gateway's uplink session on a worker gw:<gateway id> gw:<gateway id> admin uplink

If this goes wrong, a user would see the wrong name or role, or a whoami that fails for some callers.

Technical spec

Facts

Commit ccb50353.

# Fact Source
F1 Operations are defineOperation({ name, role, effect, input, output }) and listed in a name map src/contract/operations.ts:687-693,1020,1039
F2 DispatchSession is { principal, role, isGatewayUplink?, … } src/daemon/dispatch.ts:56-58
F3 A socket session's principal is the one declared at hello, else the daemon's default requester id src/daemon/server.ts:914
F4 An uplink session is accepted with role forced admin and principal gw:<gateway instance id> src/daemon/server.ts:653-655; src/daemon/main.ts:763
F5 HTTP builds the session from the identity: principal = token id, operator → admin src/http/dispatcher-session.ts:24-26,74-77
F6 Each dispatcher maps every operation to a handler ("token.list": this.#tokenList) src/daemon/dispatcher.ts:293; src/gateway/dispatcher.ts:158
F7 The gateway may import from src/daemon only dispatch.ts .dependency-cruiser.mjs:152
F8 An HTTP route dispatches with buildHttpSession(c.get("identity")) behind agentAuth src/http/app.ts:626-628
F9 CLI commands are lazy nodes in src/cli/tree.ts; token loads commands/token.ts src/cli/tree.ts:84-91
F10 The typed client has one method per operation (#call("token.list", {})) src/simlock-client/client.ts:659
F11 Protocol range is { min: 39, max: 39 }; every contract shape change bumps it by one src/contract/protocol.ts:13; docs/internal/agent-rules/toolchain.md:156-158
F12 simlock status takes --json and then prints the object with writeResult; without it, labelled lines src/cli/commands/status.ts:12-21; src/cli/args.ts:30-31
F13 #712's Identity carries principal, name, role, via, ambient, signOutUrl? #701 Shared values
F14 Every CLI command but daemon status/daemon stop and the history read connects with auto-launch, so it starts the daemon when none runs src/cli/connect.ts:44-45; src/cli/environment.ts:137,170; src/cli/commands/daemon.ts:39,52
F15 Modules get a second session shape, ModuleDispatchSession { principal, role, manageEventSubscription }, and main.ts passes the HTTP dispatch where a ModuleDispatch is expected src/modules/types.ts:66-76,90-91; src/daemon/main.ts:606-616,913-924
F16 On a worker every operation but hello, status.get and daemon.stop waits for startup to finish src/daemon/dispatch.ts:322; src/daemon/AGENTS.md Rules
F17 The socket server switches on the operation name; an unknown one answers UNKNOWN_REQUEST src/daemon/server.ts:1118,1175-1176
F18 A real uplink session can be built in a test src/daemon/test-relay-support.ts:199

Modules touched

Searches: rg '"token.list"' (every place one operation is wired: contract and its tests' role and effect tables, both dispatchers, the socket switch, HTTP, client, CLI), rg 'DispatchSession|ModuleDispatchSession', rg 'manageEventSubscription:' src -g '!*.test.ts' (every place a session object is built: src/http/app.ts:694, src/daemon/server.ts:1197, src/http/dispatcher-session.ts:75, src/daemon/test-relay-support.ts:24).

flowchart LR
  CLI["cli: whoami (new)"] --> CL["simlock-client: identity() (new)"]
  CL --> SV[daemon server]
  H["http: GET /v1/identity (new)"] --> DD
  SV --> DD["daemon dispatch.ts: identity handler (new)"]
  GD["gateway dispatcher (changed)"] --> DD
  DD --> CT["contract: identity.get (new)"]
Loading
  • src/contract/operations.ts, src/contract/operations.test.ts (role and effect tables), src/contract/protocol.ts.
  • src/daemon/dispatch.ts (session fields, the shared handler), src/daemon/dispatcher.ts, src/daemon/server.ts (socket and uplink sessions set via; the socket switch gains the case).
  • src/modules/types.ts: ModuleDispatchSession gains via and name?, so the HTTP dispatch still fits ModuleDispatch (F15).
  • src/gateway/dispatcher.ts (maps the operation to the shared handler).
  • src/http/dispatcher-session.ts, src/http/app.ts.
  • src/simlock-client/client.ts (identity() on SimlockClient, so the admin client has it too), types.ts (IdentityOutput), re-exported from src/client/index.ts; src/cli/tree.ts, new src/cli/commands/whoami.ts.
  • Docs: docs/HTTP-API.md, docs/CLI.md, docs/CLIENT.md.

Contract and event changes

  • New operation identity.get: role agent (any caller), effect read, input {}, output { principal: string, name: string, role: "admin" | "agent", via: "token" | "proxy" | "socket" | "uplink", signOutUrl?: string }. proxy and signOutUrl are never produced by this task; Sign in through a sign-in proxy's signed JWT #714 produces them.
  • DispatchSession gains name?: string, via: "token" | "proxy" | "socket" | "uplink", signOutUrl?: string. name absent means the principal. Identity.via is a string (the strategy's name); buildHttpSession parses it against this enum, and the registry test below keeps the two in step.
  • This PR bumps the protocol by one.
  • No event.

Other code on the same state

State: the session object. Search: as in Modules touched (manageEventSubscription:): the socket accept and the uplink accept (server.ts), buildHttpSession, the events-stream session (app.ts:694), and the test relay. Each sets via (socket, uplink, the identity's via); none changes principal or role. No other task writes the session except #714, which passes a proxy identity through buildHttpSession.

Failure modes

Call Outcome What happens Limit, counted from
GET /v1/identity no or bad token 401 as every route (#712) none
same worker token 403 as every route none
simlock whoami daemon not running starts it, as every command but daemon status/stop does (F14), then answers the CLI's usual launch limit
identity.get on a worker daemon still starting waits for startup, as every operation but hello, status.get, daemon.stop (F16) the daemon's usual startup parking
identity.get over a protocol-39 socket older daemon UNKNOWN_REQUEST is impossible: the hello's range already refuses the mismatch the hello

State: none written.

Rules in play

  • ADR 0003 §2, one contract behind every frontend: identity.get is an operation, and HTTP, CLI and client render it.
  • ADR 0003 §4, "the principal is declared once at hello": the socket answer is the declared principal, as Simlock sees it.
  • Architecture rule 10, one place: one handler in dispatch.ts, used by both dispatchers.
  • toolchain.md, a shape change bumps the protocol by one.

Tests

Seam: the HTTP app (createHttpApp) with the real shared handler registered on its FakeDispatcher, the daemon socket, and a real uplink session (F18); end to end in e2e/token-route.test.ts's style.

  • src/daemon/server.test.ts: over a real socket, identity.get answers the declared principal as principal and name, via: "socket", role agent with no credential, admin with the admin secret, admin with an operator token.
  • src/daemon (with test-relay-support.ts): over a real uplink session, identity.get answers gw:<id>, admin, via: "uplink".
  • src/gateway/dispatcher.test.ts: the gateway answers identity.get from its own caller's session, the same shape.
  • src/http/app.test.ts (the real handler from dispatch.ts registered): GET /v1/identity with an agent token labelled ci-runner answers principal = token id, name ci-runner, role agent, via token; with an unlabelled operator token, name = token id, role admin; with no token, 401 "Missing bearer token".
  • e2e/identity.test.ts (new, modelled on e2e/token-route.test.ts): against a real daemon, simlock whoami prints the four labelled lines for the CLI's own session, and --json prints the same as one object; GET /v1/identity with a minted agent token answers that token's id; with a worker token, 403.
  • e2e/contract-surface.test.ts passes with identity.get added (every operation has a socket case).
  • e2e/identity.test.ts: the same two checks against a gateway answer the gateway's own sessions.
  • src/simlock-client test: identity() sends identity.get and returns its output.
  • src/daemon/dispatcher.test.ts: the shared handler copies signOutUrl from a session that has one into its output, and omits it otherwise.
  • src/auth (beside the conformance suite): every registered strategy's name is a value of identity.get's via enum. The contract cannot import src/auth, so this test keeps the hand-written enum and the registry in step (architecture rule 20).

Done when

  • GET /v1/identity, simlock whoami and simlock whoami --json answer as the table in In short, against a worker and a gateway, and over an uplink.
  • The protocol range is one above main's at the time of merge.
  • The tests above pass; pnpm test:e2e e2e/identity.test.ts e2e/token-route.test.ts e2e/contract-surface.test.ts passes.
  • docs/HTTP-API.md, docs/CLI.md and docs/CLIENT.md describe the route, the command and the method.

Out of scope

Depends on

Written by an agent.

Activity

  1. added
    task:draftScope written; technical spec or its spec check missing.
    on Oct 19, 2026
  2. added
    task:readySpec checked. An agent may implement it once its Depends on allow.
    and removed
    task:draftScope written; technical spec or its spec check missing.
    on Oct 19, 2026
  3. self-assigned this
    on Oct 19, 2026
  4. added 3 commits that reference this issue on Oct 19, 2026
    aeef6bc
    7e12585
    ecb26e1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

task:readySpec checked. An agent may implement it once its Depends on allow.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions