feat(peek): share a session to a Slack channel — consent-gated share_session#156
Conversation
…layer can build .peekbundle Move packages/peek-cli/src/lib/session-bundle.ts verbatim to packages/peek-mcp/src/session-bundle.ts (updating the sha256Hex import to the local native-host/audit-chain.js copy). Add a ./session-bundle subpath export to @peekdev/mcp and add tar as a runtime dependency. Repoint all peek-cli consumers to @peekdev/mcp/session-bundle: - src/commands/sessions.ts - src/lib/import-session.ts - src/commands/sessions.bundle.test.ts - src/lib/session-bundle.test.ts Delete the old peek-cli/src/lib/session-bundle.ts (no thin re-export; zero duplicate logic). All existing bundle/import tests pass unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: harry-harish <22562634+harry-harish@users.noreply.github.com>
Adds the share_session MCP tool: given a sessionId (and optional surface
name, default 'Slack'), elicits an explicit egress consent card via MCP
elicitInput before producing a .peekbundle temp file. The design:
• Egress is never silent — if the client lacks elicitation capability the
tool denies unconditionally; deny / cancel also denies. No bundle is
written without an explicit approve.
• NOT tied to the Level-3 act gate — sharing already-recorded data is
distinct from acting on the live browser; the consent card IS the
authorization, level-independent.
• On approve: loads session events + console/network rows from the local DB
(same read paths as generate_playwright_repro + get_session_*), calls
packBundle (from the Task-1 session-bundle lib) to a temp file in
os.tmpdir() named <sanitized_sessionId>.peekbundle, returns
{ ok, bundlePath, filename, sizeBytes, caveat: FULLSNAPSHOT_CAVEAT }.
Bundle bytes are never returned inline.
• On deny: returns { ok: false, result: 'denied' }, no file written.
• Audit: every call (approve or deny) writes a share_session audit entry
(tool + sessionId + surface + approver). Bundle bytes/content never reach
the audit log.
Additions:
- elicitation.ts: buildEgressConsentMessage() — egress-specific consent copy
(distinct from the Action-verb buildElicitMessage used by execute_action).
- audit.ts: AuditTool union widened to include 'share_session'.
- server.ts: registerTools() adds tool #19 share_session; dispatchShareSession()
implements the consent-check + db-load + packBundle + audit path.
- PEEK_MCP_TOOLS constant updated (20 tools).
- test/share-session.test.ts: 9 tests covering (a) deny→no file, (b) approve→
valid verifyBundle-passing bundle, (c) approved call writes share_session
audit entry (no bytes), plus no-capability→deny, unknown-session error,
and tool-list registration.
- test/server.test.ts: updated tool count 19→20.
- .changeset/peek-share-session-tool.md: minor changeset.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: harry-harish <22562634+harry-harish@users.noreply.github.com>
… tools) Bring stdio-smoke.test.ts and server.test.ts in sync with the new tool count after adding share_session in the previous commit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: harry-harish <22562634+harry-harish@users.noreply.github.com>
…Sync guard, injection sanitize, audit approver, test gaps)
Fix 1: bundle filename now includes a Date.now() nonce (${safeId}-${nonce}.peekbundle)
so two concurrent approved exports of the same session produce distinct temp files.
Fix 2: statSync moved inside the packBundle try/catch so a post-approve FS error
returns {ok:false} instead of propagating as an unhandled MCP throw.
Fix 3: sessionId sanitized (strip non-printable / control chars) before interpolation
into the egress consent card text — prevents newline injection into the card.
Fix 4: no-elicitation-capability deny now records approver:'connector-elicit' instead
of 'user' (user was never asked); actual user decline/cancel keeps approver:'user'.
Tests: cancel test gains no-file assertion; new unknown-action test locks fail-closed
behavior; decline file check updated to pattern scan (robust after nonce addition).
Gate condition (!elicitOutcome.elicited || elicitOutcome.verdict === 'deny') unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: harry-harish <22562634+harry-harish@users.noreply.github.com>
…terception
Add optional SurfaceAdapter.postFile? hook (conversationId, filePath,
filename, comment?) and intercept approved share_session tool results in
ConnectorRuntime.handleConsentResponse: upload via adapter.postFile, then
delete the temp bundle (try/finally — file is always removed, even on upload
failure). Denied results and adapters without postFile degrade gracefully.
Four new tests: approved share_session → postFile called + file deleted;
upload failure → file still deleted (try/finally); {ok:false} → no postFile,
no throw; no postFile on adapter → text note, no crash.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: harry-harish <22562634+harry-harish@users.noreply.github.com>
…ool boundary
share_session is classified 'read' by classify(), so SdkBrain.runTurn runs it
inline via its injected callTool and feeds the {ok,bundlePath} result straight
back into the tool-use loop — it never emits a {kind:'consent'} outcome. The
09219fc interception lived in handleConsentResponse (consent-only path), so the
bundle upload + temp-file cleanup never fired in production; the old tests
passed only because a synthetic brain forced the suspend path.
Fix (wiring option a): the runtime now exposes interceptCallTool(name, input,
inner). The connector-slack composition root hands the brain
runtime.interceptCallTool(...) instead of the bare mcp.callTool, so an approved
share_session result triggers adapter.postFile + temp-file cleanup on the REAL
inline path regardless of classification. Chosen over a PeekMcp post-tool hook
because the post-processing needs the surface adapter + active conversationId,
which the runtime holds and PeekMcp deliberately does not — keeping PeekMcp a
pure transport and the brain seeing a plain (name,input)=>Promise<string>.
- Remove the share_session block from handleConsentResponse (wrong path).
- Add interceptCallTool() + private #postProcessShareSession(): postFile in try,
temp delete in finally; {ok:false}/unparseable → unchanged; no postFile →
filename-only degrade note (no raw temp path leak); upload error → surfaced to
the brain so the turn continues.
- Replace the four synthetic-consent-brain tests with four that drive the REAL
SdkBrain (read/inline routing). The MANDATORY test asserts no consent card is
posted, postFile fires with the active conversationId, and the temp file is
deleted — it fails against the 09219fc wrong-path code.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: harry-harish <22562634+harry-harish@users.noreply.github.com>
Implements SurfaceAdapter.postFile on SlackAdapter using files.uploadV2. Reads the temp bundle with readFileSync; branches on threadTs presence to satisfy the FileThreadDestinationArgument vs FileChannelDestinationArgument discriminated union under exactOptionalPropertyTypes. Temp-file deletion stays with the runtime (Task 3). Three new tests cover the threaded, slash-command (no thread_ts), and upload-error paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: harry-harish <22562634+harry-harish@users.noreply.github.com>
Add §"Session egress — share_session (SP5 / connector upload)" documenting the one path where a recorded session bundle leaves peek's local-first store and is uploaded to a third-party cloud (Slack via files.uploadV2). Covers: what the bundle contains (masked-at-capture DOM + console/network), the explicit egress consent card gate, the files:write scope requirement, temp-file lifecycle (deleted after upload, success or failure), audit log entry format, and honest residual limits (Slack retention + capture-time masking caveat). Also adds attack surface #9 to the enumeration at the top of the file. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: harry-harish <22562634+harry-harish@users.noreply.github.com>
|
Warning Review limit reached
Next review available in: 37 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughAdds a new consent-gated Changesshare_session feature
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant Server as peek-mcp server
participant Store as SQLite store
participant FS as Temp file system
Client->>Server: call share_session(sessionId, surface)
Server->>Client: elicitConsent(buildEgressConsentMessage)
alt denied
Client-->>Server: decline/cancel
Server->>Server: append audit entry (result: denied)
Server-->>Client: ok: false, result: denied
else approved
Client-->>Server: accept
Server->>Store: load session summary, events, console/network rows
Server->>FS: packBundle -> write .peekbundle
Server->>Server: append audit entry (result: ok)
Server-->>Client: ok: true, bundlePath, filename, sizeBytes, caveat
end
sequenceDiagram
participant Brain as SdkBrain
participant Runtime as ConnectorRuntime
participant MCP as MCP server
participant Adapter as SlackAdapter
Brain->>Runtime: interceptCallTool(share_session, input, inner)
Runtime->>MCP: inner(share_session, input)
MCP-->>Runtime: ok, bundlePath, filename
alt postFile available and ok
Runtime->>Adapter: postFile(conversationId, bundlePath, filename)
Adapter->>Adapter: files.uploadV2 via Slack API
Adapter-->>Runtime: success
else postFile unavailable or ok:false
Runtime->>Runtime: build fallback note
end
Runtime->>Runtime: rm(bundlePath, force)
Runtime-->>Brain: result text
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (5)
packages/connector-slack/src/slack-adapter.ts (1)
1-1: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winConsider an async read instead of
readFileSyncto avoid blocking the event loop.
readFileSyncblocks the single Node.js thread for the duration of the read, which delays every other Slack event/turn this process is handling while a (potentially large).peekbundleis loaded into memory. Streams are not a safe substitute here —files.uploadV2has had recurring stream-compatibility bugs (e.g. failing to recognize non-fs.ReadStreamreadables), so keeping aBufferis the right call. Swapping to the asyncBuffer-returningreadFileavoids the block without touching the upload-argument shape.♻️ Proposed fix
-import { readFileSync } from 'node:fs'; +import { readFile } from 'node:fs/promises';async postFile( conversationId: string, filePath: string, filename: string, comment?: string, ): Promise<void> { const r = this.route(conversationId); - const file = readFileSync(filePath); + const file = await readFile(filePath);Also applies to: 156-185
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/connector-slack/src/slack-adapter.ts` at line 1, The Slack adapter currently uses readFileSync, which blocks the event loop while loading a .peekbundle and can delay other Slack turns; update the file-loading path in slack-adapter.ts to use the async Buffer-returning readFile instead. Keep the upload payload as a Buffer for files.uploadV2 compatibility, and adjust the surrounding async flow where the bundle is read and passed into the upload logic so the existing upload argument shape stays unchanged.packages/peek-mcp/src/mcp/server.ts (3)
1161-1169: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winBound
surfacethe same way other free-text tool args are bounded.Every other short free-text field in this file (
textinset_intent,promptinrequest_user_input,labelinsuggest_element) has a.max()cap.surfacehas none, so an arbitrarily long string ends up in the consent card and the audit logargs.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/peek-mcp/src/mcp/server.ts` around lines 1161 - 1169, The `surface` field in the tool input schema is an unbounded free-text argument, unlike the similar capped fields in `set_intent`, `request_user_input`, and `suggest_element`. Add a reasonable `.max()` limit to `surface` in `server.ts`’s tool schema, keeping the existing default and description intact, so excessively long values cannot flow into the consent card or audit-log args.
1448-1492: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRaw SQL bypasses the existing
queries.tsmodule boundary.Every other tool in this file goes through
queries.tshelpers (getSessionSummaryRow,getConsoleErrors,getNetworkErrors, etc.). Here,dispatchShareSessionhand-rolls twohandle.prepare(...).all(...)queries directly againstconsole_events/network_events(needed because it wants all rows, not just errors). Extracting these into named helpers inqueries.ts(e.g.getAllConsoleEvents,getAllNetworkEvents) would keep DB access centralized and unit-testable independent of the MCP transport, consistent with the rest of the file.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/peek-mcp/src/mcp/server.ts` around lines 1448 - 1492, dispatchShareSession is bypassing the queries.ts boundary by calling handle.prepare(...).all(...) directly for console_events and network_events. Move these raw SQL reads into new named helpers in queries.ts, such as getAllConsoleEvents and getAllNetworkEvents, and have dispatchShareSession call those helpers instead. Keep the existing mapping of ts/level/message/stack and ts/method/url/status/statusText/resourceType/durationMs/errorText in the helper return shape so the bundle payload stays unchanged.
1143-1180: 🔒 Security & Privacy | 🔵 TrivialOrphaned
.peekbundletemp files for non-connector clients.The comment correctly notes "connector-core deletes it after upload," but that only holds for the Slack path. Any other MCP client (Claude Desktop, MCP Inspector, a future connector without
postFile) that callsshare_sessionand getsok:trueleaves a masked-but-real session export sitting in the OS temp dir indefinitely, since nothing else in this tool cleans it up. Given peek's local-first/no-persistence posture, is there (or should there be) a fallback TTL sweep for orphaned.peekbundlefiles, independent of the connector upload path?🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/peek-mcp/src/mcp/server.ts` around lines 1143 - 1180, The share_session tool can leave approved .peekbundle exports orphaned for non-connector clients because cleanup currently depends on connector-core uploading and deleting the temp file. Update dispatchShareSession and the share_session flow to ensure every approved export gets a fallback cleanup path, such as a TTL-based sweep or a post-response temp-file cleanup mechanism, while preserving the current connector upload behavior. Locate the export lifecycle around share_session, dispatchShareSession, and any temp file creation/return logic, and make sure orphaned bundle paths are not left behind for Claude Desktop, MCP Inspector, or future clients.packages/peek-mcp/test/share-session.test.ts (1)
380-399: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTest title promises an audit-log assertion that isn't there.
The test is titled "(c) approve with unknown session → error (no file, no audit ok entry)" but only asserts
body.ok/body.result; it never readsauditLogPathto confirm no (or a specific) audit entry was written. Given the audit-gap indispatchShareSessionfor this exact branch (noappendAuditEntrycall at all on session-not-found), adding the assertion here would have caught it.✅ Proposed addition
const body = parseJson(res as never) as Record<string, unknown>; // Should return an error (session not found). expect(body.ok).toBe(false); expect(body.result).toBe('error'); + // No audit entry should be silently dropped for an approved-but-failed export. + const contents = existsSync(auditLogPath) ? readFileSync(auditLogPath, 'utf8') : ''; + const lines = contents.split('\n').filter((l) => l.length > 0); + expect(lines).toHaveLength(1);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/peek-mcp/test/share-session.test.ts` around lines 380 - 399, The test name claims it verifies the audit log, but the body only checks the tool response and never inspects auditLogPath. Update this share_session test to read the audit log after calling client.callTool and assert the expected audit behavior for the unknown-session branch (specifically that no success/ok audit entry was written), using the existing helpers around stubElicitation, connectClient, and dispatchShareSession flow to keep the assertion aligned with the session-not-found case.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/connector-core/src/runtime.ts`:
- Around line 168-196: The cleanup in `#postProcessShareSession` should not
override the result of a successful upload or an upload error. Keep the
rm(bundlePath, { force: true }) call in the finally block, but catch and swallow
or separately log cleanup failures so they do not replace the return value from
the try/catch paths. Preserve the existing success and upload-failure messages,
and only let unexpected errors from the main upload flow propagate.
In `@packages/peek-mcp/src/mcp/elicitation.ts`:
- Around line 112-120: The consent message builder in buildEgressConsentMessage
sanitizes sessionId but interpolates surface raw into user-facing text. Apply
the same control-character scrubbing to surface before composing the consent
card, and use the sanitized value in the returned string so a caller-supplied
surface cannot inject or spoof the approval prompt.
In `@packages/peek-mcp/src/mcp/server.ts`:
- Around line 1383-1554: `dispatchShareSession` is missing audit writes on
approved paths that return errors, so consented egress attempts can disappear
from the audit log. Add the same `appendAuditEntry` handling used in the deny
and success branches to the no-DB, missing-session, event-load, and bundle-pack
error returns, using a `DraftAuditEntry` with `approver: 'connector-elicit'`,
`result: 'error'`, and the relevant `error` text. Keep each audit write wrapped
in its own try/catch so an audit failure does not mask the original `jsonResult`
error.
---
Nitpick comments:
In `@packages/connector-slack/src/slack-adapter.ts`:
- Line 1: The Slack adapter currently uses readFileSync, which blocks the event
loop while loading a .peekbundle and can delay other Slack turns; update the
file-loading path in slack-adapter.ts to use the async Buffer-returning readFile
instead. Keep the upload payload as a Buffer for files.uploadV2 compatibility,
and adjust the surrounding async flow where the bundle is read and passed into
the upload logic so the existing upload argument shape stays unchanged.
In `@packages/peek-mcp/src/mcp/server.ts`:
- Around line 1161-1169: The `surface` field in the tool input schema is an
unbounded free-text argument, unlike the similar capped fields in `set_intent`,
`request_user_input`, and `suggest_element`. Add a reasonable `.max()` limit to
`surface` in `server.ts`’s tool schema, keeping the existing default and
description intact, so excessively long values cannot flow into the consent card
or audit-log args.
- Around line 1448-1492: dispatchShareSession is bypassing the queries.ts
boundary by calling handle.prepare(...).all(...) directly for console_events and
network_events. Move these raw SQL reads into new named helpers in queries.ts,
such as getAllConsoleEvents and getAllNetworkEvents, and have
dispatchShareSession call those helpers instead. Keep the existing mapping of
ts/level/message/stack and
ts/method/url/status/statusText/resourceType/durationMs/errorText in the helper
return shape so the bundle payload stays unchanged.
- Around line 1143-1180: The share_session tool can leave approved .peekbundle
exports orphaned for non-connector clients because cleanup currently depends on
connector-core uploading and deleting the temp file. Update dispatchShareSession
and the share_session flow to ensure every approved export gets a fallback
cleanup path, such as a TTL-based sweep or a post-response temp-file cleanup
mechanism, while preserving the current connector upload behavior. Locate the
export lifecycle around share_session, dispatchShareSession, and any temp file
creation/return logic, and make sure orphaned bundle paths are not left behind
for Claude Desktop, MCP Inspector, or future clients.
In `@packages/peek-mcp/test/share-session.test.ts`:
- Around line 380-399: The test name claims it verifies the audit log, but the
body only checks the tool response and never inspects auditLogPath. Update this
share_session test to read the audit log after calling client.callTool and
assert the expected audit behavior for the unknown-session branch (specifically
that no success/ok audit entry was written), using the existing helpers around
stubElicitation, connectClient, and dispatchShareSession flow to keep the
assertion aligned with the session-not-found case.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 92e316e7-d049-43f9-8a08-e424d4228590
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml,!pnpm-lock.yaml
📒 Files selected for processing (20)
.changeset/peek-share-session-tool.mddocs/peek/THREATMODEL.mdpackages/connector-core/src/runtime.test.tspackages/connector-core/src/runtime.tspackages/connector-core/src/surface.tspackages/connector-slack/src/index.tspackages/connector-slack/src/slack-adapter.test.tspackages/connector-slack/src/slack-adapter.tspackages/peek-cli/src/commands/sessions.bundle.test.tspackages/peek-cli/src/commands/sessions.tspackages/peek-cli/src/lib/import-session.tspackages/peek-cli/src/lib/session-bundle.test.tspackages/peek-mcp/package.jsonpackages/peek-mcp/src/mcp/elicitation.tspackages/peek-mcp/src/mcp/server.tspackages/peek-mcp/src/native-host/audit.tspackages/peek-mcp/src/session-bundle.tspackages/peek-mcp/test/server.test.tspackages/peek-mcp/test/share-session.test.tspackages/peek-mcp/test/stdio-smoke.test.ts
… nitpicks) MAJOR 1 (connector-core/runtime.ts): wrap rm() in finally in its own try/catch so an EACCES/EBUSY cleanup failure never clobbers the real upload result returned by the try/catch above. MAJOR 2 (peek-mcp/elicitation.ts): sanitize surface like sessionId — add safeSurface = surface.replace(/[^\x20-\x7E]/g,'_').slice(0,60) before interpolating into the egress consent card to prevent newline/control-char injection. MAJOR 3 (peek-mcp/server.ts): add appendAuditEntry (result:'error', approver:'connector-elicit') to all 4 post-consent error returns (no DB, session-not-found, event-load failure, pack failure), each wrapped in its own try/catch. Add test: approved unknown session → audit entry with result:'error' and approver:'connector-elicit'. NITPICK A (connector-slack/slack-adapter.ts): switch readFileSync → async readFile from node:fs/promises; update test mock accordingly. NITPICK B (server.ts): add .max(60) cap to surface zod schema. NITPICK C (server.ts): document temp-file ownership in tool description (connector uploads-and-deletes; direct caller is responsible). Skipped: audit.ts:380-399 (pre-existing, 0 diff lines in this PR). Deferred: server.ts 1448-1492 SQL helpers (in diff but non-trivial refactor, correctness-neutral, follow-up cleanup). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: harry-harish <22562634+harry-harish@users.noreply.github.com>
|
@coderabbitai review |
✅ Action performedReview finished.
|
What
Completes the shared-debugging loop inside Slack:
@peek share this sessionproduces the session's portable.peekbundleand uploads it into the thread — only after an explicit egress consent. A teammate downloads it andpeek sessions imports it on their own machine (no terminal export step, no cloud sync).share_sessionMCP tool (peek-mcp) — consent-gated via the same elicitation transport asexecute_action, but independent of the Level-3 act gate (it shares recorded data, doesn't act on the live browser). Deny / no-capability / malformed → fail closed, no file. Approve →.peekbundleto a temp file; returns the path (never bytes inline). Audited (share_session, session, approver — no bundle bytes).@peekdev/mcp/session-bundle) so the MCP layer can build bundles; CLIsessions export/importbehavior-preserving.SurfaceAdapter.postFile+ runtime interception —share_sessionisread-classified, so the brain calls it inline; the connector-core runtime intercepts the result at the callTool boundary (interceptCallTool→#postProcessShareSession, wired at the composition root), uploads via the adapter, and deletes the temp bundle in afinally(success or failure).postFile—files.uploadV2to the thread (needs thefiles:writescope). The egress consent card renders through the existing consent path.Notes
files:writescope + reinstall.@peekdev/mcpminor changeset included; connector-core/connector-slack areprivate:true(no changeset).🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes