Skip to content

feat(judge): ask the host for the judge credential on first use - #64

Merged
senamakel merged 3 commits into
tinyhumansai:mainfrom
M3gA-Mind:feat/judge-host-credential
Nov 12, 2026
Merged

senamakel merged 3 commits into
tinyhumansai:mainfrom
M3gA-Mind:feat/judge-host-credential

Conversation

@M3gA-Mind

Copy link
Copy Markdown
Contributor

Summary

JudgeConfig.credential was required at init. A host whose credential sits in this module's own keyring couldn't supply it: the keyring is readable only once the module is admitted, and reading it during load deadlocks. So openhuman runs with the judge off, and every call it would judge goes to a person.

The credential is now optional. Without it, the module asks the init-bound trusted host for it through a new confidential callback, JudgeCredentialHost2.Credential.

  • When it asks: on the first judgement, never during load, within the judge's own timeout_ms.
  • Reuse: the client is kept for five minutes (HOST_CREDENTIAL_TTL_MS, fixed).
  • On an endpoint rejection (401/403): the client is dropped at once, and the next ask says rejected, so rotation and sign-out are picked up.
  • On a reinit: the held credential is dropped too.
  • No credential or a failed ask: that request goes to a person, as with an unavailable judge today.

A configured credential behaves exactly as before.

Related issue

Closes #54.

API or behavior changes

  • JudgeConfig.credential is now Option<Secret>, omitted when absent. The wire is unchanged for hosts that send one. Rust code that builds JudgeConfig must write credential: Some(..).
  • New contract types: JudgeCredentialRequest2 { reason: Initial | Rejected } and JudgeCredentialResponse2 { credential: Option<Secret> }.
  • New callback names: callback_names2::JUDGE_CREDENTIAL_HOST and CREDENTIAL. The host serves them as a confidential callback.
  • New in tinysecurity-auto:
    • CredentialSource;
    • Engine::with_credential_source;
    • SdkClient::with_credential;
    • HOST_CREDENTIAL_TTL_MS;
    • Error::Rejected, which the SDK client now returns for an authentication failure in place of Unavailable.
  • A host credential arrives after init, so it isn't registered as a redaction literal. It's used only in the outbound request header. Documented in tinysecurity-auto/README.md and the spec.
  • CONTRACT_VERSION stays (2, 1). Maintainers may bump it at release.

Validation

Commands actually run, on our Linux CI runner (rustc 1.99.1, --locked, RUSTFLAGS unset) at cafb02a:

  • cargo fmt --all -- --check: clean
  • cargo clippy --all-targets --all-features -- -D warnings: 0 warnings (also with --keep-going)
  • Focused: tinysecurity-auto judge::tests 18/0; tinysecurity-bus policy::tests 12/0; tinysecurity-module adapter::judge::tests 2/0; adapter::tests::approval_tests 20/0; tinysecurity-policy 285/0
  • cargo build -p tinysecurity-module --examples --all-features: ok
  • cargo test --all-features: 1371 passed, 0 failed
  • Red controls: with the five-minute reuse disabled, exactly the two reuse tests fail; with Authentication mapped to Unavailable, exactly the 401 test fails

Tests

  • a_hosted_judge_asks_on_first_use_and_reuses_the_credential_for_five_minutes: no ask at construction, one ask, reuse up to the TTL, then a new ask.
  • no_usable_host_credential_sends_the_judgement_to_a_person: covers None and a too-short credential.
  • a_rejected_credential_is_dropped_and_the_next_ask_says_so
  • a_reinit_drops_the_host_credential
  • a_judge_without_credential_needs_a_host_source: also checks validate_config and the redacted Debug output.
  • an_endpoint_refusing_the_credential_is_reported_as_rejected: a loopback 401.
  • Module tests:
    • the_host_supplies_the_judge_credential_with_the_reason_it_is_asked;
    • no_credential_or_no_serving_host_gives_none;
    • the configured_automatic branch, which refuses without a trusted host.
  • Bus wire pins for the callback types and the omitted credential.

Documentation

crates/tinysecurity-auto/README.md and docs/specs/security-module.md describe the host-supplied credential.

Host follow-up (openhuman, after a release):

Checklist

  • The change is focused on one logical change
  • No new #[allow(...)], #[ignore], or relaxed lints
  • No secrets, tokens, or .env contents in the diff or the description

JudgeConfig.credential was required at init, so a host whose
credential sits in this module's own keyring could not supply it: the
keyring is readable only once the module is admitted, and reading it
during load deadlocks. openhuman therefore runs with the judge off.

The credential is now optional. Without it, the module asks the
init-bound trusted host through the confidential
JudgeCredentialHost2.Credential callback on the first judgement, never
during load, within the judge's timeout. The client is reused for five
minutes; an endpoint rejection (401/403) drops it at once and the next
ask says rejected; a reinit drops it too. No credential or a failed ask
sends that request to a person, as an unavailable judge does today. A
configured credential behaves exactly as before.
@coderabbitai

coderabbitai Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8c787121-49a6-46d2-a783-e650b706471c
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@tinysweeper

tinysweeper Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Tiny Sweeper review

Reviewed head: fa45856a626689d8d63b77fbb5251ab10cc8fcec

Verdict: Ready for maintainer review · Priority: none
Coverage: Reviewed 19 of 19 files.
Pull request purpose (title): feat(judge): ask the…

Diagnostics and supporting details

Full summary

Pull request purpose (title): feat(judge): ask the host for the judge credential on first use. The summary explains that `JudgeConfig.credential` was required at init, so a host whose credential lives in this module's own keyring could not supply it (the keyring is readable only once the module is admitted, and reading it during load deadlocks), leaving the judge off and every judged call going to a person. The credential is now optional; without it the module asks the init-bound trusted host through a new confidential `JudgeCredentialHost2.Credential` callback on the first judgement (never during load, bounded by the judge's `timeout_ms`), reuses the client for five minutes, drops it on a 401/403 with the next ask flagged `rejected`, drops it on reinit, and sends the request to a person when no credential or a failed ask occurs. A configured credential behaves as before. The latest-push delta is unavailable, so no push-delta description is given. The description lane notes a behavioural gap: the reinitialization path can never acquire a host credential, so a running module cannot move from a config-supplied credential to a host-served one.

Review snapshot

Change surface Files Review signal Count
Production 10 Active findings 0
Tests 7 Noted findings 0
Documentation 2 Resolved findings 0
Configuration 0 Pending checks/questions 0

Completeness: Complete
Test assessment: Test coverage is assessed from changed tests and lane evidence; execution is not claimed without trusted check data.

What changed

`JudgeConfig.credential` becomes `Option<Secret>`, omitted on the wire when absent, with new `JudgeCredentialReason2`, `JudgeCredentialRequest2`, and `JudgeCredentialResponse2` contract types and new `callback_names2::JUDGE_CREDENTIAL_HOST`/`CREDENTIAL` constants. `tinysecurity-auto` gains a `host` module with `CredentialSource`, `HOST_CREDENTIAL_TTL_MS`, a `HostClient` that asks the host lazily and caches for five minutes, `Engine::with_credential_source`, `SdkClient::with_credential`, and `Error::Rejected` mapped from SDK authentication failures. `Engine::validate_config` validates the endpoint and timeout with a placeholder key when no credential is configured, and `Engine::reconfigured` rebuilds through `with_source` so a held host credential is dropped. The module routes credential-less judge configs through a `HostCredential` source over the confidential callback and refuses initialization without an init-bound trusted host. The policy redactor registers only a configured credential as a redaction literal. Docs and the spec describe the host-supplied path.

Features

  • Modified — Optional judge credential in JudgeConfig: Hosts may omit the credential; the field is skipped when absent, and validation bounds its length only when present. Rust constructors must write `Some(..)`. (crates/tinysecurity-bus/src/policy.rs pub struct JudgeConfig {, crates/tinysecurity-auto/src/judge.rs fn validate(config: &JudgeConfig) -> Result<()> {)
  • Added — JudgeCredentialHost2 callback contract: Adds a confidential host callback whose request carries an Initial/Rejected reason and whose reply carries an optional secret, with the callback name constants. (crates/tinysecurity-bus/src/callbacks.rs pub mod callback_names2 {, crates/tinysecurity-bus/src/policy.rs pub struct JudgeThresholdRule {)
  • Added — Lazy host credential source with TTL cache: `HostClient` asks the host on first use, reuses the client for five minutes, treats an absent or out-of-range credential as unavailable, and drops exactly the client that hit a rejection while flagging the next ask as rejected. (crates/tinysecurity-auto/src/judge.rs impl Engine {, crates/tinysecurity-auto/src/judge.rs impl SdkClient {, crates/tinysecurity-auto/src/lib.rs)
  • Added — Rejected error classification: SDK authentication failures surface as `Error::Rejected` instead of `Unavailable`, enabling the drop-and-re-ask path. (crates/tinysecurity-auto/src/error/mod.rs pub enum Error {, crates/tinysecurity-auto/src/judge.rs impl JudgeClient for SdkClient {)
  • Modified — Reconfiguration and validation for host-supplied credentials: `reconfigured` rebuilds through `with_source` so a held host credential is dropped on reinit, and `validate_config` checks endpoint and timeout with a placeholder key that is never sent. (crates/tinysecurity-auto/src/judge.rs pub struct Engine {, crates/tinysecurity-auto/src/judge.rs impl Engine {)
  • Added — Module host credential source: A credential-less judge config requires the init-bound trusted host and serves the credential through a confidential callback bounded by the judge timeout; without a host, initialization fails. (crates/tinysecurity-module/src/adapter.rs async fn setup(connection: Connection, mut config: ModuleConfig) -> BusResult<(), crates/tinysecurity-module/src/adapter.rs mod device_pairing;)
  • Modified — Redaction registration limited to configured credentials: Only a configured credential is registered as a redaction literal; a host-supplied credential is used only in the outbound request header, as documented. (crates/tinysecurity-policy/src/engine.rs fn validate(config: &ModuleConfig) -> Result<()> {, crates/tinysecurity-auto/README.md lookup or per-call credential is supported. Invalid init/reinit rejects without, docs/specs/security-module.md discretionary policy is disabled. A claimed command class cannot authorize a)

Tests

  • unit — A hosted judge asks the host only on first use, reuses the credential up to the five-minute TTL, and asks again at the boundary, with no ask at construction.: Covers the lazy-ask and TTL behavior; the tests lane reported no defect for these tests. (crates/tinysecurity-auto/src/judge_tests.rs async fn legitimate_reconfiguration_shrinks_shared_cache_and_zero_clears_it() ->)
  • unit — A missing or too-short host credential sends the judgement to a person.: Covers the absent and short credential fallback; the tests lane reported no defect. (crates/tinysecurity-auto/src/judge_tests.rs impl JudgeClient for Fake {)
  • unit — A rejected credential is dropped and the next ask carries the rejected flag, then the client is reused.: Covers the 401/403 drop-and-flag path; the tests lane reported no defect. (crates/tinysecurity-auto/src/judge_tests.rs async fn class_origin_thresholds_invalid_config_and_reconfiguration_cannot_reuse)
  • unit — A reinit drops the held host credential and forces a new ask.: Covers reinit invalidation; the tests lane reported no defect. (crates/tinysecurity-auto/src/judge_tests.rs async fn legitimate_reconfiguration_shrinks_shared_cache_and_zero_clears_it() ->)
  • unit — A judge without a credential requires a host source, validate_config accepts it, and Debug output is redacted.: Covers construction guards and redaction; the tests lane reported no defect, but its summary additionally notes this test only asserts construction succeeds for the configured-credential precedence case. (crates/tinysecurity-auto/src/judge_tests.rs impl JudgeClient for Fake {)
  • integration — A loopback 401 response is mapped to `Error::Rejected`.: Covers the real authentication-failure classification; the tests lane reported no defect. (crates/tinysecurity-auto/src/judge_tests.rs async fn class_origin_thresholds_invalid_config_and_reconfiguration_cannot_reuse)
  • unit — The callback request/response wire form and the omitted `credential` field are pinned.: Covers serialization compatibility; the tests lane reported no defect. (crates/tinysecurity-bus/src/policy_tests.rs fn a_1_18_check_payload_still_deserializes() -> TestResult {)
  • unit — The module's host credential source returns the credential with the correct reason and returns none when unserved or empty.: Covers the confidential-callback source including an unserved host; the tests lane reported no defect. (crates/tinysecurity-module/src/adapter_judge_tests.rs)

Findings

No active actionable findings.

Before merge

None.

How this fits together

flowchart LR
  n0["Stub<br/>changed"]:::changed
  n1["Engine<br/>changed"]:::changed
  n2["SdkClient<br/>changed"]:::changed
  n3["validate<br/>changed"]:::changed
  n4["Fake<br/>changed"]:::changed
  n5["with_client"]:::impacted
  n6["...g_and_reconfiguration_cannot_reuse_scores"]:::impacted
  n7["...t_attempts_share_one_atomic_request_limit"]:::impacted
  n8["config"]:::impacted
  n9["...s_contain_no_registered_or_bearer_secrets"]:::impacted
  n10["JudgeClient"]:::impacted
  n0 -->|implements| n10
  n1 -->|uses| n10
  n2 -->|implements| n10
  n4 -->|implements| n10
  n5 -->|calls| n3
  n5 -->|uses| n10
  n6 -->|calls| n5
  n6 -->|tests| n5
  n6 -->|calls| n8
  n6 -->|tests| n8
  n7 -->|calls| n5
  n7 -->|tests| n5
  n7 -->|calls| n8
  n7 -->|tests| n8
  n9 -->|calls| n5
  n9 -->|tests| n5
  n9 -->|calls| n8
  n9 -->|tests| n8
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading
Agent review details

critique

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The critique lane answered having reviewed 19 of 19 files and reports 0 findings; it notes the code index is behind this pull request, so retrieved context may be out of date.
  • Lane summary: The change adapts the judge initialization test to the new optional `credential` field and the added `host` parameter, adding coverage for the lazy host-supplied credential path and its rejection when no trusted host is given. The calls match `configured_automatic`'s signature and the assertions follow the function's real branches; I found no defect worth reporting in this file. _The code index is behind this pull request (indexed at `f555538b51fc`), so retrieved context may be out of date._

security

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: The diff only updates a test file to match the new `JudgeConfig.credential: Option<Secret>` shape and the new `configured_automatic` signature, adding coverage for the host-supplied credential path. The calls match the definitions in `adapter.rs` and `policy.rs`; no security or behavioural defect is introduced. _The code index is behind this pull request (indexed at `f555538b51fc`), so retrieved context may be out of date._

tests

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: The changes since the last review are the adapter wiring for the host-supplied judge credential and the tests around it, and they carry their own assertions: the new `configured_automatic` branch is covered by the missing-host error case and the lazy-host success case, the `HostClient` reuse/rejection/reinit paths have deterministic clock-driven tests, and the wire form of the new callback types is pinned. The reinit recovery path now keeps the credential source, so a hosted judge survives a reinit; nothing in this delta looks unsafe to merge. _The code index is behind this pull request (indexed at `f555538b51fc`), so retrieved context may be out of date._

commits

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The commits lane answered and found no sensitive content in committed files.
  • Lane summary: No sensitive content in committed files. _The code index is behind this pull request (indexed at `f555538b51fc`), so retrieved context may be out of date._

description

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The description lane answered and found the incremental diff matches the description, but flags a behavioural gap: the reinitialization path can never acquire a host credential, so a running module cannot move from a config-supplied credential to a host-served one.
  • Lane summary: The new commits only update the module's approval test to the new `configured_automatic` signature and add coverage for the no-credential branch; the description already documents that branch and the test changes. Nothing in this increment contradicts the PR body. _The code index is behind this pull request (indexed at `f555538b51fc`), so retrieved context may be out of date._
Evidence and run details
  • Models: ladder/vectors-oai3, deepseek-v4.1-flash
  • Spend: $0.003681
  • Tokens: 236117 input · 4495 output · 56320 cached · 744 embedding
  • Updated: 2026-11-11T15:17:26Z
  • Continuity: summary cache chain restarted at the storage ceiling.
Head State Pass summary
cafb02a424e4 incomplete 0 active finding(s), 0 resolved finding(s) (at 2026-11-11T11:19:54Z)
b3c67225d15c ready for maintainer review 0 active finding(s), 0 resolved finding(s) (at 2026-11-11T13:09:54Z)
fa45856a6266 ready for maintainer review 0 active finding(s), 0 resolved finding(s) (at 2026-11-11T15:17:26Z)

tinysweeper 0.1.2

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tinysweeper could not complete coverage (13 unanswered item(s)), so this is not an approval.

Diagnostics

Total spend: $0.0116

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tinysweeper found nothing blocking. Approving.

Diagnostics

Total spend: $0.0456

@senamakel
senamakel merged commit dd1efe0 into tinyhumansai:main Nov 12, 2026
27 of 31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Judge credential is init-only, so hosts can't use a credential the module's own keyring holds

2 participants