Skip to content

fix: bind agent and MCP sessions to their owner - #506

Merged
ShotaroKataoka merged 4 commits into
mainfrom
fix/session-ownership
Nov 3, 2026
Merged

ShotaroKataoka merged 4 commits into
mainfrom
fix/session-ownership

Conversation

@ShotaroKataoka

Copy link
Copy Markdown
Contributor

What changed

Agent and remote MCP sessions in Cloud mode are now bound to the user who owns them.

  • Owner-prefixed session ids. New Cloud session ids start with an owner prefix derived from the signed-in user: the first 16 hex characters of SHA-256 over the token's sub. The derivation is defined once in shared/session_owner.py and mirrored in web-ui/src/lib/sessionId.ts.
  • Agent entry (agent/session_guard.py, called from basic_agent.agent_stream before the cancel registry): an owner-prefixed id is admitted only for its owner. Each runtime process serves the first user it admits. Refusals are returned as error events with the codes auth, bad_input (header and payload ids differ), session_reload_required, or service_unavailable, without further detail.
  • Earlier-format ids. A session id in the earlier format is judged by its recorded owner: it is admitted when AgentCore Memory holds this user's events for it. This is one ListEvents call with maxResults=1, and positive results are cached per process.
  • Agent → MCP. Mcp-Session-Id always carries the owner prefix. Earlier ids get it prepended, and composer groups (<id>-gN) keep it.
  • Remote MCP server (_CaptureHeadersMiddleware): an owner-prefixed Mcp-Session-Id or runtime session header that belongs to a different user gets a 403, and each process serves one user. Requests without authorization (health checks) pass through unchanged. The session check runs before the cancel registry and the write fence.
  • Web UI. New chats, and decks without a linked chat, get owner-prefixed ids. A new error code session_reload_required has en/ja messages. @aws-crypto/sha256-js is now a direct dependency; it was already in the lockfile at 5.2.0 through aws-amplify.
  • agent/Dockerfile copies shared/session_owner.py. CHANGELOG.md and docs/en/security.md describe the binding.

Compatibility

Existing decks keep their chat. A chat id from before this change is kept while it has history and is admitted when AgentCore Memory holds that user's history for it; a deck without history continues under the user's own owner-prefixed id. A chat tab opened before the update may be asked to reload the page once (session_reload_required).

Deploy order: Web UI first, then SdpmRuntime, then SdpmAgent.

Local mode

Unchanged. Local mode has no auth and keeps its current session ids.

Checks run

On the main change (face6403, including the package.json change from d28354f7):

  • make lint: pass (ruff)
  • make test: pass (2146 passed, 18 skipped)
  • uv run pytest tests/test_remote_session_gate.py tests/test_remote_warmup_preload.py -q: pass (23 passed)
  • i18n en/ja key parity: OK
  • cd web-ui && npx eslint src/ --quiet, npx tsc --noEmit, npm test (75 files, 639 tests), npm run build:cloud: pass
  • ash scan --mode local --fail-on-findings: exit 0

On the lockfile (d28354f7):

  • npx -y npm@10.8.2 install --package-lock-only and npx -y npm@10.8.2 ci --dry-run: pass

After merging current main (#505 write fence, df51fab3):

  • make lint: pass
  • make test: 2177 passed, 18 skipped
  • uv run pytest tests/test_remote_session_gate.py tests/test_remote_write_fence.py -q: 31 passed
  • ash scan --mode local --fail-on-findings: exit 0 (bandit, checkov, detect-secrets, npm-audit, semgrep passed; cdk-nag, cfn-nag, grype, opengrep, syft not available locally)
  • web-ui: no changes from main in the merge, checks not re-run

New Cloud session ids start with the owner prefix of the signed-in user
(first 16 hex characters of SHA-256 over the token's sub), defined once in
shared/session_owner.py and mirrored in web-ui/src/lib/sessionId.ts.

- Agent entry (agent/session_guard.py, called from basic_agent.agent_stream
  before the cancel registry): an owner-prefixed id is admitted only for its
  owner; an id of an earlier format is admitted when AgentCore Memory holds
  this user's events for it (one ListEvents call, maxResults=1, positive
  results cached per process); each process serves the first user it admits.
  Refusals are error events with codes auth / bad_input (header and payload
  ids differ) / session_reload_required / service_unavailable, no details.
- Agent -> MCP: Mcp-Session-Id always carries the owner prefix (earlier ids
  get it prepended; composer groups <id>-gN keep it).
- MCP server (_CaptureHeadersMiddleware): an owner-prefixed Mcp-Session-Id or
  runtime session header of another user gets 403, and each process serves
  one user. Requests without authorization (health checks) pass unchanged.
- Web UI: new chats and decks without a linked chat get owner-prefixed ids;
  a linked chat of an earlier format is kept while it has history. New error
  code session_reload_required with en/ja message. Local mode unchanged.
- agent/Dockerfile copies shared/session_owner.py; CHANGELOG and
  docs/en/security.md describe the binding.

Checks (run on this tree plus the package.json change of the next commit):
- make lint: pass (ruff, all checks passed)
- make test: pass (2146 passed, 18 skipped)
- uv run pytest tests/test_remote_session_gate.py tests/test_remote_warmup_preload.py -q: pass (23 passed, none skipped)
- i18n en/ja key parity: OK
- cd web-ui && npx eslint src/ --quiet: pass
- npx tsc --noEmit: pass
- npm test: pass (75 files, 639 tests)
- npm run build:cloud: pass
- ash scan --mode local --fail-on-findings (uvx ASH v3): pass, exit 0
Already in the lockfile at 5.2.0 as an aws-amplify dependency; lib/sessionId.ts now imports it.

Checks: npx -y npm@10.8.2 install --package-lock-only: pass; npx -y npm@10.8.2 ci --dry-run: pass (exit 0). Code checks are in the previous commit.
Keeps both behaviours: the agent image copies turn_fence.py and the
session_owner module; the MCP server imports both the session owner
helpers and the write fence. The session check still runs before the
cancel registry and the turn fence. The fence entrypoint tests now use a
session id owned by their test user, like the other entrypoint tests.

Checks run on this commit:
- make lint: passed (ruff, no findings)
- make test: 2177 passed, 18 skipped
- uv run pytest tests/test_remote_session_gate.py tests/test_remote_write_fence.py -q: 31 passed
- ash scan --mode local --fail-on-findings: exit 0 (bandit, checkov,
  detect-secrets, npm-audit, semgrep PASSED; cdk-nag, cfn-nag, grype,
  opengrep, syft MISSING locally)
- web-ui: no changes from main, checks not re-run
The pinned owner_prefix() vectors are 16-hex-digit hashes of fixed test
user ids. detect-secrets reports them as high-entropy hex strings, which
fails the ASH scan; annotate them with pragma: allowlist secret, as
infra/ does for CDK logical-id hashes.
@ShotaroKataoka ShotaroKataoka added the blog:skip ブログ対象外 label Nov 3, 2026
@ShotaroKataoka
ShotaroKataoka merged commit e5765f0 into main Nov 3, 2026
19 of 20 checks passed
@ShotaroKataoka
ShotaroKataoka deleted the fix/session-ownership branch November 3, 2026 23:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blog:skip ブログ対象外

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant