From ad474fb2e731f9115dc5c9eef28044b834fd6bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Hanu=C5=A1?= Date: Sun, 5 Jul 2026 00:58:33 +0200 Subject: [PATCH] fix(runner): strip NODE_ENV from agent subprocess env; install gh; silence keyring warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three small hygiene fixes to the runner container / child-env plumbing that each cause noisy or hard failures for scenarios that scaffold an Actor via `apify create` and then run it. 1. `shared/src/agents/apify-env.ts` — add `NODE_ENV` to the list of env vars stripped from the agent subprocess env. The `apify/actor-node` base image sets `NODE_ENV=production` so the runner's own `npm install` (executed at image build) skips devDependencies. When that value leaks into the agent's shell, any scaffolded project it runs inherits it too — so `apify create foo -t ts-empty && cd foo && apify run` fails with `sh: tsx: not found` because `npm install --omit=dev` drops `tsx` even though the template lists it as a devDependency it needs at runtime. Test added alongside the existing stripped-keys coverage. 2. `actors/runner/Dockerfile` — install `github-cli` via apk and add it to the CLI verify step. Also add `ENV APIFY_DISABLE_KEYRING=1` so the apify-cli stops printing `OS keyring unavailable; set APIFY_DISABLE_KEYRING=1 to silence` on every login inside the container (Alpine has no gnome-keyring / kwallet, so it falls back to a file store anyway — the warning is pure noise here). 3. `shared/src/init-presets.ts` — the `which gh` diagnostic in three presets suggested `brew install gh` even though the runner runs on Alpine. Expand the hint to list `apk add github-cli / apt-get install gh / brew install gh` so it's correct on the actual host (and stays useful if the runner ever moves to a Debian-based base image). Surfaced during an evaluation of Apify surfaces for agent-driven Actor development. Co-Authored-By: Claude Opus 4.7 --- actors/runner/Dockerfile | 8 +++++++- shared/src/__tests__/apify-env.test.ts | 8 ++++++++ shared/src/agents/apify-env.ts | 9 +++++++++ shared/src/init-presets.ts | 6 +++--- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/actors/runner/Dockerfile b/actors/runner/Dockerfile index d84eb92..777a215 100644 --- a/actors/runner/Dockerfile +++ b/actors/runner/Dockerfile @@ -23,8 +23,14 @@ RUN apk add --no-cache \ build-base \ wget ca-certificates \ zip unzip \ + github-cli \ && ln -sf /usr/bin/python3 /usr/bin/python +# Silence the apify-cli "OS keyring unavailable" warning on headless Alpine — +# there's no gnome-keyring / kwallet here, so credential lookup falls back to +# the file-backed store on every login. The env var makes that fallback silent. +ENV APIFY_DISABLE_KEYRING=1 + # Claude Code CLI RUN curl -fsSL https://claude.ai/install.sh | bash \ && cp /root/.local/bin/claude /usr/local/bin/claude \ @@ -39,7 +45,7 @@ RUN curl -fsSL https://opencode.ai/install | bash \ RUN npm install -g @openai/codex apify-cli # Verify all CLIs are available -RUN claude --version && codex --version && opencode --version && apify --version +RUN claude --version && codex --version && opencode --version && apify --version && gh --version # Install runtime deps as root (npm needs write access to node_modules) COPY package*.json ./ diff --git a/shared/src/__tests__/apify-env.test.ts b/shared/src/__tests__/apify-env.test.ts index a29b915..4ab9b62 100644 --- a/shared/src/__tests__/apify-env.test.ts +++ b/shared/src/__tests__/apify-env.test.ts @@ -57,4 +57,12 @@ describe('buildChildEnv', () => { expect(APIFY_RUNTIME_KEYS_TO_STRIP).toContain('APIFY_DEFAULT_DATASET_ID'); expect(APIFY_RUNTIME_KEYS_TO_STRIP).toContain('APIFY_IS_AT_HOME'); }); + + it('strips NODE_ENV so scaffolded projects install devDependencies as-usual', () => { + // apify/actor-node sets NODE_ENV=production; if that leaks into the agent's + // shell, `apify create` templates that rely on a devDep at runtime (e.g. tsx) + // fail because `npm install --omit=dev` drops it. + expect(buildChildEnv({ NODE_ENV: 'production' }, '/tmp/ws').NODE_ENV).toBeUndefined(); + expect(APIFY_RUNTIME_KEYS_TO_STRIP).toContain('NODE_ENV'); + }); }); diff --git a/shared/src/agents/apify-env.ts b/shared/src/agents/apify-env.ts index fed853f..63c1683 100644 --- a/shared/src/agents/apify-env.ts +++ b/shared/src/agents/apify-env.ts @@ -34,6 +34,15 @@ export const APIFY_RUNTIME_KEYS_TO_STRIP = [ 'ACTOR_EVENTS_WEBSOCKET_URL', 'APIFY_ACTOR_EVENTS_WS_URL', 'APIFY_TIMEOUT_AT', 'APIFY_PROXY_PASSWORD', + // NODE_ENV is set to `production` by the apify/actor-node base image so the + // runner's own `npm install` skips devDependencies. When it leaks into the + // agent's subprocess, any scaffolded project the agent runs there inherits + // it too — so `apify create foo -t ts-empty && cd foo && apify run` fails + // with `sh: tsx: not found` because `npm install --omit=dev` drops `tsx`, + // even though the template lists it as a devDependency it needs at runtime. + // Stripping it here restores the "fresh dev machine" expectation that + // scaffolded templates are built against. + 'NODE_ENV', ] as const; /** diff --git a/shared/src/init-presets.ts b/shared/src/init-presets.ts index a08b524..b36ef1d 100644 --- a/shared/src/init-presets.ts +++ b/shared/src/init-presets.ts @@ -309,7 +309,7 @@ export function runInitPreset(ctx: InitContext): InitResult { case 'cli_native': { const script = ` - which gh >/dev/null 2>&1 || echo "gh not found (install with: brew install gh)" + which gh >/dev/null 2>&1 || echo "gh not found (install with: apk add github-cli / apt-get install gh / brew install gh)" which apify >/dev/null 2>&1 || echo "apify not found (install with: npm i -g apify-cli)" `; const result = runScript(script, ctx.workDir, 'cli_native'); @@ -390,7 +390,7 @@ export function runInitPreset(ctx: InitContext): InitResult { // (which would imply the agent bypassed the CLI). const script = ` which apify >/dev/null 2>&1 || echo "apify not found (install with: npm i -g apify-cli)" - which gh >/dev/null 2>&1 || echo "gh not found (install with: brew install gh)" + which gh >/dev/null 2>&1 || echo "gh not found (install with: apk add github-cli / apt-get install gh / brew install gh)" `; const result = runScript(script, ctx.workDir, 'cli_only'); log.push(`cli_only: ${result.output}`); @@ -456,7 +456,7 @@ export function runInitPreset(ctx: InitContext): InitResult { // it would normally have, with visibility into what's available. const script = ` which apify >/dev/null 2>&1 || echo "apify CLI not found (install with: npm i -g apify-cli)" - which gh >/dev/null 2>&1 || echo "gh not found (install with: brew install gh)" + which gh >/dev/null 2>&1 || echo "gh not found (install with: apk add github-cli / apt-get install gh / brew install gh)" which curl >/dev/null 2>&1 || echo "curl not found (required for REST API calls)" which jq >/dev/null 2>&1 || echo "jq not found (recommended for parsing API responses)" `;