Skip to content

feat: add Grok Build subscription CLI as a provider#280

Open
Sanjay Ramadugu (sanjay3290) wants to merge 3 commits into
langchain-ai:mainfrom
sanjay3290:feat/grok-build-agent-cli
Open

feat: add Grok Build subscription CLI as a provider#280
Sanjay Ramadugu (sanjay3290) wants to merge 3 commits into
langchain-ai:mainfrom
sanjay3290:feat/grok-build-agent-cli

Conversation

@sanjay3290

Copy link
Copy Markdown

Summary

Adds a grok-build provider that runs OpenWiki documentation jobs through the local Grok Build CLI (grok) using a subscription login instead of a metered API key. OpenWiki spawns the binary headlessly with --always-approve --output-format streaming-json, streams and coalesces its NDJSON events, and reuses the CLI's own session for interactive follow-ups.

Intended for local/subscription use on a machine already logged into Grok Build interactively — not a drop-in for the metered CI examples.

What changed

  • Provider config becomes a discriminated union (api | agent-cli). Agent-CLI providers carry a binary path + install hint instead of an API key, and onboarding, credential setup, and the non-interactive startup check all skip key collection for them.
  • New src/agent/engines/ module: a generic agent-CLI runner (temp prompt file to avoid ARG_MAX, detached process group with SIGTERM→SIGKILL timeout, NDJSON stream parsing) plus a Grok Build adapter and streaming-json parser that coalesces partial text tokens into a single final answer.
  • Shared run scaffolding: extracted prepareAgentRun / finalizeAgentRun so the CLI and DeepAgents paths share snapshot + .last-update.json metadata handling.
  • Docs: README section covering prerequisites (grok login), env overrides (OPENWIKI_GROK_BUILD_BINARY, OPENWIKI_GROK_BUILD_MAX_TURNS, OPENWIKI_AGENT_CLI_TIMEOUT_SECONDS), and the local-only caveat.

Config

Env var Purpose
OPENWIKI_PROVIDER=grok-build Select the provider
OPENWIKI_GROK_BUILD_BINARY Binary path when grok is not on PATH
OPENWIKI_MODEL_ID Model (default grok-4.5)
OPENWIKI_GROK_BUILD_MAX_TURNS Max agent turns (default 50)
OPENWIKI_AGENT_CLI_TIMEOUT_SECONDS Run timeout (default 1800)

Testing

  • tsc --noEmit — clean
  • eslint — clean
  • Full suite: 200/200 tests pass, including new coverage for the adapter, provider integration, and CLI runner.

audichuang added a commit to audichuang/openwiki that referenced this pull request Jul 15, 2026
Both upstream PRs are still open and unmerged, so squash-merge the
integrated feature set (grok-build langchain-ai#280 + claude-code langchain-ai#181) onto current
main (d43bd4f, AWS Bedrock langchain-ai#327) instead of the stale langchain-ai#42 base.

Conflicts re-resolved:
- constants.ts: union provider list; ProviderConfig discriminated union
  now requires `kind`, so bedrock/nebius get `kind: "api"` and the
  region/secret helpers narrow on `kind === "api"`.
- agent/index.ts: finalizeAgentRun uses main's persistRunMetadataIfChanged
  (drops the unimported writeLastUpdateMetadata path); runAgentCliRun kept.
- cli.tsx: full provider list in error; formatProviderSwitchNotice kept.
- env.ts / credentials.tsx / README.md: unions.

Verified: typecheck, lint, format, pnpm test (313), build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a `grok-build` provider that runs documentation jobs through the
local Grok Build CLI (`grok`) using a subscription login instead of a
metered API key. OpenWiki spawns the binary headlessly with
`--always-approve --output-format streaming-json`, streams and coalesces
its NDJSON events, and reuses the CLI's own session for follow-ups.

- Model provider config becomes a discriminated union (api | agent-cli)
  so agent-CLI providers carry a binary/install hint instead of an API
  key; onboarding, credential setup, and startup checks skip the key
  collection for them.
- New engines module: a generic CLI runner (temp prompt file, detached
  process group with SIGTERM/SIGKILL timeout, stream parsing) plus a
  Grok Build adapter and streaming-json parser.
- Extract shared prepareAgentRun/finalizeAgentRun so the CLI and
  DeepAgents run paths share snapshot/metadata handling.
- Docs and tests for the new provider, adapter, and runner.
@sanjay3290

Copy link
Copy Markdown
Author

Hi Brace Sproul (@bracesproul) Colin Francis (@colifran) — gentle review ping on this provider PR.

I rebased onto latest main and resolved merge conflicts with the newer providers (Bedrock, Vertex, Nebius) and related credential/setup paths. Branch is up to date and green locally:

  • pnpm run typecheck clean
  • full suite: 282/282 tests pass (including new agent-cli / grok-build coverage)

Happy to adjust the design (e.g. how agent-CLI providers fit next to Vertex-style keyless configs) if you have preferences. Thanks for taking a look!

@corridor-security corridor-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new Grok Build agent-CLI path spawns an external process that inherits the full parent environment (including all provider API keys and OAuth tokens in process.env) without a pruned env allowlist, and operates without a programmatic filesystem write boundary — both gaps are absent from the equivalent MCP child-process path.

`openwiki-agent-cli-${randomUUID()}.md`,
);
await writeFile(promptFilePath, spec.prompt, {
encoding: "utf8",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Agent-CLI run spawns an external process in the repository root without any programmatic write sandbox. The only constraint is system-prompt text instructing the LLM to stay under openwiki/ — but the DeepAgents path enforces this in code via OpenWikiLocalShellBackend. Combined with --always-approve, a prompt-injection payload in any content the agent reads (repo files, Notion, Slack, web search) can cause the Grok CLI to write to arbitrary paths without user confirmation.

const child = spawn(binary, adapter.buildArgs(spec, promptFilePath), {
  cwd: spec.cwd,
  detached: true,
  stdio: ["ignore", "pipe", "pipe"],
});

Remediation: Either drop --always-approve and add a programmatic approval callback that only permits writes under openwiki/, run the child in a filesystem namespace (chroot/landlock) restricted to that subtree, or add a post-run git diff --name-only check that rejects changes outside openwiki/ before the run is recorded as successful.

Attack Path
  1. Attacker plants adversarial content in repo files or connector raw data (Notion, Slack, web search).
  2. User runs openwiki --init/--update with OPENWIKI_PROVIDER=grok-build.
  3. runOpenWikiAgentrunAgentCliRunrunAgentCli spawns grok --always-approve.
  4. Agent reads the injected content and issues a write outside openwiki/.
  5. --always-approve auto-confirms; no TypeScript guard intercepts the write.

For more details, see the finding in Corridor.

Provide feedback: Reply with whether this is a valid vulnerability or false positive to help improve Corridor's accuracy.

Comment thread src/agent/engines/runner.ts
Address Corridor findings on the agent-CLI path:
- spawn vendor CLIs with a minimal env allowlist so OpenWiki-managed
  credentials never reach the child process
- after repository init/update runs, reject any writes outside openwiki/
  (plus AGENTS.md / CLAUDE.md) based on post-run mtime checks
@sanjay3290

Copy link
Copy Markdown
Author

Addressed both Corridor findings in 248c88f (same fix as on #356):

  1. Env scrubbing: agent-CLI children get a minimal allowlisted env only — no OpenWiki-managed credentials, OAuth tokens, or connector secrets from process.env.

  2. Docs-only write boundary: repository init/update runs fail if the vendor CLI modifies anything outside openwiki/ (plus root AGENTS.md / CLAUDE.md), enforced post-run via mtime scan rather than prompt text alone.

Tests added in test/agent-cli-security.test.ts. Happy to adjust the allowlist or permitted paths if maintainers prefer a stricter policy.

@corridor-security corridor-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs-only write-boundary scan silently skips the .git directory, allowing a prompt-injected or compromised agent CLI to plant malicious git hooks (e.g. pre-commit) without detection; the run is incorrectly reported as successful.

Comment thread src/agent/engines/write-boundary.ts
Stop skipping VCS metadata trees during the post-run path scan so
prompt-injected git hook or config writes under .git/ are rejected.
@sanjay3290

Copy link
Copy Markdown
Author

Fixed the new .git write-boundary bypass in the latest push:

  • Removed .git (and other VCS dirs) from the scan ignore list
  • Dropped the startsWith('.git') skip
  • Added tests that plant .git/hooks/pre-commit and assert the docs-only run fails

@corridor-security — valid finding; mitigated by including .git in the post-run mtime scan so hook/config writes are treated as disallowed paths.

@corridor-security corridor-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs-only write-boundary enforcement in write-boundary.ts can be bypassed by creating a symlink under openwiki/ that points outside the allowed directory tree: the scanner does not resolve symlink targets before applying the path-prefix allowlist, enabling an agent to silently modify arbitrary repository files including .git/hooks.

sinceMs: number,
boundary: AgentCliWriteBoundary,
): Promise<string[]> {
if (boundary === "none") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs-only write boundary can be bypassed by creating a symlink under openwiki/ that resolves outside the allowed tree. Two bugs combine: (1) symlinked directories are never traversed by the scanner — a dir-symlink's contents are invisible; (2) file symlinks are allowlisted using the symlink path (openwiki/evil.ts) rather than the resolved real path (src/app.ts). An injected agent can silently modify arbitrary source files or .git/hooks while the post-run check reports zero violations.

// listFilesModifiedSince — symlink to a directory is never pushed to stack:
if (entry.isDirectory()) {          // false for a dir-symlink
  stack.push(absolutePath);
  continue;
}
if (!entry.isFile() && !entry.isSymbolicLink()) { continue; }

const fileStat = await stat(absolutePath); // follows symlink
if (fileStat.mtimeMs >= sinceMs) {
  results.push(relativePath);             // pushes symlink path, not realpath
}

// findDisallowedWrites — allowlist check uses the symlink path:
return modified.filter((relativePath) => !isAllowedDocsOnlyWritePath(relativePath));
// isAllowedDocsOnlyWritePath('openwiki/evil.ts') → true (starts with 'openwiki/')

Remediation: For each collected path, resolve it with await fs.promises.realpath(absolutePath) before the allowlist check, and verify the resolved path is contained within path.resolve(rootDir, OPEN_WIKI_DIR). For directory symlinks, stat the target and push it onto the traversal stack with a cycle guard.

Attack Path
  1. User triggers an init/update run → writeBoundary = 'docs-only' (runner.ts:390-391).
  2. Injected prompt instructs the agent to run ln -s ../src openwiki/exfil or ln -s ../src/runner.ts openwiki/exfil.ts.
  3. Agent writes to openwiki/exfil/runner.ts (dir symlink) or modifies openwiki/exfil.ts (file symlink); bytes land in src/.
  4. findDisallowedWrites calls listFilesModifiedSince: dir symlink contents never scanned; file symlink path openwiki/exfil.ts passes allowlist check.
  5. disallowed.length === 0 → run reported compliant; .git/hooks or src/ write goes undetected.

For more details, see the finding in Corridor.

Provide feedback: Reply with whether this is a valid vulnerability or false positive to help improve Corridor's accuracy.

audichuang added a commit to audichuang/openwiki that referenced this pull request Jul 16, 2026
Both upstream PRs are still open and unmerged, so squash-merge the
integrated feature set (grok-build langchain-ai#280 + claude-code langchain-ai#181) onto current
main (d43bd4f, AWS Bedrock langchain-ai#327) instead of the stale langchain-ai#42 base.

Conflicts re-resolved:
- constants.ts: union provider list; ProviderConfig discriminated union
  now requires `kind`, so bedrock/nebius get `kind: "api"` and the
  region/secret helpers narrow on `kind === "api"`.
- agent/index.ts: finalizeAgentRun uses main's persistRunMetadataIfChanged
  (drops the unimported writeLastUpdateMetadata path); runAgentCliRun kept.
- cli.tsx: full provider list in error; formatProviderSwitchNotice kept.
- env.ts / credentials.tsx / README.md: unions.

Verified: typecheck, lint, format, pnpm test (313), build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant