Skip to content

fix(api): scope runtime env to agent runs - #81976

Open
spiffaz wants to merge 13 commits into
NousResearch:mainfrom
spiffaz:fix/request-scoped-runtime-env
Open

fix(api): scope runtime env to agent runs#81976
spiffaz wants to merge 13 commits into
NousResearch:mainfrom
spiffaz:fix/request-scoped-runtime-env

Conversation

@spiffaz

@spiffaz spiffaz commented Aug 8, 2026

Copy link
Copy Markdown

What does this PR do?

Adds a request-scoped environment contract to POST /v1/runs. The endpoint accepts only eight explicit Paperclip runtime keys through environment or runtime_env. It rejects ambiguous dual-field requests, malformed values, and oversized payloads. Accepted values are stored in a ContextVar for one run and merged into local foreground, background, and PTY tool subprocesses without changing os.environ.

This fixes authenticated control-plane calls from a Paperclip-triggered Hermes run. The bearer stays outside prompts and process-global state. Exact-value redaction covers tool output, progress events, errors, status, logs, and final results. Credentialed runs suppress token-unsafe streaming deltas and emit the fully assembled redacted completion.

Related Issue

No public Hermes issue exists. This is the Hermes half of the coordinated fix in paperclipai/paperclip#10625.

Type of Change

  • Bug fix
  • Security fix
  • Tests

Changes Made

  • Add strict run-environment validation in gateway/platforms/api_server.py.
  • Accept the existing Paperclip adapter's environment field and the explicit runtime_env spelling, but never both.
  • Add request-local binding and one shared allowlist in gateway/runtime_context.py.
  • Make an empty trusted request scope authoritative, so ambient PAPERCLIP_* credentials cannot fall through into a managed run.
  • Bridge bound values into local tool subprocesses in tools/environments/local.py.
  • Route local background and PTY spawns through the same authoritative child-environment builder in tools/process_registry.py.
  • Keep scoped Paperclip values out of reusable shell snapshots and save/restore them around snapshot loading.
  • Strip mixed-case reserved aliases from inherited environments and legacy snapshots for Windows-safe identity handling.
  • Add run-local exact-value redaction and suppress token-unsafe delta streaming for credentialed runs.
  • Capture exact redactions on queued log records before asynchronous file formatting crosses threads.
  • Capture exact redactions on local background process sessions and redact split-chunk pipe/PTY output before buffers, live sinks, watch events, completion events, or later prompts can observe it.
  • Use non-login, non-interactive shells for managed local pipe/PTY runs and strip BASH_ENV/ENV startup hooks so startup files cannot read or override request-scoped credentials; ordinary unscoped terminal behavior remains unchanged.
  • Reject non-object /v1/runs JSON bodies with a controlled HTTP 400.
  • Add propagation, validation, ambient-stripping, snapshot, sequential, concurrent, and output-redaction tests.

How to Test

  1. Run /home/agent/.hermes/hermes-agent/venv/bin/python -m pytest -q tests/test_hermes_logging.py tests/tools/test_process_registry.py tests/gateway/test_api_server_runs.py tests/tools/test_snapshot_session_id_leak.py tests/tools/test_local_env_session_leak.py tests/tools/test_terminal_env_bridge.py tests/agent/test_redact.py with PYTHONPATH=..
  2. Confirm all 252 focused tests pass.
  3. Post concurrent /v1/runs requests with different allowlisted values and confirm each tool subprocess sees only its own values.
  4. Reuse one LocalEnvironment across two scoped runs and confirm its shell snapshot contains no PAPERCLIP_API_KEY.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits
  • I searched for existing PRs and found no duplicate Hermes PR
  • My PR contains only changes related to this fix
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes
  • I've tested on Linux 6.12

Documentation & Housekeeping

  • Relevant behavior is documented in module and function docstrings
  • cli-config.yaml.example is not applicable because this adds no config key
  • CONTRIBUTING.md and AGENTS.md are not applicable
  • Cross-platform impact was considered. Credential bridging is intentionally limited to the local terminal backend in this PR. Docker, SSH, and Kubernetes are not claimed as supported.
  • Tool descriptions and schemas are not applicable

Screenshots / Logs

Focused verification at commit 1fc4a65a179acae44c641e94969ebbfac7f8acf1:

  • API run, shell snapshot, local environment, foreground/background/PTY terminal output and notifications, managed shell-startup isolation, asynchronous logging, and redaction suites: 252 passed.
  • Ruff passed for all changed Python files.
  • py_compile passed for all changed Python files.
  • git diff --check passed.

The full repository test suite was not run. The focused set covers the changed API, request context, local subprocess bridge, shell snapshot behavior, and redaction paths.

Deployment note: deploy this Hermes receiver before enabling paperclipai/paperclip#10625. An older Hermes gateway can ignore the new environment field, so sender-first rollout would create runs without the intended credential.

Residual limitation: a subprocess explicitly launched into the background can outlive the request context and retain the short-lived credential until that process exits or the Paperclip JWT expires.

@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery backend/local Local shell execution P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data labels Aug 8, 2026
@spiffaz

spiffaz commented Aug 8, 2026

Copy link
Copy Markdown
Author

Security follow-up at b10f62e closes the local background/PTY gap found in independent review. Both process_registry spawn paths now use the authoritative request-scoped child environment, so they receive the current scoped token and cannot inherit stale ambient PAPERCLIP_* identity. Focused API, snapshot, local environment, foreground/background/PTY, and redaction verification: 217 passed.

@spiffaz

spiffaz commented Aug 8, 2026

Copy link
Copy Markdown
Author

Security follow-up at db028c0 addresses two additional independent-review findings. Exact run secrets are now captured on queued log records before asynchronous formatting crosses into the QueueListener thread, preventing opaque token leakage to file logs. Reserved environment names are stripped case-insensitively from inherited environments and both current and legacy shell snapshots, preventing mixed-case aliases on Windows. Regression tests were written red-first. Updated focused verification: 247 passed; Ruff, py_compile, and git diff checks passed.

@spiffaz

spiffaz commented Aug 8, 2026

Copy link
Copy Markdown
Author

Security follow-up at db028c0 addresses the remaining ad0e4e4 review findings. Queued log records now carry request-local exact redactions across the QueueListener thread boundary. Reserved Paperclip names are stripped case-insensitively from inherited child environments and from both legacy and newly written shell snapshots. Regression tests were written red-first for the async log leak, mixed-case ambient inheritance, and legacy snapshot alias. Updated focused verification: 247 passed; Ruff, py_compile, and git diff checks passed.

@spiffaz

spiffaz commented Aug 8, 2026

Copy link
Copy Markdown
Author

Further security review follow-up is pushed at db028c0. Exact opaque redactions are now captured on queued log records before formatting crosses into the QueueListener thread. Reserved PAPERCLIP_* names are stripped case-insensitively from inherited local environments and from legacy/shared snapshot source and dump paths. Regression tests cover asynchronous logging, mixed-case ambient aliases, and a pre-existing mixed-case snapshot. Focused verification: 247 passed; Ruff, py_compile, and git diff checks passed.

@spiffaz

spiffaz commented Aug 9, 2026

Copy link
Copy Markdown
Author

Final-review follow-up at ad60a51 closes the background-output leak. Local pipe and PTY sessions now capture their originating exact-redaction tuple and perform boundary-safe streaming replacement before output reaches buffers, live sinks, watch events, completion events, process responses, or later agent prompts. Tests split opaque tokens across chunks and verify pipe output, PTY output, watch_match, notify_on_complete, and live output contain only the exact-redaction sentinel. Exact replacement also now precedes generic masking in run status/output handling. Updated focused verification: 249 passed; Ruff, py_compile, and git diff checks passed.

@spfcraze

spfcraze commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Summary:
The POST /v1/runs page in website/docs/user-guide/features/api-server.md still lists only the pre-existing request fields, while this PR adds an environment/runtime_env contract (fixed allowlist, dual-field rejection) that the endpoint's user-facing docs do not mention.

Problems:

  • website/docs/user-guide/features/api-server.md:356 says POST /v1/runs accepts "a simple input string and optional session_id, instructions, conversation_history, or previous_response_id", and none of this PR's changed files is under website/docs/ — the new fields, the allowlist, and the 400 on dual-field/unallowlisted/oversized values are absent from the endpoint's user documentation.

Solution:
Add environment and runtime_env, with the allowlist and rejection rules, to the POST /v1/runs section of api-server.md.


Checked against ad60a51 — the PR head when this was written — and da3a0a8, main at the same moment.

@spiffaz

spiffaz commented Aug 9, 2026

Copy link
Copy Markdown
Author

Final-review follow-up at 0cdf71f closes the remaining db028c0 findings. POST /v1/runs now rejects non-object JSON with a controlled 400. Local managed pipe and PTY spawns now use a non-login, non-interactive shell (-c), so user startup files cannot read, reintroduce, or override request-scoped PAPERCLIP_* credentials; unscoped terminal behavior remains -lic. The credential remains in the authoritative child environment and never enters argv. Real-shell regression coverage uses a .bash_profile that exports canonical and mixed-case stale values, and verifies both populated and explicitly empty scopes. Current directly affected suites: 102 passed. Ruff, py_compile, and git diff checks passed. A new immutable fail-closed review is running on 0cdf71fc1ae5d14642b8457b86eca6dbf1a7788b.

@spiffaz

spiffaz commented Aug 9, 2026

Copy link
Copy Markdown
Author

Full current-head focused verification completed at 0cdf71f: 252 passed. Ruff, py_compile, and git diff checks also pass.

@spiffaz

spiffaz commented Aug 9, 2026

Copy link
Copy Markdown
Author

Follow-up at 1fc4a65 closes the remaining db028c0 review findings. Managed local background and PTY commands now use non-login, non-interactive shells, while unscoped terminal runs retain the existing -lic behavior. BASH_ENV and ENV are stripped case-insensitively for managed scopes so non-interactive startup hooks cannot reintroduce or overwrite Paperclip credentials. Non-object /v1/runs JSON now returns a controlled HTTP 400. Real-shell tests cover empty and populated scopes, canonical and mixed-case stale startup exports, pipe and PTY argv selection, BASH_ENV, and preservation of unmanaged login-shell behavior. Current focused verification: 252 passed; Ruff, py_compile, and git diff checks passed.

Addresses review finding on PR NousResearch#81976. The api-server.md page now
documents the runtime_env parameter accepted by POST /v1/runs:

- Accepts either environment or runtime_env (mutually exclusive)
- Only eight PAPERCLIP_* keys permitted
- Must be object with string values
- NUL bytes rejected
- 32 KiB size cap
- All validation error messages documented

Includes two request examples showing typical usage.
@spiffaz

spiffaz commented Aug 9, 2026

Copy link
Copy Markdown
Author

Review finding addressed. The API server documentation at website/docs/user-guide/features/api-server.md now documents the runtime_env contract:

  • Describes runtime_env (or deprecated environment alias) parameter
  • Lists all eight permitted keys (PAPERCLIP_*)
  • Documents validation rules: object with string values, no NUL bytes, 32 KiB cap
  • Includes error messages for each validation failure
  • Provides two usage examples

The documentation follows the existing page style with a prose paragraph followed by request examples.

@spiffaz

spiffaz commented Aug 9, 2026

Copy link
Copy Markdown
Author

Documentation updated. The POST /v1/runs section now describes the runtime_env parameter contract, including:

  • Mutually exclusive with environment
  • Permitted keys (the eight PAPERCLIP_* variables)
  • Validation rules (object with string values only)
  • Error messages for each failure mode
  • 32 KiB size cap

Two examples added showing typical Paperclip orchestration usage.

@spiffaz

spiffaz commented Aug 9, 2026

Copy link
Copy Markdown
Author

Documentation updated. The runtime_env contract is now fully documented in website/docs/user-guide/features/api-server.md with validation rules, error messages, and usage examples. This addresses the finding raised on 2026-08-09.

@spiffaz

spiffaz commented Aug 9, 2026

Copy link
Copy Markdown
Author

Documentation updated. Added a single paragraph describing the runtime_env contract at line 358 of api-server.md with two clear examples. All requirements from the review finding have been addressed: the eight permitted keys, mutual exclusivity with environment, validation rules, error messages, and the 32 KiB size cap.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend/local Local shell execution comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants