8.4 KiB
8.4 KiB
Exec Event Emission Plan
Architecture Overview
- Exec/background process activity is centered in
src/agents/bash-tools.exec.tsviarunExecProcess(...). - Process lifecycle and output buffering are tracked in
src/agents/bash-process-registry.ts(addSession,appendOutput,markExited,drainSession). - Gateway event distribution already follows an emitter→listener→broadcast pattern.
- Emitter examples live in
src/infra/*-events.ts(for example,src/infra/agent-events.tsandsrc/infra/heartbeat-events.ts). - The gateway subscribes in
src/gateway/server.impl.tsand broadcasts viabroadcast(...). - Run/session linkage already exists through
runIdand agent run context. runIdis created insrc/auto-reply/reply/agent-runner-execution.ts.runIdis threaded into embedded runs and tool subscriptions insrc/agents/pi-embedded-runner/run/attempt.ts.- The gateway resolves session keys for runs via
src/gateway/server-session-key.ts.
Proposed Design
- Add a dedicated exec events infra module.
- Provide a small event bus:
emitExecEvent(...)andonExecEvent(...). - Provide a config resolver:
resolveExecEventsConfig(loadConfig())with defaults and whitelist normalization. - Capture orchestration context (run/tool/session) once at process start and store it on the session so background processes keep the link after the tool returns.
- Emit
exec.startedonce per whitelisted process after spawn/pid is known. - Emit
exec.outputin throttled and capped chunks tagged withstdoutorstderr. - Emit
exec.completedonce when the process closes (success or failure).
Run Context Strategy
- Introduce a lightweight exec event context using
AsyncLocalStorage. - Add a new module:
src/infra/exec-events-context.ts. - Provide APIs:
runWithExecEventContext(context, fn)andgetExecEventContext(). - Extend
createMoltbotCodingTools(...)options withrunId?: string. - In
src/agents/pi-embedded-runner/run/attempt.ts, passrunId: params.runId. - In
src/agents/pi-tools.ts, wrap each tool’sexecute(...)so it runs insiderunWithExecEventContext({ runId, toolCallId, sessionKey }). - In
runExecProcess(...), read the current context once and attach it to the session’s exec-event state.
Whitelist and Minimal Overhead
- Gate all exec event work behind an early, cheap check.
- If
hooks.exec.emitEvents !== true, do nothing. - If the command is not whitelisted, do nothing beyond a single boolean check in the output handlers.
- Normalize whitelist entries to lowercase command basenames.
- Extract candidate command names from the exec command string.
- Handle common wrappers: if the root command is
npx,pnpm,pnpmx,bunx,npm, oryarn, inspect the first non-flag subcommand. - Only run heavier shell parsing when the command text contains a possible whitelist token (string-contains prefilter).
Output Throttling and Capping
- Maintain per-session output state only when enabled.
- Buffer per stream (
stdoutandstderr). - Flush at most once per
outputThrottleMsper process. - Cap each emitted chunk to
outputMaxChunkBytes(default 4096 bytes). - On timer, emit one chunk per stream (bounded by max bytes).
- On completion, flush remaining buffered output before
exec.completed. - Impose a bounded in-memory buffer per stream to avoid unbounded growth under high output rates.
- If capped, drop oldest buffered data and set a
truncatedflag in the next output event payload.
Gateway Integration
- In
src/gateway/server.impl.ts, add anonExecEvent(...)subscription similar toonAgentEvent(...). - Broadcast with the existing
broadcast(eventName, payload, { dropIfSlow: true }). - Update
src/gateway/server-methods-list.tsto includeexec.started,exec.output, andexec.completed. - Review
src/gateway/server-broadcast.ts. Unless there is a policy reason, keep exec events available to operator clients without extra scopes.
Configuration Plan
- In
src/config/types.hooks.ts, addHooksExecConfig. - In
src/config/zod-schema.hooks.ts, addHooksExecSchema. - In
src/config/zod-schema.ts, includeexec: HooksExecSchemaunderhooks. - In
src/config/schema.ts, add labels forhooks.exec.emitEvents,hooks.exec.commandWhitelist,hooks.exec.outputThrottleMs, andhooks.exec.outputMaxChunkBytes. - In
src/config/schema.ts, add descriptions aligned with the requirement defaults. - Defaults (in the resolver, not mutating user config):
emitEvents: true. - Defaults (in the resolver, not mutating user config):
commandWhitelist: ["codex", "claude", "opencode", "pi", "gog", "himalaya", "playwright", "puppeteer"]. - Defaults (in the resolver, not mutating user config):
outputThrottleMs: 150. - Defaults (in the resolver, not mutating user config):
outputMaxChunkBytes: 4096.
Files to Modify
src/agents/bash-tools.exec.ts.src/agents/bash-process-registry.ts.src/agents/pi-tools.ts.src/agents/pi-embedded-runner/run/attempt.ts.src/infra/exec-events.ts(new).src/infra/exec-events-context.ts(new).src/gateway/server.impl.ts.src/gateway/server-methods-list.ts.src/gateway/server-broadcast.ts(review-only, likely no change).src/config/types.hooks.ts.src/config/zod-schema.hooks.ts.src/config/zod-schema.ts.src/config/schema.ts.
Beads (Discrete Units of Work)
- Add exec event infra: implement the event bus, config resolver, whitelist matcher, and throttled output buffer helpers.
- Add exec event context: implement
AsyncLocalStoragecontext helpers for run/tool/session metadata. - Thread runId into tool creation: extend
createMoltbotCodingTools(...)options and passrunIdfrom run attempt. - Wrap tool execution with context: apply the context wrapper in
src/agents/pi-tools.ts. - Instrument exec lifecycle: in
runExecProcess(...), decide enablement once, emitexec.started, throttle output, and emitexec.completedonce. - Gateway subscription and event registration: broadcast exec events and add them to the handshake events list.
- Config schema updates: update types, zod schemas, and CLI config labels and descriptions.
- Tests: add unit tests for matching and throttling, an integration test for a whitelisted command, a regression test for non-whitelisted commands, and a benchmark harness.
Risk Assessment
- Lost run linkage for backgrounded processes. Mitigation: capture context at process start and store it on the session.
- Duplicate
exec.completedemissions becausemarkExited(...)can be called again (for example, duringprocess poll). Mitigation: track anemittedCompletedflag in the session exec-event state and guard completion emission. - Output volume and backpressure. Mitigation: throttle, cap chunk size, and bound in-memory buffers; always use
dropIfSlow: trueon broadcast. - Command detection misses due to wrappers or env assignments. Mitigation: wrapper-aware matching and a prefilter plus fallback parse path.
Test Strategy
- Unit test whitelist matching with a direct command (
codex). - Unit test whitelist matching with a wrapper command (
npx playwright test). - Unit test whitelist matching with an env prefix (
FOO=1 codex run). - Unit test throttle behavior so burst output yields at most one emit per throttle window per process.
- Unit test chunk capping so large output is split to the max chunk size.
- Integration test a whitelisted command by using
node -eto print to stdout and stderr with short delays, then assertingexec.started, at least oneexec.output, andexec.completed. - Integration test a non-whitelisted command (for example,
echo hi) and assert no exec events are emitted. - Gateway e2e coverage if needed: start the gateway test server, subscribe via websocket, run a whitelisted exec, and assert events arrive with
runIdandsessionKeywhen available. - Performance benchmark: run non-whitelisted exec commands in a tight loop and compare baseline with exec events enabled.
- Performance benchmark acceptance: ensure overhead stays within an acceptable bound and does not regress common workflows.
Notes and Open Questions
- Event payload shape: the required fields will be present; additional fields like
runId,toolCallId,commandName, andtruncatedcan be included to improve tracing if desired. - Node host events: node host currently emits
exec.finishedandexec.denied. This plan focuses on gateway-local exec; we can optionally map node events into the same exec event bus later.