Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
743c6e9
fix(api): treat message audio/function_call null-empty as omit; fail-…
seonghobae Aug 16, 2026
e3a6e0e
fix(api): treat message weight 0/1/null as omit-equivalent; fail-clos…
seonghobae Aug 16, 2026
4a95b9a
fix(api): fail-closed on unknown chat message fields and legacy funct…
seonghobae Aug 16, 2026
82038d3
fix(api): treat message prefix null/false as omit; fail-closed on true
seonghobae Aug 16, 2026
1a196b0
fix(api): treat chat max_tool_calls null/empty as omit; fail-closed o…
seonghobae Aug 16, 2026
3a0d35e
fix(api): treat Completions max_tool_calls null/empty as omit; fail-c…
seonghobae Aug 16, 2026
9d10fa9
fix(api): treat stream_options null flags as omit-equivalent no-ops
seonghobae Aug 16, 2026
b108251
fix(api): fail-closed unknown stream_options keys and tools-path mess…
cursoragent Aug 16, 2026
856adb8
fix(api): fail-closed stream, model, and sampling on tools passthrough
cursoragent Aug 16, 2026
169a382
fix(api): fail-closed empty messages and spend keys on tools passthrough
cursoragent Aug 16, 2026
b950724
fix(api): fail-closed batch routing hints on tools passthrough
cursoragent Aug 16, 2026
dbfa0e5
docs: tell buyers to omit batch routing on tool-calling requests
cursoragent Aug 16, 2026
ffad9cb
fix(api): fail-closed seed/stop/n/logprobs on tools passthrough
cursoragent Aug 16, 2026
18e6263
fix(api): apply request temperature on streamed route completions
cursoragent Aug 16, 2026
bc73dd2
fix(api): pop null tool.function description/parameters/strict before…
cursoragent Aug 16, 2026
7e9a339
docs: record passthrough knob and stream temperature fixes in CHANGELOG
cursoragent Aug 16, 2026
af26158
fix(security): read provider host allowlist from KV at request time
cursoragent Aug 16, 2026
c91629e
test(security): lock allowlist seed-once and process-store isolation
cursoragent Aug 16, 2026
fe444d2
fix(security): keep operational email on trusted traces
cursoragent Aug 16, 2026
3cfc201
fix(security): resolve gateway Bearer tokens from the credential KV
cursoragent Aug 16, 2026
47ccf5a
fix(security): strip gateway Bearer tokens on KV seed and resolve
cursoragent Aug 16, 2026
7c58a0d
fix(security): strip register-credential --from-env mounted-secret ne…
cursoragent Aug 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,34 @@ push or open a PR.
- The reference implementation is xtrmLLMBatchPython's pgcrypto-encrypted
Postgres credential registry (`get_credential(name)`); reuse that pattern (a
DB-backed KV is fine) unless a dedicated KV is adopted.
- **Known deviation to migrate:** this repo currently resolves provider API
keys from env — `ModelClient` reads `os.environ.get(agent.api_key_env)` in
`contextual_orchestrator/orchestrator.py` (and `CONTEXTUAL_ORCHESTRATOR_*`
tokens in `__main__.py`). Move these to KV-backed reads; keep env only as the
bootstrap path that seeds the KV.
- **Known deviation to migrate:** `__main__.py` still accepts bind/TLS
flags and sqlite/Clearfolio paths from env as **process bootstrap**.
Provider API keys, gateway Bearer authenticators
(`gateway_auth_token` / `admin_auth_token` / `inference_auth_token`),
and the provider-host allowlist are KV-backed
(`get_credential`, `resolve_server_auth_tokens`,
`allowed_provider_hosts`). Env tokens are bootstrap transport via
`seed_server_auth_from_environ` only. Seed, resolve,
`register_credential`, and `get_credential` strip surrounding
whitespace. Seed skips any non-empty key — restart does not recopy a
persisted authenticator. `--auth-token` overrides this process; persist
with `register-credential`. Do not reintroduce `os.getenv`
inside `ModelClient._validate_provider`, `ModelClient.chat`, or
`serve_security_tokens` resolution.

### This repo: the org LLM gateway

- `contextual-orchestrator` is the org's **LLM-communication hub** — the
OpenAI-compatible front door consumed by **gyeot** and **scopeweave**.
- **Direction:** grow it toward a **LiteLLM-class multi-provider gateway**. The
org is open to a **Rust/Python hybrid** to cut overhead.
- Its `ModelClient` currently reads `os.environ.get(agent.api_key_env)` — this
is the KV-principle deviation above. Resolve the API key (including the org
`OPENAI_API_KEY`) from the **KV / credential registry**, not env.
- Provider API keys and gateway Bearer authenticators resolve through
`get_credential` (`gateway_auth_token`, `admin_auth_token`,
`inference_auth_token`). The provider-host allowlist resolves through
`allowed_provider_hosts()` on the **process-wide runtime ConfigStore**
(KV category `provider_egress`). That store is `InMemoryConfigStore`
unless bootstrap installed another backend with
`set_runtime_config_store()`. Env remains bootstrap transport only.
- The **OpenCode review pipeline is separate** and stays on **GitHub Models** —
do not change it.

Expand Down
87 changes: 87 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Changelog

All notable changes to this project are documented in this file.

The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed

- Trusted orchestration traces no longer irreversibly mask email addresses.
Credential shapes (`api_key=`, `Bearer …`) stay `[REDACTED]`. Access control
(opt-in `include_orchestration_trace`) plus audit is the PII control, not
destruction of the identifier an operator needs to close an invoice or HR
ticket. Next action: request the trace only from a trusted caller; do not
expect emails in that trace to become `[REDACTED]`.

### Fixed

- Gateway Bearer authenticators (`gateway_auth_token`, `admin_auth_token`,
`inference_auth_token`) resolve from the credential KV. `--auth-token`
and the split pair still win. `CONTEXTUAL_ORCHESTRATOR_TOKEN` /
`_ADMIN_TOKEN` / `_INFERENCE_TOKEN` are copied into those KV names once
at process start (`seed_server_auth_from_environ`). Seed and resolve
strip surrounding whitespace so a mounted-secret newline still
authorizes. Changing the env var on a running process — or restarting
after the KV name is already set — no longer changes who can call the
API. Buyer next action: pass `--auth-token` (or start once with the
env var so bootstrap can copy it), then send that Bearer value.
`--auth-token` overrides this process only; persist a new authenticator
with `register-credential`.
- `register-credential --from-env` and `register_credential` strip
surrounding whitespace (matching `--value-stdin`). A Docker/K8s secret
trailing newline is not persisted and does not become
`Authorization: Bearer sk-…` plus a newline on the provider request. Whitespace-only
values are rejected as empty. `get_credential` also strips so a secret
stored before this write-path still authorizes. Buyer next action: seed
with `--from-env` or `--value-stdin` from the mounted secret, then send
traffic.
- Provider host allowlisting (`provider_egress.allowed_provider_hosts`) is
read from the **process-wide runtime ConfigStore** at request time, not from
`os.getenv` and not from a separately constructed Postgres `com_config`
unless that store was installed with `set_runtime_config_store()` at
bootstrap. `CONTEXTUAL_ORCHESTRATOR_ALLOWED_PROVIDER_HOSTS` is copied into
that KV key once at process start (`seed_provider_egress_from_environ`).
Changing the env var on a running process no longer changes egress policy.
Buyer next action: call `set_runtime_config("provider_egress",
"allowed_provider_hosts", "api.example.com")` (or start the process with
the env var set so bootstrap can copy it). Do not write the key only into
a new `get_config_store(postgres_dsn=...)` instance and expect egress to
honor it.
- Treat official-SDK JSON `null` on optional `tools[].function.description`,
`parameters`, and `strict` as omit-real: the keys are popped before
`proxy_completion` so upstream providers see an omitted field, not a null
schema. Non-null wrong types still fail closed with named `invalid_tools`.
Next action: send those fields only when you have a real string, JSON Schema
object, or boolean; SDK defaults of `null` are safe.
- Fail closed on tools passthrough for `seed`, `stop`, `n>1`, `logprobs`,
`logit_bias`, and out-of-range penalties — the same named errors as the
orchestration path. Next action: omit those knobs on tool-calling requests.
- Apply the request `temperature` on streamed route completions instead of
silently using `0.2`. Next action: send the temperature you want; streaming
no longer changes the sampling policy.

### References

- McCallister, E., Grance, T., & Scarfone, K. (2010). *Guide to protecting
the confidentiality of personally identifiable information (PII)* (NIST
Special Publication 800-122). National Institute of Standards and
Technology. https://doi.org/10.6028/NIST.SP.800-122
- Joint Task Force. (2020). *Security and privacy controls for information
systems and organizations* (NIST Special Publication 800-53 Rev. 5).
National Institute of Standards and Technology.
https://doi.org/10.6028/NIST.SP.800-53r5
- Grassi, P. A., Garcia, M. E., & Fenton, J. L. (2017). *Digital identity
guidelines: Authentication and lifecycle management* (NIST Special
Publication 800-63B). National Institute of Standards and Technology.
https://doi.org/10.6028/NIST.SP.800-63b
- International Organization for Standardization. (2022). *Information
security, cybersecurity and privacy protection — Information security
controls* (ISO/IEC 27001:2022). https://www.iso.org/standard/27001
- OpenAI. (2024). *Create chat completion*. OpenAI API reference.
https://platform.openai.com/docs/api-reference/chat/create
- Bray, T. (Ed.). (2017). *The JavaScript Object Notation (JSON) data
interchange format* (RFC 8259). Internet Engineering Task Force.
https://doi.org/10.17487/RFC8259
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ A stdlib-Python lab implementing a single OpenAI-compatible API that routes, del

### Modules (`contextual_orchestrator/`)

- `orchestrator.py` — the domain heart: `ModelAgent`, `WorkflowStep`, `OrchestrationPolicy`, `ModelClient`, `TaskOrchestrator`, secret/PII redaction, budget enforcement, spend analytics, and the commercial-readiness report generators behind `/api/v1/*`. Domain code stays here until a second implementation forces extraction (see `docs/code_conventions.md`).
- `orchestrator.py` — the domain heart: `ModelAgent`, `WorkflowStep`, `OrchestrationPolicy`, `ModelClient`, `TaskOrchestrator`, credential redaction (operational email kept on trusted traces), budget enforcement, spend analytics, and the commercial-readiness report generators behind `/api/v1/*`. Domain code stays here until a second implementation forces extraction (see `docs/code_conventions.md`).
- `server.py` — HTTP delivery adapter and `SecurityConfig`; all request validation lives here.
- `admin.py` — static HTML/CSS/JS for the `/admin` operator console (stays inline while the product is dependency-free).
- `credentials.py` / `kv_config.py` — the KV seam: `get_credential`/`register_credential` over pluggable backends (`InMemoryCredentialBackend` default; pgcrypto-encrypted `PostgresCredentialBackend`, selected via `CONTEXTUAL_ORCHESTRATOR_KV_BACKEND`).
- `credentials.py` / `kv_config.py` — the KV seam: `get_credential`/`register_credential` over pluggable backends (`InMemoryCredentialBackend` default; pgcrypto-encrypted `PostgresCredentialBackend`, selected via `CONTEXTUAL_ORCHESTRATOR_KV_BACKEND`). Request-time provider host allowlisting uses `allowed_provider_hosts()` (`provider_egress.allowed_provider_hosts`). Gateway Bearer authenticators use `gateway_auth_token` / `admin_auth_token` / `inference_auth_token` via `seed_server_auth_from_environ` + `resolve_server_auth_tokens`; env is bootstrap only. Seed, resolve, `register_credential`, and `get_credential` strip surrounding whitespace; seed skips any non-empty key. `--auth-token` overrides this process; persist with `register-credential`.
- `cost_ledger.py` / `cost_router.py` / `batch_routing.py` / `token_counting.py` — the cost-review + routing hub: prompt-safe usage ledger with seven attribution dimensions, `RoutingPolicy` (sync vs batch from request hints + KV thresholds), and the [pg-llm-batch](https://github.com/ContextualWisdomLab/pg-llm-batch) batch/embeddings backends (a local in-process backend keeps the standalone path working with no external service).
- `api_contract.py` / `conventions.py` — API-shape and naming-rule enforcement helpers.
- `__main__.py` — the single entry point: CLI completion, `--serve`, `--eval`, and the `register-credential` bootstrap subcommand.
Expand Down
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ curl -s http://127.0.0.1:8000/v1/chat/completions \

HTTP serving is hardened for local lab use:

- `/admin`, `/admin/state`, `/api/v1/*`, and `/v1/chat/completions` require a Bearer token. Use `--admin-token` and `--inference-token` to separate operator and runtime access, or `--auth-token` / `CONTEXTUAL_ORCHESTRATOR_TOKEN` for one local-development token.
- `/admin`, `/admin/state`, `/api/v1/*`, and `/v1/chat/completions` require a Bearer token. Use `--admin-token` and `--inference-token` to separate operator and runtime access, or `--auth-token` for one local-development token. `CONTEXTUAL_ORCHESTRATOR_TOKEN` (and the split admin/inference vars) are copied into the credential KV once at process start — send the stripped seeded token; a later env edit, or a restart after the KV name is already set, does not change who can call the API. `--auth-token` overrides this process only; persist a new authenticator with `register-credential`. `register-credential --from-env` and `--value-stdin` strip surrounding whitespace so a mounted-secret newline is not stored or sent upstream.
- Binding to `0.0.0.0` or `::` requires `--allow-public-bind`.
- JSON request bodies, chat message roles, orchestration modes, body sizes, request rate, and concurrent run counts are validated before orchestration runs.
- Full orchestration traces are not returned by default. Set `include_orchestration_trace: true` per chat request or start with `--expose-trace-by-default` when the caller is trusted.
- Full orchestration traces are not returned by default. Set `include_orchestration_trace: true` per chat request or start with `--expose-trace-by-default` when the caller is trusted. Trusted traces keep operational email so invoice and HR tickets stay workable; credential material is still `[REDACTED]`.
- State is in-memory by default. Pass `--state-db PATH` (or `CONTEXTUAL_ORCHESTRATOR_STATE_DB`) to persist workflow runs, evaluation runs, audit, and analytics to a stdlib sqlite file so they survive a restart; without it, behavior is unchanged.
- Response caching is off by default. Pass `--cache-ttl SECONDS` to serve identical requests (same messages + mode) from an in-memory TTL+LRU cache and skip the provider calls; `0` disables it.
- `ModelClient.batch_chat(agent, {custom_id: messages})` runs many requests through the provider's Batch API (async, 24h completion window, typically ~50% cheaper) — suited to evaluation/benchmark workloads, not latency-sensitive chat. The mock path answers synchronously.
Expand Down Expand Up @@ -68,15 +68,15 @@ Seed the credential into the KV once at bootstrap:
echo "$OPENAI_API_KEY" | python -m contextual_orchestrator register-credential --name OPENAI_API_KEY --value-stdin
```

Non-mock providers must use `https://` URLs and a **resolvable KV credential** — a non-mock agent whose credential is missing raises `NotConfigured` rather than falling back to an environment variable. The runtime blocks loopback, private, link-local, multicast, and reserved provider addresses before sending a key. Set `CONTEXTUAL_ORCHESTRATOR_ALLOWED_PROVIDER_HOSTS` to a comma-separated host allowlist when only approved model gateways should be reachable. External calls use a timeout and default output token cap.
Non-mock providers must use `https://` URLs and a **resolvable KV credential** — a non-mock agent whose credential is missing raises `NotConfigured` rather than falling back to an environment variable. The runtime blocks loopback, private, link-local, multicast, and reserved provider addresses before sending a key. Seed `provider_egress.allowed_provider_hosts` on the **process-wide runtime ConfigStore** with `set_runtime_config` (or set `CONTEXTUAL_ORCHESTRATOR_ALLOWED_PROVIDER_HOSTS` at process start so bootstrap can copy it once) when only approved model gateways should be reachable. A write to a separately constructed Postgres `get_config_store()` is ignored unless that store was installed with `set_runtime_config_store()` at bootstrap. External calls use a timeout and default output token cap.

> The legacy `api_key_env` field is still accepted for back-compat, but its value is now treated as the **credential name** in the KV, not as an environment variable to read. This supersedes the old `api_key_env` env pattern.

## Architecture

One public interface:

- `/v1/chat/completions` accepts normal chat messages, and `"stream": true` returns an OpenAI-compatible `text/event-stream` of `chat.completion.chunk` deltas terminated by `data: [DONE]`. In **route** mode the worker's tokens are streamed live as they arrive from the provider (real token streaming); in **conduct** mode the multi-step answer is produced then framed as deltas (a workflow can't honestly token-stream a synthesizer that hasn't run yet).
- `/v1/chat/completions` accepts normal chat messages, and `"stream": true` returns an OpenAI-compatible `text/event-stream` of `chat.completion.chunk` deltas terminated by `data: [DONE]`. In **route** mode the worker's tokens are streamed live as they arrive from the provider (real token streaming); in **conduct** mode the multi-step answer is produced then framed as deltas (a workflow can't honestly token-stream a synthesizer that hasn't run yet). `/v1/completions`, `/v1/responses`, and `/v1/embeddings` are the matching compatibility surfaces. Send `stream_options.include_usage` / `include_obfuscation` as omitted, `null`, or `false`; `true` and unknown keys fail closed. Tool-calling bodies must send a non-empty `messages` array of objects — empty, omitted, or non-object entries fail closed before passthrough. Omit `routing.channel=batch` and `latency_tolerant=true` on tool-calling requests; this gateway has no batch job plane on passthrough. Omit `seed`, `stop`, `n>1`, and `logprobs` on tool-calling requests — those knobs are not applied on passthrough and fail closed instead of billing a silent drop. Streamed route requests apply the `temperature` you send; they do not silently fall back to `0.2`. Optional `tools[].function.description`, `parameters`, and `strict` sent as JSON `null` are popped before the provider hop so SDK defaults stay omit-equivalent.
- `TaskOrchestrator.complete()` decides whether to route to one worker or run a short workflow.
- `TaskOrchestrator.compare_to_baseline(prompts, mode)` (CLI `--eval PROMPT...`) measures the orchestration engine against a single-worker baseline — per-prompt and aggregate latency plus a structural coverage delta (contributing steps + verifier-pass presence). It is a measured tradeoff report, not a human-quality claim.
- Responses include orchestration mode metadata, and trusted callers can request the full trace for audit.
Expand Down Expand Up @@ -256,6 +256,9 @@ python tests/test_admin_contract.py
python tests/test_conventions.py
python tests/test_api_contract.py
python tests/test_security_hardening.py
python tests/test_provider_host_allowlist_kv.py
python tests/test_server_auth_kv.py
python tests/test_kv_credentials.py
python tests/test_repository_security_metadata.py
python tests/test_product_planning_contract.py
python tests/test_plugin_driven_artifacts.py
Expand Down
Loading
Loading