From 4340dc4301a5498114e1bdcaee0379ef6b941f24 Mon Sep 17 00:00:00 2001 From: teernisse Date: Thu, 12 Feb 2026 16:14:01 -0500 Subject: [PATCH] Update beads: close sync/external-refs epics, track Wave 8 work --- .beads/issues.jsonl | 1 + 1 file changed, 1 insertion(+) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 586b49e..d533432 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -2,6 +2,7 @@ {"id":"bd-161","title":"Epic: Sync and Updates","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-12T16:22:23.251895Z","created_by":"tayloreernisse","updated_at":"2026-02-12T20:56:19.814767Z","closed_at":"2026-02-12T20:56:19.814725Z","close_reason":"All child beads completed","compaction_level":0,"original_size":0,"labels":["epic"]} {"id":"bd-16o","title":"Wire fetch command with local file, stdin, and remote URL support","description":"## Background\nThe fetch command is the primary entry point for getting specs into swagger-cli. It orchestrates: HTTP download (or local file/stdin read), format detection, YAML normalization, index building, pointer validation, and crash-consistent cache write. It supports auth headers, bearer tokens, auth profiles, --force overwrite, and robot output.\n\n## Approach\nImplement src/cli/fetch.rs with FetchArgs struct and async execute() function:\n\n**FetchArgs (clap derive):**\n- url: String (positional, required — can be URL, file path, or \"-\" for stdin)\n- alias: String (--alias, required)\n- header: Vec (--header, repeatable)\n- auth_header: Vec (--auth-header, alias for --header)\n- bearer: Option (--bearer)\n- auth_profile: Option (--auth-profile)\n- force: bool (--force)\n- timeout_ms: u64 (--timeout-ms, default 10000)\n- max_bytes: u64 (--max-bytes, default 26214400)\n- retries: u32 (--retries, default 2)\n- input_format: Option (--input-format, values: auto/json/yaml)\n- resolve_external_refs: bool (--resolve-external-refs)\n- ref_allow_host: Vec (--ref-allow-host, repeatable)\n- ref_max_depth: u32 (--ref-max-depth, default 3)\n- ref_max_bytes: u64 (--ref-max-bytes, default 10MB)\n- allow_private_host: Vec (--allow-private-host, repeatable)\n- allow_insecure_http: bool (--allow-insecure-http)\n\n**Execute flow:**\n1. Validate alias format (validate_alias)\n2. Check if alias exists — error unless --force\n3. Resolve auth (merge --auth-profile with explicit headers, --bearer)\n4. Determine source: URL (http/https), local file (file:// or path), or stdin (-)\n5. For URL: use AsyncHttpClient.fetch_spec() with SSRF/HTTPS policy\n6. For local file: canonicalize path, read bytes directly (no network policy)\n7. For stdin: read all bytes from stdin\n8. Detect format, normalize to JSON\n9. Parse as serde_json::Value\n10. Build index (build_index)\n11. Write cache (write_cache with all artifacts)\n12. Output robot JSON or human success message\n\n## Acceptance Criteria\n- [ ] `swagger-cli fetch ./petstore.json --alias pet` succeeds (local file)\n- [ ] `swagger-cli fetch https://... --alias pet --robot` outputs JSON with ok:true, data.alias, data.endpoint_count\n- [ ] `swagger-cli fetch - --alias stdin-api` reads from stdin\n- [ ] Alias validation rejects \"../bad\" before any network call\n- [ ] Existing alias without --force returns ALIAS_EXISTS (exit 6)\n- [ ] --force overwrites existing alias\n- [ ] --bearer TOKEN adds Authorization: Bearer TOKEN header\n- [ ] --auth-profile loads from config.toml\n- [ ] Robot output includes: alias, url, version, title, endpoint_count, schema_count, cached_at, source_format, cache_dir, files, content_hash\n- [ ] Auth header values never appear in output or error messages\n\n## Files\n- MODIFY: src/cli/fetch.rs (FetchArgs, execute, auth resolution, source routing)\n- MODIFY: src/output/robot.rs (add output_fetch function)\n- MODIFY: src/output/human.rs (add output_fetch function)\n\n## TDD Anchor\nRED: Write integration test `test_fetch_local_file` — use assert_cmd to run `swagger-cli fetch tests/fixtures/petstore.json --alias test-pet --robot` with SWAGGER_CLI_HOME set to tempdir. Assert exit 0, stdout JSON has ok:true.\nGREEN: Implement full fetch pipeline.\nVERIFY: `cargo test test_fetch_local_file`\n\nAdditional tests:\n- test_fetch_alias_exists_error\n- test_fetch_force_overwrites\n- test_fetch_stdin\n- test_fetch_bearer_auth\n- test_fetch_yaml_file (if YAML fixture exists)\n\n## Edge Cases\n- stdin (\"-\") is not a URL — don't try to HTTP-fetch it\n- Local file paths must be canonicalized (resolve symlinks, relative paths) before reading\n- file:// URLs must be converted to local paths (strip scheme)\n- If auth-profile references an EnvVar source, resolve the env var at fetch time\n- Robot output: redact --bearer and --auth-header values even in success output\n\n## Dependency Context\nUses AsyncHttpClient from bd-3b6 (async HTTP client with SSRF protection). Uses build_index, detect_format, normalize_to_json from bd-189 (spec format detection and index building). Uses CacheManager.write_cache and validate_alias from bd-1ie (cache write path). Uses Config for auth profiles from bd-1sb (configuration system).","status":"closed","priority":2,"issue_type":"task","created_at":"2026-02-12T16:26:35.220966Z","created_by":"tayloreernisse","updated_at":"2026-02-12T19:25:35.255563Z","closed_at":"2026-02-12T19:25:35.255378Z","close_reason":"Implemented in Wave 4 commit","compaction_level":0,"original_size":0,"labels":["fetch","phase1"],"dependencies":[{"issue_id":"bd-16o","depends_on_id":"bd-189","type":"blocks","created_at":"2026-02-12T16:34:06.059316Z","created_by":"tayloreernisse"},{"issue_id":"bd-16o","depends_on_id":"bd-1ie","type":"blocks","created_at":"2026-02-12T16:34:06.154074Z","created_by":"tayloreernisse"},{"issue_id":"bd-16o","depends_on_id":"bd-1sb","type":"blocks","created_at":"2026-02-12T16:34:06.248426Z","created_by":"tayloreernisse"},{"issue_id":"bd-16o","depends_on_id":"bd-3b6","type":"blocks","created_at":"2026-02-12T16:34:06.005417Z","created_by":"tayloreernisse"},{"issue_id":"bd-16o","depends_on_id":"bd-3d2","type":"blocks","created_at":"2026-02-12T16:26:35.222663Z","created_by":"tayloreernisse"},{"issue_id":"bd-16o","depends_on_id":"bd-3ny","type":"parent-child","created_at":"2026-02-12T16:26:35.222248Z","created_by":"tayloreernisse"}]} {"id":"bd-189","title":"Implement spec format detection, YAML normalization, and index building","description":"## Background\nAfter downloading raw spec bytes, swagger-cli must detect the input format (JSON vs YAML), normalize YAML to JSON, parse the spec as serde_json::Value, and build a SpecIndex from it. The index building is the heart of the query performance -- it pre-extracts endpoints, schemas, and tags into a compact, sorted structure with JSON pointers back to raw.json. All pointers must be validated.\n\n## Approach\nCreate src/core/indexer.rs with:\n\n**Format detection:** `detect_format(bytes, filename_hint, content_type_hint) -> Format` where Format is Json or Yaml. Priority: explicit --input-format flag > Content-Type header > file extension > content sniffing (try JSON parse first, fall back to YAML).\n\n**YAML normalization:** `normalize_to_json(bytes, format) -> Result>` -- if YAML, parse with serde_yaml then serialize to serde_json. If JSON, pass through (validate it parses).\n\n**Index building:** `build_index(raw_json: &serde_json::Value, content_hash: &str, generation: u64) -> Result`:\n1. Extract info.title, info.version, openapi version\n2. Iterate paths.* -> methods -> build IndexedEndpoint with: path, method (uppercased), summary, description, operation_id, tags, deprecated, parameters (name/location/required/desc), request_body_required, request_body_content_types, security_schemes (effective: operation-level overrides root-level), security_required, operation_ptr (JSON pointer format: /paths/~1pet~1{petId}/get)\n3. Iterate components.schemas.* -> build IndexedSchema with name, schema_ptr\n4. Compute tags from endpoints + root-level tags -> IndexedTag with endpoint_count\n5. Sort deterministically: endpoints by (path ASC, method_rank ASC), schemas by (name ASC), tags by (name ASC)\n6. Validate ALL operation_ptr and schema_ptr resolve in the raw Value\n7. Set index_version to current version constant (1)\n\n**Canonical method ranking:** GET=0, POST=1, PUT=2, PATCH=3, DELETE=4, OPTIONS=5, HEAD=6, TRACE=7, unknown=99.\n\n**JSON pointer encoding:** Path segments use RFC 6901: `~` -> `~0`, `/` -> `~1`. So `/pet/{petId}` becomes `/paths/~1pet~1{petId}`.\n\n## Acceptance Criteria\n- [ ] JSON input detected and passed through correctly\n- [ ] YAML input detected and normalized to equivalent JSON\n- [ ] build_index extracts correct endpoint count from petstore spec (19 endpoints)\n- [ ] Endpoints sorted by path ASC, method_rank ASC (deterministic)\n- [ ] Schemas sorted by name ASC\n- [ ] Tags have correct endpoint_count\n- [ ] All operation_ptr values resolve in raw Value (validated during build_index)\n- [ ] Invalid pointer causes fetch failure (not silent corruption)\n- [ ] JSON pointer encoding handles /pet/{petId} correctly (escapes /)\n- [ ] Security inheritance: operation without security inherits root; operation with empty [] means no auth\n\n## Files\n- CREATE: src/core/indexer.rs (detect_format, normalize_to_json, build_index, method_rank, json_pointer_encode, validate_pointers)\n- MODIFY: src/core/mod.rs (pub mod indexer;)\n\n## TDD Anchor\nRED: Write `test_build_index_petstore` -- load tests/fixtures/petstore.json as Value, call build_index, assert endpoint_count == 19 and endpoints are sorted.\nGREEN: Implement full index building.\nVERIFY: `cargo test test_build_index_petstore`\n\nAdditional tests:\n- test_detect_format_json, test_detect_format_yaml\n- test_yaml_normalization_roundtrip\n- test_json_pointer_encoding\n- test_method_rank_ordering\n- test_security_inheritance\n- test_invalid_pointer_rejected\n\n## Edge Cases\n- Some specs have paths with special chars in operation IDs -- don't assume alphanumeric\n- serde_yaml may produce different JSON than direct JSON parse (number types, null handling) -- normalize consistently\n- Large specs (8MB+ GitHub) should still build index in <1s\n- OpenAPI 3.1 may use `webhooks` key -- ignore for MVP (only extract from `paths`)\n- Tags defined at root level but not used by any operation should still appear with endpoint_count=0\n\n## Dependency Context\nUses SpecIndex, IndexedEndpoint, IndexedSchema, IndexedTag, IndexedParam types from bd-ilo (error types + core models bead).","status":"closed","priority":2,"issue_type":"task","created_at":"2026-02-12T16:26:35.194671Z","created_by":"tayloreernisse","updated_at":"2026-02-12T17:41:12.926777Z","closed_at":"2026-02-12T17:41:12.926730Z","close_reason":"Format detection, YAML normalization, index building with pointer validation","compaction_level":0,"original_size":0,"labels":["fetch","phase1"],"dependencies":[{"issue_id":"bd-189","depends_on_id":"bd-3ny","type":"parent-child","created_at":"2026-02-12T16:26:35.195659Z","created_by":"tayloreernisse"},{"issue_id":"bd-189","depends_on_id":"bd-ilo","type":"blocks","created_at":"2026-02-12T16:26:35.196142Z","created_by":"tayloreernisse"}]} +{"id":"bd-19m","title":"Detect HTML responses and suggest correct OpenAPI spec URL","description":"When fetching a URL like /api/docs that returns an HTML page (e.g. Swagger UI), the tool falls through format detection to YAML parsing, producing a cryptic error: 'YAML parse error: mapping values are not allowed in this context'. Should detect text/html Content-Type or HTML content patterns early and return a helpful error like: 'URL returned an HTML page, not an API spec. FastAPI serves specs at /openapi.json -- did you mean that?' Context: Content-Type is already extracted in http.rs read_response(). Detection can happen in normalize_to_json() or earlier in the fetch pipeline before format sniffing.","status":"open","priority":3,"issue_type":"feature","created_at":"2026-02-12T21:26:46.705830Z","created_by":"tayloreernisse","updated_at":"2026-02-12T21:26:46.727015Z","compaction_level":0,"original_size":0,"labels":["error-handling","ux"]} {"id":"bd-1bp","title":"Implement fetch-time external ref bundling","description":"## Background\nOptional fetch-time external ref bundling. When --resolve-external-refs is passed during fetch, external $ref targets are fetched and inlined into raw.json. Requires explicit --ref-allow-host allowlist. Bounded by --ref-max-depth and --ref-max-bytes.\n\n## Approach\nAfter fetching the main spec and parsing as Value:\n1. Walk the Value tree looking for $ref values that don't start with \"#/\"\n2. For each external ref: parse URL, check host against --ref-allow-host allowlist (reject if not listed)\n3. Fetch external ref content (uses AsyncHttpClient, respects SSRF policy)\n4. Track total bytes fetched (abort at --ref-max-bytes)\n5. Track resolution depth (abort at --ref-max-depth for ref chains)\n6. Replace the $ref object with the fetched content\n7. Store bundled result as raw.json; original (with $ref pointers) as raw.source\n\n## Acceptance Criteria\n- [ ] --resolve-external-refs without --ref-allow-host returns USAGE_ERROR\n- [ ] External refs to allowed hosts are fetched and inlined\n- [ ] External refs to disallowed hosts are rejected with PolicyBlocked\n- [ ] --ref-max-depth limits chain resolution\n- [ ] --ref-max-bytes limits total fetched content\n- [ ] Bundled raw.json has all external refs resolved; raw.source preserves originals\n\n## Edge Cases\n- **Circular external refs:** A.yaml $refs B.yaml which $refs A.yaml. Must detect and break cycles (return error or annotate).\n- **Relative refs:** `$ref: \"./schemas/Pet.yaml\"` must resolve relative to the base spec URL, not CWD.\n- **Mixed internal + external refs:** Only resolve external refs (not starting with #/). Internal refs stay as-is.\n- **External ref returns non-JSON/YAML:** Return InvalidSpec error for that ref, don't fail the entire fetch.\n- **--ref-max-bytes exceeded:** Abort cleanly and report how many refs were resolved before the limit.\n\n## Files\n- CREATE: src/core/external_refs.rs (resolve_external_refs, walk_refs, fetch_ref)\n- MODIFY: src/cli/fetch.rs (integrate external ref resolution after main fetch)\n- MODIFY: src/core/mod.rs (pub mod external_refs;)\n\n## TDD Anchor\nRED: Write `test_external_ref_resolution` — create a spec with external $ref, mock the external URL, run fetch --resolve-external-refs --ref-allow-host localhost, verify ref is inlined.\nGREEN: Implement ref walking and fetching.\nVERIFY: `cargo test test_external_ref_resolution`\n\n## Dependency Context\nUses AsyncHttpClient from bd-3b6. Extends fetch pipeline from bd-16o.","status":"closed","priority":3,"issue_type":"task","created_at":"2026-02-12T16:29:50.213098Z","created_by":"tayloreernisse","updated_at":"2026-02-12T20:55:47.311619Z","closed_at":"2026-02-12T20:55:47.311573Z","close_reason":"Completed by agent swarm","compaction_level":0,"original_size":0,"labels":["global","phase2"],"dependencies":[{"issue_id":"bd-1bp","depends_on_id":"bd-16o","type":"blocks","created_at":"2026-02-12T16:29:50.214577Z","created_by":"tayloreernisse"},{"issue_id":"bd-1bp","depends_on_id":"bd-3ll","type":"parent-child","created_at":"2026-02-12T16:29:50.214143Z","created_by":"tayloreernisse"}]} {"id":"bd-1ck","title":"Implement diff command with structural comparison and CI gates","description":"## Background\nThe diff command compares two spec states structurally. It reports added, removed, and modified endpoints and schemas by comparing normalized indexes. Supports alias-vs-alias, alias-vs-URL, and --fail-on breaking for CI gates.\n\n## Approach\nImplement src/cli/diff.rs with DiffArgs and async execute():\n\n**DiffArgs:** left (String, positional), right (String, positional), fail_on (Option — \"breaking\"), details (bool).\n\n**Source resolution:** LEFT and RIGHT can be alias names or URLs. If URL, fetch into a temp location, build index, use for comparison (don't persist to cache).\n\n**Diff computation (reuse from sync):**\n- Compare endpoint sets by (path, method): added = in right not left, removed = in left not right, modified = in both but different (compare summary, tags, deprecated, parameters, security)\n- Compare schema sets by name: added, removed, modified (compare schema_ptr target content hashes? or just name-based for Phase 2)\n- Summary: total_changes, has_breaking (Phase 2: just structural; Phase 3: semantic classification)\n\n**--fail-on breaking:** If breaking changes detected, exit non-zero (exit code 17). Phase 2 heuristic: removed endpoint = breaking, removed required parameter = breaking. Added endpoint/schema = non-breaking.\n\n## Acceptance Criteria\n- [ ] `diff alias1 alias2 --robot` reports structural changes\n- [ ] Added/removed/modified endpoints correctly identified\n- [ ] --fail-on breaking exits non-zero when breaking changes exist\n- [ ] URL as right-side source works (temp fetch, not persisted)\n- [ ] Robot output: left, right, changes.endpoints, changes.schemas, summary\n\n## Edge Cases\n- **Same spec on both sides:** Return ok:true with changed:false, empty change lists. Not an error.\n- **URL-as-source fails to fetch:** Return Network error, not a diff-specific error.\n- **Breaking change heuristic false positives:** Endpoint renamed (removed old + added new) looks like a breaking removal. Phase 2 is heuristic-only — document this limitation.\n- **Very large diff (thousands of changes):** Apply same 200-item cap as sync --details with truncated flag.\n- **Schema comparison:** Phase 2 is name-based only (added/removed by name). Modified schema detection is Phase 3 (requires deep structural comparison).\n\n## Files\n- MODIFY: src/cli/diff.rs (DiffArgs, execute, compute_diff, classify_breaking)\n- MODIFY: src/output/robot.rs (add output_diff)\n- MODIFY: src/output/human.rs (add output_diff)\n\n## TDD Anchor\nRED: Write `test_diff_detects_added_endpoint` — fetch petstore twice (one modified), run diff, assert added endpoint appears.\nGREEN: Implement index comparison.\nVERIFY: `cargo test test_diff_detects_added`\n\n## Dependency Context\nReuses index diff logic from sync command (bd-3f4). Uses AsyncHttpClient for URL-as-source (bd-3b6). Uses indexer for building temp index from URL (bd-189).","status":"closed","priority":3,"issue_type":"task","created_at":"2026-02-12T16:30:58.972480Z","created_by":"tayloreernisse","updated_at":"2026-02-12T20:06:06.273101Z","closed_at":"2026-02-12T20:06:06.273054Z","close_reason":"Diff command implemented with structural comparison, CI gates, and 9 unit tests","compaction_level":0,"original_size":0,"labels":["diff","phase2"],"dependencies":[{"issue_id":"bd-1ck","depends_on_id":"bd-189","type":"blocks","created_at":"2026-02-12T16:30:58.976840Z","created_by":"tayloreernisse"},{"issue_id":"bd-1ck","depends_on_id":"bd-21m","type":"parent-child","created_at":"2026-02-12T16:30:58.975608Z","created_by":"tayloreernisse"},{"issue_id":"bd-1ck","depends_on_id":"bd-3f4","type":"blocks","created_at":"2026-02-12T16:30:58.976270Z","created_by":"tayloreernisse"}]} {"id":"bd-1cv","title":"Implement global network policy (offline/auto/online-only)","description":"## Background\nThe --network global flag controls whether swagger-cli makes network calls. \"auto\" (default) allows network when needed. \"offline\" blocks all network calls (fetch/sync fail with OFFLINE_MODE). \"online-only\" flags when network would be needed but is not available. This is critical for CI reproducibility and agent sandboxing.\n\n## Approach\n\n### NetworkPolicy Enum (src/core/network.rs)\nCreate `NetworkPolicy` enum with three variants:\n- **Auto** (default): Allow network calls when needed, no restrictions. Standard behavior.\n- **Offline**: Block ALL outbound network calls preemptively. Any command that would make a network request (fetch, sync) returns `SwaggerCliError::OfflineMode` immediately without attempting the call. Commands that are index-only (list, search, show, tags) work normally.\n- **OnlineOnly**: Allow network calls but surface a clear, distinct error if a network call would be needed AND DNS resolution fails or the host is unreachable. The error is different from OfflineMode — it indicates \"you requested online-only mode but network is unavailable\" rather than \"network calls are blocked by policy.\"\n\n### Implementation\n1. Parse --network flag in CLI (already in Cli struct from skeleton bead)\n2. Create `NetworkPolicy` enum and `check_network_policy()` function in `src/core/network.rs`\n3. Check policy before any HTTP call in AsyncHttpClient — if Offline, return `SwaggerCliError::OfflineMode` without making the request\n4. For OnlineOnly: attempt the request, but if it fails due to network issues, return a specific `SwaggerCliError::NetworkUnavailable` (different exit code or error code than OfflineMode)\n5. `SWAGGER_CLI_NETWORK` env var as alternative to --network flag (flag takes precedence)\n\n### Scope boundaries\n- **Local file sources (file:// paths, stdin):** Network policy does NOT apply. These are local I/O operations. `fetch ./local-spec.yaml --network offline` should succeed because no network call is made.\n- **Stdin sources:** Same as local files — no network needed, policy irrelevant.\n- **sync --dry-run in offline mode:** ALLOWED. Dry-run compares cached state without actually fetching, so no network call is needed. Returns cached state comparison only.\n\n## Acceptance Criteria\n- [ ] NetworkPolicy enum with Auto, Offline, OnlineOnly variants in src/core/network.rs\n- [ ] --network offline causes fetch (remote URL) to fail with OFFLINE_MODE error (exit 15)\n- [ ] --network offline allows fetch from local file path (no network needed)\n- [ ] --network offline allows fetch from stdin (no network needed)\n- [ ] --network offline allows list/search/show/tags (index-only, no network needed)\n- [ ] --network offline allows sync --dry-run (cached comparison only, no network)\n- [ ] --network online-only surfaces clear error when network is unavailable (distinct from OFFLINE_MODE)\n- [ ] --network auto allows all commands (default behavior, no restrictions)\n- [ ] SWAGGER_CLI_NETWORK=offline env var works same as --network offline flag\n- [ ] Flag takes precedence over env var when both are set\n- [ ] Robot error for offline mode has code OFFLINE_MODE with suggestion to remove --network flag or unset env var\n- [ ] Robot error for online-only network failure has code NETWORK_UNAVAILABLE with distinct suggestion\n\n## Edge Cases\n- **sync --dry-run in offline mode:** Allowed — returns cached state comparison only, no actual fetch happens. This is a read-only operation on cached data.\n- **Local file + offline mode:** Allowed — `fetch ./spec.yaml --network offline` succeeds because it is local I/O, not a network call.\n- **OnlineOnly vs Offline distinction:** Offline blocks proactively (never attempts the call). OnlineOnly attempts the call and fails with a specific error if the network is down. This matters for agents that want to know \"was this blocked by policy or by actual network unavailability?\"\n- **Mixed sources:** If a future version supports specs that reference remote $ref URLs but the base spec is local, the network policy should apply to the remote $ref resolution (not the local file read).\n\n## Files\n- CREATE: src/core/network.rs (NetworkPolicy enum, check_network_policy function)\n- MODIFY: src/core/http.rs (check policy before fetch)\n- MODIFY: src/cli/fetch.rs (check policy at start, skip for local/stdin sources)\n- MODIFY: src/cli/sync.rs (check policy at start, allow --dry-run in offline)\n\n## TDD Anchor\nRED: Write `test_offline_blocks_fetch` — set SWAGGER_CLI_NETWORK=offline, run fetch with remote URL, assert exit 15 and OFFLINE_MODE error.\nRED: Write `test_offline_allows_local_file` — set offline, fetch local file, assert success.\nRED: Write `test_offline_allows_list` — set offline, run list on cached alias, assert success.\nRED: Write `test_online_only_network_unavailable` — set online-only, mock DNS failure, assert NETWORK_UNAVAILABLE error.\nGREEN: Implement policy check.\nVERIFY: `cargo test test_offline_blocks_fetch`\n\n## Dependency Context\nModifies AsyncHttpClient in bd-3b6 (async HTTP client) to check network policy before requests. Uses SwaggerCliError variants (OfflineMode) from bd-ilo (error types and core data models).","status":"closed","priority":3,"issue_type":"task","created_at":"2026-02-12T16:29:50.156478Z","created_by":"tayloreernisse","updated_at":"2026-02-12T19:36:49.525619Z","closed_at":"2026-02-12T19:36:49.525325Z","close_reason":"Implemented NetworkPolicy enum (Auto/Offline/OnlineOnly), resolve_policy with CLI flag > env var precedence, check_remote_fetch enforcement. Integrated into AsyncHttpClient and fetch command.","compaction_level":0,"original_size":0,"labels":["global","phase2"],"dependencies":[{"issue_id":"bd-1cv","depends_on_id":"bd-16o","type":"blocks","created_at":"2026-02-12T16:29:50.158591Z","created_by":"tayloreernisse"},{"issue_id":"bd-1cv","depends_on_id":"bd-3b6","type":"blocks","created_at":"2026-02-12T16:29:50.158232Z","created_by":"tayloreernisse"},{"issue_id":"bd-1cv","depends_on_id":"bd-3ll","type":"parent-child","created_at":"2026-02-12T16:29:50.157784Z","created_by":"tayloreernisse"}]}