Skip to content

Configurable model binding for Reach and an operator opt-in for Portal dispatch - #150

Draft
NavSrijan wants to merge 3 commits into
devfrom
reach-model-binding-and-portal-dispatch
Draft

NavSrijan wants to merge 3 commits into
devfrom
reach-model-binding-and-portal-dispatch

Conversation

@NavSrijan

@NavSrijan NavSrijan commented Nov 10, 2026 •

Copy link
Copy Markdown

Change and scope

Two hardcoded bindings stop an operator from using a model they already run or pay for. This PR lets each be configured, leaves the defaults unchanged, and keeps the existing refusals for anyone who doesn't opt in.

1. Opt one Portal into dispatch (allow_dispatch)

Every Portal grant is refused until the egress-admission gate exists, so a declared remote endpoint can't answer even when its operator intends it to.

  • New Portal rune field allow_dispatch (default false, requires probe = true). An opted-in Portal is granted like any warm capability once its probe lists the declared model; every other Portal is refused exactly as before (portal egress admission is not configured).
  • The opted-in probe authenticates with the Portal's own secret, because a credentialed gateway answers an anonymous /models with 401. A missing secret leaves the capability COLD with a reason naming the secret. Un-opted Portals keep the unauthenticated probe.
  • Among equally ready candidates a local Soulstone still outranks a Portal.
  • Archive, semantic, Scout and Creation workflows keep their Soulstone-only checks.

This is an operator's explicit choice, not the egress gate from State of Work: none of the privatization, secure-mode, local-before-paid or spend gates exist, and the Portal doc says so next to the flag.

2. Reach operator conversation: any model and endpoint

The conversation profile was frozen to Qwen3.8-27B on night-qwen38 at the literal http://127.0.0.1:28000, so any other model needed a local adapter that impersonated that name.

  • animator_id and model_id become closed, validated strings defaulting to the previous values. Existing profiles keep their digest (the legacy-digest test is unchanged and passes).
  • --model-endpoint admits a loopback HTTP IP literal with any explicit port (as before), or any HTTPS endpoint with an optional path prefix. Credentials, queries, fragments, trailing slashes and plain remote HTTP are refused. --model-transport podman stays loopback-only.
  • --model-api-key-file reads one bearer credential from a private file; it never appears in argv.
  • A remote endpoint has no standard /health, so its /v1/models inventory is the probe. A catalogue larger than the 32-entry wire bound reports only the selected model to the worker.
  • A completion may report the bare model a gateway reached for a routed id (vendor/model → model). That exact suffix is the only accepted alias; any other model is still refused, on both the host broker and the worker side.
  • reach_probe.py accepts the same endpoint and credential options.

Verification

  • New tests: Portal opt-in requires probe, authenticated probe and grant, unauthenticated probe for un-opted Portals, missing secret → COLD (test_portals.py); endpoint admission and refusal cases, remote gateway probe/inventory/routed-model flow with bearer auth, different-model refusal, credential validation (cli/test_reach_operator_model.py); configurable binding with unchanged legacy digest, routed-suffix matching, identifier validation (compositions/test_reach_operator_model.py).
  • pytest tests/unit/interface tests/unit/cli tests/unit/compositions tests/unit/domain/animation tests/unit/domain/cortex against this branch and against unmodified dev, both with --basetemp outside any Git checkout: identical failure set (22 failed in both, environment-dependent mount/transmute and Reach portability/research-image suites), 39 more passing here. ruff check and ruff format --check clean on changed files.
  • Not run in the OCI test image.
  • Live, on a rootless Podman 6.1.4 host:
    • Portal: an allow_dispatch Portal pointed at a credentialed OpenAI-compatible gateway registered WARM and answered a real Bridge message.
    • Reach: images built from this branch (plus Make the Reach operator's display name configurable and fix Podman hosts #149's Podman fixes, needed to build on Podman at all), then reach_probe.py --model-transport http --model-endpoint https://<gateway> --model-api-key-file <file> with a routed vendor/model id passed: probe and generation succeeded against the remote endpoint with no local adapter.

Documentation and delivery claims

  • docs/sepulcher/animator/portal.md: documents allow_dispatch and what it does not provide.
  • docs/compositions/reach/operator.md: documents the configurable binding, the endpoint rules, the credential file, and that the remote road sends conversation, persona and context to that provider.

Author review

  • I reviewed the full diff, including any AI-assisted changes, and can explain the submitted code and its verification.
  • I identified any reused upstream material and checked its provenance, changes, license obligations and shipped notices under the reuse procedure, or confirmed that this change incorporates none.

Every Portal grant is refused until the egress-admission gate exists, so a
declared remote endpoint can never answer even when its operator intends it.

Add `allow_dispatch` to Portal runes (default false, requires probe = true).
An opted-in Portal is granted like any warm capability once its probe lists
the declared model; every other Portal is refused exactly as before.

- The opted-in probe authenticates with the Portal's own secret, since a
  credentialed gateway answers an anonymous `/models` with 401. A missing
  secret leaves the capability COLD with a reason naming the secret.
- Among equally ready candidates a local Soulstone still outranks a Portal.
- Archive, semantic, Scout and Creation workflows keep their Soulstone-only
  checks.
The conversation profile was frozen to one local model (`Qwen3.8-27B` on
`night-qwen38`) at one literal loopback endpoint, so using any other model
required an adapter that impersonated it.

- `animator_id` and `model_id` are closed, validated strings defaulting to
  the previous values; existing profiles keep their digest.
- `--model-endpoint` admits a loopback HTTP origin (as before, any port) or
  any HTTPS endpoint with an optional path prefix. Credentials, queries,
  fragments and plain remote HTTP are refused. The Podman transport stays
  loopback-only.
- `--model-api-key-file` reads one bearer credential from a private file.
- A remote endpoint has no standard /health, so its /v1/models inventory is
  the probe. A catalogue larger than the wire bound reports only the
  selected model to the worker.
- A completion may report the bare model a gateway reached for a routed id
  (`vendor/model` -> `model`); a different model is still refused.
- reach_probe.py accepts the same endpoint and credential options.
Capture probe headers through a typed respx side effect instead of the
untyped call log, and validate the identifier cases from a dict.

Copy link
Copy Markdown
Contributor

Please keep both the direct Reach API path and the per-Portal allow_dispatch opt-in for Bridge experiments. The intended use for this opt-in is a developer instance with test data. Keep the flag off by default. This path can send prompts and history to the provider as-is.

Follow-up in #25: route both Bridge Portal calls and direct Reach calls through the Egress Gate. The gate should authorize the exact destination and payload before sending. Apply and verify the required privacy transformation, including redaction or anonymization where policy requires it, before the payload is admitted.

This branch has not been deployed

No deployments
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.

2 participants