Skip to content

fix(gateway): block internal session history over HTTP#55928

Closed
RichardCao wants to merge 4 commits into
openclaw:mainfrom
RichardCao:fix/gateway-http-block-internal-session-history
Closed

fix(gateway): block internal session history over HTTP#55928
RichardCao wants to merge 4 commits into
openclaw:mainfrom
RichardCao:fix/gateway-http-block-internal-session-history

Conversation

@RichardCao

Copy link
Copy Markdown
Contributor

Summary

  • reject HTTP session history requests that target internal subagent, cron, or ACP session namespaces
  • keep ordinary main-session history reads unchanged
  • add regression coverage for subagent, cron, and ACP session keys

Testing

  • pnpm exec vitest run src/gateway/sessions-history-http.test.ts

@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime size: S labels Mar 27, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a175b6cd2f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/gateway/sessions-history-http.ts Outdated

function isInternalSessionHistoryTarget(sessionKey: string): boolean {
return (
isSubagentSessionKey(sessionKey) || isCronSessionKey(sessionKey) || isAcpSessionKey(sessionKey)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Block bare cron keys before serving HTTP history

isInternalSessionHistoryTarget relies on isCronSessionKey(sessionKey), but that helper only returns true for already agent-scoped keys; a request like GET /sessions/cron:daily/history passes this guard. The handler then canonicalizes via resolveGatewaySessionStoreTarget to agent:<default>:cron:daily and will return that internal transcript, so the new HTTP block can be bypassed for cron sessions. This affects any deployment where internal cron sessions exist and callers can hit the history endpoint with a bare cron:* key.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bc5f5b6. The HTTP guard now rejects both raw and canonicalized internal session keys, so bare aliases like cron:daily are denied before history can be served. Added regression coverage for the raw cron:* path in src/gateway/sessions-history-http.test.ts, and re-ran pnpm exec vitest run src/gateway/sessions-history-http.test.ts.

@greptile-apps

greptile-apps Bot commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a targeted HTTP-level guard that rejects session history requests whose session key belongs to an internal namespace (subagent, cron, or ACP). The check is placed correctly in the request pipeline — after bearer auth and scope authorization, and before any session store I/O — so internal transcript data is never read for blocked requests. The implementation delegates cleanly to the existing isSubagentSessionKey, isCronSessionKey, and isAcpSessionKey predicates from session-key-utils.ts, and the new test.each suite covers all three namespaces with realistic seeded sessions.

  • The 403 response shape (ok: false, error.type: "forbidden") matches the existing scope-enforcement pattern in the same handler.
  • The only gap worth noting: the WebSocket chat.history handler (src/gateway/server-methods/chat.ts) does not have the equivalent guard — authenticated callers with operator.read scope can still retrieve internal session history over the WS surface.

Confidence Score: 5/5

Safe to merge — the HTTP guard is correctly implemented with no logic errors or regressions.

The only finding is a P2 observation about the WebSocket surface being outside the stated scope of this PR. The HTTP fix itself is correct, well-tested, and does not introduce any new issues.

No files require special attention; the WebSocket gap exists in src/gateway/server-methods/chat.ts but is pre-existing and outside this PR's scope.

Important Files Changed

Filename Overview
src/gateway/sessions-history-http.ts Adds isInternalSessionHistoryTarget helper and a 403 guard placed correctly after auth/scope checks. Logic and error shape are consistent with the rest of the handler.
src/gateway/sessions-history-http.test.ts Adds seedNamedSession helper and a test.each covering all three internal namespaces (subagent, cron, ACP). Tests correctly verify that sessions with real data still return 403, not 404, confirming the block fires before the store read.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/gateway/sessions-history-http.ts
Line: 196-205

Comment:
**WebSocket `chat.history` has no equivalent guard**

The HTTP path is now protected, but the WebSocket `chat.history` handler in `src/gateway/server-methods/chat.ts` (around line 1095) calls `loadSessionEntry(sessionKey)` directly — without any check against `isInternalSessionHistoryTarget`. An authenticated client with `operator.read` scope can still retrieve subagent, cron, or ACP session history over the WebSocket surface.

If the intent is to block all external access to internal session history, the same `isInternalSessionHistoryTarget` guard should be applied in the WS handler. If the WS surface is intentionally left unrestricted (e.g., internal-only callers), a comment explaining that distinction would help future readers.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(gateway): block internal session his..." | Re-trigger Greptile

Comment thread src/gateway/sessions-history-http.ts Outdated
Comment on lines +196 to +205
if (isInternalSessionHistoryTarget(sessionKey)) {
sendJson(res, 403, {
ok: false,
error: {
type: "forbidden",
message: "internal sessions are not available over HTTP history",
},
});
return true;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 WebSocket chat.history has no equivalent guard

The HTTP path is now protected, but the WebSocket chat.history handler in src/gateway/server-methods/chat.ts (around line 1095) calls loadSessionEntry(sessionKey) directly — without any check against isInternalSessionHistoryTarget. An authenticated client with operator.read scope can still retrieve subagent, cron, or ACP session history over the WebSocket surface.

If the intent is to block all external access to internal session history, the same isInternalSessionHistoryTarget guard should be applied in the WS handler. If the WS surface is intentionally left unrestricted (e.g., internal-only callers), a comment explaining that distinction would help future readers.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/sessions-history-http.ts
Line: 196-205

Comment:
**WebSocket `chat.history` has no equivalent guard**

The HTTP path is now protected, but the WebSocket `chat.history` handler in `src/gateway/server-methods/chat.ts` (around line 1095) calls `loadSessionEntry(sessionKey)` directly — without any check against `isInternalSessionHistoryTarget`. An authenticated client with `operator.read` scope can still retrieve subagent, cron, or ACP session history over the WebSocket surface.

If the intent is to block all external access to internal session history, the same `isInternalSessionHistoryTarget` guard should be applied in the WS handler. If the WS surface is intentionally left unrestricted (e.g., internal-only callers), a comment explaining that distinction would help future readers.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in bc5f5b6. I reused the internal-session guard on the WebSocket chat.history path as well, so operator.read callers can no longer read subagent / cron / ACP transcripts over WS either. Added WS regression coverage in src/gateway/sessions-history-http.test.ts and re-ran pnpm exec vitest run src/gateway/sessions-history-http.test.ts src/gateway/server.chat.gateway-server-chat.test.ts src/gateway/server.chat.gateway-server-chat-b.test.ts.

@openclaw-barnacle openclaw-barnacle Bot added the app: web-ui App: web-ui label Mar 27, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bc5f5b6e7e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/gateway/server-methods/chat.ts Outdated
sessionKey: string;
limit?: number;
};
if (isInternalGatewaySessionKey(sessionKey)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve internal chat.history access for subagent flows

This new unconditional rejection blocks all internal namespace keys on WebSocket chat.history, but internal runtime features still rely on that method with subagent keys. For example, sendControlledSubagentMessage calls chat.history for targetSessionKey before and after a steer attempt (src/agents/subagent-control.ts:904 and src/agents/subagent-control.ts:945), and those keys are created as agent:<id>:subagent:<uuid> (src/agents/subagent-spawn.ts:411). With this guard, those calls now return INVALID_REQUEST, so subagent control/steer paths can fail even for authorized internal operators.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted. That WebSocket guard was too broad because internal subagent-control flows still depend on chat.history for subagent session keys. I reverted the WS-side rejection in c66e932 and narrowed this PR back to the HTTP surface only, while keeping the raw/canonical internal HTTP history block and its regressions. Re-ran pnpm exec vitest run src/gateway/sessions-history-http.test.ts src/gateway/server.chat.gateway-server-chat.test.ts src/gateway/server.chat.gateway-server-chat-b.test.ts.

@clawsweeper

clawsweeper Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

Keep open. Current main still has no internal-namespace denial on the HTTP session-history path, while the PR's potential merge commit adds the focused 403 guard and regression coverage; the remaining blockers are real gateway proof, documentation of the new exception, and maintainer acceptance of the compatibility break.

Canonical path: Close this PR as superseded by #81815.

So I’m closing this here and keeping the remaining discussion on #81815.

Review details

Best possible solution:

Close this PR as superseded by #81815.

Do we have a high-confidence way to reproduce the issue?

Yes. Source inspection on current main shows the HTTP history handler resolves and loads the target store without an internal-namespace denial, so seeding an internal session row and calling /sessions/:key/history is a high-confidence source reproduction path.

Is this the best way to solve the issue?

Mostly yes. The runtime guard is narrow and placed before transcript reads while preserving WebSocket runtime flows; the patch should also document the new public HTTP exception and include real gateway proof.

Security review:

Security review cleared: The diff tightens an existing gateway HTTP access-control path and does not add dependency, workflow, lockfile, secret, download, publish, or code-execution surface changes.

What I checked:

  • linked superseding PR: fix(gateway): allow bearer-auth session history reads #81815 (fix(gateway): allow bearer-auth session history reads) is merged at 2026-05-21T20:23:18Z.
  • cluster evidence: the durable review links that PR in the work cluster or recommended risk path.
  • no human follow-up: live comments and timeline hydrated by apply contain no non-automation activity after the ClawSweeper review.

Likely related people:

  • clay-datacurve: GitHub commit history shows 7b61ca1b0615 added the direct session history HTTP and SSE endpoint and its first test coverage. (role: feature introducer; confidence: high; commits: 7b61ca1b0615; files: src/gateway/sessions-history-http.ts, src/gateway/sessions-history-http.test.ts, src/gateway/server-http.ts)
  • medns: Authored the recently merged bearer-auth session-history change that modifies the same HTTP handler and must remain compatible with this PR. (role: adjacent access-control contributor; confidence: medium; commits: 01d95b9757a0; files: src/gateway/sessions-history-http.ts, src/gateway/sessions-history-http.test.ts)
  • steipete: Recent session-history performance work touched sessions-history-http.ts, session-utils.ts, and session-history-state.ts around the same read path. (role: recent area contributor; confidence: medium; commits: 4d9c658f4058; files: src/gateway/sessions-history-http.ts, src/gateway/session-utils.ts, src/gateway/session-history-state.ts)
  • Kaspre: Recent cron-run routing work changed src/routing/session-key.ts and src/sessions/session-key-utils.ts, including one namespace family this PR classifies as internal. (role: adjacent routing/session-key contributor; confidence: medium; commits: 7eefb26bc8d8; files: src/routing/session-key.ts, src/sessions/session-key-utils.ts)

Codex review notes: model gpt-5.5, reasoning high; reviewed against 483d7be6c40a.

@RichardCao RichardCao force-pushed the fix/gateway-http-block-internal-session-history branch from c66e932 to b5d0a08 Compare April 30, 2026 05:41
@RichardCao

Copy link
Copy Markdown
Contributor Author

Refreshed this PR onto latest upstream/main (99950c7f1272dff6e2c34c2be45dfc5f89e62a60) and force-pushed the updated branch.

New head: b5d0a08b074284035875e2765f37246c65d00501

What changed:

  • Rebuilt the original minimal HTTP-only fix on top of current main.
  • Blocks HTTP /sessions/:sessionKey/history access to internal session namespaces before any transcript read/SSE setup.
  • Checks both the raw requested key and the resolved canonical key, covering aliases like cron:daily as well as agent-scoped internal keys.
  • Preserves WebSocket chat.history behavior for runtime/internal flows.
  • Added extra coverage for bare subagent: and acp: aliases, and a positive WebSocket chat.history regression test for an internal session key so the HTTP-only boundary is explicit.

Review:

  • Fresh gpt-5.5 xhigh review found no important issues requiring changes before push.
  • Prior review feedback was considered:
    • The WebSocket concern is handled by preserving WS chat.history intentionally, rather than broadening this PR into a WS access-control change that could break runtime/internal flows.
    • The HTTP response shape is intentionally 403 with { ok: false, error: { type: "forbidden" } }, matching the existing authorization style in this handler after auth/scope checks.
    • This is coordinated with fix(gateway): block internal HTTP session overrides #55789 by keeping different failure classes distinct: invalid OpenAI/OpenResponses session override headers return 400 invalid_request_error, while an authenticated HTTP history read of an internal session returns 403 forbidden.

Verification:

  • Passed: git diff --check upstream/main..HEAD
  • Blocked locally: focused sessions-history-http.test.ts execution failed during import because this local node_modules is missing the declared typebox package. No test assertions ran in that suite.

@RichardCao

Copy link
Copy Markdown
Contributor Author

Follow-up after the latest push:

New head is now f5a11bc078542e218840f9c7d20cd4fbb96632e5.

I added the remaining review-driven coverage for the access-control discussion:

  • HTTP rejection table now includes bare subagent: and bare acp: aliases in addition to canonical agent-scoped keys and cron: aliases.
  • Added a positive WebSocket chat.history regression test for an internal session key to document the intentional HTTP-only boundary and preserve runtime/internal WS flows.
  • Re-reviewed this latest shape with gpt-5.5 xhigh; no important issues were found.

Response-shape decision remains intentionally split from #55789:

  • fix(gateway): block internal HTTP session overrides #55789 invalid OpenAI/OpenResponses x-openclaw-session-key overrides return 400 invalid_request_error.
  • This PR’s authorized HTTP history read of an internal session returns 403 forbidden using the existing { ok: false, error: { type: "forbidden" } } history/access-control shape.

Local targeted test still cannot execute assertions in this shell because typebox is missing from local node_modules; CI is now running against the pushed head.

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. labels May 19, 2026
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 19, 2026
@clawsweeper

clawsweeper Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper PR egg

🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat.

Where did the egg go?
  • The egg game starts only after the PR passes the real-behavior proof check.
  • Before that, no creature or rarity is rolled. The treat waits for real proof.
  • This is still just collectible flavor: proof affects review readiness, not creature quality.

@clawsweeper

clawsweeper Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper applied the proposed close for this PR.

@clawsweeper clawsweeper Bot closed this May 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. P2 Normal backlog priority with limited blast radius. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. size: S status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant