Skip to content

feat(prompt): add --prompt-file and take the prompt off argv when large - #152

Merged
David Koleczek (DavidKoleczek) merged 1 commit into
mainfrom
feat/prompt-file-input
Aug 26, 2026
Merged

feat(prompt): add --prompt-file and take the prompt off argv when large#152
David Koleczek (DavidKoleczek) merged 1 commit into
mainfrom
feat/prompt-file-input

Conversation

@DavidKoleczek

Copy link
Copy Markdown
Collaborator

Scope of impact

Coordinated cross-component. Engine, both wrappers, and the two specs that define the contract.

Problem

The prompt travelled as the final positional argv element. That imposed two transport limits on what is unbounded caller content.

Size. Linux caps a single argv element at MAX_ARG_STRLEN (131072 bytes); Windows caps the whole command line at 32767 chars. Past either, execve fails with E2BIG before the engine boots. The boundary is exact and reproducible:

131071 bytes -> spawns
131072 bytes -> [Errno 7] Argument list too long

Through the Python SDK this surfaced as a bare spawn_failed with no actionable remedy, indistinguishable from a missing binary. The engine had no other input path.

Leading -. A positional beginning with - is parsed as an option, so the turn died before the engine booted:

prompt: "--- turn context ---\nsay hi"
result: Error: No such option '--- turn context ---\nsay hi'   exit 2

Any caller composing a prompt from templated context blocks hits this routinely.

Both trace to one decision. docs/spec/wrapper-contract.md already applied the right mitigation to a much smaller field:

MCP server configuration is always spilled to a file, never passed on argv, so a large server map cannot overflow the OS argv limit.

The prompt, the field most likely to be large, was left on argv. This extends the same treatment to it.

Changes

Engine gains --prompt-file <path>, read as UTF-8 and delivered verbatim with no stripping or newline translation. The positional PROMPT remains valid and unchanged; the two are mutually exclusive. Supplying both raises argv_prompt_conflict; an unreadable or non-UTF-8 file raises argv_prompt_file_unreadable. Both are §4.1 envelopes at exit 2, matching the existing argv-validation convention.

Both wrappers now emit an unconditional -- before a positional prompt, and spill a prompt of 16384 UTF-8 bytes or more to a 0600 file. The threshold is measured on encoded byte length rather than character count, because the OS limits are byte limits and a multibyte prompt is larger on the wire than its character count suggests. It sits under the smaller Windows ceiling rather than the Linux one.

Also fixed: the Python wrapper's MCP config spill wrote in text mode with no explicit encoding, inheriting the locale codepage (cp1252 on Windows) while the reader opens the file as utf-8-sig and degrades a decode failure into a warning reporting no configured servers. The TypeScript wrapper was already correct; this brings the two into parity.

Compatibility

Additive. No existing caller changes behavior:

  • The positional prompt still works at every size.
  • -- was already accepted by the engine for every prompt, so emitting it unconditionally changes no outcome.
  • --prompt-file is new surface, not a replacement. A wrapper may ignore it entirely.

Tests

New tests/e2e/suites/prompt_input/, three cases:

prompt-file-oversized            a 200KB prompt beginning with '---' runs and replies
argv-separator-leading-dashes    CONTROL: '--' makes a '-'-leading positional work
prompt-file-rejects-both-inputs  supplying both inputs is a caller error

On the previous code the two --prompt-file cases fail with No such option '--prompt-file'; all three pass here. The control was green before and after, and exists to pin the engine-side -- guarantee that the wrapper fix depends on, so a future parser change cannot break the wrappers silently.

The oversized payload is generated inside the container. Passing 200KB from the host would hit the host's own argv limit first and test the harness rather than the engine.

Also adds wrappers/typescript/test/prompt-spill.test.ts covering the new module: the threshold from both sides (16383 does not spill, 16384 does), the multibyte-measurement case that would silently regress if the check moved to .length, and a verbatim round trip.

Verification

make verify                              ALL GATES PASSED
prompt_input + run + modes (DTU e2e)     19 passed
TypeScript                               21 files, 131 tests
verify-parity                            10/10 fixtures, Python and TS agree

Not covered

No Windows regression case. The Linux suite exercises the 131072-byte ceiling, but Windows binds tighter at 32767, and that path is unproven by any test. The Windows container harness would need the CLI-level case added separately.

Pre-existing and untouched: both spill modules unlink their file but never remove the per-session directory, so empty directories accumulate. Not a regression, but the prompt spill now places a second file in that same directory.

The prompt travelled as the final positional argv element, which imposed two
transport limits on caller content.

Size: Linux caps a single argv element at MAX_ARG_STRLEN (131072 bytes) and
Windows caps the whole command line at 32767 chars. Past either, execve fails
with E2BIG before the engine boots, surfacing as an opaque spawn failure with no
actionable remedy. The engine had no other input path.

Leading '-': a positional beginning with '-' is parsed as an option, so the turn
died with exit 2 and "No such option" before the engine booted. Callers that
compose a prompt from templated context blocks hit this routinely.

The wrapper contract already applied the right mitigation to a much smaller
field, spilling MCP server config to a file "so a large server map cannot
overflow the OS argv limit". The prompt, the field most likely to be large, was
left on argv. This extends that same treatment to it.

Engine gains --prompt-file <path>, read as UTF-8 and delivered verbatim. The
positional PROMPT remains valid and unchanged; the two are mutually exclusive,
with argv_prompt_conflict and argv_prompt_file_unreadable as section 4.1
envelopes at exit 2, matching the existing argv-validation convention.

Both wrappers now emit an unconditional -- before a positional prompt, and spill
a prompt of 16384 UTF-8 bytes or more to a 0600 file. The threshold is measured
on encoded byte length rather than character count, because the OS limits are
byte limits, and sits under the smaller Windows ceiling rather than the Linux
one.

Also fixes the Python wrapper's MCP config spill, which wrote in text mode with
no explicit encoding. That inherits the locale codepage, cp1252 on Windows,
while the reader opens the file as utf-8-sig and degrades a decode failure into
a warning reporting no configured servers. The TypeScript wrapper was already
correct.

Scope of impact: coordinated cross-component. Engine, both wrappers, and the
two specs that define the contract.

docs/spec/cli.md and docs/spec/wrapper-contract.md updated in the same change.

Adds tests/e2e/suites/prompt_input/, which fails on the previous code with
"No such option '--prompt-file'" and passes here, plus TypeScript coverage for
the new spill module.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
@DavidKoleczek
David Koleczek (DavidKoleczek) merged commit 7e5bbc0 into main Aug 26, 2026
1 check passed
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