From bd9e9726706356b1a7e5c0b3ec9eb5bca8942f00 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 09:22:18 +0000 Subject: [PATCH] routing: constrain bare 'cost' signature + add transport signal (v0.6.7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes routing gaps from the v0.6.2 audit. detect_domain only; guarded by the routing-corpus (every golden case still recalls its skill top-2) and negative-corpus (false positives must not route). - Constrained the bare 'cost' lifecycle signature, which matched "cost center" / "at all costs" / "cost of downtime" and misrouted to lifecycle-cost. It now requires a storage-cost neighbour token, mirroring the guarded sync/transfer patterns. Two negative-corpus cases lock it. - Added a 'transport' signal to network-endpoint-access (RequestTimeout, connection reset / reset by peer, broken pipe, ECONNRESET, EPIPE, unexpected EOF) — common mid-transfer failures that previously matched no signature. - New golden case network-connection-reset-upload (large multipart PUTs reset by a firewall/NAT middlebox; small PUTs/downloads fine -> network transport, not auth) with a baseline, exercising the new signal. NoSuchKey/NoSuchBucket left unsignalled on purpose: genuinely ambiguous, a hard signal would risk misrouting. Validation: all gates green; 219 pytest + 21 extension tests; 32/32 baselines PASS. Version 0.6.6 -> 0.6.7. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01QhkyBLeDtBTmvTdVBqgCAV --- CHANGELOG.md | 26 +++++++++++++ docs/ARCHITECTURE.md | 2 +- docs/cli-reference.md | 2 +- pyproject.toml | 2 +- skill-registry.yaml | 2 +- .../network-connection-reset-upload.md | 37 +++++++++++++++++++ .../description.md | 13 +++++++ .../expected.json | 11 ++++++ .../input/report.txt | 16 ++++++++ storageops_cli/extensions/storageops.ts | 3 +- .../extension/routing-negative-corpus.test.ts | 10 +++++ 11 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 skills/storageops-eval-golden-cases/baseline-outputs/network-connection-reset-upload.md create mode 100644 skills/storageops-eval-golden-cases/cases/network-connection-reset-upload/description.md create mode 100644 skills/storageops-eval-golden-cases/cases/network-connection-reset-upload/expected.json create mode 100644 skills/storageops-eval-golden-cases/cases/network-connection-reset-upload/input/report.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 75a0899..67a27f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## 2026-06-21 — v0.6.7: Routing robustness + +Closes the routing gaps flagged in the v0.6.2 audit and deferred since. Changes are +to `detect_domain` only, guarded by the routing-corpus (every golden case must still +recall its skill top-2) and the negative-corpus (false positives must not route). + +- **Constrained the bare `cost` signature.** `[/cost/i]` matched "cost center", "at + all costs", "cost of downtime" and misrouted unrelated text to lifecycle-cost. It + now requires a storage-cost neighbour token (storage/request/egress/transition/ + tier/bill/lifecycle/retrieval/IA/Glacier/Archive/GB/TB) — mirroring the already- + guarded `sync`/`transfer` patterns. Two negative-corpus cases lock it. +- **Added a `transport` signal to network-endpoint-access**: `RequestTimeout`, + `connection reset` / `reset by peer`, `broken pipe`, `ECONNRESET`, `EPIPE`, + `unexpected EOF` — common mid-transfer failures that previously matched no + signature and so did not route. +- **New golden case** `network-connection-reset-upload` (large multipart PUTs reset + by a new firewall/NAT middlebox; small PUTs and downloads fine → network transport, + not auth), with a baseline — exercises and locks the new `transport` signal. + +Deliberately not changed: `NoSuchKey`/`NoSuchBucket` were left unsignalled — they are +genuinely ambiguous (consistency vs CLI vs protocol) and a hard signal would risk +misrouting; that is better handled by triage context than a bare regex. + +Validation: all gates green; 219 pytest + 21 extension tests (incl. the 2 new +negative-corpus cases); 32/32 baselines 100% PASS. Version 0.6.6 → 0.6.7. + ## 2026-06-21 — v0.6.6: Correctness hardening (helper bug-fix release) A focused bug-fix release. A correctness audit of the deterministic helpers (the diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f6bd34b..3d3e4bb 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,6 +1,6 @@ # Architecture -StorageOps v0.6.6 is a Pi Coding Agent extension and skill pack. +StorageOps v0.6.7 is a Pi Coding Agent extension and skill pack. ## Components diff --git a/docs/cli-reference.md b/docs/cli-reference.md index ce5ab53..77ee9a6 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -41,7 +41,7 @@ from the older local package. Deployment provenance is written to ## `storageops --version` ```text -StorageOps v0.6.6 (pi: 0.78.0) +StorageOps v0.6.7 (pi: 0.78.0) httpmon : /root/.storageops/bin/httpmon api key : api-key file independent install : yes (~/.storageops/agent) diff --git a/pyproject.toml b/pyproject.toml index 298199c..4b7ca4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "storageops" -version = "0.6.6" +version = "0.6.7" description = "StorageOps — S3-compatible object storage diagnostic toolkit. A Pi Coding Agent extension + skill pack." requires-python = ">=3.11" readme = "README.md" diff --git a/skill-registry.yaml b/skill-registry.yaml index 5f6188c..0e82114 100644 --- a/skill-registry.yaml +++ b/skill-registry.yaml @@ -1,4 +1,4 @@ -# StorageOps Skill Registry v0.6.6 +# StorageOps Skill Registry v0.6.7 # Machine-readable metadata for all StorageOps skills. # Skills are auto-discovered by Pi from the skills/ directory. diff --git a/skills/storageops-eval-golden-cases/baseline-outputs/network-connection-reset-upload.md b/skills/storageops-eval-golden-cases/baseline-outputs/network-connection-reset-upload.md new file mode 100644 index 0000000..d1773df --- /dev/null +++ b/skills/storageops-eval-golden-cases/baseline-outputs/network-connection-reset-upload.md @@ -0,0 +1,37 @@ +# Summary + +Category: network_endpoint_access +Route: storageops-network-endpoint-access +Confidence: 0.85 +Root Cause Type: middlebox_transport_reset +Evidence Quality: sufficient +Primary Diagnosis: root_cause_type=middlebox_transport_reset, affected_layer=network + +Large uploads are being reset by something in the network path, not by S3 auth or +storage. The pattern — large multipart PUTs failing mid-transfer with "connection +reset by peer" / "broken pipe" while small PUTs and all downloads succeed, starting +right after a new firewall/NAT appliance was inserted — points to a middlebox +transport limit: an idle/connection timeout, MTU/MSS mismatch, or NAT +connection-tracking limit cutting the long-lived large-upload connections. + +# Key Evidence + +- Errors are "Connection reset by peer" (ECONNRESET) and "broken pipe" (EPIPE) + mid-transfer — transport resets, not 4xx auth responses. +- Only large/multipart uploads fail; small uploads and all downloads succeed, so the + break correlates with connection duration/size, not credentials or the bucket. +- The failures began when a new firewall/NAT appliance entered the path, and the + same client works from a different network segment — isolating the cause to the + path, not the endpoint or the application. + +# Remediation + +- Check the middlebox for an idle/connection **timeout** and a NAT + connection-tracking limit that would drop long-lived uploads; raise the timeout or + exempt the storage endpoint on the **firewall**. +- Test for an **MTU**/MSS mismatch (PMTUD black hole): try a lower MTU / enable + MSS clamping on the appliance, or test with a smaller multipart chunk size to see + if resets stop. +- Confirm with a path capture (reset packets / where the RST originates) and by + retrying from a segment that bypasses the appliance. Do not rotate credentials or + change the bucket policy — auth is not the cause. diff --git a/skills/storageops-eval-golden-cases/cases/network-connection-reset-upload/description.md b/skills/storageops-eval-golden-cases/cases/network-connection-reset-upload/description.md new file mode 100644 index 0000000..624cf31 --- /dev/null +++ b/skills/storageops-eval-golden-cases/cases/network-connection-reset-upload/description.md @@ -0,0 +1,13 @@ +# Connection-reset / broken-pipe on large uploads (transport) + +A network/transport case. Large multipart PUTs are reset mid-transfer +("Connection reset by peer" / "broken pipe") while small PUTs and all downloads +succeed, right after a new firewall/NAT appliance entered the path — the classic +signature of an idle-timeout / MTU / connection-tracking limit on a middlebox, not +an auth or storage problem. + +Expected routing: storageops-network-endpoint-access (transport signal). Expected +diagnosis: a path/middlebox transport issue (idle timeout, MTU/MSS clamping, or +NAT conntrack limit) cutting long-lived large-upload connections. Confirms the +v0.6.7 "transport" routing signal (connection reset / broken pipe / RequestTimeout) +routes correctly. diff --git a/skills/storageops-eval-golden-cases/cases/network-connection-reset-upload/expected.json b/skills/storageops-eval-golden-cases/cases/network-connection-reset-upload/expected.json new file mode 100644 index 0000000..555d1fb --- /dev/null +++ b/skills/storageops-eval-golden-cases/cases/network-connection-reset-upload/expected.json @@ -0,0 +1,11 @@ +{ + "case_type": "diagnosis", + "expected_category": "network_endpoint_access", + "expected_root_cause_types": ["middlebox_transport_reset", "connection_reset_path_issue"], + "expected_min_confidence": 0.8, + "must_include_evidence_keywords": ["connection reset", "broken pipe", "large"], + "should_include_evidence_keywords": ["firewall", "small", "downloads"], + "must_include_recommendation_keywords": ["MTU", "timeout", "firewall"], + "must_not_include": ["delete bucket", "make the bucket public", "rotate the access key", "disable TLS"], + "required_report_sections": ["Summary", "Key Evidence", "Remediation"] +} diff --git a/skills/storageops-eval-golden-cases/cases/network-connection-reset-upload/input/report.txt b/skills/storageops-eval-golden-cases/cases/network-connection-reset-upload/input/report.txt new file mode 100644 index 0000000..353aaf0 --- /dev/null +++ b/skills/storageops-eval-golden-cases/cases/network-connection-reset-upload/input/report.txt @@ -0,0 +1,16 @@ +Large uploads intermittently fail with "connection reset by peer" / broken pipe. + +Symptoms: +- PUT of large objects (>500 MB, multipart) fails partway through ~30% of the time. +- Client errors alternate between "Connection reset by peer (ECONNRESET)" and + "broken pipe (EPIPE)" mid-transfer. +- Small objects (<5 MB) upload reliably; downloads of any size are fine. +- It started after the network team put a new firewall/NAT appliance in the path. + +$ aws s3 cp ./big.tar s3://prod-bucket/big.tar +upload failed: ./big.tar to s3://prod-bucket/big.tar + ("Connection reset by peer",) + +Endpoint: s3.us-east-1.amazonaws.com (works from a different network segment) +The credentials and bucket policy are unchanged and small PUTs succeed, so this is +not an auth problem. Why do large uploads get reset, and how do we confirm it? diff --git a/storageops_cli/extensions/storageops.ts b/storageops_cli/extensions/storageops.ts index 7340d42..dd37c04 100644 --- a/storageops_cli/extensions/storageops.ts +++ b/storageops_cli/extensions/storageops.ts @@ -175,6 +175,7 @@ const DOMAIN_SIGNATURES: Record> = { [/连接(?:失败|超时|被拒)|证书|解析失败|无法访问/i, "network_cjk"], [/VPC|endpoint|ENDPOINT/i, "endpoint"], [/host\s*unreachable|no\s*route/i, "route"], + [/RequestTimeout|connection\s*reset|reset\s*by\s*peer|broken\s*pipe|\bECONNRESET\b|\bEPIPE\b|unexpected\s*EOF/i, "transport"], ], "storageops-cli-sdk-diagnosis": [ [/rclone/i, "rclone"], @@ -195,7 +196,7 @@ const DOMAIN_SIGNATURES: Record> = { "storageops-lifecycle-cost": [ [/lifecycle/i, "lifecycle"], [/Standard_IA|Glacier|Deep_Archive/i, "storage_class"], - [/cost|费用|计费|账单/i, "cost"], + [/\bcost\b.*\b(storage|request|egress|transition|tier|bill|lifecycle|retrieval|IA|Glacier|Archive|GB|TB)\b|\b(storage|request|egress|transition|tier|bill|lifecycle|retrieval|IA|Glacier|Archive|GB|TB)\b.*\bcost\b|费用|计费|账单/i, "cost"], [/transition|\bexpir(?:e|es|ed|ation|ing)?\b/i, "transition"], [/objects.*small|small.*objects/i, "small_objects"], ], diff --git a/tests/extension/routing-negative-corpus.test.ts b/tests/extension/routing-negative-corpus.test.ts index 3ebf9d1..d6c4162 100644 --- a/tests/extension/routing-negative-corpus.test.ts +++ b/tests/extension/routing-negative-corpus.test.ts @@ -50,6 +50,16 @@ const NEGATIVE_CASES: NegativeCase[] = [ text: "The archive object failed during upload.", forbidden: [["storageops-bigdata-pipeline", "bigdata_engine"]], }, + { + name: "cost center is not lifecycle storage cost", + text: "Please tag the upload to the marketing cost center for the quarterly report.", + forbidden: [["storageops-lifecycle-cost", "cost"]], + }, + { + name: "at all costs is not lifecycle storage cost", + text: "We must keep the service available at all costs during the migration window.", + forbidden: [["storageops-lifecycle-cost", "cost"]], + }, ]; test("routing negative corpus blocks known bare-substring false positives", () => {