Skip to content

R.5: remove D16 allowlist; keyless local runtimes send no Authorization (D16, D24; stacked on #22) - #23

Open
cunninghamcard-bit wants to merge 26 commits into
exp/restartfrom
exp/u1R.5-boundary-no-allowlist
Open

cunninghamcard-bit wants to merge 26 commits into
exp/restartfrom
exp/u1R.5-boundary-no-allowlist

Conversation

@cunninghamcard-bit

@cunninghamcard-bit cunninghamcard-bit commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Phase 1R unit R.5 (D16, D24/D15; A6 item 5). Stacked on #22 (R.4 @ f5e72b0), #21, #20 and #19, so this diff includes them until they merge. The R.5-only change is commit 18b31ea.

Changes

  • scripts/check_provider_boundaries.sh: the D16 allowlist is gone entirely.
    • The d16_allow() function and its use in rule 2 (fn config_snapshot / fn retry_config / RetryConfig / api_key_source).
    • The rule 1 exception for provider_handle.rs ... options.max_retries.
    • The rule 5 exception for provider_handle.rs ... PLACEHOLDER_API_KEY.
    • The rule 9 exception for RetryConfig in the decision files. This one wasn't in the plan's line list; it's at :256 on this stack.
    • The rules themselves are unchanged. grep -ni d16 scripts/check_provider_boundaries.sh finds nothing.
  • provider_handle.rs: PLACEHOLDER_API_KEY ("not-needed") is deleted. A local runtime (ollama/llamacpp/localai/vllm/sglang/laya) with no key passes api_key: None to create_openai_compatible. That is auth = none, so no Authorization header is sent (D24, RFC-0040:155/:192; matches AI SDK openai-compatible-provider.ts:156).
  • The R.3 env_tests helper wrote options.max_retries = Some(0) inside src/, which rule 1 now catches. It no longer writes it, because both fixtures already contain "max_retries": 0.

Tests

  • decision_handle_test::keyless_local_runtime_sends_placeholder_bearer is renamed to keyless_local_runtime_sends_no_authorization_header. It asserts !headers.contains_key("authorization"), i.e. the header is absent, not empty. The recording-leak check for "not-needed" is dropped because the value no longer exists anywhere.

Boundary script

  • On this branch: bash scripts/check_provider_boundaries.sh; echo $? prints provider boundaries: ok and 0.
  • Temporary probe, not committed; the files were restored afterwards. I appended these four lines:
    • // probe: fn config_snapshot to jev.rs (decision file)
    • const PLACEHOLDER_API_KEY to provider_handle.rs (decision file)
    • // probe: options.timeout to groq/mod.rs (non-decision file)
    • // probe: api_key_source to aimux-core/src/recording.rs
  • The script exited 1 and reported all four: rule 1 (groq), rule 2 (fn config_snapshot in jev, api_key_source in recording.rs) and rule 5 (PLACEHOLDER_API_KEY). Decision files and non-decision files both fail now. After restoring the files it exits 0 again.

Results vs R.0 (local linux x64; new and renamed test names confirmed in the log)

R.0 (8455db9) R.4 (f5e72b0) R.5 (18b31ea)
cargo test --workspace 2871 / 0 / 7 2883 / 0 / 7 2883 / 0 / 7
fmt --check / clippy -D warnings — ok / ok ok / ok
check_provider_boundaries.sh 0 (with allowlist) 0 (with allowlist) 0 (no allowlist)
Python pytest 93 pass, 5 fail 95 pass, same 5 95 pass, same 5 (same KeyErrors)
Node ava (node 22) 87 pass, 1 fail 93 pass, same 1 93 pass, same 1

Go/Java/Kotlin/Swift/Flutter were not run locally.

Review follow-up

  • Review fix (54c1606): unused recorder removed from the keyless-local-runtime header test; new test local_runtime_empty_key_sends_no_authorization_header (Some("") -> no Authorization). Top of stack: cargo 2884/0/7, node 93 pass / 1 fail (pre-existing decision usage test).

Drop ProviderOptions.body_overrides and both arcships#226 rejections in
provider_handle.rs; ProviderOptions now denies unknown fields. Node
ProviderConfig loses bodyOverrides and reports any unknown key as
InvalidArgumentError 'unknown field `x`'. Python docs updated; Python
already rejects unknown keys via serde. Tests in Rust/Python/Node.
Adds docs/amendments/A6-remove-226-per-rfc.md verbatim.
…keys)

Replace the hand-written key allowlist: the JS object is read as a raw
JSON map (undefined values skipped), maxRetries is rejected first with
the call-level message, then the map deserializes into ProviderConfig
with #[serde(deny_unknown_fields, rename_all = "camelCase")]. The
max_retries struct field is gone (index.d.ts loses maxRetries?). Tests:
maxRetries gets the call-level message, undefined keys are absent.
…ies only (A6 item 2)

Drop RetryConfig and prepare_retries_with_config from aimux-core,
DecisionModel::retry_config, the retry fields/overrides in jev,
openai/decision (with_retry_config), runtime_decision and
decision_support (snapshot no longer records max_retries), and the
handle-level ProviderOptions.max_retries default in provider_handle.
decide() uses retry::prepare_retries(options.max_retries, ...).
New mock test: max_retries = 0/1/3/None makes exactly 1/2/4/3 requests.
…n jev_test

Retry counting moves into aimux-core decision_model::retry_tests, using a
crate-internal #[cfg(test)] PreparedRetries::with_initial_delay(ZERO);
asserts attempts = max_retries + 1 and RetryErrorReason::MaxRetriesExceeded
with the last 503/429/529 error. jev_test keeps status classification at
max_retries = 0 plus one max_retries = 1 run on 503 asserting 2 requests
and retry exhaustion. No public API, feature or binding change.
…st time (A6 item 4, D23)

provider_handle no longer reads the generic format!("{}_API_KEY") for local
runtimes (arcships#226). openai with api_key None keeps the package default
OPENAI_API_KEY, now read at request time (arcships#200:538); Some("") is used as
given (RFC-0040:67); a changed base URL without a key sends no credential and
reads no env. Tests inject env as a plain-data map (crate-internal
EnvSource::Vars), never std::env::set_var: generic key not read, env set after
construction is used, Some("") no fallback, replay/auth=none/changed base URL
read nothing. api_key_source field stays for R.4; placeholder key for R.5.
…eep identity only (A6 item 1, D3)

decide() records ProviderRecord::from_model(provider, model_id). Removed
DecisionModel::config_snapshot and its jev/openai/runtime/handle overrides,
DecisionEndpoint::snapshot, ProviderHandle's api_key_source and
HandleDecisionModel, and ProviderRecord.api_key_source (ts-rs regenerated).
Recording tests now assert no apiKeySource / baseUrl / providerOptions in the
provider record; config_snapshot() callers rewritten to the recorded identity.
…only)

Nothing writes them since config_snapshot is gone. Also removes the arcships#226-only
ProviderRecord::redacted and redact_provider_options. ProviderRecord is not
deny_unknown_fields; a new test shows an old record carrying baseUrl /
apiKeySource / profile / providerOptions still deserializes (and
re-serializes as identity only). ts-rs regenerated.
…tion (D16, D24/D15)

check_provider_boundaries.sh has no d16_allow or per-file D16 exceptions left
(rule 1 provider_handle max_retries, rule 2 removed names, rule 5
PLACEHOLDER_API_KEY, rule 9 RetryConfig) and exits 0. PLACEHOLDER_API_KEY is
deleted: a local runtime with no key is auth = none and the mock test asserts
the Authorization header is absent. The R.3 env tests rely on the fixtures'
max_retries: 0 instead of writing options.max_retries in src.
… the struct

Drop #[napi(skip)] invalid: Option<String>; from_napi_value now returns the
InvalidArgumentError directly. The constructors' 3rd arg becomes ConfigArg
(dispatch on JS type) because napi's Either swallowed the member error.
… from env_tests

Prod compiles no Handles / fetch / env plumbing. The env is injected, so the
tests need no serialization; the replay test keeps only its own recording
(unique state) since the recorder is process-global.
Constructors accept the config argument as serde_json::Value (d.ts unchanged),
so conversion never throws synchronously; maxRetries, unknown keys and wrong
types are refused inside the async body as InvalidArgumentError. ConfigArg and
the custom ProviderConfig FromNapiValue are gone.
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