openclaudeimproved
§ · Nested detail page

Bridge session isolation 深度解析

这一页聚焦 bridge/session 层如何隔离环境变量、处理 ingress 消息、构造 websocket URL 和注册 worker。

§ 00 · IMPORT CHAIN

导入链与契约

▸环境隔离

Bridge session 的安全边界从 src/bridge/sessionRunner.ts:24 开始,allowlist 明确哪些系统变量可以进入 child process;再通过 src/bridge/workSecret.ts:81 和 src/bridge/workSecret.ts:120 构造 v1/v2 session URL。

src/bridge/sessionRunner.ts · L24-80TypeScript
24const CHILD_ENV_ALLOWLIST = new Set([
25  // System / shell
26  'PATH', 'HOME', 'USERPROFILE', 'HOMEPATH', 'HOMEDRIVE',
27  'USERNAME', 'USER', 'LOGNAME',
28  'TEMP', 'TMP', 'TMPDIR',
29  'SYSTEMROOT', 'SYSTEMDRIVE', 'COMSPEC', 'WINDIR',
30  'LANG', 'LC_ALL', 'LC_CTYPE',
31  // Node.js runtime
32  'NODE_OPTIONS', 'NODE_PATH', 'NODE_ENV',
33  // OpenClaude session / bridge (non-secret)
34  'CLAUDE_CODE_ENVIRONMENT_KIND',
35  'CLAUDE_CODE_FORCE_SANDBOX',
36  'CLAUDE_CODE_BUBBLEWRAP',
37  'CLAUDE_CODE_ENTRYPOINT',
38  'CLAUDE_CODE_COORDINATOR_MODE',
39  'CLAUDE_CODE_PERMISSIONS_VERSION',
40  'CLAUDE_CODE_PERMISSIONS_SETTING',
41  // Display / terminal
42  'TERM', 'COLORTERM', 'FORCE_COLOR', 'NO_COLOR',
43])
44
45type BuildChildEnvOpts = {
46  accessToken: string
47  useCcrV2: boolean
48  workerEpoch?: number
49  sandbox?: boolean
50}
51
52/**
53 * Build the environment for the child CC process from an explicit allowlist.
54 * This prevents the parent's API keys and credentials from leaking to the child.
55 */
56export function buildChildEnv(
57  parentEnv: NodeJS.ProcessEnv,
58  opts: BuildChildEnvOpts,
59): NodeJS.ProcessEnv {
60  // Start from allowlisted parent vars only
61  const env: NodeJS.ProcessEnv = {}
62  for (const key of Object.keys(parentEnv)) {
63    if (CHILD_ENV_ALLOWLIST.has(key)) {
64      env[key] = parentEnv[key]
65    }
66  }
67
68  // Bridge-required overrides
69  env.CLAUDE_CODE_OAUTH_TOKEN = undefined // explicitly strip
70  env.CLAUDE_CODE_ENVIRONMENT_KIND = 'bridge'
71  if (opts.sandbox) env.CLAUDE_CODE_FORCE_SANDBOX = '1'
72  env.CLAUDE_CODE_SESSION_ACCESS_TOKEN = opts.accessToken
73  env.CLAUDE_CODE_POST_FOR_SESSION_INGRESS_V2 = '1'
74  if (opts.useCcrV2) {
75    env.CLAUDE_CODE_USE_CCR_V2 = '1'
76    env.CLAUDE_CODE_WORKER_EPOCH = String(opts.workerEpoch)
77  }
78
79  return env
80}

▸测试锁住 secret 边界

环境隔离的关键不是删除某一个 token,而是默认拒绝。src/bridge/sessionRunner.ts:56 从空对象开始,src/bridge/sessionRunner.ts:62 只复制 allowlist 里的 key,src/bridge/sessionRunner.ts:69 还显式去掉 OAuth token。

src/bridge/sessionRunner.test.ts · L13-85TypeScript
13test('buildChildEnv does not leak ANTHROPIC_API_KEY to child', () => {
14  const parentEnv = {
15    PATH: '/usr/bin',
16    HOME: '/home/user',
17    ANTHROPIC_API_KEY: 'sk-ant-secret-key',
18    CLAUDE_CODE_SESSION_ACCESS_TOKEN: 'will-be-overwritten',
19  }
20  const env = buildChildEnv(parentEnv, baseOpts)
21  expect(env.ANTHROPIC_API_KEY).toBeUndefined()
22})
23
24test('buildChildEnv does not leak OPENAI_API_KEY to child', () => {
25  const parentEnv = {
26    PATH: '/usr/bin',
27    HOME: '/home/user',
28    OPENAI_API_KEY: 'sk-openai-secret',
29  }
30  const env = buildChildEnv(parentEnv, baseOpts)
31  expect(env.OPENAI_API_KEY).toBeUndefined()
32})
33
34test('buildChildEnv does not leak arbitrary secrets to child', () => {
35  const parentEnv = {
36    PATH: '/usr/bin',
37    HOME: '/home/user',
38    DB_PASSWORD: 'super-secret',
39    AWS_SECRET_ACCESS_KEY: 'aws-secret',
40    GITHUB_TOKEN: 'ghp_token',
41  }
42  const env = buildChildEnv(parentEnv, baseOpts)
43  expect(env.DB_PASSWORD).toBeUndefined()
44  expect(env.AWS_SECRET_ACCESS_KEY).toBeUndefined()
45  expect(env.GITHUB_TOKEN).toBeUndefined()
46})
47
48test('buildChildEnv includes PATH and HOME from parent', () => {
49  const parentEnv = {
50    PATH: '/usr/bin:/usr/local/bin',
51    HOME: '/home/user',
52    ANTHROPIC_API_KEY: 'sk-secret',
53  }
54  const env = buildChildEnv(parentEnv, baseOpts)
55  expect(env.PATH).toBe('/usr/bin:/usr/local/bin')
56  expect(env.HOME).toBe('/home/user')
57})
58
59test('buildChildEnv sets CLAUDE_CODE_SESSION_ACCESS_TOKEN from opts', () => {
60  const env = buildChildEnv({ PATH: '/usr/bin' }, { ...baseOpts, accessToken: 'my-token' })
61  expect(env.CLAUDE_CODE_SESSION_ACCESS_TOKEN).toBe('my-token')
62})
63
64test('buildChildEnv sets CLAUDE_CODE_ENVIRONMENT_KIND to bridge', () => {
65  const env = buildChildEnv({ PATH: '/usr/bin' }, baseOpts)
66  expect(env.CLAUDE_CODE_ENVIRONMENT_KIND).toBe('bridge')
67})
68
69test('buildChildEnv does not pass CLAUDE_CODE_OAUTH_TOKEN to child', () => {
70  const parentEnv = {
71    PATH: '/usr/bin',
72    CLAUDE_CODE_OAUTH_TOKEN: 'oauth-token-to-strip',
73  }
74  const env = buildChildEnv(parentEnv, baseOpts)
75  expect(env.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined()
76})
77
78test('buildChildEnv sets CCR v2 vars when useCcrV2 is true', () => {
79  const env = buildChildEnv(
80    { PATH: '/usr/bin' },
81    { accessToken: 'tok', useCcrV2: true, workerEpoch: 42 },
82  )
83  expect(env.CLAUDE_CODE_USE_CCR_V2).toBe('1')
84  expect(env.CLAUDE_CODE_WORKER_EPOCH).toBe('42')
85})

▸URL 与 worker 注册

workSecret 层处理网络边界:src/bridge/workSecret.ts:44 精确识别 localhost,src/bridge/workSecret.ts:64 拒绝非本地 HTTP,src/bridge/workSecret.ts:136 注册 worker 并校验 worker_epoch。

src/bridge/workSecret.ts · L44-165TypeScript
44export function isLocalhostBaseUrl(baseUrl: string): boolean {
45  let hostname: string
46  try {
47    hostname = new URL(baseUrl).hostname
48  } catch {
49    return false
50  }
51  return hostname === 'localhost' || hostname === '127.0.0.1'
52}
53
54/**
55 * Whether a base URL would send traffic to a non-localhost host over plaintext
56 * HTTP, which must be rejected to keep credentials off the wire.
57 *
58 * The scheme is read from the parsed URL (`protocol === 'http:'`) rather than a
59 * `startsWith('http://')` test so that mixed-case schemes like `HTTP://` — which
60 * the URL parser normalizes to `http:` and would otherwise carry credentials in
61 * the clear — are still caught. A malformed URL is treated as not-insecure (the
62 * safe default: the callers fall through to their normal handling).
63 */
64export function isInsecureHttpBaseUrl(baseUrl: string): boolean {
65  let protocol: string
66  try {
67    protocol = new URL(baseUrl).protocol
68  } catch {
69    return false
70  }
71  return protocol === 'http:' && !isLocalhostBaseUrl(baseUrl)
72}
73
74/**
75 * Build a WebSocket SDK URL from the API base URL and session ID.
76 * Strips the HTTP(S) protocol and constructs a ws(s):// ingress URL.
77 *
78 * Uses /v2/ for localhost (direct to session-ingress, no Envoy rewrite)
79 * and /v1/ for production (Envoy rewrites /v1/ → /v2/).
80 */
81export function buildSdkUrl(apiBaseUrl: string, sessionId: string): string {
82  const isLocalhost = isLocalhostBaseUrl(apiBaseUrl)
83  const protocol = isLocalhost ? 'ws' : 'wss'
84  const version = isLocalhost ? 'v2' : 'v1'
85  const host = apiBaseUrl.replace(/^https?:\/\//, '').replace(/\/+$/, '')
86  return `${protocol}://${host}/${version}/session_ingress/ws/${sessionId}`
87}
88
89/**
90 * Compare two session IDs regardless of their tagged-ID prefix.
91 *
92 * Tagged IDs have the form {tag}_{body} or {tag}_staging_{body}, where the
93 * body encodes a UUID. CCR v2's compat layer returns `session_*` to v1 API
94 * clients (compat/convert.go:41) but the infrastructure layer (sandbox-gateway
95 * work queue, work poll response) uses `cse_*` (compat/CLAUDE.md:13). Both
96 * have the same underlying UUID.
97 *
98 * Without this, replBridge rejects its own session as "foreign" at the
99 * work-received check when the ccr_v2_compat_enabled gate is on.
100 */
101export function sameSessionId(a: string, b: string): boolean {
102  if (a === b) return true
103  // The body is everything after the last underscore — this handles both
104  // `{tag}_{body}` and `{tag}_staging_{body}`.
105  const aBody = a.slice(a.lastIndexOf('_') + 1)
106  const bBody = b.slice(b.lastIndexOf('_') + 1)
107  // Guard against IDs with no underscore (bare UUIDs): lastIndexOf returns -1,
108  // slice(0) returns the whole string, and we already checked a === b above.
109  // Require a minimum length to avoid accidental matches on short suffixes
110  // (e.g. single-char tag remnants from malformed IDs).
111  return aBody.length >= 4 && aBody === bBody
112}
113
114/**
115 * Build a CCR v2 session URL from the API base URL and session ID.
116 * Unlike buildSdkUrl, this returns an HTTP(S) URL (not ws://) and points at
117 * /v1/code/sessions/{id} — the child CC will derive the SSE stream path
118 * and worker endpoints from this base.
119 */
120export function buildCCRv2SdkUrl(
121  apiBaseUrl: string,
122  sessionId: string,
123): string {
124  const base = apiBaseUrl.replace(/\/+$/, '')
125  return `${base}/v1/code/sessions/${sessionId}`
126}
127
128/**
129 * Register this bridge as the worker for a CCR v2 session.
130 * Returns the worker_epoch, which must be passed to the child CC process
131 * so its CCRClient can include it in every heartbeat/state/event request.
132 *
133 * Mirrors what environment-manager does in the container path
134 * (api-go/environment-manager/cmd/cmd_task_run.go RegisterWorker).
135 */
136export async function registerWorker(
137  sessionUrl: string,
138  accessToken: string,
139): Promise<number> {
140  const response = await axios.post(
141    `${sessionUrl}/worker/register`,
142    {},
143    {
144      headers: {
145        Authorization: `Bearer ${accessToken}`,
146        'Content-Type': 'application/json',
147        'anthropic-version': '2023-06-01',
148      },
149      timeout: 10_000,
150    },
151  )
152  // protojson serializes int64 as a string to avoid JS number precision loss;
153  // the Go side may also return a number depending on encoder settings.
154  const raw = response.data?.worker_epoch
155  const epoch = typeof raw === 'string' ? Number(raw) : raw
156  if (
157    typeof epoch !== 'number' ||
158    !Number.isFinite(epoch) ||
159    !Number.isSafeInteger(epoch)
160  ) {
161    throw new Error(
162      `registerWorker: invalid worker_epoch in response: ${jsonStringify(response.data)}`,
163    )
164  }
165  return epoch

▸消息去重与控制请求

Ingress 处理把 control_response、control_request 和 SDKMessage 分开。src/bridge/bridgeMessaging.ts:143 先处理 control_response,src/bridge/bridgeMessaging.ts:151 再处理 control_request,src/bridge/bridgeMessaging.ts:171 和 src/bridge/bridgeMessaging.ts:183 分别处理 echo 与重放。

src/bridge/bridgeMessaging.ts · L124-211TypeScript
124// ─── Ingress routing ─────────────────────────────────────────────────────────
125
126/**
127 * Parse an ingress WebSocket message and route it to the appropriate handler.
128 * Ignores messages whose UUID is in recentPostedUUIDs (echoes of what we sent)
129 * or in recentInboundUUIDs (re-deliveries we've already forwarded — e.g.
130 * server replayed history after a transport swap lost the seq-num cursor).
131 */
132export function handleIngressMessage(
133  data: string,
134  recentPostedUUIDs: BoundedUUIDSet,
135  recentInboundUUIDs: BoundedUUIDSet,
136  onInboundMessage: ((msg: SDKMessage) => void | Promise<void>) | undefined,
137  onPermissionResponse?: ((response: SDKControlResponse) => void) | undefined,
138  onControlRequest?: ((request: SDKControlRequest) => void) | undefined,
139): void {
140  try {
141    const parsed: unknown = normalizeControlMessageKeys(jsonParse(data))
142
143    // control_response is not an SDKMessage — check before the type guard
144    if (isSDKControlResponse(parsed)) {
145      const controlResponse = parsed as SDKControlResponse
146      logForDebugging('[bridge:repl] Ingress message type=control_response')
147      onPermissionResponse?.(controlResponse)
148      return
149    }
150
151    // control_request from the server (initialize, set_model, can_use_tool).
152    // Must respond promptly or the server kills the WS (~10-14s timeout).
153    if (isSDKControlRequest(parsed)) {
154      const controlRequest = parsed as SDKControlRequest
155      logForDebugging(
156        `[bridge:repl] Inbound control_request subtype=${controlRequest.request.subtype}`,
157      )
158      onControlRequest?.(controlRequest)
159      return
160    }
161
162    if (!isSDKMessage(parsed)) return
163    const sdkMessage = parsed as SDKMessage
164
165    // Check for UUID to detect echoes of our own messages
166    const uuid =
167      'uuid' in sdkMessage && typeof sdkMessage.uuid === 'string'
168        ? sdkMessage.uuid
169        : undefined
170
171    if (uuid && recentPostedUUIDs.has(uuid)) {
172      logForDebugging(
173        `[bridge:repl] Ignoring echo: type=${sdkMessage.type} uuid=${uuid}`,
174      )
175      return
176    }
177
178    // Defensive dedup: drop inbound prompts we've already forwarded. The
179    // SSE seq-num carryover (lastTransportSequenceNum) is the primary fix
180    // for history-replay; this catches edge cases where that negotiation
181    // fails (server ignores from_sequence_num, transport died before
182    // receiving any frames, etc).
183    if (uuid && recentInboundUUIDs.has(uuid)) {
184      logForDebugging(
185        `[bridge:repl] Ignoring re-delivered inbound: type=${sdkMessage.type} uuid=${uuid}`,
186      )
187      return
188    }
189
190    logForDebugging(
191      `[bridge:repl] Ingress message type=${sdkMessage.type}${uuid ? ` uuid=${uuid}` : ''}`,
192    )
193
194    if (sdkMessage.type === 'user') {
195      if (uuid) recentInboundUUIDs.add(uuid)
196      logEvent('tengu_bridge_message_received', {
197        is_repl: true,
198      })
199      // Fire-and-forget — handler may be async (attachment resolution).
200      void onInboundMessage?.(sdkMessage)
201    } else {
202      logForDebugging(
203        `[bridge:repl] Ignoring non-user inbound message: type=${sdkMessage.type}`,
204      )
205    }
206  } catch (err) {
207    logForDebugging(
208      `[bridge:repl] Failed to parse ingress message: ${errorMessage(err)}`,
209    )
210  }
211}

▸逐行证据

Bridge 子页面 的证据点 src/bridge/sessionRunner.ts:24 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/sessionRunner.ts:56 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/sessionRunner.ts:72 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/sessionRunner.ts:87 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/sessionRunner.ts:96 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/sessionRunner.ts:132 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/sessionRunner.ts:170 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/sessionRunner.ts:201 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/sessionRunner.ts:247 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/sessionRunner.ts:310 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/replBridge.ts:92 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/replBridge.ts:120 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/replBridge.ts:151 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/replBridge.ts:166 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/replBridge.ts:183 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/replBridge.ts:193 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/replBridge.ts:214 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/bridgeMessaging.ts:36 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/bridgeMessaging.ts:77 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/bridgeMessaging.ts:103 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/bridgeMessaging.ts:132 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/bridgeMessaging.ts:143 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/bridgeMessaging.ts:151 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/bridgeMessaging.ts:171 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/bridgeMessaging.ts:183 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/bridgeMessaging.ts:194 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/bridgeMessaging.ts:206 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/workSecret.ts:5 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/workSecret.ts:44 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/workSecret.ts:64 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/workSecret.ts:81 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/workSecret.ts:101 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/workSecret.ts:136 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/sessionRunner.test.ts:13 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/sessionRunner.test.ts:34 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Bridge 子页面 的证据点 src/bridge/sessionRunner.test.ts:78 不是孤立行号:这一处把状态、权限或传输边界固定在源码里,和相邻模块形成可测试契约;阅读时应把它当成调用链的一段,而不是 README 里的营销描述。

Generated · 2026-07-29Source · 0xwilliamortiz/openclaude-improved