feat: consolidated open build — KAIROS, bridge, cleanup, feature flags, security#660
feat: consolidated open build — KAIROS, bridge, cleanup, feature flags, security#660Flo5k5 wants to merge 16 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Activates the “KAIROS” assistant/daemon mode in the open build by wiring in assistant-mode infrastructure, adding stubs for proactive mode, introducing an interruptible Sleep tool, exposing new /assistant and /proactive commands, and updating the build pipeline to reliably constant-fold feature() flags under Bun v1.3.9+.
Changes:
- Add assistant-mode modules (
src/assistant/*) and assistant/proactive commands (src/commands/*) to enable and inspect KAIROS behavior at runtime. - Introduce
SleepTool(interruptible, settings-clamped) and proactive-mode stubs to satisfy gated imports without enabling full ticking. - Update the build script to pre-process
feature('FLAG')calls into boolean literals and flip multiple feature flags on in the open build; enable team memory by default and add a settings knob for bypass-permissions availability.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/build.ts | Enables KAIROS-related flags and replaces the broken bun:bundle shim with in-place source pre-processing + restore. |
| src/assistant/gate.ts | Open-build runtime gate for KAIROS via local GrowthBook stub / feature-flags file. |
| src/assistant/index.ts | Assistant-mode detection, team bootstrap for in-process subagents, and system-prompt addendum loading. |
| src/assistant/sessionDiscovery.ts | Open-build stub for remote session discovery (returns none). |
| src/commands/assistant/index.ts | Re-export entrypoint for the /assistant command module. |
| src/commands/assistant/assistant.ts | Implements /assistant command and provides stub wizard exports used by dialog launchers. |
| src/commands/proactive.ts | Adds /proactive toggle command (using the proactive stub module). |
| src/proactive/index.ts | Proactive-mode state stub (activate/deactivate/pause/subscribe) without tick scheduling. |
| src/proactive/useProactive.ts | No-op React hook stub to satisfy gated imports in the REPL UI. |
| src/tools/SleepTool/SleepTool.ts | New interruptible sleep tool, enabled only when KAIROS is active; clamps sleep duration via settings. |
| src/memdir/teamMemPaths.ts | Defaults the team-memory feature gate to enabled when auto-memory is enabled. |
| src/utils/permissions/permissionSetup.ts | Adds support for permissions.allowBypassPermissionsMode when deciding bypass-mode availability. |
| src/utils/settings/settings.ts | Registers permissions.allowBypassPermissionsMode for managed-settings logging key expansion. |
| src/utils/settings/types.ts | Adds permissions.allowBypassPermissionsMode to the settings schema. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| preProcessFeatureFlags(join(import.meta.dir, '..', 'src')) | ||
| const numModified = modifiedFiles.size | ||
|
|
||
| try { |
There was a problem hiding this comment.
preProcessFeatureFlags() runs before the try/finally; if it throws (e.g., unreadable file/dir) the process exits without restoring any already-modified source files. Move the pre-processing call inside the try and ensure restoreModifiedFiles() runs in a finally that covers both pre-processing and Bun.build().
| preProcessFeatureFlags(join(import.meta.dir, '..', 'src')) | |
| const numModified = modifiedFiles.size | |
| try { | |
| let numModified = 0 | |
| try { | |
| preProcessFeatureFlags(join(import.meta.dir, '..', 'src')) | |
| numModified = modifiedFiles.size |
| // Clamp to configured bounds — max takes precedence over min | ||
| const minSleep = typeof settings.minSleepDurationMs === 'number' | ||
| ? settings.minSleepDurationMs | ||
| : 0 | ||
| const maxSleep = typeof settings.maxSleepDurationMs === 'number' | ||
| ? settings.maxSleepDurationMs | ||
| : 5 * 60 * 1000 // 5 minutes default cap | ||
|
|
||
| duration_ms = Math.max(duration_ms, minSleep) | ||
| if (maxSleep >= 0) { | ||
| duration_ms = Math.min(duration_ms, maxSleep) | ||
| } |
There was a problem hiding this comment.
settings.maxSleepDurationMs is documented as allowing -1 for “indefinite sleep (waits for user input)”, but the tool never special-cases maxSleep === -1; it just removes the cap and still uses setTimeout(duration_ms). Consider implementing an abort-signal-only wait when maxSleepDurationMs is -1 (and/or guarding against extremely large duration_ms causing setTimeout overflow).
| const timer = setTimeout(resolve, duration_ms) | ||
| const abortController = (context as { abortController?: AbortController }).abortController | ||
|
|
||
| if (!abortController) return | ||
|
|
||
| if (abortController.signal.aborted) { | ||
| clearTimeout(timer) | ||
| resolve() | ||
| return | ||
| } | ||
|
|
||
| let cleanupTimer: ReturnType<typeof setTimeout> | undefined | ||
| const onAbort = () => { | ||
| clearTimeout(timer) | ||
| if (cleanupTimer !== undefined) clearTimeout(cleanupTimer) | ||
| resolve() | ||
| } | ||
| abortController.signal.addEventListener('abort', onAbort, { once: true }) | ||
| // Clean up listener on normal completion to avoid leak | ||
| cleanupTimer = setTimeout(() => { | ||
| abortController.signal.removeEventListener('abort', onAbort) | ||
| }, duration_ms + 1) |
There was a problem hiding this comment.
The abort listener cleanup uses a second setTimeout(duration_ms + 1) to removeEventListener, which schedules an extra timer per sleep and can keep the listener around until that timer fires. Prefer removing the listener in the main timer completion path (and in onAbort) instead of creating a cleanupTimer.
| const timer = setTimeout(resolve, duration_ms) | |
| const abortController = (context as { abortController?: AbortController }).abortController | |
| if (!abortController) return | |
| if (abortController.signal.aborted) { | |
| clearTimeout(timer) | |
| resolve() | |
| return | |
| } | |
| let cleanupTimer: ReturnType<typeof setTimeout> | undefined | |
| const onAbort = () => { | |
| clearTimeout(timer) | |
| if (cleanupTimer !== undefined) clearTimeout(cleanupTimer) | |
| resolve() | |
| } | |
| abortController.signal.addEventListener('abort', onAbort, { once: true }) | |
| // Clean up listener on normal completion to avoid leak | |
| cleanupTimer = setTimeout(() => { | |
| abortController.signal.removeEventListener('abort', onAbort) | |
| }, duration_ms + 1) | |
| const abortController = (context as { abortController?: AbortController }).abortController | |
| let settled = false | |
| let onAbort: (() => void) | undefined | |
| const finish = () => { | |
| if (settled) return | |
| settled = true | |
| if (abortController && onAbort) { | |
| abortController.signal.removeEventListener('abort', onAbort) | |
| } | |
| resolve() | |
| } | |
| const timer = setTimeout(() => { | |
| finish() | |
| }, duration_ms) | |
| if (!abortController) return | |
| if (abortController.signal.aborted) { | |
| clearTimeout(timer) | |
| finish() | |
| return | |
| } | |
| onAbort = () => { | |
| clearTimeout(timer) | |
| finish() | |
| } | |
| abortController.signal.addEventListener('abort', onAbort, { once: true }) |
| * returning safe "off" defaults is sufficient. A full implementation would | ||
| * fire periodic <tick> prompts; this stub keeps the mode permanently off | ||
| * while satisfying the API contract. |
There was a problem hiding this comment.
The header comment says this stub keeps proactive mode “permanently off”, but activateProactive()/deactivateProactive() do flip the _active flag and notify subscribers. Update the comment to match the actual behavior (e.g., “no tick scheduling” rather than “permanently off”).
| * returning safe "off" defaults is sufficient. A full implementation would | |
| * fire periodic <tick> prompts; this stub keeps the mode permanently off | |
| * while satisfying the API contract. | |
| * returning safe defaults is sufficient. A full implementation would | |
| * fire periodic <tick> prompts; this stub preserves the API's state-change | |
| * behavior but does not schedule or emit proactive ticks. |
| const assistant = { | ||
| type: 'local-jsx' as const, | ||
| name: 'assistant', | ||
| description: 'Toggle assistant mode', | ||
| isEnabled: () => true, |
There was a problem hiding this comment.
This command is described as “Toggle assistant mode”, but it only reports status and instructs the user to edit settings.json + restart. Update the command description/header to reflect that it’s a status/help command (or implement actual toggling if that’s intended).
4f18fd5 to
5a16225
Compare
…wb#668) Replace Anthropic-internal refusal text in CYBER_RISK_INSTRUCTION with positive guidance for security testing, red teaming, and CTF. Simplify FileReadTool mitigation logic (always include, no conditional check).
…awb#637, Gitlawb#644) Consolidates PRs Gitlawb#637 and Gitlawb#644: - Remove ~45 dead USER_TYPE=ant guards across 28+ files (-1200 lines) - Open 13 useful features to all users (agent nesting, effort persistence, plan mode interview, sandbox config, tips visibility) - Key: src/constants/tools.ts — unlocks recursive agent spawning
Consolidates PRs Gitlawb#667 and Gitlawb#633: - Activate 18 build-time flags: KAIROS, BRIDGE_MODE, AGENT_TRIGGERS, ULTRATHINK, TOKEN_BUDGET, HISTORY_PICKER, EXTRACT_MEMORIES, etc. - Add _openBuildDefaults in GrowthBook stub for runtime gate overrides: bridge flags (tengu_ccr_bridge, tengu_bridge_repl_v2), KAIROS flags (tengu_kairos, tengu_kairos_brief), feature flags (AWAY_SUMMARY, VERIFICATION_AGENT, EXTRACT_MEMORIES) - Priority: ~/.claude/feature-flags.json > _openBuildDefaults > defaultValue
Minimal changes to 3 bridge files for local bridge server support: - bridgeEnabled.ts: bypass OAuth subscriber check (local bridge needs no subscription), let _openBuildDefaults control runtime gates - bridgeConfig.ts: add localhost:4080 fallback + default token - initReplBridge.ts: skip org policy check when using token override
5a16225 to
613c752
Compare
Replace the proprietary @ant/claude-for-chrome-mcp stub with a local MCP server package enabling Chrome browser automation via the extension. Package (packages/openclaude-for-chrome-mcp/): - 17 browser tool definitions with full MCP schemas - Unix socket client with lazy connection and backpressure-safe writes - MCP server factory using @modelcontextprotocol/sdk - Socket protocol types (4-byte LE + JSON) Build integration: - onResolve redirects openclaude-for-chrome-mcp to local package - Import renames across 4 consumer files
The build system's scanForMissingImports scans for require() calls using
a regex that matches inside comments too. Documented import paths like:
* const X = require('./commands/proactive.js').default;
were resolved from the WRONG directory (the file containing the comment,
not the actual importer), marked as missing, then the global onResolve
handler intercepted ALL imports of that specifier — including the valid
ones. This caused 6 KAIROS modules to be replaced with truthy noop stubs,
breaking the TUI input.
Also fixes: assistant/index.ts imported from ../utils/state.js (doesn't
exist) instead of ../utils/cwd.js.
The scanForMissingImports regex matched require() and import() patterns
inside JSDoc comments, causing false-positive missing module detection.
A documented path like `require('./commands/proactive.js')` in a comment
was resolved from the wrong directory, marked as missing, then the global
onResolve handler intercepted ALL imports of that specifier — including
valid ones — replacing them with truthy noop stubs that broke runtime.
Strip block (/* */) and line (//) comments from source before scanning.
- promptIdentity.test.ts: define MACRO global (ISSUES_EXPLAINER etc.) for test mode where Bun.define build-time replacements aren't active - context.test.ts: clear OPENAI_MODEL env var in each test — the user's environment (e.g. OPENAI_MODEL=github_copilot/gpt-5.4) polluted the provider-qualified lookup, returning wrong context windows - openclaudePaths.test.ts: set CLAUDE_CONFIG_DIR to force .openclaude path when ~/.openclaude doesn't exist on the test machine
1e05dcc to
2640d88
Compare
The build system's noop stub for missing modules only exports `default`,
not named exports. When KAIROS=true, initBundledSkills() destructures
`{ registerDreamSkill }` from the dream.js stub, getting `undefined`.
Calling `undefined()` throws a silent TypeError that breaks the Ink
render loop, freezing the TUI input.
Fix: add src/skills/bundled/dream.ts with a no-op registerDreamSkill().
Also removes debug breadcrumbs from main.tsx and index.ts.
Implement the missing modules that prevented 3 feature flags from activating: TRANSCRIPT_CLASSIFIER (auto-approval classifier): - Add VerifyPlanExecutionTool/constants.ts (tool name string) - Add open-source classifier prompt templates (auto_mode_system_prompt.txt, permissions_external.txt) for the existing yoloClassifier.ts (1600 lines) FORK_SUBAGENT (implicit context-forking): - Add /fork command (delegates to existing forkSubagent.ts via Agent tool) - Add UserForkBoilerplateMessage.tsx (renders fork-boilerplate XML tags) MCP_SKILLS (skill discovery from MCP resources): - Add mcpSkills.ts: fetchMcpSkillsForClient() discovers skills from MCP server resources (URI scheme skill://, MIME skill, or .skill.md name) - Converts to Commands via getMCPSkillBuilders() registry CACHED_MICROCOMPACT remains disabled (requires Anthropic cache editing API).
The TUI displayed `https://claude.ai/code/session_*` when connected to the local bridge server, instead of the actual `http://localhost:4080`. Two coupled bugs caused this: 1. `getBridgeBaseUrl()` had a dead localhost fallback — `getOauthConfig() .BASE_API_URL` always returns `https://api.anthropic.com`, so the `'http://localhost:4080'` fallback was never reached. 2. `CLAUDE_AI_LOCAL_BASE_URL` is hardcoded to `:4000` (the upstream Anthropic dev frontend port), but the open-build bridge server runs on `:4080`. Decouple bridge config from the LLM provider — in this fork they are independent, so a user with Anthropic OAuth tokens (for inference) must still target the local bridge by default. `getClaudeAiBaseUrl()` now derives the origin from the actual `ingressUrl` for localhost sessions. Defense-in-depth: `createSession.ts` (v1 path, currently inactive in the fork) gets the same fallback fix in case `tengu_bridge_repl_v2` is flipped off. BLOCKER comments document the remaining `getOrganization UUID()` blocker for fully unblocking the open-build v1 path.
Make isClaudeAISubscriber() return true when Anthropic auth is not enabled, so subscription-gated features (Chrome extension, Remote Control, Brief) work with local providers. The underlying infrastructure is local: - Bridge runs on localhost:4080 (no OAuth needed) - Chrome MCP uses packages/openclaude-for-chrome-mcp - OpenAI-compatible endpoints replace the Anthropic API Fixes: "Claude in Chrome requires a claude.ai subscription" error when using the /chrome command or the chrome extension notification popup.
…tension The MCP server exposes tools as 'tabs_context_mcp' and 'tabs_create_mcp' (with suffix) to Claude, matching the names in prompts, skills, and tool rendering throughout the codebase. However, the Chrome extension's native messaging handler expects bare method names (e.g. 'tabs_context') and returns "Unknown method" otherwise. Strip the _mcp suffix at the transport layer so Claude-facing names stay consistent while extension-facing names match what the extension accepts.
The Anthropic internal build outputs cli.js; our open build uses Bun to produce cli.mjs (ESM). The wrapper script installed as the Chrome NativeMessagingHost pointed to cli.js which doesn't exist, causing Chrome to fall back to the Claude Desktop native host (which doesn't recognize openclaude's MCP method names) and return "Unknown method: tabs_context". Users who ran /chrome in a previous session need to re-run setup to regenerate ~/.claude/chrome/chrome-native-host with the corrected path.
The previous commit (60812d5) defaulted `getBridgeBaseUrl()` to localhost:4080 unconditionally, which broke /remote-control for users on the Anthropic provider (OAuth or API key) — they expect to reach the production bridge at api.anthropic.com, not a local server. Route by provider instead: - Anthropic provider (firstParty + api.anthropic.com) → Anthropic bridge with the user's OAuth tokens. - Any other provider (OpenAI, Gemini, Ollama, Bedrock, Vertex…) → local bridge at localhost:4080 with the 'openclaude-local-bridge' token. When the local bridge is targeted but the server is not running, the user was seeing "Session creation failed — see debug log". Now createCodeSession / fetchRemoteCredentials throw a typed BridgeConnectionRefusedError on ECONNREFUSED/ECONNRESET; remoteBridgeCore catches it and surfaces an actionable message: Local bridge server is not running at http://localhost:4080. Start it with: bun run packages/bridge-server/index.ts
The Chrome extension service worker (fcoeoabgfenejglbffodgkkbkcdhcgfn v1.0.68)
dispatches tool_request messages based on method name. Reverse-engineering
the service-worker.js shows it only handles method === 'execute_tool':
switch (message.type) {
case 'tool_request':
if (params.method === 'execute_tool') {
// Dispatch using params.tool, params.args, params.client_id
} else {
return { content: `Unknown method: ${method}` }
}
}
Our native host was forwarding the raw tool name as the top-level method,
causing "Unknown method: tabs_context_mcp" from the extension.
Fix: native host wraps each tool_request in execute_tool envelope with
the actual tool name in params.tool. Also revert the _mcp suffix stripping
since the extension accepts the full name (with suffix) as a tool name.
Verified against extension service worker at
~/Library/Application Support/Google/Chrome/Default/Extensions/
fcoeoabgfenejglbffodgkkbkcdhcgfn/1.0.68_0/assets/service-worker.ts-*.js
Vasanthdev2004
left a comment
There was a problem hiding this comment.
Review: PR #660 — Consolidated open build: KAIROS, bridge, cleanup, feature flags, security (head 01e875a)
CI green ✅. 93 files, +3661/-1031.
Assessment
This PR consolidates 5 previously separate PRs (#633, #637, #644, #667, #668) plus new KAIROS/bridge work. I previously approved #637 and #644, and requested changes on #667 and #668.
✅ What's improved from the sub-PRs
Feature flag runtime gates (#667 concern — partially addressed):
The _openBuildDefaults layer in the GrowthBook stub now provides a 3-tier priority: ~/.claude/feature-flags.json > _openBuildDefaults > defaultValue. This means EXTRACT_MEMORIES (via tengu_passport_quail) will be enabled in the open build even if the GrowthBook default is false. Tests cover this. ✅
Bridge server auth:
The bridge server properly authenticates requests with JWT + Bearer token. Worker endpoints have separate auth. This is well-designed. ✅
🔴 Still-blocked concerns from #668
Security guidance still removes authorization/scope gating:
// Before (#668):
"Assist with authorized security testing... Refuse requests for destructive techniques, DoS attacks, mass targeting..."
// After (this PR):
"Assist with security testing... Dual-use security tools can be used in pentesting engagements, red team operations, CTF..."
Two problems remain:
- "authorized" was removed — This removes the expectation that the user has clear authorization context. Without it, the guidance provides no basis to distinguish a red team engagement from an unauthenticated attack.
- The refusal clause was removed — "Refuse requests for destructive techniques, DoS attacks, mass targeting, supply chain compromise, or detection evasion for malicious purposes" is gone. The replacement text implicitly allows these by omission.
I understand the open-build philosophy of not imposing Anthropic-internal refusal language. But the fix should be to replace the Anthropic-specific refusal with open-build-appropriate safety guidance (e.g., "Exercise caution with techniques that could cause harm without clear authorization context"), not to remove all safety gating.
🟡 Non-blocking (from Copilot review + my observations)
preProcessFeatureFlags()runs before try/finally — if it throws, modified source files aren't restored. Move inside the try block.- SleepTool
-1indefinite sleep — documented but not implemented;maxSleep === -1just removes the cap instead of waiting for user input. - Abort listener cleanup — extra
setTimeout(duration_ms + 1)creates unnecessary timer. Preferonceevent orAbortSignal.timeout(). activateProactive()comment mismatch — comment says "permanently off" but code toggles_active./assistantcommand description — says "Toggle" but only reports status + instructs manual edit.
New KAIROS/bridge additions
src/assistant/— Gate, API, session discovery stub. Reasonable skeleton. ✅src/proactive/— No-op stubs. Clean. ✅src/tools/SleepTool/— Interruptible sleep. Good concept, minor issues noted above.- Bridge server — Auth, SSE, session management. Well-structured. ✅
Verdict: Needs changes 🔧
The _openBuildDefaults fix addresses the #667 runtime gate concern. But the #668 security guidance concern remains: removing both "authorized" and the refusal clause creates a blanket green light for dual-use tools without authorization context. Please restore authorization-aware language appropriate for the open build.
|
I opened a focused follow-up PR with the review-unblock fixes here: Scope:
Verification on the follow-up branch:
If it helps, the patch can also be cherry-picked from commit |
|
closing as stale, please feel free to re-open later with more focused PR's this one currently spiders out far too broadly |
Summary
Consolidated PR merging all open work into a single coherent changeset, rebased on latest main. Supersedes PRs #633, #637, #644, #667, #668.
Partially addresses #654 — the bridge server provides HTTP/SSE infrastructure for web-based session interaction.
Changes (4 commits, 62 files, +2171/-942)
1. Security guidance (#668, 2 files)
2. Cleanup + open features (#637 + #644, 35 files, -1200 lines)
USER_TYPE=antguards across 28+ filessrc/constants/tools.ts— unlock recursive agent spawning3. Feature flags + runtime defaults (#667 + #633, 3 files)
KAIROS,BRIDGE_MODE,AGENT_TRIGGERS,ULTRATHINK,TOKEN_BUDGET,HISTORY_PICKER,EXTRACT_MEMORIES,FORK_SUBAGENT, etc._openBuildDefaultsin GrowthBook stub for centralized runtime gate controltengu_ccr_bridge,tengu_bridge_repl_v2,tengu_kairos_brief~/.claude/feature-flags.json>_openBuildDefaults>defaultValue4. KAIROS + local bridge server (22 files, new)
KAIROS assistant mode:
src/assistant/: gate, API, session discovery stubsrc/proactive/: no-op stubs for proactive modesrc/tools/SleepTool/: interruptible sleep (0-5min)src/commands/assistant/,src/commands/proactive.ts: /assistant, /proactiveLocal bridge server:
packages/bridge-server/: standalone Bun server emulating CCR v2 protocol--port,--host 0.0.0.0for Tailscale/WireGuard remote accessBRIDGE_MODE activation:
bridgeEnabled.ts: bypass OAuth check, let_openBuildDefaultscontrol gatesbridgeConfig.ts: addlocalhost:4080fallback + default tokeninitReplBridge.ts: skip org policy check with token overrideTools:
SendUserFileTool: file delivery via BriefTool attachment pipelinePushNotificationTool: OS-native notifications (osascript/notify-send)MonitorMcpDetailDialog: detail UI for monitor background tasksRuntime activation
PRs superseded
Test plan
bun run build— 211 files pre-processedbun run smoke— version OKbun test— 809 pass, 10 fail (pre-existing)--host 0.0.0.0