Skip to content

✨ feat: ADR-011 local mode + credential source abstraction#71

Merged
chaizhenhua merged 6 commits into
masterfrom
adr-011-local-mode-and-credential-sources
May 24, 2026
Merged

✨ feat: ADR-011 local mode + credential source abstraction#71
chaizhenhua merged 6 commits into
masterfrom
adr-011-local-mode-and-credential-sources

Conversation

@chaizhenhua
Copy link
Copy Markdown
Contributor

@chaizhenhua chaizhenhua commented May 24, 2026

Summary

Introduces oversight local, a single-user, no-login, host-native deployment that boots from any project directory and auto-discovers the user's claude / codex / openclaw / hermes CLI agents. Built on a generalised CredentialSource abstraction so cloud and local share one dispatch path.

  • CredentialSource enum (Vault / HostNative / EnvPassthrough / ExternalRef) carried on ResolvedCredential; resolver dispatches on credential_id prefixes (host-native:<adapter>, env:, ext:, vault:), fully backward-compatible with the legacy <ns>:<id> form.
  • oversight local subcommand wires the permissive Authorizer, injects a synthetic local user, runs host discovery on boot, and idempotently seeds agent_definitions rows with credential_id = "host-native:<adapter>".
  • SecretMaterial::EnvFromHost building block for worker env passthrough (resolver side reserved as stub).
  • Frontend consumes the new mode: "local" field on /api/auth/status and skips the Login route.
  • Migration V095 adds per-profile scope / scope_id for future multi-tenant filtering (additive, no data migration). The originally-planned V094 credential_source column was dropped — the credential_id prefix is authoritative and the source is derived at read time via derive_credential_source_kind.

Details and tradeoffs: apps/www/src/content/docs/architecture/adr-011-local-mode-and-credential-sources.md (中文镜像同步)

Test plan

  • cargo test --workspace --lib1179 passed, 0 failed across 10 crates
  • npx vitest run — 13 frontend auth tests pass
  • ESLint + Prettier + rustfmt clean (commit hooks)
  • End-to-end oversight local:
    • /api/auth/status returns {mode: "local", bootstrapped: true} (no login required)
    • Discovery seeded claude / codex / openclaw / hermes from PATH with correct credential_id = "host-native:<kind>"
    • /api/agent-definitions/:id/resolved-credential returns source: {kind: "host-native", adapter_kind: "claude"}
    • Re-running the subcommand is idempotent (logs "already seeded; skipping"; no duplicate rows)
  • Manual smoke: drive a real agent invocation in local mode against ~/.claude/ config
  • Manual smoke: cloud-mode regression (existing oversight serve still requires login and resolves vault credentials)

@chaizhenhua
Copy link
Copy Markdown
Contributor Author

Pushed 5 commits addressing all 10 review items.

Blockers

#1 dispatch adapter mismatch (72d38ac3) — build_dispatch_payload now rejects with 400 when req.adapter_type != agent_def.agent_type; regression test pair (dispatch_payload_rejects_adapter_type_mismatch_against_agent_def + dispatch_payload_accepts_matching_adapter_type). Verified at runtime: mismatched call returns {"error":"dispatch request adapter_type='codex' does not match agent 'claude' (agent_type='claude'); refused (fail-closed)"}.

#2 server_mode vs permissive coupling (72d38ac3) — new explicit ServerMode::{Cloud, Local} field on AppState. auth_mw::require_auth and /api/auth/status consult state.server_mode, never authorizer.is_permissive(). Tests cover both branches.

#3 status depends on auth store (72d38ac3) — local mode short-circuits to {bootstrapped: true, mode: "local"} before any count_auth_users() call. Cloud mode unchanged. Two new tests (auth_status_local_mode_short_circuits_to_bootstrapped, auth_status_cloud_mode_reports_bootstrap_false_on_empty_store).

#4 credential_source orphan column (f5a141d1) — chose the simpler of the two reviewer options: dropped V094 entirely. The credential_id prefix is already authoritative; added derive_credential_source_kind(Option<&str>) -> Option<&'static str> static helper for admin code that wants to group rows by source without re-running the async resolver. Truth-table test pins every prefix. ADR-011 updated to drop the V094 mention.

#5 #6 local seed conflict + fail-fast (538b86aa) — local seed now distinguishes StoreError::NotFound from other DB errors; existing row with non-host-native credential_id emits a clear warning instead of silently leaving the conflict in place; any non-NotFound DB error fails fast (anyhow::bail!). list_active_agent_defs() no longer hides errors in local mode.

P1/P2

#7 discovery binary path accuracy (e5c3c7bf) — description text now reads "detected at <path> during local-mode startup; worker re-resolves PATH at dispatch" so log readers can't mistake it for the canonical exec path. find_in_path canonicalizes via std::fs::canonicalize so symlinks resolve to the real on-disk binary.

#8 login_status gates active (e5c3c7bf) — LoginStatus::NeedsLogin now seeds the row with active = false; LoggedIn and Unknown stay active. Two new tests pin both branches.

#9 resolved-credential fast path (7110e80d) — the endpoint now inspects the credential_id prefix and synthesises the response for host-native: / env: / ext: without touching the credential pool. Vault and Auto still require the pool.

#10 EnvFromHost fail-closed (7110e80d) — added required: bool field (defaults true via serde, so legacy payloads decode safely). Worker materializer errors with a named-var error message when a required host var is missing. Two new worker tests (env_from_host_required_and_missing_fails_materialization, env_from_host_not_required_yields_empty_string_when_missing) plus a vault-side round-trip test for the default.

Test totals

Crate passed failed
oversight-vault 25 0
oversight-authz 56 0
oversight-agents 120 0
oversight-server 363 0
oversight-worker 268 0
oversight-store 181 0
oversight-models 86 0
oversight-tools 37 0
oversight-policy 44 0
oversight-store-fs 8 0
total 1188 0

End-to-end against a fresh data dir with claude / codex / openclaw / hermes on PATH: /api/auth/status returns {mode:"local", bootstrapped:true}, 4 host-native agents seeded (all active=true because all four have config dirs), resolved-credential surfaces source.kind="host-native", dispatch refuses adapter mismatch.

@chaizhenhua
Copy link
Copy Markdown
Contributor Author

Pushed c89c7d2e with two product-bug fixes uncovered during end-to-end verification against the awaken-managed-agents project (local mode + host-native claude).

Fix A — stale agent.model no longer aborts the run

Symptom in e2e: PUT /api/agent-definitions with model="claude-sonnet-4-5" silently saved; the next dispatch died 3s in with failed to set ACP model to 'claude-sonnet-4-5'. The adapter's actual advertised list was ["default", "sonnet", "haiku"].

Fix (crates/oversight-worker/src/adapter.rs): extracted the model-set policy into decide_set_model(requested, advertised) -> SetModelDecision. Empty → skip. No advertised list → apply (don't second-guess). In list → apply. Not in list → warn + fall back to session default so the run continues.

Live evidence after re-run with the same bad value:

WARN configured model is not in the adapter's advertised list;
     falling back to the session default. requested_model=claude-sonnet-4-5
     advertised=["default", "sonnet", "haiku"]

Run terminated natural_end with 4476 chars of real analysis. Pre-fix: error: failed to set ACP model.

Truth-table tested in 5 unit tests (empty/no-advert/match/mismatch/no-fallback-when-listless).

Fix B — agent's final_output surfaces on the issue

Symptom in e2e: requirement_analyzer (Claude) completed natural_end with a real design analysis, but /api/issues/:id/comments was empty, no document/attachment, no child issue. The output existed only in .oversight/threads/runs/<id>.json. Per ADR-007 it's the agent's job to call comment_on_issue / create_document, but a safety net for the case where the agent doesn't call any surfacing tool was missing.

Fix (crates/oversight-server/src/api.rs::handle_scheduled_agent_terminal_event): on NaturalEnd with non-empty response_text, insert a dedicated agent_output activity carrying the final output (capped at 8KB; full text always lives in the thread store). Skipped on errors (run_finished already carries the reason) and on empty/whitespace-only output. Additive — run_finished activity unchanged.

Live evidence after re-run:

sqlite> SELECT activity_type, summary FROM agent_activities WHERE issue_id='...';
run_finished | termination=NaturalEnd
agent_output | ANALYSIS_DECISION: approved
               ANALYSIS_SUMMARY: Add a project-root CHANGELOG.md following...

3 unit tests cover NaturalEnd→surfaced, error→skipped, whitespace→skipped.

Total test counts

Per-crate lib totals (existing + new):

  • oversight-worker: 273 (was 268; +5 for decide_set_model_*)
  • oversight-server: 368 (was 365; +3 for terminal_event_*_agent_output_activity_*)
  • vault/authz/agents/store/models/tools/policy/store-fs unchanged
  • Workspace total: 1196 passed, 0 failed

Findings noted but NOT fixed in this PR

  • Codex CLI bubblewrap sandbox fails on this host with bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted — every file/shell op blocked. Mitigation: use Claude. Not an Oversight bug; would be a host or codex-CLI config issue.
  • Token/cost tracking for CLI-native runs shows tokens=0 cost=0.0 because the CLI's usage stream isn't captured into the run record. Real gap, but out of ADR-011 scope.
  • Requirement workflow doesn't auto-spawn implementation child issues — by design per ADR-007 (agents drive decomposition via tool calls). Would need a workflow/prompt customization, not a code fix.

@chaizhenhua
Copy link
Copy Markdown
Contributor Author

chaizhenhua commented May 24, 2026

Pushed 2f8b86ee addressing the remaining e2e findings.

Code fix — token totals on activity rows

⚠️ Correction (2026-05-24): the original wording below claimed
tokens flow to BOTH run_finished AND agent_output. That was
wrong — R2 review #5 (commit 9953903c) made run_finished the
canonical billing row to prevent SUM double-counting. Updated:

Token totals from RunRecord propagate to the canonical run_finished
activity row. agent_output intentionally keeps token fields at 0
to avoid double-counting when both activities exist for one run
(SUM(token_input) grouped by issue stays accurate). For host-native
CLI runs the run record's tokens are 0 (the CLI does its own LLM
calls and doesn't feed usage back into awaken yet); the activity
honestly carries 0 instead of fabricating a number.

handle_scheduled_agent_terminal_event loads the RunRecord once,
lifts started_at for the agent_output dedupe window (R3 review
P2a adds a handler-entry-time fallback so the dedupe still works
when the record is missing), and fans (input_tokens, output_tokens)
onto the run_finished row only. Pinned by
terminal_event_propagates_run_record_tokens_to_run_finished_only.

(The rest of the original comment — about server URL plumbing and
the other e2e fixes pushed in this commit — still applies.)

@chaizhenhua
Copy link
Copy Markdown
Contributor Author

Pushed 6 commits resolving all 8 actionable R2 items (skipping #9 per reviewer's "not blocking" note).

Commits

a2ab771e ✨ feat(local): overlay host-native pin on seeded role agents; fail-fast conflict
9953903c 🛡️ fix(activity): lock ordering, dedupe agent_output, single token row
84b5c90a 🛡️ fix(dispatch): no-pool path honors host-native credential_id
4aa9abd4 ♻️ refactor(credentials): share credential-id prefix parser
ddc8845a 📝 docs(agents): correct credential_source comment to runtime-derive

Per-item resolution

Blockers

  • R2#1 — seeded role agents now wired to host-native in local mode. After discovery, oversight local iterates list_agent_defs(), finds rows with credential_id is None whose agent_type matches a discovered adapter (and that aren't the discovery-seeded agent_id == adapter_type rows themselves), and overlays credential_id = "host-native:<agent_type>". Explicit pins are preserved. Live evidence on a fresh wipe of awaken-managed-agents/.oversight: 10 role agents (coder/reviewer/deployer/requirement_analyzer/triage/workflow_designer/insight_extractor/acceptance_verifier/claude_delegate/codex_delegate) all got the right host-native pin; log lines local mode: overlaid host-native pin onto seeded role agent agent_id=coder agent_type=codex pin=host-native:codex.

  • R2#2 — conflict is now fail-fast, not warn-and-continue. When oversight local finds an existing agent_id with a non-host-native credential_id, anyhow::bail! with the conflicting id + actual pin. Operator must delete/rename the row to make progress. No more "did local mode actually use my CLI?" ambiguity.

  • R2#3 — agent_output now deduped against agent-authored comments. Sequence is now: insert run_finished (always) → evaluate_rules (lets ReactionAction::Comment post) → check list_issue_comments for any agent:<this agent_id> comment with created_at >= run.started_at → only insert agent_output when none found. New test terminal_event_skips_agent_output_when_agent_already_commented pins the contract.

P1/P2

  • R2#4 — thread_store.load_run lifted out of the store-mutex section. RunRecord lookup runs before acquiring state.store.lock(), so file IO no longer serialises every store-touching request.

  • R2#5 — tokens on run_finished only. agent_output is documented as a surfacing event, not a billing event. Test asserts agent_output.token_input == 0 and agent_output.token_output == 0 even when the run has nonzero totals.

  • R2#6 — dispatch no-pool fallback honours credential_id prefixes. Pre-fix, state.credential_pool is None fell through to resolve_for_adapter which ignored credential_id entirely. Now uses the shared parse_credential_id_ref helper to short-circuit host-native: / env: / ext: correctly. Two new tests cover the happy path and the adapter-mismatch fail-closed.

  • R2#7 — stale comment in discovery.rs removed. agent_def_for_discovered doc no longer claims it sets credential_source (it doesn't; the field was dropped before merge).

  • R2#8 — parse_credential_id_ref shared between resolver + read endpoint. Returns a typed CredentialIdRef enum (HostNative / HostNativeMissingAdapter / EnvPassthrough / ExternalRef / Vault / Malformed). Both resolve_credential_for_agent and get_agent_resolved_credential now route through it — single source of truth for the prefix vocabulary. Truth-table test parse_credential_id_ref_truth_table pins every variant.

R2#9 (not blocking, per reviewer) — skipped. decide_set_model already logs the chosen fallback at WARN with the requested + advertised list; adding effective_model to the run record is a follow-up.

Test totals after this round

Crate Tests
oversight-vault 25
oversight-authz 56
oversight-agents 120
oversight-worker 273
oversight-store 181
oversight-models 86
oversight-tools 37
oversight-policy 44
oversight-store-fs 8
oversight-server 372 (was 368; +1 dispatch no-pool happy, +1 mismatch fail-closed, +1 parser truth-table, +1 surfacing dedupe)
Total 1202 passed / 0 failed

@chaizhenhua
Copy link
Copy Markdown
Contributor Author

Pushed cc9d7bcc covering all R3 review items.

P1 — local host-native overlay now honors NeedsLogin

main.rs overlay used to collect adapter kinds into a HashSet<String>, losing the per-adapter LoginStatus. That meant a claude binary on PATH but unauthenticated would mark the directly-discovered claude row inactive (correct), but requirement_analyzer / coder / reviewer all got the host-native:claude pin overlaid AND stayed active=true — letting dispatch call an unauthenticated CLI.

  • Extracted oversight_agents::discovery::apply_host_native_overlay(&mut def, &HashMap<String, LoginStatus>) -> bool so the rule is testable in isolation
  • NeedsLogin adapter now flips overlaid seeded role agents to active=false (LoggedIn / Unknown leave active untouched — same policy as the direct adapter row's agent_def_for_discovered)
  • Operator-set credential_id and the discovery-seeded adapter row itself (agent_id == agent_type) are still skipped
  • main.rs builds discovered_by_adapter: HashMap<String, LoginStatus> from the discovery scan and delegates to the helper, then logs both pin and active so operators see the resolved state in startup logs
  • 5 new unit tests covering: logged-in pinning, NeedsLogin → inactive, operator pin preserved, adapter-row skipped, unmatched agent_type ignored

P2a — agent_output dedupe now safe when RunRecord is missing

Before: dedupe checked comment.created_at >= run_record.started_at. When RunRecord was absent the check fell through to unwrap_or(false) — meaning a reaction-posted comment landing during evaluate_rules would not block a duplicate agent_output from being inserted.

After: let handler_started_at = chrono::Utc::now(); taken at handler entry, and dedupe_cutoff = run_started_at.unwrap_or(handler_started_at) is used by both the rule-input enrichment AND the post-rule dedupe. Reaction-posted comments (which always land after handler entry) are now reliably detected. Pinned by new test terminal_event_skips_agent_output_when_comment_arrives_without_run_record.

P2b — PR-comment / docs token wording corrected

Edited #issuecomment-4527813177 to add a correction header. Code/test contract: token totals flow to run_finished only; agent_output keeps 0 to avoid SUM(token_*) double-counting.

P3 — malformed *_ACP_ARGS_JSON now logs a warning

resolve_acp_bin_and_args used to silently discard a malformed env var and fall back to manifest defaults, which made "why are my custom args ignored?" hard to diagnose. Now emits tracing::warn!(env_var, error) before falling back. Existing resolve_acp_bin_invalid_json_args_falls_back test still passes (behaviour preserved; observability added).

Test results

  • cargo test -p oversight-agents --lib125 passed, 0 failed
  • cargo test -p oversight-worker --lib275 passed, 0 failed
  • cargo test -p oversight-server --lib375 passed, 0 failed (was 374; +1 R3 test)

@chaizhenhua
Copy link
Copy Markdown
Contributor Author

Pushed 2222d68a.

Note on review cache

The latest review was against a stale view (15 commits / 2f8b86e). The branch HEAD is now 2222d68a. Several items flagged were already fixed in cc9d7bcc (pushed earlier, CI green):

  • P1 local overlay → NeedsLogin: apply_host_native_overlay flips overlaid seeded role agents to active=false when the adapter is NeedsLogin (5 unit tests in discovery.rs)
  • P2a missing-RunRecord dedupe: dedupe_cutoff = run_started_at.unwrap_or(handler_started_at) (test terminal_event_skips_agent_output_when_comment_arrives_without_run_record)
  • P2b token wording: PR comment #issuecomment-4527813177 corrected
  • P3 ACP arg warn: resolve_acp_bin_and_args now tracing::warn!s on malformed *_ACP_ARGS_JSON

Genuinely new in 2222d68a

P0 — second dispatch ingress (/api/cli-agents/:id/dispatch) was still fail-open

Correct — cli_agent_api.rs::dispatch_issue had the same no-pool fall-through to resolve_for_adapter(..., None, ...) that ignored agent_def.credential_id. Rather than duplicate the fix, extracted a single shared resolver:

// credentials.rs
pub async fn resolve_dispatch_credential(
    vault, pool: Option<&Arc<CredentialPool>>, agent_def,
) -> Result<Option<ResolvedCredential>, DispatchCredentialRefusal>

Both /internal/dispatch (dispatch_api.rs) and /api/cli-agents/:id/dispatch (cli_agent_api.rs) now call it, so the two ingresses can't drift again. Contract: pool present → resolve_credential_for_agent (explicit-refusal → Err); no pool + matching host-native:<adapter> → fast path; no pool + mismatch / env: / ext: / vault pin / malformed → Err (fail-closed); no pin → legacy adapter pick.

6 new helper tests in credentials.rs: dispatch_cred_no_pool_{host_native_match,host_native_mismatch,vault_pin,malformed_id,env_pin,unpinned}. The existing dispatch_payload_no_pool_* tests still pass through the refactored path.

P1 — existing host-native adapter row now refreshed on re-run

oversight local re-runs discovery every boot; the "already seeded; skipping" branch left active stale across claude login/logout. New discovery::refresh_host_native_login_state(&mut def, &discovered) -> bool resyncs active + description (only login-derived fields; operator edits to other fields survive). Wired into the seed loop's already-seeded branch. 3 tests: flips off on lost login, flips on on restored login, no-op when unchanged.

P3 — ADR migration wording

ADR-011 (EN + zh-CN) "V094 and V095 are additive" corrected — V094 (credential_source column) was dropped; only V095 ships. Regenerated apps/www docs tree (drift gate).

Test results

  • oversight-agents --lib128 passed (+3)
  • oversight-server --lib381 passed (+6)
  • oversight-worker --lib — 275 passed (unchanged this round)

@chaizhenhua chaizhenhua force-pushed the adr-011-local-mode-and-credential-sources branch from 2222d68 to ba08723 Compare May 24, 2026 23:17
@chaizhenhua
Copy link
Copy Markdown
Contributor Author

History rewritten into 6 atomic commits (force-pushed; tree byte-identical to the previously CI-green 2222d68a+follow-up). The ~27 incremental review-response fix(...) commits are folded into the feature commit they belong to:

cd2c5dd4 📐 docs(adr): ADR-011 local mode & credential source abstraction
a72ea195 ✨ feat(vault): SecretMaterial::EnvFromHost variant + worker host-env reader
22071a38 ✨ feat(credentials): CredentialSource abstraction + unified fail-closed dispatch resolver
16065562 ✨ feat(agents): host-native discovery + codex ACP arg/MCP plumbing
7ee37cbd ✨ feat(server): oversight local subcommand, ServerMode, host-native dispatch wiring
ba087233 ✨ feat(web): mode-aware UI — skip login in local mode

Review items — status

P0 (direct CLI dispatch no-pool fail-open) — fixed. Both ingresses now route through one shared resolver credentials::resolve_dispatch_credential(vault, pool, agent_def):

  • /internal/dispatch (dispatch_api.rs) and /api/cli-agents/:id/dispatch (cli_agent_api.rs) both call it
  • no pool + matching host-native:<adapter> → fast path; no pool + mismatch / env: / ext: / vault pin / malformed → FAILED_DEPENDENCY (fail-closed); no pin → legacy adapter pick
  • 6 helper tests dispatch_cred_no_pool_* + the existing dispatch_payload_no_pool_* go through the refactored path
    (this landed in 2222d68a, which your cached view at cc9d7bc predated)

P1 (stale host-native row on re-run) — fixed, both cases:

  • direct adapter row (agent_id == agent_type): refresh_host_native_login_statesafety-only, forces inactive on NeedsLogin, never auto-reactivates (preserves manual disable, per your note)
  • already-overlaid role agent (credential_id == host-native:<adapter>): apply_host_native_overlay no longer early-returns on credential_id.is_some() — it falls through to the same safety refresh
  • new tests: apply_host_native_overlay_marks_already_overlaid_role_inactive_when_login_lost, apply_host_native_overlay_does_not_reactivate_already_overlaid_role, refresh_does_not_reactivate_on_login_restored

P3 (PR summary stale migration) — fixed. PR description now reads "Migration V095 … the originally-planned V094 credential_source column was dropped". ADR (EN+zh-CN) already corrected.

Tests

  • oversight-agents --lib 130 passed
  • oversight-server --lib 381 passed
  • oversight-worker --lib 275 passed
    (one pre-existing flaky acp_bridge::…_forwards_each_agent_message_chunk_through_sink passes in isolation — unrelated to this PR)

@chaizhenhua chaizhenhua force-pushed the adr-011-local-mode-and-credential-sources branch from ba08723 to 350514f Compare May 24, 2026 23:35
@chaizhenhua
Copy link
Copy Markdown
Contributor Author

Pushed 350514f0. History kept at the same 6 atomic commits (force-push); the new fixes are folded into the feat(server) commit, so the log stays clean.

P0/P1 (blocker) — inactive agents now refused at both dispatch ingresses

active = false is the local-mode NeedsLogin gate, but neither explicit ingress enforced it (both fetch by id, bypassing list_active_agent_defs). Added a shared guard:

// dispatch_api.rs
pub(crate) fn ensure_agent_dispatchable(agent_def: &AgentDef) -> Result<(), Response>
  • /internal/dispatch (build_dispatch_payload) calls it right after the adapter-mismatch check, before credential resolution / scheduling
  • /api/cli-agents/:id/dispatch calls it right after ensure_cli_authorized, before resolution
  • inactive → 424 FAILED_DEPENDENCY with agent_id / agent_type in the body
  • uniform refusal for all inactive agents (no disabled_reason field yet, so operator-disabled and login-disabled are treated the same — safest default)
  • tests: dispatch_payload_rejects_inactive_agent, ensure_agent_dispatchable_refuses_inactive_allows_active

P2 — resolved-credential read endpoint now mirrors dispatch

get_agent_resolved_credential no-pool branch previously returned a generic unavailable for vault / malformed pins. It now calls the same resolve_dispatch_credential(vault, None, def) helper and maps a refusal → { "mode": "pinned", "note": <refusal message> }. Read and dispatch can't diverge by construction. (The 6 dispatch_cred_no_pool_* tells already cover the helper's refusal output the endpoint surfaces.)

P3 — updated_at bumped on login-state refresh

main.rs now sets def.updated_at = Utc::now() before persisting in both the direct-adapter-row refresh and the role-agent overlay paths (update_agent_def writes the field verbatim, so it needed an explicit bump).

Tests

  • oversight-server --lib 383 passed (+2)
  • oversight-agents --lib 130 passed
  • oversight-worker --lib 275 passed

Note on base

origin/master advanced 4 commits (adapter tool catalogs) during this round; the branch is intentionally kept on its original merge-base so the reviewed diff stays scoped. Happy to rebase onto latest master as a separate step before merge if you'd prefer.

@chaizhenhua chaizhenhua merged commit b84f4d8 into master May 24, 2026
5 checks passed
@chaizhenhua chaizhenhua deleted the adr-011-local-mode-and-credential-sources branch May 24, 2026 23:40
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