feat: add Grok Build subscription CLI as a provider#280
feat: add Grok Build subscription CLI as a provider#280Sanjay Ramadugu (sanjay3290) wants to merge 3 commits into
Conversation
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.
5e7ef70 to
4444531
Compare
|
Hi Brace Sproul (@bracesproul) Colin Francis (@colifran) — gentle review ping on this provider PR. I rebased onto latest
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! |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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
- Attacker plants adversarial content in repo files or connector raw data (Notion, Slack, web search).
- User runs
openwiki --init/--updatewithOPENWIKI_PROVIDER=grok-build. runOpenWikiAgent→runAgentCliRun→runAgentClispawnsgrok --always-approve.- Agent reads the injected content and issues a write outside
openwiki/. --always-approveauto-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.
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
|
Addressed both Corridor findings in 248c88f (same fix as on #356):
Tests added in |
Stop skipping VCS metadata trees during the post-run path scan so prompt-injected git hook or config writes under .git/ are rejected.
|
Fixed the new
@corridor-security — valid finding; mitigated by including |
There was a problem hiding this comment.
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") { |
There was a problem hiding this comment.
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
- User triggers an
init/updaterun →writeBoundary = 'docs-only'(runner.ts:390-391). - Injected prompt instructs the agent to run
ln -s ../src openwiki/exfilorln -s ../src/runner.ts openwiki/exfil.ts. - Agent writes to
openwiki/exfil/runner.ts(dir symlink) or modifiesopenwiki/exfil.ts(file symlink); bytes land insrc/. findDisallowedWritescallslistFilesModifiedSince: dir symlink contents never scanned; file symlink pathopenwiki/exfil.tspasses allowlist check.disallowed.length === 0→ run reported compliant;.git/hooksorsrc/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.
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>
Summary
Adds a
grok-buildprovider 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
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.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.prepareAgentRun/finalizeAgentRunso the CLI and DeepAgents paths share snapshot +.last-update.jsonmetadata handling.grok login), env overrides (OPENWIKI_GROK_BUILD_BINARY,OPENWIKI_GROK_BUILD_MAX_TURNS,OPENWIKI_AGENT_CLI_TIMEOUT_SECONDS), and the local-only caveat.Config
OPENWIKI_PROVIDER=grok-buildOPENWIKI_GROK_BUILD_BINARYgrokis not onPATHOPENWIKI_MODEL_IDgrok-4.5)OPENWIKI_GROK_BUILD_MAX_TURNSOPENWIKI_AGENT_CLI_TIMEOUT_SECONDSTesting
tsc --noEmit— cleaneslint— clean