mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
feat: enable Remote Control (BRIDGE_MODE) with stub completions
- Add BRIDGE_MODE to DEFAULT_FEATURES in dev.ts - Implement peerSessions.ts: cross-session messaging via bridge API - Implement webhookSanitizer.ts: redact secrets from webhook payloads - Replace any stubs in controlTypes.ts with Zod schema-inferred types - Fix tengu_bridge_system_init default to true for app "active" status Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ const defineArgs = Object.entries(defines).flatMap(([k, v]) => [
|
||||
|
||||
// Bun --feature flags: enable feature() gates at runtime.
|
||||
// Default features enabled in dev mode.
|
||||
const DEFAULT_FEATURES = ["BUDDY", "TRANSCRIPT_CLASSIFIER"];
|
||||
const DEFAULT_FEATURES = ["BUDDY", "TRANSCRIPT_CLASSIFIER", "BRIDGE_MODE"];
|
||||
|
||||
// Any env var matching FEATURE_<NAME>=1 will also enable that feature.
|
||||
// e.g. FEATURE_PROACTIVE=1 bun run dev
|
||||
|
||||
@@ -1,3 +1,73 @@
|
||||
// Auto-generated stub — replace with real implementation
|
||||
export {};
|
||||
export const postInterClaudeMessage: (target: string, message: string) => Promise<{ ok: boolean; error?: string }> = () => Promise.resolve({ ok: false });
|
||||
import axios from 'axios'
|
||||
import { logForDebugging } from '../utils/debug.js'
|
||||
import { errorMessage } from '../utils/errors.js'
|
||||
import { getReplBridgeHandle } from './replBridgeHandle.js'
|
||||
import { toCompatSessionId } from './sessionIdCompat.js'
|
||||
|
||||
/**
|
||||
* Send a plain-text message to another Claude session via the bridge API.
|
||||
*
|
||||
* Called by SendMessageTool when the target address scheme is "bridge:".
|
||||
* Uses the current ReplBridgeHandle to derive the sender identity and
|
||||
* the session ingress URL for the POST request.
|
||||
*
|
||||
* @param target - Target session ID (from the "bridge:<sessionId>" address)
|
||||
* @param message - Plain text message content (structured messages are rejected upstream)
|
||||
* @returns { ok: true } on success, { ok: false, error } on failure. Never throws.
|
||||
*/
|
||||
export async function postInterClaudeMessage(
|
||||
target: string,
|
||||
message: string,
|
||||
): Promise<{ ok: boolean; error?: string }> {
|
||||
try {
|
||||
const handle = getReplBridgeHandle()
|
||||
if (!handle) {
|
||||
return { ok: false, error: 'Bridge not connected' }
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
return { ok: false, error: 'No target session specified' }
|
||||
}
|
||||
|
||||
const compatTarget = toCompatSessionId(target)
|
||||
const from = toCompatSessionId(handle.bridgeSessionId)
|
||||
const baseUrl = handle.sessionIngressUrl
|
||||
|
||||
const url = `${baseUrl}/v1/sessions/${compatTarget}/messages`
|
||||
|
||||
const response = await axios.post(
|
||||
url,
|
||||
{
|
||||
type: 'peer_message',
|
||||
from,
|
||||
content: message,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
timeout: 10_000,
|
||||
validateStatus: (s: number) => s < 500,
|
||||
},
|
||||
)
|
||||
|
||||
if (response.status === 200 || response.status === 204) {
|
||||
logForDebugging(
|
||||
`[bridge:peer] Message sent to ${compatTarget} (${response.status})`,
|
||||
)
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
const detail =
|
||||
typeof response.data === 'object' && response.data?.error?.message
|
||||
? response.data.error.message
|
||||
: `HTTP ${response.status}`
|
||||
logForDebugging(`[bridge:peer] Send failed: ${detail}`)
|
||||
return { ok: false, error: detail }
|
||||
} catch (err: unknown) {
|
||||
const msg = errorMessage(err)
|
||||
logForDebugging(`[bridge:peer] postInterClaudeMessage error: ${msg}`)
|
||||
return { ok: false, error: msg }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,57 @@
|
||||
// Auto-generated stub — replace with real implementation
|
||||
export {};
|
||||
export const sanitizeInboundWebhookContent: (content: string) => string = (content) => content;
|
||||
/**
|
||||
* Sanitize inbound GitHub webhook payload content before it enters the session.
|
||||
*
|
||||
* Called from useReplBridge.tsx when feature('KAIROS_GITHUB_WEBHOOKS') is enabled.
|
||||
* Strips known secret patterns (tokens, API keys, credentials) while preserving
|
||||
* the meaningful content (PR titles, descriptions, commit messages, etc.).
|
||||
*
|
||||
* Must be synchronous and never throw — on error, returns the original content.
|
||||
*/
|
||||
|
||||
/** Patterns that match known secret/token formats. */
|
||||
const SECRET_PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [
|
||||
// GitHub tokens (PAT, OAuth, App, Server-to-server)
|
||||
{ pattern: /\b(ghp|gho|ghs|ghu|github_pat)_[A-Za-z0-9_]{10,}\b/g, replacement: '[REDACTED_GITHUB_TOKEN]' },
|
||||
// Anthropic API keys
|
||||
{ pattern: /\bsk-ant-[A-Za-z0-9_-]{10,}\b/g, replacement: '[REDACTED_ANTHROPIC_KEY]' },
|
||||
// Generic Bearer tokens in headers
|
||||
{ pattern: /(Bearer\s+)[A-Za-z0-9._\-/+=]{20,}/gi, replacement: '$1[REDACTED_TOKEN]' },
|
||||
// AWS access keys
|
||||
{ pattern: /\b(AKIA|ASIA)[A-Z0-9]{16}\b/g, replacement: '[REDACTED_AWS_KEY]' },
|
||||
// AWS secret keys (40-char base64-like strings after common labels)
|
||||
{ pattern: /(aws_secret_access_key|secret_key|SecretAccessKey)['":\s=]+[A-Za-z0-9/+=]{30,}/gi, replacement: '$1=[REDACTED_AWS_SECRET]' },
|
||||
// Generic API key patterns (key=value or "key": "value")
|
||||
{ pattern: /(api[_-]?key|apikey|secret|password|token|credential)['":\s=]+["']?[A-Za-z0-9._\-/+=]{16,}["']?/gi, replacement: '$1=[REDACTED]' },
|
||||
// npm tokens
|
||||
{ pattern: /\bnpm_[A-Za-z0-9]{36}\b/g, replacement: '[REDACTED_NPM_TOKEN]' },
|
||||
// Slack tokens
|
||||
{ pattern: /\bxox[bporas]-[A-Za-z0-9-]{10,}\b/g, replacement: '[REDACTED_SLACK_TOKEN]' },
|
||||
]
|
||||
|
||||
/** Maximum content length before truncation (100KB). */
|
||||
const MAX_CONTENT_LENGTH = 100_000
|
||||
|
||||
export function sanitizeInboundWebhookContent(content: string): string {
|
||||
try {
|
||||
if (!content) return content
|
||||
|
||||
let sanitized = content
|
||||
|
||||
// Truncate excessively large payloads
|
||||
if (sanitized.length > MAX_CONTENT_LENGTH) {
|
||||
sanitized = sanitized.slice(0, MAX_CONTENT_LENGTH) + '\n... [truncated]'
|
||||
}
|
||||
|
||||
// Redact known secret patterns
|
||||
for (const { pattern, replacement } of SECRET_PATTERNS) {
|
||||
// Reset lastIndex for global regexes
|
||||
pattern.lastIndex = 0
|
||||
sanitized = sanitized.replace(pattern, replacement)
|
||||
}
|
||||
|
||||
return sanitized
|
||||
} catch {
|
||||
// Never throw — return original content on any error
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
/**
|
||||
* Stub: SDK Control Types (not yet published in open-source).
|
||||
* Used by bridge/transport layer for the control protocol.
|
||||
* SDK Control Types — inferred from Zod schemas in controlSchemas.ts / coreSchemas.ts.
|
||||
*
|
||||
* These types define the control protocol between the CLI bridge and the server.
|
||||
* Used by bridge/transport layer, remote session manager, and CLI print/IO paths.
|
||||
*/
|
||||
export type SDKControlRequest = { type: string; [key: string]: unknown }
|
||||
export type SDKControlResponse = { type: string; [key: string]: unknown }
|
||||
export type StdoutMessage = any;
|
||||
export type SDKControlInitializeRequest = any;
|
||||
export type SDKControlInitializeResponse = any;
|
||||
export type SDKControlMcpSetServersResponse = any;
|
||||
export type SDKControlReloadPluginsResponse = any;
|
||||
export type StdinMessage = any;
|
||||
export type SDKPartialAssistantMessage = any;
|
||||
export type SDKControlPermissionRequest = any;
|
||||
export type SDKControlCancelRequest = any;
|
||||
export type SDKControlRequestInner = any;
|
||||
import type { z } from 'zod'
|
||||
import type {
|
||||
SDKControlRequestSchema,
|
||||
SDKControlResponseSchema,
|
||||
SDKControlInitializeRequestSchema,
|
||||
SDKControlInitializeResponseSchema,
|
||||
SDKControlMcpSetServersResponseSchema,
|
||||
SDKControlReloadPluginsResponseSchema,
|
||||
SDKControlPermissionRequestSchema,
|
||||
SDKControlCancelRequestSchema,
|
||||
SDKControlRequestInnerSchema,
|
||||
StdoutMessageSchema,
|
||||
StdinMessageSchema,
|
||||
} from './controlSchemas.js'
|
||||
import type { SDKPartialAssistantMessageSchema } from './coreSchemas.js'
|
||||
|
||||
export type SDKControlRequest = z.infer<ReturnType<typeof SDKControlRequestSchema>>
|
||||
export type SDKControlResponse = z.infer<ReturnType<typeof SDKControlResponseSchema>>
|
||||
export type StdoutMessage = z.infer<ReturnType<typeof StdoutMessageSchema>>
|
||||
export type SDKControlInitializeRequest = z.infer<ReturnType<typeof SDKControlInitializeRequestSchema>>
|
||||
export type SDKControlInitializeResponse = z.infer<ReturnType<typeof SDKControlInitializeResponseSchema>>
|
||||
export type SDKControlMcpSetServersResponse = z.infer<ReturnType<typeof SDKControlMcpSetServersResponseSchema>>
|
||||
export type SDKControlReloadPluginsResponse = z.infer<ReturnType<typeof SDKControlReloadPluginsResponseSchema>>
|
||||
export type StdinMessage = z.infer<ReturnType<typeof StdinMessageSchema>>
|
||||
export type SDKPartialAssistantMessage = z.infer<ReturnType<typeof SDKPartialAssistantMessageSchema>>
|
||||
export type SDKControlPermissionRequest = z.infer<ReturnType<typeof SDKControlPermissionRequestSchema>>
|
||||
export type SDKControlCancelRequest = z.infer<ReturnType<typeof SDKControlCancelRequestSchema>>
|
||||
export type SDKControlRequestInner = z.infer<ReturnType<typeof SDKControlRequestInnerSchema>>
|
||||
|
||||
@@ -290,7 +290,7 @@ export function useReplBridge(messages: Message[], setMessages: (action: React.S
|
||||
// to put system/init on the REPL-bridge wire. Skills load is
|
||||
// async (memoized, cheap after REPL startup); fire-and-forget
|
||||
// so the connected-state transition isn't blocked.
|
||||
if (getFeatureValue_CACHED_MAY_BE_STALE('tengu_bridge_system_init', false)) {
|
||||
if (getFeatureValue_CACHED_MAY_BE_STALE('tengu_bridge_system_init', true)) {
|
||||
void (async () => {
|
||||
try {
|
||||
const skills = await getSlashCommandToolSkills(getCwd());
|
||||
|
||||
Reference in New Issue
Block a user