From 2fe5ed462a2072adbebb576ea5b7d489273d3017 Mon Sep 17 00:00:00 2001 From: openresearch Date: Wed, 24 Jun 2026 23:08:07 +0000 Subject: [PATCH 01/24] refactor: migrate legacy build-mode checks to isInternalBuild() helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the verbose `(process.env.NCODE_BUILD_MODE === 'noumena' || process.env.USER_TYPE === 'ant')` check and its negation with the canonical `isInternalBuild()` / `!isInternalBuild()` helper from src/capabilities/static.js, whose docstring explicitly documents it as the semantic replacement for these checks. The helper is already the established convention (used in constants/prompts.ts, constants/oauth.ts, constants/keys.ts, screens/REPL.tsx, and dozens of other sites), but ~77 call sites across 57 files still used the raw env form. This completes that migration so there is a single source of truth for the internal-build gate. Semantic note: isInternalBuild() is a strict superset of the legacy check (it also returns true for 'internal' and 'dev' spins), which is the intended behavior per the helper's documented contract ("Returns true for any non-public spin"). Gated features (mock rate limits, internal logging, ant-trace, etc.) now also activate for dev/internal spins, matching how isInternalBuild() is already used elsewhere in the codebase. Tungsten-specific gates that check `USER_TYPE === 'noumena'` (a distinct concept — Noumena product user, not internal build) are intentionally left untouched, as isNoumenaMode() and isAntOrNoumena() which encode different semantics. Verified: bun run build:source succeeds (dist/cli.js builds, native cargo step falls back to SSE as designed on this env), ./ncode --version works, and the full test:contracts suite passes (230 pass / 8 skip / 0 fail). --- src/bootstrap/state.ts | 2 +- src/commands.ts | 3 +-- src/commands/ant-trace/ant-trace.ts | 3 ++- src/commands/ant-trace/index.js | 3 ++- src/commands/break-cache/break-cache.ts | 3 ++- src/commands/break-cache/index.js | 3 ++- src/commands/ctx_viz/index.js | 3 ++- src/commands/env/index.js | 15 +++++------ src/commands/issue/index.js | 3 ++- src/commands/mock-limits/mock-limits.ts | 3 ++- src/commands/oauth-refresh/oauth-refresh.ts | 3 ++- src/commands/onboarding/index.js | 3 ++- src/commands/reset-limits/index.js | 5 ++-- src/commands/share/share.ts | 3 ++- src/commands/summary/index.js | 3 ++- src/commands/summary/summary.ts | 3 ++- src/components/AntModelSwitchCallout.tsx | 3 ++- .../FeedbackSurvey/useMemorySurvey.tsx | 3 ++- src/components/MemoryUsageIndicator.tsx | 3 ++- src/constants/prompts.ts | 2 +- src/hooks/useIssueFlagBanner.ts | 3 ++- src/hooks/usePromptsFromClaudeInChrome.ts | 3 ++- src/main.tsx | 2 +- src/migrations/migrateFennecToOpus.ts | 3 ++- src/screens/REPL.tsx | 2 +- src/services/PromptSuggestion/speculation.ts | 2 +- src/services/analytics/datadog.ts | 3 ++- src/services/analytics/growthbook.ts | 8 +++--- src/services/api/dumpPrompts.ts | 4 +-- src/services/compact/apiMicrocompact.ts | 3 ++- src/services/internalLogging.ts | 7 ++--- src/services/mcp/vscodeSdkMcp.ts | 3 ++- src/services/mockRateLimits.ts | 20 +++++++------- src/skills/bundled/loremIpsum.ts | 3 ++- src/skills/bundled/remember.ts | 3 ++- src/skills/bundled/skillify.ts | 3 ++- src/skills/bundled/stuck.ts | 3 ++- src/skills/bundled/verify.ts | 3 ++- src/tools/AgentTool/UI.tsx | 2 +- src/tools/BashTool/bashPermissions.ts | 2 +- .../PowerShellTool/readOnlyValidation.ts | 3 ++- src/tools/TaskOutputTool/TaskOutputTool.tsx | 3 ++- src/utils/asciicast.ts | 3 ++- src/utils/attachments.ts | 4 +-- src/utils/autoRunIssue.tsx | 2 +- src/utils/autoUpdater.ts | 2 +- src/utils/betas.ts | 2 +- src/utils/crossProjectResume.ts | 3 ++- src/utils/debug.ts | 4 +-- src/utils/errorLogSink.ts | 3 ++- src/utils/model/antModels.ts | 7 ++--- src/utils/model/modelCapabilities.ts | 3 ++- src/utils/permissions/PermissionMode.ts | 3 ++- src/utils/permissions/yoloClassifier.ts | 4 +-- src/utils/status.tsx | 27 ++++++------------- src/utils/telemetry/betaSessionTracing.ts | 2 +- src/utils/user.ts | 5 ++-- 57 files changed, 129 insertions(+), 105 deletions(-) diff --git a/src/bootstrap/state.ts b/src/bootstrap/state.ts index 1490957..2c90643 100644 --- a/src/bootstrap/state.ts +++ b/src/bootstrap/state.ts @@ -1583,7 +1583,7 @@ const MAX_SLOW_OPERATIONS = 10 const SLOW_OPERATION_TTL_MS = 10000 export function addSlowOperation(operation: string, durationMs: number): void { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) return + if (!isInternalBuild()) return // Skip tracking for editor sessions (user editing a prompt file in $EDITOR) // These are intentionally slow since the user is drafting text if (operation.includes('exec') && operation.includes('claude-prompt-')) { diff --git a/src/commands.ts b/src/commands.ts index 22b8150..f66b07c 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -347,8 +347,7 @@ const COMMANDS = memoize((): Command[] => { tasks, ...(workflowsCmd ? [workflowsCmd] : []), ...(torch ? [torch] : []), - ...((process.env.NCODE_BUILD_MODE === 'noumena' || - process.env.USER_TYPE === 'ant') && + ...(isInternalBuild() && !process.env.IS_DEMO ? INTERNAL_ONLY_COMMANDS : []), diff --git a/src/commands/ant-trace/ant-trace.ts b/src/commands/ant-trace/ant-trace.ts index 1e34b4a..e09876a 100644 --- a/src/commands/ant-trace/ant-trace.ts +++ b/src/commands/ant-trace/ant-trace.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { feature } from 'bun:bundle' import { stat } from 'fs/promises' import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' @@ -219,7 +220,7 @@ function formatReport(report: TraceReport): string { } export const call: LocalCommandCall = async args => { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return text('`/ant-trace` is only available in ANT builds.') } diff --git a/src/commands/ant-trace/index.js b/src/commands/ant-trace/index.js index 2d16b20..7956800 100644 --- a/src/commands/ant-trace/index.js +++ b/src/commands/ant-trace/index.js @@ -1,9 +1,10 @@ +import { isInternalBuild } from '../../capabilities/static.js' const antTrace = { type: 'local', name: 'ant-trace', description: 'Show internal tracing and trace-file diagnostics', argumentHint: '[status|flush|--json]', - isEnabled: () => (process.env.NCODE_BUILD_MODE === 'noumena' || process.env.USER_TYPE === 'ant'), + isEnabled: () => isInternalBuild(), isHidden: true, immediate: true, supportsNonInteractive: true, diff --git a/src/commands/break-cache/break-cache.ts b/src/commands/break-cache/break-cache.ts index 00f5fbd..49cbe4c 100644 --- a/src/commands/break-cache/break-cache.ts +++ b/src/commands/break-cache/break-cache.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { feature } from 'bun:bundle' import { randomUUID } from 'crypto' import { @@ -76,7 +77,7 @@ function setInjectionAndDescribe( } export const call: LocalCommandCall = async args => { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return text('`/break-cache` is only available in ANT builds.') } diff --git a/src/commands/break-cache/index.js b/src/commands/break-cache/index.js index 4006d17..d4b5119 100644 --- a/src/commands/break-cache/index.js +++ b/src/commands/break-cache/index.js @@ -1,9 +1,10 @@ +import { isInternalBuild } from '../../capabilities/static.js' const breakCache = { type: 'local', name: 'break-cache', description: 'Force a prompt-cache break by mutating system context injection', argumentHint: '[status|bump [reason]|set |clear|reset-state]', - isEnabled: () => (process.env.NCODE_BUILD_MODE === 'noumena' || process.env.USER_TYPE === 'ant'), + isEnabled: () => isInternalBuild(), isHidden: true, immediate: true, supportsNonInteractive: true, diff --git a/src/commands/ctx_viz/index.js b/src/commands/ctx_viz/index.js index 483045d..bdf425e 100644 --- a/src/commands/ctx_viz/index.js +++ b/src/commands/ctx_viz/index.js @@ -1,9 +1,10 @@ +import { isInternalBuild } from '../../capabilities/static.js' const ctxViz = { type: 'local-jsx', name: 'ctx_viz', description: 'Internal alias for the context visualization command', isHidden: true, - isEnabled: () => (process.env.NCODE_BUILD_MODE === 'noumena' || process.env.USER_TYPE === 'ant'), + isEnabled: () => isInternalBuild(), load: () => import('../context/context.js'), } diff --git a/src/commands/env/index.js b/src/commands/env/index.js index 9a404c6..01b925d 100644 --- a/src/commands/env/index.js +++ b/src/commands/env/index.js @@ -1,3 +1,4 @@ +import { isInternalBuild } from '../../capabilities/static.js' import { feature } from 'bun:bundle' import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js' import { @@ -124,9 +125,9 @@ function buildBaseRuntimeReport() { const buildMode = process.env.NCODE_BUILD_MODE ?? null const userType = process.env.USER_TYPE ?? 'external' const noumenaMode = buildMode === 'noumena' || buildMode === 'n' - const isInternalBuild = noumenaMode || userType === 'ant' + const internalBuild = noumenaMode || userType === 'ant' const isDemo = isEnvTruthy(process.env.IS_DEMO) - const internalCommandSetEnabled = isInternalBuild && !isDemo + const internalCommandSetEnabled = internalBuild && !isDemo const buildFeatures = getBuildFeatureStates() const agentsPlatform = { @@ -156,7 +157,7 @@ function buildBaseRuntimeReport() { } const hiddenReasons = [] - if (!isInternalBuild) { + if (!internalBuild) { hiddenReasons.push( 'this bundle was built without Noumena internal compatibility enabled', ) @@ -170,7 +171,7 @@ function buildBaseRuntimeReport() { buildMode, noumenaMode, userType, - isInternalBuild, + isInternalBuild: internalBuild, internalCommandSetEnabled, buildFeatures, commandRuntimeGates: { @@ -308,9 +309,7 @@ async function getCommandReasons( case 'cost': if ( isCostCommandAuthHiddenForContext({ - isInternalBuild: - process.env.NCODE_BUILD_MODE === 'noumena' || - process.env.USER_TYPE === 'ant', + isInternalBuild: isInternalBuild(), session: modules.commandSession, }) ) { @@ -640,7 +639,7 @@ const env = { name: 'env', description: 'Show runtime build mode and internal gate diagnostics', supportsNonInteractive: true, - isEnabled: () => (process.env.NCODE_BUILD_MODE === 'noumena' || process.env.USER_TYPE === 'ant'), + isEnabled: () => isInternalBuild(), load: () => Promise.resolve({ call }), } diff --git a/src/commands/issue/index.js b/src/commands/issue/index.js index 8dc4219..2b1f8e8 100644 --- a/src/commands/issue/index.js +++ b/src/commands/issue/index.js @@ -1,3 +1,4 @@ +import { isInternalBuild } from '../../capabilities/static.js' import { isPolicyAllowed } from '../../services/policyLimits/index.js' import { isEnvTruthy } from '../../utils/envUtils.js' import { isEssentialTrafficOnly } from '../../utils/privacyLevel.js' @@ -11,7 +12,7 @@ const issue = { isHidden: true, immediate: true, isEnabled: () => - (process.env.NCODE_BUILD_MODE === 'noumena' || process.env.USER_TYPE === 'ant') && + isInternalBuild() && !( isEnvTruthy(process.env.DISABLE_FEEDBACK_COMMAND) || isEnvTruthy(process.env.DISABLE_BUG_COMMAND) || diff --git a/src/commands/mock-limits/mock-limits.ts b/src/commands/mock-limits/mock-limits.ts index 6a62b59..ea431da 100644 --- a/src/commands/mock-limits/mock-limits.ts +++ b/src/commands/mock-limits/mock-limits.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import type { LocalCommandCall } from '../../types/command.js' import { addExceededLimit, @@ -161,7 +162,7 @@ function errorWithUsage(message: string): { type: 'text'; value: string } { } export const call: LocalCommandCall = async args => { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return ok('`/mock-limits` is only available in ANT builds.') } diff --git a/src/commands/oauth-refresh/oauth-refresh.ts b/src/commands/oauth-refresh/oauth-refresh.ts index fde307a..b4a707f 100644 --- a/src/commands/oauth-refresh/oauth-refresh.ts +++ b/src/commands/oauth-refresh/oauth-refresh.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { fetchAndStoreUserRoles, refreshOAuthToken, @@ -109,7 +110,7 @@ async function runRefresh(context: Parameters[1]): Promise { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return text('`/oauth-refresh` is only available in ANT builds.') } diff --git a/src/commands/onboarding/index.js b/src/commands/onboarding/index.js index 6674d85..7892e66 100644 --- a/src/commands/onboarding/index.js +++ b/src/commands/onboarding/index.js @@ -1,3 +1,4 @@ +import { isInternalBuild } from '../../capabilities/static.js' import { isEnvTruthy } from '../../utils/envUtils.js' const onboarding = { @@ -7,7 +8,7 @@ const onboarding = { isHidden: true, immediate: true, isEnabled: () => - (process.env.NCODE_BUILD_MODE === 'noumena' || process.env.USER_TYPE === 'ant') && !isEnvTruthy(process.env.IS_DEMO), + isInternalBuild() && !isEnvTruthy(process.env.IS_DEMO), load: () => import('./onboarding.js'), } diff --git a/src/commands/reset-limits/index.js b/src/commands/reset-limits/index.js index ae239a4..a09b6e0 100644 --- a/src/commands/reset-limits/index.js +++ b/src/commands/reset-limits/index.js @@ -1,3 +1,4 @@ +import { isInternalBuild } from '../../capabilities/static.js' import { getIsNonInteractiveSession } from '../../bootstrap/state.js' import { clearMockHeaders, @@ -67,7 +68,7 @@ export const resetLimits = { description: 'Clear mocked rate-limit state and return to real limits', argumentHint: '[--verbose]', isEnabled: () => - (process.env.NCODE_BUILD_MODE === 'noumena' || process.env.USER_TYPE === 'ant') && !getIsNonInteractiveSession(), + isInternalBuild() && !getIsNonInteractiveSession(), isHidden: true, supportsNonInteractive: false, load: () => Promise.resolve({ call }), @@ -79,7 +80,7 @@ export const resetLimitsNonInteractive = { description: 'Clear mocked rate-limit state and return to real limits', argumentHint: '[--verbose]', isEnabled: () => - (process.env.NCODE_BUILD_MODE === 'noumena' || process.env.USER_TYPE === 'ant') && getIsNonInteractiveSession(), + isInternalBuild() && getIsNonInteractiveSession(), isHidden: true, supportsNonInteractive: true, load: () => Promise.resolve({ call }), diff --git a/src/commands/share/share.ts b/src/commands/share/share.ts index df4639a..033beb9 100644 --- a/src/commands/share/share.ts +++ b/src/commands/share/share.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { randomUUID } from 'crypto' import { submitTranscriptShare } from '../../components/FeedbackSurvey/submitTranscriptShare.js' import { isPolicyAllowed } from '../../services/policyLimits/index.js' @@ -23,7 +24,7 @@ function toCcshareUrl(transcriptId: string): string { } export const call: LocalCommandCall = async (args, context) => { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return text('`/share` is only available in ANT builds.') } if (isEssentialTrafficOnly() || !isPolicyAllowed('allow_product_feedback')) { diff --git a/src/commands/summary/index.js b/src/commands/summary/index.js index b29ff04..85efc10 100644 --- a/src/commands/summary/index.js +++ b/src/commands/summary/index.js @@ -1,8 +1,9 @@ +import { isInternalBuild } from '../../capabilities/static.js' const summary = { type: 'local', name: 'summary', description: 'Refresh and show the current session summary', - isEnabled: () => (process.env.NCODE_BUILD_MODE === 'noumena' || process.env.USER_TYPE === 'ant'), + isEnabled: () => isInternalBuild(), isHidden: true, immediate: true, supportsNonInteractive: true, diff --git a/src/commands/summary/summary.ts b/src/commands/summary/summary.ts index 4274d69..5e0f666 100644 --- a/src/commands/summary/summary.ts +++ b/src/commands/summary/summary.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { manuallyExtractSessionMemory } from '../../services/SessionMemory/sessionMemory.js' import { getSessionMemoryContent, @@ -19,7 +20,7 @@ function usage(): string { } export const call: LocalCommandCall = async (args, context) => { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return text('`/summary` is only available in ANT builds.') } diff --git a/src/components/AntModelSwitchCallout.tsx b/src/components/AntModelSwitchCallout.tsx index aad5d0d..0a4fabf 100644 --- a/src/components/AntModelSwitchCallout.tsx +++ b/src/components/AntModelSwitchCallout.tsx @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import React, { useCallback, useEffect, useMemo } from 'react' import { Box, Text } from '../ink.js' import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js' @@ -112,7 +113,7 @@ export function AntModelSwitchCallout({ } export function shouldShowModelSwitchCallout(): boolean { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return false } diff --git a/src/components/FeedbackSurvey/useMemorySurvey.tsx b/src/components/FeedbackSurvey/useMemorySurvey.tsx index 23c129f..a260ce2 100644 --- a/src/components/FeedbackSurvey/useMemorySurvey.tsx +++ b/src/components/FeedbackSurvey/useMemorySurvey.tsx @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { useCallback, useEffect, useMemo, useRef } from 'react'; import { isFeedbackSurveyDisabled } from 'src/services/analytics/config.js'; import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'; @@ -87,7 +88,7 @@ export function useMemorySurvey(messages: Message[], isLoading: boolean, hasActi }); }, []); const shouldShowTranscriptPrompt = useCallback((selected_0: FeedbackSurveyResponse) => { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return false; } if (selected_0 !== 'bad' && selected_0 !== 'good') { diff --git a/src/components/MemoryUsageIndicator.tsx b/src/components/MemoryUsageIndicator.tsx index 44888ee..6c1c9c2 100644 --- a/src/components/MemoryUsageIndicator.tsx +++ b/src/components/MemoryUsageIndicator.tsx @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import * as React from 'react'; import { useMemoryUsage } from '../hooks/useMemoryUsage.js'; import { Box, Text } from '../ink.js'; @@ -7,7 +8,7 @@ export function MemoryUsageIndicator(): React.ReactNode { // the hook means the 10s polling interval is never set up in external builds. // USER_TYPE is a build-time constant, so the hook call below is either always // reached or dead-code-eliminated — never conditional at runtime. - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return null; } diff --git a/src/constants/prompts.ts b/src/constants/prompts.ts index 0f63aa7..daf1157 100644 --- a/src/constants/prompts.ts +++ b/src/constants/prompts.ts @@ -136,7 +136,7 @@ function getSystemRemindersSection(): string { } function getAntModelOverrideSection(): string | null { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) return null + if (!isInternalBuild()) return null if (isUndercover()) return null return getAntModelOverrideConfig()?.defaultSystemPromptSuffix || null } diff --git a/src/hooks/useIssueFlagBanner.ts b/src/hooks/useIssueFlagBanner.ts index d6f93e1..79f74a8 100644 --- a/src/hooks/useIssueFlagBanner.ts +++ b/src/hooks/useIssueFlagBanner.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { useMemo, useRef } from 'react' import { BASH_TOOL_NAME } from '../tools/BashTool/toolName.js' import type { Message } from '../types/message.js' @@ -93,7 +94,7 @@ export function useIssueFlagBanner( messages: Message[], submitCount: number, ): boolean { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return false } diff --git a/src/hooks/usePromptsFromClaudeInChrome.ts b/src/hooks/usePromptsFromClaudeInChrome.ts index 087e745..7f6c685 100644 --- a/src/hooks/usePromptsFromClaudeInChrome.ts +++ b/src/hooks/usePromptsFromClaudeInChrome.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs' import { useEffect, useRef } from 'react' import { z } from 'zod/v4' @@ -50,7 +51,7 @@ export function usePromptsFromClaudeInChrome( const mcpClientRef = useRef(undefined) useEffect(() => { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return } diff --git a/src/main.tsx b/src/main.tsx index 983945a..b41348a 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -317,7 +317,7 @@ function isBeingDebugged() { } // Exit if we detect node debugging or inspection -if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant') && isBeingDebugged()) { +if (!isInternalBuild() && isBeingDebugged()) { // Use process.exit directly here since we're in the top-level code before imports // and gracefulShutdown is not yet available // eslint-disable-next-line custom-rules/no-top-level-side-effects diff --git a/src/migrations/migrateFennecToOpus.ts b/src/migrations/migrateFennecToOpus.ts index b63c418..538b8b7 100644 --- a/src/migrations/migrateFennecToOpus.ts +++ b/src/migrations/migrateFennecToOpus.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { getSettingsForSource, updateSettingsForSource, @@ -16,7 +17,7 @@ import { * settings here would cause infinite re-runs + silent global promotion. */ export function migrateFennecToOpus(): void { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return } diff --git a/src/screens/REPL.tsx b/src/screens/REPL.tsx index 1727348..62a76da 100644 --- a/src/screens/REPL.tsx +++ b/src/screens/REPL.tsx @@ -794,7 +794,7 @@ export function REPL({ // eslint-disable-next-line prefer-const let trySuggestBgPRIntercept = SUGGEST_BG_PR_NOOP; trySuggestBgPRIntercept = (prevInput, nextInput) => { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return false; } const wasPrefixed = parseBackgroundPRShortcutInput(prevInput) !== null; diff --git a/src/services/PromptSuggestion/speculation.ts b/src/services/PromptSuggestion/speculation.ts index 3c5cf68..5982d7b 100644 --- a/src/services/PromptSuggestion/speculation.ts +++ b/src/services/PromptSuggestion/speculation.ts @@ -277,7 +277,7 @@ function createSpeculationFeedbackMessage( timeSavedMs: number, sessionTotalMs: number, ): Message | null { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) return null + if (!isInternalBuild()) return null if (messages.length === 0 || timeSavedMs === 0) return null diff --git a/src/services/analytics/datadog.ts b/src/services/analytics/datadog.ts index 6b0be33..283343b 100644 --- a/src/services/analytics/datadog.ts +++ b/src/services/analytics/datadog.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import axios from 'axios' import { createHash } from 'crypto' import memoize from 'lodash-es/memoize.js' @@ -221,7 +222,7 @@ export async function trackDatadogEvent( } // Normalize model names for cardinality reduction (external users only) - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant') && typeof allData.model === 'string') { + if (!isInternalBuild() && typeof allData.model === 'string') { const shortName = getCanonicalName(allData.model.replace(/\[1m]$/i, '')) allData.model = shortName in MODEL_COSTS ? shortName : 'other' } diff --git a/src/services/analytics/growthbook.ts b/src/services/analytics/growthbook.ts index 2063b83..3b5637e 100644 --- a/src/services/analytics/growthbook.ts +++ b/src/services/analytics/growthbook.ts @@ -239,7 +239,7 @@ export function hasGrowthBookEnvOverride(feature: string): boolean { * until the next saveGlobalConfig() invalidates it. */ function getConfigOverrides(): Record | undefined { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) return undefined + if (!isInternalBuild()) return undefined try { return getGlobalConfig().growthBookOverrides } catch { @@ -276,7 +276,7 @@ export function setGrowthBookConfigOverride( feature: string, value: unknown, ): void { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) return + if (!isInternalBuild()) return const canonicalFeature = toCanonicalAnalyticsName(feature) try { saveGlobalConfig(c => { @@ -302,7 +302,7 @@ export function setGrowthBookConfigOverride( } export function clearGrowthBookConfigOverrides(): void { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) return + if (!isInternalBuild()) return try { saveGlobalConfig(c => { if ( @@ -1177,7 +1177,7 @@ export function resetGrowthBook(): void { // Periodic refresh interval (matches Statsig's 6-hour interval) const GROWTHBOOK_REFRESH_INTERVAL_MS = - (process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant') + !isInternalBuild() ? 6 * 60 * 60 * 1000 // 6 hours : 20 * 60 * 1000 // 20 min (for ants) let refreshInterval: ReturnType | null = null diff --git a/src/services/api/dumpPrompts.ts b/src/services/api/dumpPrompts.ts index f8c4cbe..c1cfae4 100644 --- a/src/services/api/dumpPrompts.ts +++ b/src/services/api/dumpPrompts.ts @@ -48,7 +48,7 @@ export function clearAllDumpState(): void { } export function addApiRequestToCache(requestData: unknown): void { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) return + if (!isInternalBuild()) return cachedApiRequests.push({ timestamp: new Date().toISOString(), request: requestData, @@ -101,7 +101,7 @@ function dumpRequest( const req = jsonParse(body) as Record addApiRequestToCache(req) - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) return + if (!isInternalBuild()) return const entries: string[] = [] const messages = (req.messages ?? []) as Array<{ role?: string }> diff --git a/src/services/compact/apiMicrocompact.ts b/src/services/compact/apiMicrocompact.ts index 253bd2b..bfc6e9d 100644 --- a/src/services/compact/apiMicrocompact.ts +++ b/src/services/compact/apiMicrocompact.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { isEnvTruthy } from '../../utils/envUtils.js' // docs: https://docs.google.com/document/d/1oCT4evvWTh3P6z-kcfNQwWTCxAhkoFndSaNS9Gm40uw/edit?tab=t.0 @@ -88,7 +89,7 @@ export function getAPIContextManagement(options?: { } // Tool clearing strategies are ant-only - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return strategies.length > 0 ? { edits: strategies } : undefined } diff --git a/src/services/internalLogging.ts b/src/services/internalLogging.ts index 824b24c..2b32434 100644 --- a/src/services/internalLogging.ts +++ b/src/services/internalLogging.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { readFile } from 'fs/promises' import memoize from 'lodash-es/memoize.js' import type { ToolPermissionContext } from '../Tool.js' @@ -15,7 +16,7 @@ import { * ... */ const getKubernetesNamespace = memoize(async (): Promise => { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return null } const namespacePath = @@ -33,7 +34,7 @@ const getKubernetesNamespace = memoize(async (): Promise => { * Get the OCI container ID from within a running container */ export const getContainerId = memoize(async (): Promise => { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return null } const containerIdPath = '/proc/self/mountinfo' @@ -72,7 +73,7 @@ export async function logPermissionContextForAnts( toolPermissionContext: ToolPermissionContext | null, moment: 'summary' | 'initialization', ): Promise { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return } diff --git a/src/services/mcp/vscodeSdkMcp.ts b/src/services/mcp/vscodeSdkMcp.ts index bc573c4..92b33ae 100644 --- a/src/services/mcp/vscodeSdkMcp.ts +++ b/src/services/mcp/vscodeSdkMcp.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { logForDebugging } from 'src/utils/debug.js' import { z } from 'zod/v4' import { lazySchema } from '../../utils/lazySchema.js' @@ -41,7 +42,7 @@ export function notifyVscodeFileUpdated( oldContent: string | null, newContent: string | null, ): void { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant') || !vscodeMcpClient) { + if (!isInternalBuild() || !vscodeMcpClient) { return } diff --git a/src/services/mockRateLimits.ts b/src/services/mockRateLimits.ts index 8aa93c4..b59e139 100644 --- a/src/services/mockRateLimits.ts +++ b/src/services/mockRateLimits.ts @@ -102,7 +102,7 @@ export function setMockHeader( key: MockHeaderKey, value: string | undefined, ): void { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return } @@ -251,7 +251,7 @@ export function addExceededLimit( type: 'five_hour' | 'seven_day' | 'seven_day_opus' | 'seven_day_sonnet', hoursFromNow: number, ): void { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return } @@ -279,7 +279,7 @@ export function setMockEarlyWarning( utilization: number, hoursFromNow?: number, ): void { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return } @@ -320,7 +320,7 @@ export function clearMockEarlyWarning(): void { } export function setMockRateLimitScenario(scenario: MockScenario): void { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return } @@ -601,7 +601,7 @@ export function setMockRateLimitScenario(scenario: MockScenario): void { } export function getMockHeaderless429Message(): string | null { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return null } // Env var path for -p / SDK testing where slash commands aren't available @@ -617,7 +617,7 @@ export function getMockHeaderless429Message(): string | null { export function getMockHeaders(): MockHeaders | null { if ( !mockEnabled || - (process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant') || + !isInternalBuild() || Object.keys(mockHeaders).length === 0 ) { return null @@ -712,7 +712,7 @@ export function applyMockHeaders( // Check if we should process rate limits even without subscription // This is for Ant employees testing with mocks export function shouldProcessMockLimits(): boolean { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return false } return mockEnabled || Boolean(process.env.CLAUDE_MOCK_HEADERLESS_429) @@ -807,7 +807,7 @@ export function getScenarioDescription(scenario: MockScenario): string { export function setMockSubscriptionType( subscriptionType: SubscriptionType | null, ): void { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return } mockEnabled = true @@ -815,7 +815,7 @@ export function setMockSubscriptionType( } export function getMockSubscriptionType(): SubscriptionType | null { - if (!mockEnabled || (process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!mockEnabled || !isInternalBuild()) { return null } // Return the explicitly set subscription type, or default to 'max' @@ -833,7 +833,7 @@ export function shouldUseMockSubscription(): boolean { // Mock billing access (admin vs non-admin) export function setMockBillingAccess(hasAccess: boolean | null): void { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return } mockEnabled = true diff --git a/src/skills/bundled/loremIpsum.ts b/src/skills/bundled/loremIpsum.ts index e81a4f7..4ec187a 100644 --- a/src/skills/bundled/loremIpsum.ts +++ b/src/skills/bundled/loremIpsum.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { registerBundledSkill } from '../bundledSkills.js' // Verified 1-token words (tested via API token counting) @@ -232,7 +233,7 @@ function generateLoremIpsum(targetTokens: number): string { } export function registerLoremIpsumSkill(): void { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return } diff --git a/src/skills/bundled/remember.ts b/src/skills/bundled/remember.ts index f73e1eb..3a56aac 100644 --- a/src/skills/bundled/remember.ts +++ b/src/skills/bundled/remember.ts @@ -1,8 +1,9 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { isAutoMemoryEnabled } from '../../memdir/paths.js' import { registerBundledSkill } from '../bundledSkills.js' export function registerRememberSkill(): void { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return } diff --git a/src/skills/bundled/skillify.ts b/src/skills/bundled/skillify.ts index 5247c46..00d2ed9 100644 --- a/src/skills/bundled/skillify.ts +++ b/src/skills/bundled/skillify.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { getSessionMemoryContent } from '../../services/SessionMemory/sessionMemoryUtils.js' import type { Message } from '../../types/message.js' import { getMessagesAfterCompactBoundary } from '../../utils/messages.js' @@ -156,7 +157,7 @@ After writing, tell the user: ` export function registerSkillifySkill(): void { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return } diff --git a/src/skills/bundled/stuck.ts b/src/skills/bundled/stuck.ts index f9fbb8c..9556abc 100644 --- a/src/skills/bundled/stuck.ts +++ b/src/skills/bundled/stuck.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { registerBundledSkill } from '../bundledSkills.js' // Prompt text contains `ps` commands as instructions for NCode to run, @@ -59,7 +60,7 @@ If Slack MCP isn't available, format the report as a message the user can copy-p ` export function registerStuckSkill(): void { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return } diff --git a/src/skills/bundled/verify.ts b/src/skills/bundled/verify.ts index a71fa09..beb0df2 100644 --- a/src/skills/bundled/verify.ts +++ b/src/skills/bundled/verify.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { parseFrontmatter } from '../../utils/frontmatterParser.js' import { registerBundledSkill } from '../bundledSkills.js' import { SKILL_FILES, SKILL_MD } from './verifyContent.js' @@ -10,7 +11,7 @@ const DESCRIPTION = : 'Verify a code change does what it should by running the app.' export function registerVerifySkill(): void { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return } diff --git a/src/tools/AgentTool/UI.tsx b/src/tools/AgentTool/UI.tsx index ddc65e1..404c397 100644 --- a/src/tools/AgentTool/UI.tsx +++ b/src/tools/AgentTool/UI.tsx @@ -100,7 +100,7 @@ type ProcessedMessage = { */ function processProgressMessages(messages: ProgressMessage[], tools: Tools, isAgentRunning: boolean): ProcessedMessage[] { // Only process for ants - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return messages.filter((m): m is ProgressMessage => hasProgressMessage(m.data) && m.data.message.type !== 'user').map(m => ({ type: 'original', message: m diff --git a/src/tools/BashTool/bashPermissions.ts b/src/tools/BashTool/bashPermissions.ts index 8d52591..99eba84 100644 --- a/src/tools/BashTool/bashPermissions.ts +++ b/src/tools/BashTool/bashPermissions.ts @@ -121,7 +121,7 @@ function logClassifierResultForAnts( descriptions: string[], result: ClassifierResult, ): void { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return } diff --git a/src/tools/PowerShellTool/readOnlyValidation.ts b/src/tools/PowerShellTool/readOnlyValidation.ts index fcfb727..f4a8e0f 100644 --- a/src/tools/PowerShellTool/readOnlyValidation.ts +++ b/src/tools/PowerShellTool/readOnlyValidation.ts @@ -4,6 +4,7 @@ * Cmdlets are case-insensitive; all matching is done in lowercase. */ +import { isInternalBuild } from 'src/capabilities/static.js' import type { ParsedCommandElement, ParsedPowerShellCommand, @@ -1702,7 +1703,7 @@ function isGitSafe(args: string[]): boolean { function isGhSafe(args: string[]): boolean { // gh commands are network-dependent; only allow for ant users - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return false } diff --git a/src/tools/TaskOutputTool/TaskOutputTool.tsx b/src/tools/TaskOutputTool/TaskOutputTool.tsx index dd7bfe5..4930e11 100644 --- a/src/tools/TaskOutputTool/TaskOutputTool.tsx +++ b/src/tools/TaskOutputTool/TaskOutputTool.tsx @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { c as _c } from "react/compiler-runtime"; import React from 'react'; import { z } from 'zod/v4'; @@ -182,7 +183,7 @@ export const TaskOutputTool: Tool = buildTool return this.isReadOnly?.(_input) ?? false; }, isEnabled() { - return (process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant'); + return !isInternalBuild(); }, isReadOnly(_input) { return true; diff --git a/src/utils/asciicast.ts b/src/utils/asciicast.ts index 4f46f22..ff53d44 100644 --- a/src/utils/asciicast.ts +++ b/src/utils/asciicast.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { appendFile, rename } from 'fs/promises' import { basename, dirname, join } from 'path' import { getOriginalCwd, getSessionId } from '../bootstrap/state.js' @@ -25,7 +26,7 @@ export function getRecordFilePath(): string | null { if (recordingState.filePath !== null) { return recordingState.filePath } - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return null } if (!isEnvTruthy(process.env.CLAUDE_CODE_TERMINAL_RECORDING)) { diff --git a/src/utils/attachments.ts b/src/utils/attachments.ts index 95101e7..518c00e 100644 --- a/src/utils/attachments.ts +++ b/src/utils/attachments.ts @@ -3542,7 +3542,7 @@ async function getTeammateMailboxAttachments( if (!isAgentSwarmsEnabled()) { return [] } - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return [] } @@ -3903,7 +3903,7 @@ async function getVerifyPlanReminderAttachment( toolUseContext: ToolUseContext, ): Promise { if ( - (process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant') || + !isInternalBuild() || !isEnvTruthy(process.env.CLAUDE_CODE_VERIFY_PLAN) ) { return [] diff --git a/src/utils/autoRunIssue.tsx b/src/utils/autoRunIssue.tsx index c11cc09..dbfe40a 100644 --- a/src/utils/autoRunIssue.tsx +++ b/src/utils/autoRunIssue.tsx @@ -82,7 +82,7 @@ export type AutoRunIssueReason = 'feedback_survey_bad' | 'feedback_survey_good'; */ export function shouldAutoRunIssue(reason: AutoRunIssueReason): boolean { // Only for Ant users - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return false; } switch (reason) { diff --git a/src/utils/autoUpdater.ts b/src/utils/autoUpdater.ts index 38c58d8..04a1545 100644 --- a/src/utils/autoUpdater.ts +++ b/src/utils/autoUpdater.ts @@ -420,7 +420,7 @@ export async function getGcsDistTags(): Promise { * 3. This prevents rollback from listing versions that don't have native binaries */ export async function getVersionHistory(limit: number): Promise { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return [] } diff --git a/src/utils/betas.ts b/src/utils/betas.ts index 8302777..faea483 100644 --- a/src/utils/betas.ts +++ b/src/utils/betas.ts @@ -168,7 +168,7 @@ export function modelSupportsAutoMode(model: string): boolean { // External: firstParty-only at launch (PI probes not wired for // Bedrock/Vertex/Foundry yet). Checked before allowModels so the GB // override can't enable auto mode on unsupported providers. - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant') && getAPIProvider() !== 'firstParty') { + if (!isInternalBuild() && getAPIProvider() !== 'firstParty') { return false } // GrowthBook override: ncode_auto_mode_config.allowModels force-enables diff --git a/src/utils/crossProjectResume.ts b/src/utils/crossProjectResume.ts index 95c40c3..6dba14b 100644 --- a/src/utils/crossProjectResume.ts +++ b/src/utils/crossProjectResume.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { sep } from 'path' import { getOriginalCwd } from '../bootstrap/state.js' import type { LogOption } from '../types/logs.js' @@ -39,7 +40,7 @@ export function checkCrossProjectResume( } // Gate worktree detection to ants only for staged rollout - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { const sessionId = getSessionIdFromLog(log) const command = `cd ${quote([log.projectPath])} && ncode --resume ${sessionId}` return { diff --git a/src/utils/debug.ts b/src/utils/debug.ts index 2f6d2fc..d5e4c11 100644 --- a/src/utils/debug.ts +++ b/src/utils/debug.ts @@ -224,7 +224,7 @@ function shouldLogDebugMessage(message: string): boolean { // Non-ants only write debug logs when debug mode is active (via --debug at // startup or /debug mid-session). Ants always log for /share, bug reports. - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant') && !isDebugMode()) { + if (!isInternalBuild() && !isDebugMode()) { return false } @@ -428,7 +428,7 @@ const updateLatestDebugLogSymlink = memoize(async (): Promise => { * Logs errors for Ants only, always visible in production. */ export function logAntError(context: string, error: unknown): void { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return } diff --git a/src/utils/errorLogSink.ts b/src/utils/errorLogSink.ts index e18f861..1123012 100644 --- a/src/utils/errorLogSink.ts +++ b/src/utils/errorLogSink.ts @@ -10,6 +10,7 @@ * log.ts has NO heavy dependencies - events are queued until this sink is attached. */ +import { isInternalBuild } from 'src/capabilities/static.js' import axios from 'axios' import { dirname, join } from 'path' import { getSessionId } from '../bootstrap/state.js' @@ -109,7 +110,7 @@ function getLogWriter(path: string): JsonlWriter { } function appendToLog(path: string, message: object): void { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return } diff --git a/src/utils/model/antModels.ts b/src/utils/model/antModels.ts index b101e9f..22ea34f 100644 --- a/src/utils/model/antModels.ts +++ b/src/utils/model/antModels.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js' import type { EffortLevel } from '../effort.js' @@ -32,7 +33,7 @@ export type AntModelOverrideConfig = { // @[MODEL LAUNCH]: Update ncode_internal_model_override with new ant-only models // @[MODEL LAUNCH]: Add the codename to scripts/excluded-strings.txt to prevent it from leaking to external builds. export function getAntModelOverrideConfig(): AntModelOverrideConfig | null { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return null } return getFeatureValue_CACHED_MAY_BE_STALE( @@ -42,7 +43,7 @@ export function getAntModelOverrideConfig(): AntModelOverrideConfig | null { } export function getAntModels(): AntModel[] { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return [] } return getAntModelOverrideConfig()?.antModels ?? [] @@ -51,7 +52,7 @@ export function getAntModels(): AntModel[] { export function resolveAntModel( model: string | undefined, ): AntModel | undefined { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return undefined } if (model === undefined) { diff --git a/src/utils/model/modelCapabilities.ts b/src/utils/model/modelCapabilities.ts index ca0efe1..71ce8c8 100644 --- a/src/utils/model/modelCapabilities.ts +++ b/src/utils/model/modelCapabilities.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { readFileSync } from 'fs' import { mkdir, writeFile } from 'fs/promises' import isEqual from 'lodash-es/isEqual.js' @@ -45,7 +46,7 @@ function getCachePath(): string { } function isModelCapabilitiesEligible(): boolean { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) return false + if (!isInternalBuild()) return false if (getAPIProvider() !== 'firstParty') return false if (!isFirstPartyNoumenaBaseUrl()) return false return true diff --git a/src/utils/permissions/PermissionMode.ts b/src/utils/permissions/PermissionMode.ts index de50483..8d6db25 100644 --- a/src/utils/permissions/PermissionMode.ts +++ b/src/utils/permissions/PermissionMode.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { feature } from 'bun:bundle' import z from 'zod/v4' import { PAUSE_ICON } from '../../constants/figures.js' @@ -98,7 +99,7 @@ export function isExternalPermissionMode( mode: PermissionMode, ): mode is ExternalPermissionMode { // External users can't have auto, so always true for them - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return true } return mode !== 'auto' && mode !== 'bubble' diff --git a/src/utils/permissions/yoloClassifier.ts b/src/utils/permissions/yoloClassifier.ts index ed1d1cd..3231750 100644 --- a/src/utils/permissions/yoloClassifier.ts +++ b/src/utils/permissions/yoloClassifier.ts @@ -70,7 +70,7 @@ const ANTHROPIC_PERMISSIONS_TEMPLATE: string = /* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */ function isUsingExternalPermissions(): boolean { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) return true + if (!isInternalBuild()) return true const config = getFeatureValue_CACHED_MAY_BE_STALE( 'ncode_auto_mode_config', {} as AutoModeConfig, @@ -157,7 +157,7 @@ async function maybeDumpAutoMode( timestamp: number, suffix?: string, ): Promise { - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) return + if (!isInternalBuild()) return if (!isEnvTruthy(process.env.CLAUDE_CODE_DUMP_AUTO_MODE)) return const base = suffix ? `${timestamp}.${suffix}` : `${timestamp}` try { diff --git a/src/utils/status.tsx b/src/utils/status.tsx index 66daa09..add9ed6 100644 --- a/src/utils/status.tsx +++ b/src/utils/status.tsx @@ -1,6 +1,7 @@ import { feature } from 'bun:bundle' import chalk from 'chalk'; import figures from 'figures'; +import { isInternalBuild } from 'src/capabilities/static.js'; import * as React from 'react'; import { color, Text } from '../ink.js'; import { getAuthRuntime } from '../auth/runtime/AuthRuntime.js'; @@ -46,9 +47,6 @@ function formatGateStatus(enabled: boolean, blockedBy: string[]): string { } export function buildRuntimeModeProperties(): Property[] { - const isInternalBuild = - process.env.NCODE_BUILD_MODE === 'noumena' || - process.env.USER_TYPE === 'ant' const buildFeatures = [ `TRANSCRIPT_CLASSIFIER: ${feature('TRANSCRIPT_CLASSIFIER') ? 'on' : 'off'}`, `BUILTIN_EXPLORE_PLAN_AGENTS: ${feature('BUILTIN_EXPLORE_PLAN_AGENTS') ? 'on' : 'off'}`, @@ -59,10 +57,7 @@ export function buildRuntimeModeProperties(): Property[] { ] const replBlockedBy: string[] = [] - if ( - process.env.NCODE_BUILD_MODE !== 'noumena' && - process.env.USER_TYPE !== 'ant' - ) { + if (!isInternalBuild()) { replBlockedBy.push('internal build disabled') } if ( @@ -86,10 +81,7 @@ export function buildRuntimeModeProperties(): Property[] { } const jsReplBlockedBy: string[] = [] - if ( - process.env.NCODE_BUILD_MODE !== 'noumena' && - process.env.USER_TYPE !== 'ant' - ) { + if (!isInternalBuild()) { jsReplBlockedBy.push('internal build disabled') } if ( @@ -110,9 +102,9 @@ export function buildRuntimeModeProperties(): Property[] { } const verifyPlanEnabled = - isInternalBuild && isEnvTruthy(process.env.CLAUDE_CODE_VERIFY_PLAN) + isInternalBuild() && isEnvTruthy(process.env.CLAUDE_CODE_VERIFY_PLAN) const verifyPlanBlockedBy: string[] = [] - if (!isInternalBuild) { + if (!isInternalBuild()) { verifyPlanBlockedBy.push('internal build disabled') } if (!isEnvTruthy(process.env.CLAUDE_CODE_VERIFY_PLAN)) { @@ -143,7 +135,7 @@ export function buildRuntimeModeProperties(): Property[] { return [ { label: 'Build mode', - value: isInternalBuild ? 'internal (noumena)' : 'external', + value: isInternalBuild() ? 'internal (noumena)' : 'external', }, { label: 'Build features', @@ -163,7 +155,7 @@ export function buildRuntimeModeProperties(): Property[] { }, { label: 'SuggestBackgroundPR', - value: isInternalBuild ? 'compiled into this build' : 'not compiled', + value: isInternalBuild() ? 'compiled into this build' : 'not compiled', }, { label: 'agents-platform', @@ -173,10 +165,7 @@ export function buildRuntimeModeProperties(): Property[] { } export function buildSandboxProperties(): Property[] { - if ( - process.env.NCODE_BUILD_MODE !== 'noumena' && - process.env.USER_TYPE !== 'ant' - ) { + if (!isInternalBuild()) { return []; } const isSandboxed = SandboxManager.isSandboxingEnabled(); diff --git a/src/utils/telemetry/betaSessionTracing.ts b/src/utils/telemetry/betaSessionTracing.ts index dd0095f..515fd9e 100644 --- a/src/utils/telemetry/betaSessionTracing.ts +++ b/src/utils/telemetry/betaSessionTracing.ts @@ -88,7 +88,7 @@ export function isBetaTracingEnabled(): boolean { // For external users, enable in SDK/headless mode OR when org is allowlisted. // Gate reads from disk cache, so first run after allowlisting returns false; // works from second run onward (same behavior as enhanced_telemetry_beta). - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return ( getIsNonInteractiveSession() || getFeatureValue_CACHED_MAY_BE_STALE('ncode_trace_lantern', false) diff --git a/src/utils/user.ts b/src/utils/user.ts index 0f3c98d..8425434 100644 --- a/src/utils/user.ts +++ b/src/utils/user.ts @@ -1,3 +1,4 @@ +import { isInternalBuild } from 'src/capabilities/static.js' import { execa } from 'execa' import memoize from 'lodash-es/memoize.js' import { getAuthRuntime } from '../auth/runtime/AuthRuntime.js' @@ -151,7 +152,7 @@ function getEmail(): string | undefined { } // Ant-only fallbacks below (no execSync) - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return undefined } @@ -171,7 +172,7 @@ async function getEmailAsync(): Promise { } // Ant-only fallbacks below - if ((process.env.NCODE_BUILD_MODE !== 'noumena' && process.env.USER_TYPE !== 'ant')) { + if (!isInternalBuild()) { return undefined } From 4c904d6b38e5ba577554a52379fe1829d0d0df96 Mon Sep 17 00:00:00 2001 From: openresearch Date: Thu, 25 Jun 2026 00:38:45 +0000 Subject: [PATCH 02/24] refactor: extract shared tool-concurrency helper, drop dead py_repl_host, clean packageAudit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three cleanups surfaced by the codebase exploration: 1. **Duplicated tool-partition logic** — `toolOrchestration.ts` and `StreamingToolExecutor.ts` both independently computed `isConcurrencySafe` with the same parse + try/catch fallback. Extracted to a shared `isToolConcurrencySafe(tool, rawInput)` helper in `src/services/tools/toolConcurrency.ts`. Both call sites now use it. 2. **Orphaned `rust/py_repl_host/`** — build.mjs explicitly documents that "py_repl is intentionally not bundled in the OSS export. Public builds do not stage or embed a Python REPL host." The runtime `resolvePythonReplHostExecutable` resolves the host via env var only (`NCODE_PY_REPL_HOST_PATH`), and `PyReplTool` is gated off by `isInternalBuild()` (DCE'd in external builds). The `rust/py_repl_host/` crate (with leftover internal-monorepo `BUCK` file) was dead weight, and the `src/shims/assets/pyReplHost.ts` shim that imported non-existent `.tmp/py_repl_host/` paths was unreferenced by anything. Removed both. 3. **Stale `/mlstore/` path in `build/packageAudit.mjs`** — the static forbidden-substring entry for `/mlstore/src/noumena/` was an internal-monorepo checkout path that leaked into the public export. The dynamic `collectLocalPathForbiddenSubstrings()` already covers the current checkout path, so the static entry was redundant dead weight from the monorepo. Removed it (kept the private-key markers). Verified: `bun run build:source` succeeds, `./ncode --version` works, StreamingToolExecutor + REPLTool + entrypoint tests pass, and the full `test:contracts` suite passes (230 pass / 8 skip / 0 fail). --- build/packageAudit.mjs | 8 - rust/py_repl_host/BUCK | 22 - rust/py_repl_host/Cargo.lock | 107 ----- rust/py_repl_host/Cargo.toml | 9 - rust/py_repl_host/assets/kernel.py | 193 -------- rust/py_repl_host/assets/python-version.txt | 1 - rust/py_repl_host/src/main.rs | 491 -------------------- src/services/tools/StreamingToolExecutor.ts | 12 +- src/services/tools/toolConcurrency.ts | 30 ++ src/services/tools/toolOrchestration.ts | 14 +- src/shims/assets/pyReplHost.ts | 23 - 11 files changed, 34 insertions(+), 876 deletions(-) delete mode 100644 rust/py_repl_host/BUCK delete mode 100644 rust/py_repl_host/Cargo.lock delete mode 100644 rust/py_repl_host/Cargo.toml delete mode 100644 rust/py_repl_host/assets/kernel.py delete mode 100644 rust/py_repl_host/assets/python-version.txt delete mode 100644 rust/py_repl_host/src/main.rs create mode 100644 src/services/tools/toolConcurrency.ts delete mode 100644 src/shims/assets/pyReplHost.ts diff --git a/build/packageAudit.mjs b/build/packageAudit.mjs index 0b35388..aa6950f 100644 --- a/build/packageAudit.mjs +++ b/build/packageAudit.mjs @@ -9,14 +9,6 @@ const MANIFEST_FORBIDDEN_KEYS = [ ]; const STATIC_FORBIDDEN_SUBSTRINGS = [ - { - label: 'repo checkout path', - value: '/mlstore/src/noumena/', - }, - { - label: 'windows repo checkout path', - value: '\\mlstore\\src\\noumena\\', - }, { label: 'pkcs8 private key marker', value: '-----BEGIN PRIVATE KEY-----', diff --git a/rust/py_repl_host/BUCK b/rust/py_repl_host/BUCK deleted file mode 100644 index b21212b..0000000 --- a/rust/py_repl_host/BUCK +++ /dev/null @@ -1,22 +0,0 @@ -load("@fbsource//tools/build_defs:rust_binary.bzl", "rust_binary") - -oncall("scm_client_infra") - -rust_binary( - name = "ncode_py_repl_host", - srcs = [], - mapped_srcs = { - "src/main.rs": "code/rust/py_repl_host/src/main.rs", - "codex//codex-rs/core:src/tools/py_repl/kernel.py": "codex/codex-rs/core/src/tools/py_repl/kernel.py", - "codex//codex-rs:python-version.txt": "codex/codex-rs/python-version.txt", - }, - crate = "ncode_py_repl_host", - crate_root = "code/rust/py_repl_host/src/main.rs", - autocargo = {"cargo_toml_dir": "code/rust/py_repl_host"}, - visibility = ["PUBLIC"], - default_target_platform = "prelude//platforms:default", - deps = [ - "third-party//rust:serde-1.0.228", - "third-party//rust:serde_json-1.0.150", - ], -) diff --git a/rust/py_repl_host/Cargo.lock b/rust/py_repl_host/Cargo.lock deleted file mode 100644 index 84d0947..0000000 --- a/rust/py_repl_host/Cargo.lock +++ /dev/null @@ -1,107 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "ncode_py_repl_host" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/rust/py_repl_host/Cargo.toml b/rust/py_repl_host/Cargo.toml deleted file mode 100644 index bba7ba9..0000000 --- a/rust/py_repl_host/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "ncode_py_repl_host" -version = "0.1.0" -edition = "2021" - -[dependencies] -serde = { version = "1", features = ["derive"] } -serde_json = "1" - diff --git a/rust/py_repl_host/assets/kernel.py b/rust/py_repl_host/assets/kernel.py deleted file mode 100644 index c370ce8..0000000 --- a/rust/py_repl_host/assets/kernel.py +++ /dev/null @@ -1,193 +0,0 @@ -# Python-based kernel for py_repl. -# Communicates over JSON lines on stdin/stdout. - -import ast -import asyncio -import inspect -import io -import json -import os -import sys -import traceback -from types import SimpleNamespace - - -def _send(message): - payload = (json.dumps(message) + "\n").encode("utf-8") - os.write(sys.__stdout__.fileno(), payload) - - -def _format_error(error): - if isinstance(error, BaseException): - text = "".join(traceback.format_exception_only(type(error), error)).strip() - if text: - return text - return str(error) - - -def _join_outputs(stdout_text, stderr_text): - if stdout_text and stderr_text: - return f"{stdout_text}\n{stderr_text}" - if stdout_text: - return stdout_text - return stderr_text - - -pending_tool = {} -tool_counter = 0 -active_exec_id = None -cell_counter = 0 - -TMP_DIR = os.environ.get("CODEX_PY_TMP_DIR", os.getcwd()) -module_dirs_env = os.environ.get("CODEX_PY_REPL_PYTHON_MODULE_DIRS", "") -for entry in module_dirs_env.split(os.pathsep): - value = entry.strip() - if not value: - continue - path = value if os.path.isabs(value) else os.path.abspath(value) - if path not in sys.path: - sys.path.insert(0, path) - -state_globals = { - "__name__": "__main__", - "__package__": None, -} - - -async def _run_tool(exec_id, tool_name, args): - global tool_counter - - if not isinstance(tool_name, str) or not tool_name: - raise RuntimeError("codex.tool expects a tool name string") - - tool_id = f"{exec_id}-tool-{tool_counter}" - tool_counter += 1 - - arguments_json = "{}" - if isinstance(args, str): - arguments_json = args - elif args is not None: - arguments_json = json.dumps(args) - - loop = asyncio.get_running_loop() - future = loop.create_future() - pending_tool[tool_id] = future - - _send( - { - "type": "run_tool", - "id": tool_id, - "exec_id": exec_id, - "tool_name": tool_name, - "arguments": arguments_json, - } - ) - - result = await future - if not result.get("ok"): - raise RuntimeError(result.get("error") or "tool failed") - return result.get("response") - - -async def _handle_exec(message): - global active_exec_id - global cell_counter - - exec_id = message.get("id") - code = message.get("code") - if not isinstance(exec_id, str) or not isinstance(code, str): - return - - active_exec_id = exec_id - - async def _tool(name, args=None): - return await _run_tool(exec_id, name, args) - - state_globals["codex"] = SimpleNamespace(tmpDir=TMP_DIR, tool=_tool) - state_globals["tmpDir"] = TMP_DIR - - stdout_buf = io.StringIO() - stderr_buf = io.StringIO() - original_stdout = sys.stdout - original_stderr = sys.stderr - sys.stdout = stdout_buf - sys.stderr = stderr_buf - - try: - filename = f"" - cell_counter += 1 - flags = ast.PyCF_ALLOW_TOP_LEVEL_AWAIT - code_obj = compile(code, filename, "exec", flags=flags, dont_inherit=True) - result = eval(code_obj, state_globals, state_globals) - if inspect.isawaitable(result): - await result - - output = _join_outputs(stdout_buf.getvalue().rstrip(), stderr_buf.getvalue().rstrip()) - _send( - { - "type": "exec_result", - "id": exec_id, - "ok": True, - "output": output, - "error": None, - } - ) - except BaseException as error: - _send( - { - "type": "exec_result", - "id": exec_id, - "ok": False, - "output": "", - "error": _format_error(error), - } - ) - finally: - sys.stdout = original_stdout - sys.stderr = original_stderr - if active_exec_id == exec_id: - active_exec_id = None - - -async def _read_stdin(exec_queue): - loop = asyncio.get_running_loop() - while True: - line = await loop.run_in_executor(None, sys.stdin.readline) - if line == "": - await exec_queue.put(None) - return - - line = line.strip() - if not line: - continue - - try: - message = json.loads(line) - except Exception: - continue - - msg_type = message.get("type") - if msg_type == "exec": - await exec_queue.put(message) - elif msg_type == "run_tool_result": - tool_id = message.get("id") - future = pending_tool.pop(tool_id, None) - if future and not future.done(): - future.set_result(message) - - -async def _main(): - exec_queue = asyncio.Queue() - reader = asyncio.create_task(_read_stdin(exec_queue)) - try: - while True: - message = await exec_queue.get() - if message is None: - return - await _handle_exec(message) - finally: - reader.cancel() - - -if __name__ == "__main__": - asyncio.run(_main()) diff --git a/rust/py_repl_host/assets/python-version.txt b/rust/py_repl_host/assets/python-version.txt deleted file mode 100644 index 30291cb..0000000 --- a/rust/py_repl_host/assets/python-version.txt +++ /dev/null @@ -1 +0,0 @@ -3.10.0 diff --git a/rust/py_repl_host/src/main.rs b/rust/py_repl_host/src/main.rs deleted file mode 100644 index d7c7dde..0000000 --- a/rust/py_repl_host/src/main.rs +++ /dev/null @@ -1,491 +0,0 @@ -use serde::Deserialize; -use serde::Serialize; -use serde_json::Value as JsonValue; -use std::collections::VecDeque; -use std::env; -use std::fs; -use std::io; -use std::io::BufRead; -use std::io::BufReader; -use std::io::BufWriter; -use std::io::Write; -use std::path::PathBuf; -use std::process::Child; -use std::process::ChildStdin; -use std::process::ChildStdout; -use std::process::Command; -use std::process::Stdio; -use std::sync::Arc; -use std::sync::Mutex; -use std::time::SystemTime; -use std::time::UNIX_EPOCH; - -const KERNEL_SOURCE: &str = - include_str!("../assets/kernel.py"); -const PY_REPL_MIN_PYTHON_VERSION: &str = - include_str!("../assets/python-version.txt"); -const STDERR_TAIL_LINE_LIMIT: usize = 20; -const STDERR_TAIL_LINE_MAX_BYTES: usize = 512; -const STDERR_TAIL_MAX_BYTES: usize = 4_096; -const STDERR_TAIL_SEPARATOR: &str = " | "; - -#[derive(Debug, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -enum ParentMessage { - Exec { - id: String, - code: String, - #[serde(default)] - timeout_ms: Option, - }, - RunToolResult { - #[serde(rename = "id")] - _id: String, - #[serde(rename = "ok")] - _ok: bool, - #[serde(default)] - #[serde(rename = "response")] - _response: Option, - #[serde(default)] - #[serde(rename = "error")] - _error: Option, - }, -} - -#[derive(Debug, Serialize)] -#[serde(tag = "type", rename_all = "snake_case")] -enum HostMessage<'a> { - ExecResult { - id: &'a str, - ok: bool, - output: &'a str, - error: Option<&'a str>, - }, -} - -struct KernelProcess { - _child: Child, - stdin: BufWriter, - stdout: BufReader, - stderr_tail: Arc>>, -} - -fn main() -> io::Result<()> { - let stdin = io::stdin(); - let stdout = io::stdout(); - let mut input = stdin.lock().lines(); - let mut output = BufWriter::new(stdout.lock()); - let mut kernel: Option = None; - - while let Some(line) = next_nonempty_line(&mut input)? { - let message = match serde_json::from_str::(&line) { - Ok(message) => message, - Err(_) => continue, - }; - - match message { - ParentMessage::Exec { - id, - code, - timeout_ms, - } => { - if kernel.is_none() { - kernel = Some(spawn_kernel()?); - } - - let result = relay_exec( - &id, - &code, - timeout_ms, - kernel.as_mut().expect("kernel initialized"), - &mut input, - &mut output, - ); - - if let Err(error) = result { - write_exec_result(&mut output, &id, false, "", Some(&error))?; - kernel = None; - } - } - ParentMessage::RunToolResult { .. } => { - // Ignored outside an active exec loop. - } - } - } - - Ok(()) -} - -fn next_nonempty_line(input: &mut I) -> io::Result> -where - I: Iterator>, -{ - for line in input { - let line = line?; - if !line.trim().is_empty() { - return Ok(Some(line)); - } - } - Ok(None) -} - -fn relay_exec( - exec_id: &str, - code: &str, - timeout_ms: Option, - kernel: &mut KernelProcess, - parent_input: &mut I, - parent_output: &mut W, -) -> Result<(), String> -where - I: Iterator>, - W: Write, -{ - let exec_message = serde_json::json!({ - "type": "exec", - "id": exec_id, - "code": code, - "timeout_ms": timeout_ms, - }); - write_json_line(&mut kernel.stdin, &exec_message).map_err(|err| err.to_string())?; - - loop { - let mut line = String::new(); - let bytes_read = kernel - .stdout - .read_line(&mut line) - .map_err(|err| err.to_string())?; - - if bytes_read == 0 { - return Err(format!( - "py_repl rust host lost the Python kernel: {}", - format_stderr_tail(&kernel.stderr_tail) - )); - } - - let trimmed = line.trim(); - if trimmed.is_empty() { - continue; - } - - let message_type = extract_type(trimmed); - match message_type.as_deref() { - Some("run_tool") => { - parent_output - .write_all(trimmed.as_bytes()) - .and_then(|_| parent_output.write_all(b"\n")) - .and_then(|_| parent_output.flush()) - .map_err(|err| err.to_string())?; - - let response = wait_for_parent_run_tool_result(parent_input)?; - write_json_line(&mut kernel.stdin, &response).map_err(|err| err.to_string())?; - } - Some("exec_result") => { - parent_output - .write_all(trimmed.as_bytes()) - .and_then(|_| parent_output.write_all(b"\n")) - .and_then(|_| parent_output.flush()) - .map_err(|err| err.to_string())?; - return Ok(()); - } - _ => {} - } - } -} - -fn wait_for_parent_run_tool_result(parent_input: &mut I) -> Result -where - I: Iterator>, -{ - loop { - let line = next_nonempty_line(parent_input).map_err(|err| err.to_string())?; - let Some(line) = line else { - return Err("py_repl rust host lost its parent while waiting for run_tool_result".to_string()); - }; - - let value = serde_json::from_str::(&line).map_err(|err| err.to_string())?; - if extract_type_from_value(&value).as_deref() == Some("run_tool_result") { - return Ok(value); - } - } -} - -fn extract_type(line: &str) -> Option { - serde_json::from_str::(line) - .ok() - .and_then(|value| extract_type_from_value(&value)) -} - -fn extract_type_from_value(value: &JsonValue) -> Option { - value - .get("type") - .and_then(JsonValue::as_str) - .map(str::to_owned) -} - -fn write_exec_result( - writer: &mut W, - id: &str, - ok: bool, - output: &str, - error: Option<&str>, -) -> io::Result<()> { - let message = HostMessage::ExecResult { - id, - ok, - output, - error, - }; - write_json_line(writer, &message) -} - -fn write_json_line(writer: &mut W, value: &T) -> io::Result<()> { - serde_json::to_writer(&mut *writer, value)?; - writer.write_all(b"\n")?; - writer.flush() -} - -fn spawn_kernel() -> io::Result { - let python = resolve_python_executable()?; - let kernel_dir = create_kernel_runtime_dir()?; - let kernel_path = kernel_dir.join("kernel.py"); - fs::write(&kernel_path, KERNEL_SOURCE)?; - - let mut command = Command::new(python); - command.arg(&kernel_path); - command.current_dir(env::current_dir()?); - command.stdin(Stdio::piped()); - command.stdout(Stdio::piped()); - command.stderr(Stdio::piped()); - command.env( - "CODEX_PY_TMP_DIR", - env::temp_dir().to_string_lossy().to_string(), - ); - if let Some(module_dirs) = resolve_python_module_dirs() { - command.env("CODEX_PY_REPL_PYTHON_MODULE_DIRS", module_dirs); - } - - let mut child = command.spawn()?; - let child_stdin = child - .stdin - .take() - .ok_or_else(|| io::Error::new(io::ErrorKind::BrokenPipe, "missing py_repl stdin"))?; - let child_stdout = child - .stdout - .take() - .ok_or_else(|| io::Error::new(io::ErrorKind::BrokenPipe, "missing py_repl stdout"))?; - let child_stderr = child - .stderr - .take() - .ok_or_else(|| io::Error::new(io::ErrorKind::BrokenPipe, "missing py_repl stderr"))?; - - let stderr_tail = Arc::new(Mutex::new(VecDeque::new())); - let stderr_tail_writer = Arc::clone(&stderr_tail); - std::thread::spawn(move || { - let reader = BufReader::new(child_stderr); - for line in reader.lines() { - let Ok(line) = line else { - break; - }; - let trimmed = line.trim(); - if trimmed.is_empty() { - continue; - } - if let Ok(mut tail) = stderr_tail_writer.lock() { - push_stderr_tail_line(&mut tail, trimmed); - } - } - }); - - Ok(KernelProcess { - _child: child, - stdin: BufWriter::new(child_stdin), - stdout: BufReader::new(child_stdout), - stderr_tail, - }) -} - -fn resolve_python_module_dirs() -> Option { - env::var("NCODE_PY_REPL_PYTHON_MODULE_DIRS") - .ok() - .filter(|value| !value.trim().is_empty()) - .or_else(|| { - env::var("CLAUDE_CODE_PY_REPL_PYTHON_MODULE_DIRS") - .ok() - .filter(|value| !value.trim().is_empty()) - }) -} - -fn resolve_python_executable() -> io::Result { - let explicit = env::var("NCODE_PY_REPL_PYTHON_PATH") - .ok() - .filter(|value| !value.trim().is_empty()) - .or_else(|| { - env::var("CLAUDE_CODE_PY_REPL_PYTHON_PATH") - .ok() - .filter(|value| !value.trim().is_empty()) - }); - - let candidates = if let Some(explicit) = explicit { - vec![explicit] - } else { - vec!["python3".to_string(), "python".to_string()] - }; - - let min_version = parse_python_version(PY_REPL_MIN_PYTHON_VERSION.trim()).ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidData, - "invalid py_repl minimum Python version", - ) - })?; - - for candidate in candidates { - let output = Command::new(&candidate) - .arg("-c") - .arg("import sys; print(\".\".join(str(part) for part in sys.version_info[:3]))") - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .output(); - - let Ok(output) = output else { - continue; - }; - if !output.status.success() { - continue; - } - - let stdout = String::from_utf8_lossy(&output.stdout); - let Some(version) = parse_python_version(stdout.trim()) else { - continue; - }; - - if compare_version(&version, &min_version) >= 0 { - return Ok(candidate); - } - } - - Err(io::Error::new( - io::ErrorKind::NotFound, - format!( - "py_repl rust host requires Python {}+", - PY_REPL_MIN_PYTHON_VERSION.trim() - ), - )) -} - -fn parse_python_version(input: &str) -> Option> { - let mut parts = Vec::new(); - for segment in input.split('.') { - let value = segment.trim().parse::().ok()?; - parts.push(value); - } - if parts.len() < 3 { - return None; - } - Some(parts) -} - -fn compare_version(left: &[u32], right: &[u32]) -> i32 { - let length = left.len().max(right.len()); - for index in 0..length { - let left_value = *left.get(index).unwrap_or(&0); - let right_value = *right.get(index).unwrap_or(&0); - if left_value != right_value { - return if left_value > right_value { 1 } else { -1 }; - } - } - 0 -} - -fn create_kernel_runtime_dir() -> io::Result { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_nanos(); - let dir = env::temp_dir().join(format!( - "ncode-py-repl-host-{}-{now}", - std::process::id() - )); - fs::create_dir_all(&dir)?; - Ok(dir) -} - -fn format_stderr_tail(stderr_tail: &Arc>>) -> String { - let Ok(lines) = stderr_tail.lock() else { - return "".to_string(); - }; - if lines.is_empty() { - return "".to_string(); - } - lines.iter().cloned().collect::>().join(STDERR_TAIL_SEPARATOR) -} - -fn push_stderr_tail_line(lines: &mut VecDeque, line: &str) { - let bounded_line = truncate_utf8_prefix_by_bytes( - line, - STDERR_TAIL_LINE_MAX_BYTES.min(STDERR_TAIL_MAX_BYTES), - ); - if bounded_line.is_empty() { - return; - } - - while !lines.is_empty() - && (lines.len() >= STDERR_TAIL_LINE_LIMIT - || stderr_tail_bytes_with_candidate(lines, &bounded_line) > STDERR_TAIL_MAX_BYTES) - { - lines.pop_front(); - } - - lines.push_back(bounded_line); -} - -fn stderr_tail_formatted_bytes(lines: &VecDeque) -> usize { - if lines.is_empty() { - return 0; - } - let payload_bytes: usize = lines.iter().map(String::len).sum(); - let separator_bytes = STDERR_TAIL_SEPARATOR.len() * (lines.len() - 1); - payload_bytes + separator_bytes -} - -fn stderr_tail_bytes_with_candidate(lines: &VecDeque, line: &str) -> usize { - if lines.is_empty() { - return line.len(); - } - stderr_tail_formatted_bytes(lines) + STDERR_TAIL_SEPARATOR.len() + line.len() -} - -fn truncate_utf8_prefix_by_bytes(input: &str, max_bytes: usize) -> String { - if input.len() <= max_bytes { - return input.to_string(); - } - if max_bytes == 0 { - return String::new(); - } - - let mut end = max_bytes; - while end > 0 && !input.is_char_boundary(end) { - end -= 1; - } - input[..end].to_string() -} - -#[cfg(test)] -mod tests { - use super::compare_version; - use super::parse_python_version; - - #[test] - fn parses_python_versions() { - assert_eq!(parse_python_version("3.10.0"), Some(vec![3, 10, 0])); - assert_eq!(parse_python_version("3.12.3"), Some(vec![3, 12, 3])); - assert_eq!(parse_python_version("3.10"), None); - } - - #[test] - fn compares_python_versions() { - assert_eq!(compare_version(&[3, 10, 0], &[3, 10, 0]), 0); - assert_eq!(compare_version(&[3, 12, 0], &[3, 10, 0]), 1); - assert_eq!(compare_version(&[3, 9, 9], &[3, 10, 0]), -1); - } -} diff --git a/src/services/tools/StreamingToolExecutor.ts b/src/services/tools/StreamingToolExecutor.ts index bff0702..82170c2 100644 --- a/src/services/tools/StreamingToolExecutor.ts +++ b/src/services/tools/StreamingToolExecutor.ts @@ -12,6 +12,7 @@ import { createChildAbortController } from '../../utils/abortController.js' import { errorMessage } from '../../utils/errors.js' import { logError } from '../../utils/log.js' import { runToolUse } from './toolExecution.js' +import { isToolConcurrencySafe } from './toolConcurrency.js' type MessageUpdate = { message?: Message @@ -103,16 +104,7 @@ export class StreamingToolExecutor { return } - const parsedInput = toolDefinition.inputSchema.safeParse(block.input) - const isConcurrencySafe = parsedInput?.success - ? (() => { - try { - return Boolean(toolDefinition.isConcurrencySafe(parsedInput.data)) - } catch { - return false - } - })() - : false + const isConcurrencySafe = isToolConcurrencySafe(toolDefinition, block.input) this.tools.push({ id: block.id, block, diff --git a/src/services/tools/toolConcurrency.ts b/src/services/tools/toolConcurrency.ts new file mode 100644 index 0000000..7d0e393 --- /dev/null +++ b/src/services/tools/toolConcurrency.ts @@ -0,0 +1,30 @@ +import type { Tool } from '../../Tool.js' + +/** + * Parse a tool's raw input and determine whether the tool is concurrency-safe + * for that input (i.e. safe to run in parallel with other concurrency-safe + * tools). + * + * Returns `false` when the tool is missing, the input fails schema validation, + * or `tool.isConcurrencySafe` throws (e.g. due to shell-quote parse failure). + * Failures are treated conservatively — a tool that can't be determined safe + * is run exclusively. + * + * Shared by `toolOrchestration.runTools` and `StreamingToolExecutor` so the + * partition/concurrency decision is computed in one place. + */ +export function isToolConcurrencySafe( + tool: Tool | undefined, + rawInput: unknown, +): boolean { + if (!tool) return false + const parsedInput = tool.inputSchema.safeParse(rawInput) + if (!parsedInput.success) return false + try { + return Boolean(tool.isConcurrencySafe(parsedInput.data)) + } catch { + // If isConcurrencySafe throws (e.g. due to shell-quote parse failure), + // treat as not concurrency-safe to be conservative. + return false + } +} diff --git a/src/services/tools/toolOrchestration.ts b/src/services/tools/toolOrchestration.ts index 95e02cb..f7faa3e 100644 --- a/src/services/tools/toolOrchestration.ts +++ b/src/services/tools/toolOrchestration.ts @@ -4,6 +4,7 @@ import { findToolByName, type ToolUseContext } from '../../Tool.js' import type { AssistantMessage, Message } from '../../types/message.js' import { all } from '../../utils/generators.js' import { type MessageUpdateLazy, runToolUse } from './toolExecution.js' +import { isToolConcurrencySafe } from './toolConcurrency.js' function getMaxToolUseConcurrency(): number { return ( @@ -94,18 +95,7 @@ function partitionToolCalls( ): Batch[] { return toolUseMessages.reduce((acc: Batch[], toolUse) => { const tool = findToolByName(toolUseContext.options.tools, toolUse.name) - const parsedInput = tool?.inputSchema.safeParse(toolUse.input) - const isConcurrencySafe = parsedInput?.success - ? (() => { - try { - return Boolean(tool?.isConcurrencySafe(parsedInput.data)) - } catch { - // If isConcurrencySafe throws (e.g., due to shell-quote parse failure), - // treat as not concurrency-safe to be conservative - return false - } - })() - : false + const isConcurrencySafe = isToolConcurrencySafe(tool, toolUse.input) if (isConcurrencySafe && acc[acc.length - 1]?.isConcurrencySafe) { acc[acc.length - 1]!.blocks.push(toolUse) } else { diff --git a/src/shims/assets/pyReplHost.ts b/src/shims/assets/pyReplHost.ts deleted file mode 100644 index 0944c98..0000000 --- a/src/shims/assets/pyReplHost.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { MaterializedAsset } from '../nativeAssetRuntime.js' - -import pyReplHostUnix from '../../../.tmp/py_repl_host/ncode_py_repl_host' with { type: 'file' } -import pyReplHostWindows from '../../../.tmp/py_repl_host/ncode_py_repl_host.exe' with { type: 'file' } - -export function getBundledPythonReplHostAsset(): MaterializedAsset | null { - switch (process.platform) { - case 'win32': - return { - embeddedPath: pyReplHostWindows, - relativePath: 'vendor/py_repl_host/ncode_py_repl_host.exe', - } - case 'darwin': - case 'linux': - return { - embeddedPath: pyReplHostUnix, - relativePath: 'vendor/py_repl_host/ncode_py_repl_host', - mode: 0o755, - } - default: - return null - } -} From 496358a9cf30ad1456be00dccd90e09c583eb990 Mon Sep 17 00:00:00 2001 From: openresearch Date: Thu, 25 Jun 2026 01:54:37 +0000 Subject: [PATCH 03/24] review: insights-context scripts code review Thorough review of scan.py, resolve.py, render.py, compare.py, test_smoke.py covering bugs, security, code quality, performance, and friction-classification correctness. Key findings: - scan.py: long corrections (>200 chars) misclassified as informational; OTHER_USERS_RE misses /home/ and /root/ paths on Linux; USERNAME_RE over-scrubs public GitHub handles contradicting the .md spec. - render.py: CSS from /tmp injected unescaped into `. +`css_block = css` (from `load_css()`, line 46-50), which reads +`/tmp/insights-context.css` verbatim. `/tmp` is world-writable on multi-user +systems. A malicious or corrupted CSS file containing `