feat: open useful USER_TYPE-gated features to all users (#644)
* feat: open useful USER_TYPE-gated features to all users Remove 13 process.env.USER_TYPE === 'ant' gates that restricted useful features to Anthropic employees. These features work without Anthropic infrastructure and are now available to all open-build users. Features opened: - Agent nesting (sub-agents can spawn sub-agents) - Effort 'max' persistence in settings - Plan mode interview phase (controlled by feature flags) - Sandbox disabled commands (via ~/.claude/feature-flags.json) - All tips visible to all users (plan mode, feedback, shift-tab) Simplified: - Fullscreen defaults to off (use /config to enable) - Explore agent always uses haiku model - Plan mode tool uses conservative prompt for all users Continues the USER_TYPE cleanup from #637 (dead code) and builds on #639 (local feature flags). * fix: address Copilot review comments — remove residual dead code 1. bridgeConfig.ts: ungate bridge override functions — return env vars directly instead of hardcoded undefined 2. bridgeMain.ts + initReplBridge.ts: ungate sessionIngressUrl — read CLAUDE_BRIDGE_SESSION_INGRESS_URL without USER_TYPE check 3. tools.ts: remove dead ConfigTool/TungstenTool imports, narrow eslint-disable scope, stub REPLTool/SuggestBackgroundPRTool to null 4. readOnlyValidation.ts: remove orphaned ANT_ONLY_COMMAND_ALLOWLIST and unused GH_READ_ONLY_COMMANDS import 5. insights.ts: remove entire remote collection plumbing (types, functions, options, display logic) 6. osc.ts: hardcode supportsTabStatus() to false (internal-only feature) 7. state.ts: simplify addSlowOperation/getSlowOperations to no-ops, remove dead constants * fix: address Copilot review on PR #644 1. settings/types.ts: allow 'max' effort level for all users in Zod schema — was still gated behind USER_TYPE=ant, causing 'max' to be silently dropped on settings reload 2. shouldUseSandbox.ts: defensively normalize disabledCommands from feature flag config with Array.isArray() guards * fix: address second round of Copilot review on PR #644 1. shouldUseSandbox.ts: validate top-level shape of disabledCommands before accessing properties (handles null/primitive from feature flag) 2. fullscreen.ts: update JSDoc to reflect removal of USER_TYPE default 3. osc.ts: update JSDoc — "Ant-only" → "Currently disabled"
This commit is contained in:
committed by
GitHub
parent
658d076909
commit
c1beea9867
@@ -15,7 +15,6 @@ import {
|
||||
DOCKER_READ_ONLY_COMMANDS,
|
||||
EXTERNAL_READONLY_COMMANDS,
|
||||
type FlagArgType,
|
||||
GH_READ_ONLY_COMMANDS,
|
||||
GIT_READ_ONLY_COMMANDS,
|
||||
PYRIGHT_READ_ONLY_COMMANDS,
|
||||
RIPGREP_READ_ONLY_COMMANDS,
|
||||
@@ -1136,68 +1135,6 @@ const COMMAND_ALLOWLIST: Record<string, CommandConfig> = {
|
||||
...DOCKER_READ_ONLY_COMMANDS,
|
||||
}
|
||||
|
||||
// gh commands are internal-only since they make network requests, which goes against
|
||||
// the read-only validation principle of no network access
|
||||
const ANT_ONLY_COMMAND_ALLOWLIST: Record<string, CommandConfig> = {
|
||||
// All gh read-only commands from shared validation map
|
||||
...GH_READ_ONLY_COMMANDS,
|
||||
// aki — internal knowledge-base search CLI.
|
||||
// Network read-only (same policy as gh). --audit-csv omitted: writes to disk.
|
||||
aki: {
|
||||
safeFlags: {
|
||||
'-h': 'none',
|
||||
'--help': 'none',
|
||||
'-k': 'none',
|
||||
'--keyword': 'none',
|
||||
'-s': 'none',
|
||||
'--semantic': 'none',
|
||||
'--no-adaptive': 'none',
|
||||
'-n': 'number',
|
||||
'--limit': 'number',
|
||||
'-o': 'number',
|
||||
'--offset': 'number',
|
||||
'--source': 'string',
|
||||
'--exclude-source': 'string',
|
||||
'-a': 'string',
|
||||
'--after': 'string',
|
||||
'-b': 'string',
|
||||
'--before': 'string',
|
||||
'--collection': 'string',
|
||||
'--drive': 'string',
|
||||
'--folder': 'string',
|
||||
'--descendants': 'none',
|
||||
'-m': 'string',
|
||||
'--meta': 'string',
|
||||
'-t': 'string',
|
||||
'--threshold': 'string',
|
||||
'--kw-weight': 'string',
|
||||
'--sem-weight': 'string',
|
||||
'-j': 'none',
|
||||
'--json': 'none',
|
||||
'-c': 'none',
|
||||
'--chunk': 'none',
|
||||
'--preview': 'none',
|
||||
'-d': 'none',
|
||||
'--full-doc': 'none',
|
||||
'-v': 'none',
|
||||
'--verbose': 'none',
|
||||
'--stats': 'none',
|
||||
'-S': 'number',
|
||||
'--summarize': 'number',
|
||||
'--explain': 'none',
|
||||
'--examine': 'string',
|
||||
'--url': 'string',
|
||||
'--multi-turn': 'number',
|
||||
'--multi-turn-model': 'string',
|
||||
'--multi-turn-context': 'string',
|
||||
'--no-rerank': 'none',
|
||||
'--audit': 'none',
|
||||
'--local': 'none',
|
||||
'--staging': 'none',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
function getCommandAllowlist(): Record<string, CommandConfig> {
|
||||
let allowlist: Record<string, CommandConfig> = COMMAND_ALLOWLIST
|
||||
// On Windows, xargs can be used as a data-to-code bridge: if a file contains
|
||||
@@ -1208,9 +1145,6 @@ function getCommandAllowlist(): Record<string, CommandConfig> {
|
||||
const { xargs: _, ...rest } = allowlist
|
||||
allowlist = rest
|
||||
}
|
||||
if (process.env.USER_TYPE === 'ant') {
|
||||
return { ...allowlist, ...ANT_ONLY_COMMAND_ALLOWLIST }
|
||||
}
|
||||
return allowlist
|
||||
}
|
||||
|
||||
|
||||
@@ -19,34 +19,43 @@ type SandboxInput = {
|
||||
// It is not a security bug to be able to bypass excludedCommands — the sandbox permission
|
||||
// system (which prompts users) is the actual security control.
|
||||
function containsExcludedCommand(command: string): boolean {
|
||||
// Check dynamic config for disabled commands and substrings (only for ants)
|
||||
if (process.env.USER_TYPE === 'ant') {
|
||||
const disabledCommands = getFeatureValue_CACHED_MAY_BE_STALE<{
|
||||
commands: string[]
|
||||
substrings: string[]
|
||||
}>('tengu_sandbox_disabled_commands', { commands: [], substrings: [] })
|
||||
// Check dynamic config for disabled commands and substrings
|
||||
const raw = getFeatureValue_CACHED_MAY_BE_STALE<{
|
||||
commands: string[]
|
||||
substrings: string[]
|
||||
}>('tengu_sandbox_disabled_commands', { commands: [], substrings: [] })
|
||||
|
||||
// Check if command contains any disabled substrings
|
||||
for (const substring of disabledCommands.substrings) {
|
||||
if (command.includes(substring)) {
|
||||
const disabledCommands =
|
||||
typeof raw === 'object' && raw !== null
|
||||
? raw
|
||||
: { commands: [], substrings: [] }
|
||||
const substrings = Array.isArray(disabledCommands.substrings)
|
||||
? disabledCommands.substrings
|
||||
: []
|
||||
const commands = Array.isArray(disabledCommands.commands)
|
||||
? disabledCommands.commands
|
||||
: []
|
||||
|
||||
// Check if command contains any disabled substrings
|
||||
for (const substring of substrings) {
|
||||
if (command.includes(substring)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if command starts with any disabled commands
|
||||
try {
|
||||
const commandParts = splitCommand_DEPRECATED(command)
|
||||
for (const part of commandParts) {
|
||||
const baseCommand = part.trim().split(' ')[0]
|
||||
if (baseCommand && commands.includes(baseCommand)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if command starts with any disabled commands
|
||||
try {
|
||||
const commandParts = splitCommand_DEPRECATED(command)
|
||||
for (const part of commandParts) {
|
||||
const baseCommand = part.trim().split(' ')[0]
|
||||
if (baseCommand && disabledCommands.commands.includes(baseCommand)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// If we can't parse the command (e.g., malformed bash syntax),
|
||||
// treat it as not excluded to allow other validation checks to handle it
|
||||
// This prevents crashes when rendering tool use messages
|
||||
}
|
||||
} catch {
|
||||
// If we can't parse the command (e.g., malformed bash syntax),
|
||||
// treat it as not excluded to allow other validation checks to handle it
|
||||
// This prevents crashes when rendering tool use messages
|
||||
}
|
||||
|
||||
// Check user-configured excluded commands from settings
|
||||
|
||||
Reference in New Issue
Block a user