Skip to content

feat(webhook): bridge Orca completions to Hermes - #95408

Open
saedol-lang wants to merge 2 commits into
NousResearch:mainfrom
saedol-lang:feat/orca-webhook-bridge
Open

feat(webhook): bridge Orca completions to Hermes#95408
saedol-lang wants to merge 2 commits into
NousResearch:mainfrom
saedol-lang:feat/orca-webhook-bridge

Conversation

@saedol-lang

Copy link
Copy Markdown

Summary

Adds an opt-in, authenticated Hermes-to-Orca completion-webhook bridge so successful Orca runs can wake the matching Hermes delegation exactly once, with durable restart recovery and additive CLI lifecycle controls.

Also includes a narrow pre-existing webhook hardening fix: malformed non-UTF-8 signature-header bytes now fail closed with HTTP 401 instead of escaping the request handler as HTTP 500.

Design / footprint

This follows Hermes' narrow-waist guidance:

  • extends the existing webhook listener and hermes webhook CLI rather than adding a core model tool;
  • remains disabled unless an orca_bridge: true route is configured;
  • lazily imports bridge machinery only when configured;
  • stores durable completion state alongside existing async delegation state;
  • preserves ordinary webhook behavior and all legacy CLI verbs.

Security contracts

  • Bridge routes require replay-protected V2 HMAC; startup rejects INSECURE_NO_AUTH.
  • Same-machine source enforcement runs before body read.
  • Auth is computed over raw bytes before rate limiting/parsing.
  • Body-only Linear/GitHub/GitLab schemes cannot downgrade bridge-route V2 authentication. Linear support on ordinary routes remains unchanged.
  • Run identity and final state are re-queried from Orca; request-body goal, session_key, summary, and other attacker-controlled fields are never published.
  • Only Orca workerState == succeeded publishes; failure/unknown states fail closed. unsupervised/unknown sentinel states defer to the durable Task ledger.
  • Orca subprocess calls use argv arrays (shell=False) with timeout and strict run-id validation.
  • Durable claim/publish uses a cross-process SQLite CAS; a mutation-checked race harness produced exactly one publisher.
  • No outbound HTTP is introduced, so the bridge adds no SSRF surface.
  • Secrets/signatures are not logged or returned.

Current-main integration

Final branch is based on exact upstream main:

  • base: 1fe0f2f3ac9748ce799272eb93bee2937b5ab802
  • bridge: 85f6b5cd9f728210e53866b8d6c7437b1c9c3286
  • malformed-header hardening: 4ecc3562cd2cab4e8ba9f21f4c09a01fd12dcdb9

The accepted bridge candidate was replayed onto current main without conflicts. Patch IDs, changed blobs, authorship, commit messages, and trailers were verified preserved. The sole intervening upstream commit touched only cron files and had zero overlap with this 13-file diff.

Verification on final exact SHA

Static / integrity

  • git diff --check origin/main..HEAD — clean
  • ruff check over all changed Python files — clean
  • py_compile over all changed Python files — clean
  • worktree status — clean

Focused bridge contract suite

Using the repository-mandated scripts/run_tests.sh wrapper:

  • tests/tools/test_orca_bridge.py — 102 passed
  • tests/gateway/test_webhook_orca_bridge.py — 70 passed
  • tests/gateway/test_orca_bridge_lifecycle.py — 26 passed
  • tests/hermes_cli/test_webhook_orca_cli.py — 16 passed
  • total — 214 passed, 0 failed

Malformed signature-header regression

TestMalformedSignatureHeaderBytes drives real invalid UTF-8 bytes over a real socket for all five signature schemes, plus a valid control:

  • fixed helper — 11 passed
  • mutation control restoring strict encoding — 10 failed, 1 passed

This defect was independently reproduced on pristine upstream main with the bridge absent: valid signature returned 200, while malformed GitHub/Linear/GitLab/legacy-V1 signature bytes returned 500. The fix converts the full class to 401 while retaining constant-time byte comparison.

Risk-scaled regressions

Final aggregate gate: 269 passed, 1 failed. The sole failure is:

tests/tools/test_shutdown_forensics.py::TestSpawnAsyncDiagnostic::test_spawns_subprocess_and_writes_output

It reproduces identically on a pristine export of the exact base (1fe0f2f3a): 1 failed / 9 passed. On macOS the test invokes a missing GNU timeout executable and receives None; relevant files are byte-identical between base and candidate. It is unrelated to this change.

The new upstream cron tests also pass: 17 passed.

Real E2E

  • Real WebhookAdapter.connect() loopback listener on an ephemeral TCP socket: 46 auth/replay/route checks passed.
  • Valid V2 request accepted; replay recognized as duplicate.
  • V1, GitHub, GitLab, and Linear downgrade attempts rejected on bridge routes.
  • 14-row Orca completion-domain sweep passed with durable restore and field-injection checks.
  • Real CLI loopback passed: orca-register -> orca-runs -> signed POST -> replay -> orca-sweep.
  • Live installed Orca CLI vocabulary checked: 73 workers, no state outside the bridge domain; sentinel states exercised.

Mutation controls

All controls turned the focused suite RED and the checkout was restored byte-for-byte:

  • hoist Linear outside the V2 guard: 3 failed / 169 passed
  • hoist all body-only schemes: 6 failed / 166 passed
  • move V2 commit after handler: 8 failed / 164 passed
  • break published-state CAS: race harness detected 4 publishers in every round instead of 1

Configuration

See the updated webhook guide and cli-config.yaml.example. The feature is opt-in and requires a bridge route with replay-protected HMAC plus the documented Orca CLI registration/reconcile flow.

Known non-blocking note

The CLI currently passes the webhook secret to the local orca bridge register subprocess in argv for runtime compatibility. This is documented as a local process-list exposure tradeoff; no secret is written to logs. A stdin/fd transport can be adopted when the Orca CLI supports it.

saedol-lang and others added 2 commits August 26, 2026 17:50
Ports the accepted Orca completion bridge (86a2b138e, written against
470cf66) onto current main. The two histories share no merge-base, so
the port was computed as a per-file three-way merge of blobs supplied by
hash — no rebase, cherry-pick or history join was used.

Adds tools/orca_bridge.py plus the webhook bridge route, a gateway
supervisor with start/stop lifecycle, durable async_delegations records
for external completions, four additive `hermes webhook orca-*` CLI
verbs, a config example block and docs. The POST body is never trusted:
run identity is re-queried from Orca, and only a real workerState of
`succeeded` publishes. Failure states and unrecognized states fail
closed; the no-ledger sentinels (`unsupervised`, `unknown`) defer to the
Task ledger. Attacker-supplied goal/session_key/summary never reach the
published event.

Security decision this port had to make: current main's Linear branch
(`linear-signature`, NousResearch#87348) signs the body only with no timestamp
binding, which made it a fourth downgrade path around the V2
replay-protected HMAC on an `orca_bridge: true` route — a captured Linear
signature would have authenticated a bridge POST. Linear now sits inside
the `require_replay_protection` guard alongside GitHub and GitLab, which
is the class sweep rather than a fix limited to the schemes the original
review enumerated. Ordinary routes are unchanged and NousResearch#87348 stays green.

Verified in this checkout:
- scripts/run_tests.sh over the four bridge suites -> 214 passed,
  0 failed (26 + 16 + 70 + 102), including 4 new Linear downgrade cases.
- Mutation control: hoisting Linear back out of the guard in a scratch
  copy turns tests/gateway/test_webhook_orca_bridge.py red -> 3 failed,
  67 passed, and the failure mode is the request reaching the bridge
  handler, i.e. auth passed. Checkout never mutated.
- Current-main webhook/delegation regression set (15 files) -> 132
  passed, 1 failed. That failure,
  test_shutdown_forensics.py::TestSpawnAsyncDiagnostic::test_spawns_subprocess_and_writes_output,
  reproduces identically on a pristine export of 708b245, so it is
  pre-existing and not caused by this port.
- ruff check on the 10 changed .py files -> All checks passed!
- git diff --check clean; no conflict markers in any of the 12 files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCFhX81bc5AujbbdGoKAQN
(cherry picked from commit 746f6abc29cf67bb584fbbbb405d05ae98d9f6f3)
…g to 500

PRE-EXISTING UPSTREAM DEFECT — not introduced by the Orca bridge port in the
preceding commit. `_hmac_str_equal` is byte-identical on f0c0c98 (current
main); that commit only re-indented its call sites when it moved the body-only
schemes inside the replay-protection guard.

aiohttp decodes header bytes that are not valid UTF-8 using `surrogateescape`,
so an attacker-supplied signature header arrives holding lone surrogates
(`\udce9` for a latin-1 byte, and similarly for a raw 0xFF). The helper then
called `provided.encode()` with the strict codec, which raises
`UnicodeEncodeError` out of the request handler and returns HTTP 500. That is
the exact failure the helper's own docstring says it exists to prevent — the
original fix reached non-ASCII input that is still *valid* UTF-8, one codec
short of the malformed case.

Reproduced on a real socket against a pristine export of f0c0c98 with the
bridge absent entirely, on a 0.0.0.0-bound (network-reachable) route:

    valid github signature        -> 200
    X-Hub-Signature-256, latin-1  -> 500
    linear-signature, latin-1     -> 500
    X-Gitlab-Token, 0xFF          -> 500
    X-Webhook-Signature, 0xFF     -> 500

It fails closed — no request is ever authenticated by this path and the 500
body is aiohttp's generic text, so no internal state reaches the client — but
the contract is 401, and every scheme reaches the same helper, so this is the
whole class rather than the one header that surfaced it.

Re-encoding with the same error handler round-trips the surrogates back to the
original wire bytes, keeping the constant-time comparison while letting a
hostile header be rejected as bytes.

Verified in this checkout:
- New `TestMalformedSignatureHeaderBytes` drives REAL wire bytes over a real
  `adapter.connect()` socket (no client library, which would re-encode them),
  parametrized over all five schemes x two invalid-UTF-8 byte patterns, plus a
  positive control -> 11 passed.
- Load-bearing: the same test against the unfixed helper is RED, 10 failed /
  1 passed — every scheme x pattern combination, control still green.
- tests/gateway/test_webhook_adapter.py -> 47 passed (36 pre-existing + 11).
- ruff check + py_compile on both changed files -> clean; git diff --check clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019EZ1X3rQHTcKdmsyRbDDR1
@saedol-lang

Copy link
Copy Markdown
Author

CI is currently waiting on the standard maintainer approval required for workflows from a new fork contributor. All three workflow runs (CI, Nix flake check, and Docker Build, Test, and Publish) are in action_required with zero jobs started. Local acceptance evidence on exact head 4ecc3562cfced32e7705b216f3bbd702a09b6372 is included in the PR body. A maintainer will need to approve the workflow runs before CI can execute.

@alt-glitch alt-glitch added type/feature New feature or request comp/gateway Gateway runner, session dispatch, delivery comp/cli CLI entry point, hermes_cli/, setup wizard tool/delegate Subagent delegation area/config Config system, migrations, profiles P3 Low — cosmetic, nice to have needs-decision Awaiting maintainer decision before any implementation sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 26, 2026
@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference, author can ignore or act on any point.

This diff is too large for an automated line-level review (>30 files or >200KB). Recommend a manual review.

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

Labels

area/config Config system, migrations, profiles comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data tool/delegate Subagent delegation type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants