6596b259fcc00d4a58668b4bae813a08e843d4b4
16
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b0868be6b3 |
fix(runtime): recognize heartbeat interval env and wire production lifecycle tests
Address REQUEST_CHANGES review #652 on PR #976 (issue #975). Blocker 1: name GITEA_WORKER_HEARTBEAT_INTERVAL_SECONDS individually in RECOGNIZED_GITEA_ENV_KEYS so the production-consumed operator override is no longer classified as unsupported-env / runtime_reconnect_required. No prefix broadening; unknown overrides remain fail-closed. Blocker 2: add focused production-path tests that call _active_worker_identity and _start_worker_heartbeat, proving supervisor attachment, identity/session/ generation/pid/epoch agreement, no duplicates, failed/fenced paths, orderly shutdown, configured interval, and off-loop sqlite heartbeats. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> |
||
|
|
1a38ef95e3 |
fix(runtime): recognize client identity environment and refresh worker registrations
Two defects left behind by #948 made sanctioned multi-client operation
impossible. They are inseparable: fixing either alone still leaves the
multi-client canary unable to run.
1. Client-identity environment keys were not recognized.
gitea_mcp_server reads GITEA_MCP_CLIENT, GITEA_MCP_CLIENT_INSTANCE and
GITEA_MCP_CLIENT_SESSION as the authoritative client-identity inputs for
worker registration, but none of the three appeared in
RECOGNIZED_GITEA_ENV_KEYS or matched a recognized prefix. The runtime
diagnostic scans every peer mcp_server.py process environment and classifies
any unlisted GITEA_* key as an unsupported override, which is raised as a
runtime blocker, so gitea_resolve_task_capability returned
blocker_kind=runtime_reconnect_required with stop_required=true. Because that
resolver is the mandatory preflight for every author, reviewer and merger
mutation, setting the very variable #948 requires closed the mutation gate
for the whole fleet, and reconnecting could not clear it: the variable is
re-exported from the client's server definition on every launch.
The three keys are now named individually in the recognized-key set. No
prefix is added, so an unrecognized GITEA_* override is still refused
exactly as before.
A related inconsistency in the same path is also fixed. The diagnostic
reasons are raised as one RuntimeError, but the preflight re-raise
recognized only "stale-runtime:", so an "unsupported-env:" reason was
silently swallowed there while still failing the resolver. Both reason
families now live in RUNTIME_DIAGNOSTIC_HARD_PREFIXES beside the function
that produces them, and both propagate identically. This only widens what is
refused, never what is permitted.
2. WorkerRegistry.heartbeat() had no production caller.
#948 delivered heartbeat() but only tests called it. The single production
writer registers once per process behind an attempted-once flag, and
register() stamps the same timestamp into both started_at and
last_heartbeat_at. Nothing advanced it afterwards: no lifespan hook, no
background task, no atexit handler in a process that blocks in mcp.run().
Since liveness is age against heartbeat_ttl_seconds, that TTL was not a
liveness window at all but a hard cap on how long any client could stay
attached; at 900 seconds a healthy, connected, client-managed process became
session_ownership=unowned with blocker_kind=session_attachment_missing.
WorkerHeartbeatSupervisor in mcp_worker_identity is the missing caller,
started from _active_worker_identity() at the moment register() succeeds,
because that is the only point where identity and fencing_epoch are both
known. It is a daemon thread rather than an asyncio task or a request-driven
refresh because renewal must survive an idle session, and because the
registry performs blocking BEGIN IMMEDIATE sqlite writes that must not run on
the server's event loop. daemon=True is deliberate: a hard kill takes the
thread with it, so a dead worker still goes stale on the normal TTL.
heartbeat_interval_for() returns one third of the TTL, hard-capped at one
half, so two consecutive beats can be lost without the row expiring and no
override can produce an interval that outlives the registration it renews.
heartbeat() gains optional keyword-only expectations (session, generation,
client name, pid); each supplied one must match the recorded row or the
renewal is refused with the existing BLOCKER_FENCED literal rather than a new
blocker_kind, since consumers switch on that value. Omitting them preserves
the pre-existing behavior exactly. Client names are compared normalized, so
several namespaces of one application stay one client while separate
applications stay distinct.
A terminal refusal stops the supervisor permanently and records why, so a
fenced session can never beat its way back into ownership. A transient
failure is counted and beating continues. An atexit hook stops it on orderly
shutdown. status() is surfaced read-only as worker_heartbeat on
gitea_get_runtime_context so a stopped heartbeat is diagnosable before the
TTL turns it into session_attachment_missing; it grants nothing.
claim_generation() still has no production caller. It bumps fencing_epoch,
which would fence the supervisor's cached epoch, and the strict refusal is
left in place deliberately: auto-re-adopting a bumped epoch would defeat
fencing.
No lock or lease TTL is changed, including the author issue-lock TTL, and no
mutation refused today becomes permitted.
Tests: tests/test_issue_975_client_identity_heartbeat.py adds 40 focused tests
covering all 13 acceptance criteria. Every TTL assertion uses an injected
clock; no test waits for a real TTL. The thread-loop tests use a
millisecond-scale interval with bounded polling.
Focused: 40 passed, 15 subtests passed.
Full suite from inside the branches worktree: 28 failed, 6105 passed, 6 skipped,
1105 subtests — an identical failure set to the
|
||
|
|
6a53308473 | fix(runtime): support validated cross-repository canonical roots (#973) | ||
|
|
dfe8d7c28d | fix(mcp): detect and reject manually launched duplicate MCP role servers (Closes #686) | ||
|
|
61c3a57df5 |
fix(mcp): consume canonical target root in cross-repository operations (Closes #741)
#706 introduced the immutable `canonical_repository_root` and #739/#740 routed
three consumption paths through it. The paths #740 left behind all shared one
shape: the *filesystem* guards had been migrated to the canonical root, but
*repository identity* still bottomed out in `_local_git_remote_url`, which ran
`git remote get-url` with `cwd=PROJECT_ROOT` — always the Gitea-Tools install
checkout. The two halves of a single assessment therefore described two
different repositories.
For a namespace bound to another repository this inverts the guards rather than
merely weakening them: an operation naming the genuinely bound target is
rejected, while one naming Gitea-Tools is accepted.
Central helper
--------------
Adds `_canonical_local_git_root()` as the single place a target-repository root
is resolved: the configured canonical root when one is declared, else
`PROJECT_ROOT`. A configured-but-invalid root is never silently replaced by the
install checkout. Single-repository behaviour is byte-for-byte unchanged.
Defects fixed
-------------
* `_local_git_remote_url` now runs in the canonical target root, which corrects
every downstream identity consumer at once (`_resolve`, the #530 remote/repo
guard, the anti-stomp org/repo fill, `_workspace_repository_slug`).
* `_verify_role_mutation_workspace` omitted `configured_canonical_root`, so the
#274 branches-only / worktree-membership guards validated
`Gitea-Tools/branches/` instead of the bound target. It now threads the root
exactly as `_resolve_namespace_mutation_context` does.
* `_resolve` derived omitted coordinates by looking a remote up *by name*. A
target checkout commonly names its remote `origin` rather than `prgs`, so the
lookup returned None and the coordinates fell through to the remote-wide
default target — an unrelated repository. It now prefers
`_canonical_repository_slug`, which probes candidate remote names.
* Explicit `org`/`repo` short-circuit the #530 match check, so caller
coordinates bypassed validation entirely. They may now only *confirm* a
canonical binding, never override it, and fail closed in both directions.
* Reconciler ancestry, fetch, worktree-inventory and cleanup call sites, the
author worktree derivation in `gitea_lock_issue`/`gitea_create_pr`, and the
`control_clean` porcelain probe now use the canonical target root.
* `gitea_config`: v2-`environments` silently *dropped* `canonical_repository_root`
during flattening, so such a namespace fell back to the install root — a
fail-open. It is now validated and propagated. The v1 path validates it too,
so all three loaders behave identically.
Preserved as installation-scoped
--------------------------------
Server parity (`master_parity_gate`), workflow/schema/skill loading, self-code
hashing and stale-runtime detection, and `mirror_refs.sh` lookup remain anchored
to `PROJECT_ROOT`. The `server_implementation` vs `target_repository` parity
dimensions from #740 stay separately labelled, and only the server dimension
gates mutations.
Tests
-----
`tests/test_issue_741_canonical_root_consumers.py` (51 tests, 52 subtests) drives
a role-by-operation matrix over author/reviewer/merger/reconciler against:
correct cross-repository target, wrong repository, wrong worktree, explicit
matching and mismatched coordinates, missing/invalid canonical root, immutable
first-bind, and request-override attempts — plus both cross-repository
directions and all three config loaders. Real git repositories, no network, no
branch deletion, no merge.
The module reinstalls the genuine `_local_git_remote_url` per test: an autouse
conftest fixture permanently reassigns it to a working-directory-independent
stub, which is precisely the behaviour under test.
Full suite: 3408 passed, 6 skipped, 2 failed. Both failures
(`test_issue_702_review_findings_f1_f6`, `test_reconciler_supersession_close`)
reproduce identically on clean master
|
||
|
|
0d8a2c2b1d |
feat(mcp): immutable canonical_repository_root for cross-repo namespaces (Closes #706)
The MCP server derived the canonical repository root from the install checkout the server script lives in (PROJECT_ROOT), so any namespace that ran the server against an external repository (e.g. eagenda-author, or the mcp-control-plane reviewer/merger namespaces) failed every mutation: the branches-only / worktree-membership guards (#274) compared the task workspace against the Gitea-Tools .git directory it can never belong to. Separate the two concepts: - PROJECT_ROOT stays the immutable code/install root. - A new, optional, namespace-scoped canonical_repository_root binds the session to the working root of the target repository. Implementation: - canonical_repository_root.py: resolve the binding from profile/env (GITEA_CANONICAL_REPOSITORY_ROOT overrides the profile field), validate existence + git identity, fail closed on missing/conflicting/forged bindings. Preserves the single-repo default when unconfigured. - session_context_binding.py: pin canonical_repository_root into the immutable #714 session context; drift fails closed. - namespace_workspace_binding.py: route the configured target root through resolve_namespace_mutation_context so #274 / membership guards evaluate against the target repository. - gitea_mcp_server.py: seed + enforce the binding in the mutation preflight (both purity-order and FORCE_PRODUCTION_GUARDS paths); validate repository identity and git common-directory membership. - gitea_config.py: validate the optional absolute-path profile field. - gitea_auth.py: surface the config-sourced field through get_profile. No weakening of root-checkout, remote/repository, or anti-stomp guards; the single-repo Gitea-Tools path is unchanged. Tests: two distinct real git repositories/worktrees prove cross-repository bindings resolve, enforce membership, and fail closed on forged/conflicting identity and simultaneous prgs/mdcps isolation. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> |
||
|
|
943d40270e |
fix(session): bind workspace-verified repository scope at first bind (#714)
The independent review reproduced a real hole: the production first-bind path never pinned repository/org, so the drift checks it feeds were skipped. Root cause ---------- gitea_whoami, gitea_get_runtime_context, gitea_resolve_task_capability and gitea_activate_profile all seeded the session context without repository or org. First-bind-wins then made the mutation gate's later seed a no-op, and assess_session_context only compares repository/org when the bound fields are non-null. Result, reproduced in production order with the real REMOTES: after whoami: repository=None org=None same-host repo=Other-Tools -> NOT BLOCKED same-host org=Other-Org -> NOT BLOCKED cross-host -> blocked (this part always worked) Binding REMOTES defaults would not fix it: REMOTES['prgs'].repo is 'Timesheet' (a default *target*, not an authorization scope, see #530), so pinning it fails closed on every legitimate Gitea-Tools mutation. There was no trusted source for repository scope, so this adds one. Design ------ - New optional profile field `allowed_repositories`: canonical owner/repository slugs. An authorization boundary, never the binding itself. Config-only — no env var can widen or forge it. - The session repository is derived from the verified, workspace-aligned git remote, never from caller-supplied org/repo, and validated against that allowlist. The session binds immutably to exactly one canonical slug; org is derived from it. A profile authorizing several repositories still binds to the single verified one. - Every first-bind entry point now pins the same complete context. Activation rejects an unauthorized/unverifiable workspace. Mutations fail closed when repository/org is unverified, and a tool-level org/repo override that disagrees with the binding is rejected before the write. - A mutation request can no longer establish, complete, or replace the binding (it previously seeded from `org or REMOTES[...]`, i.e. caller-controlled). - Enforcement is opt-in per profile: a profile without the field keeps prior behaviour, so existing static-profile namespaces stay functional. First-bind-wins, immutability, the RLock, profile isolation, host/identity validation and the private pytest-only reset are unchanged. gitea_auth.get_profile() built an explicit whitelist dict and silently dropped `allowed_repositories`; the new integration tests caught that the scope was never enforced through the real path. Tests ----- New tests/test_issue_714_production_first_bind.py drives the real entry points in production order rather than constructing _SessionContext directly. Covers complete first bind via each entry point, same-host repo/org drift, Timesheet and other-repo/other-org rejection, unverified and unauthorized workspaces, caller values unable to establish the binding, concurrent init selecting one repository, and the PR #715 author path staying functional. Mutation-verified: disabling trusted derivation, override validation, the scope check, or the get_profile passthrough each fails these tests (9/8/4/5 failures). No security assertion was weakened, removed, skipped, or rewritten. Focused: 26 passed. Issue #714 total: 46 passed. Prior failing modules: 240 passed. Config/profile/remote modules: 130 passed. Full suite: 2739 passed, 6 skipped, 1 pre-existing warning, 161 subtests in 25.80s. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> |
||
|
|
75ed0632b4 | feat(mcp): implement operation-scoped role selection and automatic dispatch switching (#228) | ||
|
|
3953b6bdd2 |
feat: allow gitea.issue.comment for author and reviewer profiles (#137)
- Add aliases 'issue.comment' and 'issue_comment' to GITEA_OPERATION_ALIASES so short names normalize to gitea.issue.comment - Grant 'issue.comment' (and short) in v2 example profiles for example-author and example-reviewer - Grant in gitea-mcp.example.json mdcps-reviewer - Update docs example profiles - Add test coverage for new aliases and normalization This enables gitea_create_issue_comment and list for profiles that include it, while preserving fail-closed for others and separation of duties (issue.comment does not imply pr.review etc). Closes #137 |
||
|
|
a0e7d3360e | feat: implement profile activation and runtime identity clarity (#131) | ||
|
|
e0861bcb03 |
feat: operation-name normalization table with fail-closed enforcement (#106)
Promote the #103 minimal alias map to the documented public table GITEA_OPERATION_ALIASES and add the #106 enforcement layer: - normalize_operation(op, service): canonical namespaced names; legacy spellings accepted only via the explicit table; unknown, ambiguous, and cross-service names fail closed. - check_operation(op, allowed, forbidden, service): normalizes BOTH the requested operation and the profile lists before any membership check; forbidden always overrides allowed; unnormalizable allowed entries grant nothing and unnormalizable forbidden entries deny the request, so normalization can never silently widen permissions; empty/missing allowed list denies everything. - gitea_check_pr_eligibility now routes its capability check through check_operation, fixing the mismatch where canonical namespaced profile ops (gitea.pr.merge) never matched the raw action (merge) and namespaced forbidden entries were never enforced. - Document the normalization table and enforcement rules in docs/gitea-execution-profiles.md, replacing the stale 'enforcement out of scope' caveat. - tests/test_op_normalization.py: full #106 matrix (27 tests) — qualified/legacy allowed and forbidden, unknown, ambiguous, service mismatch, forbidden-overrides-allowed, empty/missing allowed, duplicates after normalization, no permission widening, and eligibility integration proving normalization happens before enforcement. Existing v1/env unqualified behaviour stays compatible. Closes #106 Co-Authored-By: Claude Fable 5 <[email protected]> |
||
|
|
ff920a6496 |
feat: load profiles.json v2 contexts shape with enabled enforcement and LLM-safe output (#120)
Support the canonical contexts-shape version 2 config (contexts / profiles / projects / rules) alongside the existing environments shape and v1: - Require a boolean 'enabled' on every context, profile, service, and project. Disabled entries are surfaced in audits but fail closed at selection/resolution — never a silent fallback to another profile, service, or credential source. - Resolve the active identity from GITEA_MCP_PROFILE via the existing select_profile path; profile base_url falls back to the context's enabled gitea block. - Add resolve_service() and project_for_path() for context service and project-to-context resolution (internal use; fail closed on disabled). - get_auth_header now propagates ConfigError when GITEA_MCP_CONFIG is set instead of silently degrading to Basic auth. - Hide endpoint URLs and keychain ids from normal LLM-facing output: gitea_whoami / gitea_get_profile report logical names and auth status only; new gitea_audit_config tool reports enabled/disabled state and safe one-line service summaries. The GITEA_MCP_REVEAL_ENDPOINTS opt-in (and 'python3 gitea_config.py audit --reveal-endpoints' locally) restores endpoints and auth source names for admin diagnostics; token values are never printed on any path. - Ship gitea-mcp.v2-contexts.example.json (synthetic values) and validate it in tests. Implements #120 Co-Authored-By: Claude Fable 5 <[email protected]> |
||
|
|
65ea7514d2 |
feat: profiles.json v2 parser with validation invariants (#103)
Add version-2 support to gitea_config: environment -> service -> identity
hierarchy flattened at load into v1-shaped profiles keyed by the canonical
dotted address {env}.{service}.{identity}, with aliases for legacy names
(mdcps, prgs-author, prgs-reviewer) and service-level defaults inherited by
identities.
Fail-closed validation: missing required version (v1 files must now declare
version: 1), unknown versions, malformed environment/service/identity
structure, dotted segment names, missing base_url, missing auth reference,
inline secrets in identities or auth entries, alias/address selector
conflicts, aliases to unknown targets, and unqualified operations that
cannot be normalized safely. TBD-* usernames fail closed at selection
without blocking other identities in the file.
Reviewer-identity deadlock rule enforced at load: any identity allowed
gitea.pr.approve or gitea.pr.merge must forbid gitea.pr.create and
gitea.branch.push (prevents the PR #102-style self-authored-PR deadlock).
Selector resolution is strict: exact alias -> exact dotted address -> fail
closed; no fuzzy matching. Minimal operation normalization only (the known
v1 unqualified Gitea ops and single-word non-Gitea ops); the full table and
enforcement matrix remain issue #106.
Tests: new tests/test_config_v2.py (29 cases) covering the acceptance
criteria; test_config.py missing-version case flipped to fail-closed per
the issue. resolve_token/auth_source_name proven against flattened v2
profiles.
Refs #100. Closes #103.
Co-Authored-By: Claude Fable 5 <[email protected]>
|
||
|
|
835fbbf324 |
feat: interactive setup menu for canonical Gitea MCP profiles (#31)
Add an interactive utility so users create/edit/validate canonical runtime profiles and generate safe LLM launcher snippets without hand-editing JSON or pasting tokens into Claude/Gemini/Codex configs. Run: `python gitea_config.py menu` (or `python gitea_config_menu.py`). gitea_config.py — pure, testable authoring helpers: - is_valid_profile_name, build_profile, keychain_auth/env_auth, empty_config - validate_config (reports missing base_url/auth, inline token/password — never echoing the secret value) - add_profile (preserves existing, rejects dup/invalid name/missing base_url), upsert_profile, remove_profile - save_config: mkdir parents + atomic temp-then-os.replace, pretty JSON - launcher_entry: thin MCP entry (command/args + GITEA_MCP_CONFIG/PROFILE only) - keychain_set: store a token via `security add-generic-password` (token passed as an arg, never returned/printed/logged; injectable runner) - `menu` __main__ dispatch gitea_config_menu.py — interactive loop with fully injectable IO/secret/HTTP/ keychain so it is testable without a real terminal, keychain, or network: - list / add / edit / remove / validate profiles - test authentication + show authenticated user (calls /user only on request) - reviewer-eligibility helper (authenticated user vs PR author, open state) — read-only, never approves/merges - launcher snippets for Claude / Gemini / Codex (no secrets) Security: tokens are never written to profiles.json, launcher snippets, logs, or errors — only keychain ids / env var names are stored. Backwards compatible: menu is optional; env-only mode and MCP server startup are unchanged. Tests: tests/test_config_menu.py (21 cases) — name validation, preserve-on-add, dup/invalid/missing-field rejection, atomic write (+ replace-failure leaves the original intact, no temp debris), keychain_set stores-without-printing, launcher snippets secret-free, eligibility eligible/self-author/closed, and a full menu add→list→quit flow proving the token value never reaches disk or stdout. Stacked on #30 (canonical profiles); base branch feat/json-runtime-profiles. Refs #10, #19. Closes #31. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> |
||
|
|
b88ca0c929 |
feat: canonical shared runtime-profiles config with typed auth refs (#19)
Rework the JSON runtime-profile config from the earlier ad-hoc schema (profiles + token_env) to the canonical single-file model in #19, so every LLM launcher can reference one shared Gitea profiles file instead of duplicating GITEA_USER_*/GITEA_PASS_* blocks or embedding tokens. Canonical schema (gitea_config.py): - top-level "version" (1) + "profiles" map. - each profile: base_url, username, default_owner, execution_profile, and a typed auth reference: { "type": "keychain", "id": "..." } -> macOS keychain (security(1)) { "type": "env", "name": "..." } -> named environment variable - inline "token"/"password" keys are rejected (never accepted or echoed). - select via GITEA_MCP_CONFIG (path) + GITEA_MCP_PROFILE (name). gitea_auth integration: - get_profile() overlays env over the selected profile (env wins; JSON fills the rest); profile_name <- execution_profile; token_source_name <- the non-secret auth reference name (env var name or "keychain:<id>"); now also surfaces username + default_owner. - get_auth_header() resolves the profile's auth reference (env/keychain) as a token fallback after explicit env tokens; a ConfigError there fails closed. Security / safety: - Secrets referenced only (keychain id / env name); token values never stored in or returned as metadata. Errors never print file contents, tokens, or passwords (JSONDecodeError context suppressed). - Missing file / invalid JSON / unsupported version / unknown-or-unset profile / unresolvable secret reference all raise a clear, safe ConfigError. - No network calls during config parsing; keychain lookup is on-demand and injectable for tests. - Backwards compatible: GITEA_MCP_CONFIG unset => legacy env-only mode (existing get_profile/get_auth_header tests unchanged). Docs: README canonical-profile + thin-launcher (Claude/Gemini/Codex) sections and a migration note away from duplicated GITEA_PASS_* blocks; .env.example and gitea-mcp.example.json updated to the canonical shape (safe placeholders only). Tests: tests/test_config.py (31 cases) — legacy env-only, JSON selection, multiple profiles, missing/unset profile, invalid JSON, unsupported version, env-override precedence, keychain + env auth-reference parsing and resolution, missing-secret errors, inline token/password redaction, and no-network parse. Refs #10. Completes the closed #19 (env-based profiles) by adding the canonical shared-file model. Supersedes this PR's earlier simpler JSON schema. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> |
||
|
|
3aaba73127 |
feat: JSON multi-profile runtime config for Gitea MCP (roadmap #10)
Let one MCP server select among named Gitea runtime profiles from a JSON file
instead of editing code or juggling many .env files:
GITEA_MCP_CONFIG=/path/to/gitea-mcp.json
GITEA_MCP_PROFILE=dev
- New gitea_config.py: load/validate the JSON, select the named profile, and
resolve its token by env-var reference. Profiles supply base_url,
profile_name, token_env, owner/repo, allowed/forbidden operations, and audit
label.
- gitea_auth.get_profile() now overlays env over the selected JSON profile:
explicit env vars win, the JSON profile fills only what env leaves unset.
- gitea_auth.get_auth_header() gains a JSON token_env fallback after explicit
env tokens (env still wins).
Security / safety:
- Tokens are referenced by env-var NAME (token_env); an inline "token" is
rejected and never echoed. The value is never stored in or returned as
profile metadata.
- Fail-safe errors: missing file / invalid JSON / unknown or unset selected
profile raise a clear ConfigError that never prints file contents or tokens
(JSONDecodeError context is suppressed so the raw file text can't surface).
- No network calls during config parsing.
- Real config files are gitignored (gitea-mcp*.json), example kept.
Backwards compatible: with GITEA_MCP_CONFIG unset, behaviour is exactly the
prior env-only behaviour (all existing get_profile/get_auth_header tests pass
unchanged).
Docs: README JSON-profiles section + env table rows, .env.example placeholders,
gitea-mcp.example.json.
Tests: tests/test_config.py (22 cases) — env-only, selection, multiple
profiles, env-override precedence, missing file, invalid JSON, missing/unset
profile, inline-token rejection + redaction, and no-network-during-parse.
Refs #10. Note: issue #19 (env-based profiles) was already implemented and
closed; this JSON-file capability is adjacent new scope tracked under the
roadmap rather than reopening #19.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
|