Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,9 @@ commands live in the `## Validation Commands` section above, `Makefile`, and
report everything `ok` except the API key. If `~/.storageops` is ever missing,
re-run `storageops install` with the nvm Node on `PATH` (needs network for the
npm Pi install).
- **Offline vs. live.** All gates and `make ci-local` / `storageops eval --baselines`
run fully offline and need no key. **Live diagnosis** needs install + Node/Pi + API key;
check `storageops doctor --json` field `live_diagnosis_available`, then run
`make live-smoke` (3 golden cases + model smoke).
- **Offline vs. live.** All gates and `make ci-local` run fully offline. **Live
diagnosis** needs install + Node/Pi + API key; check `live_diagnosis_available`
in `storageops doctor --json`, then run `make live-smoke` (5 golden cases).
- **Devcontainer.** Open in VS Code / Codespaces with `.devcontainer/devcontainer.json`
(Python 3.12 + Node 22; postCreate runs `dev_setup.sh --persist-path`).
- **PR gate.** Run `make ci-local` before opening a PR. Use `make dev` for first-time
Expand Down
23 changes: 22 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Changelog

## 2026-06-29 — v0.8.0: Extension modules, eval depth, live smoke rotation

### Track A — Extension modularization
Split `storageops_cli/extensions/storageops.ts` into focused modules with zero
behavior change (21 extension tests unchanged):
- `secrets.ts`, `routing.ts`, `memory.ts`, `trace.ts`, plus a thin `storageops.ts`
entry/re-export barrel.
- `storageops install` now deploys **all** `extensions/*.ts` modules.

### Track B — Eval & routing depth
- New golden cases: `cors-method-put-blocked` (3rd `cors_configuration` diagnosis),
`routing-triage-vague-evidence` (triage routing without deep diagnosis).
- Baseline output for `cors-method-put-blocked`.
- `detect_domain` triage signatures for vague first-contact reports.
- Negative corpus case: CORS workshop text must not route to protocol CORS.
- `coverage_check` floor raised to **≥3 cases per baseline category**.

### Track C — Live eval rotation
- `make live-smoke` / CI `diagnosis-smoke` now runs **5** golden cases:
throttling, access-denied, signature-clock-skew, cors-preflight, kms-denied.

## 2026-06-29 — v0.7.2: Live diagnosis smoke & devcontainer

- **`make live-smoke` / `scripts/live_smoke.sh`** — model round-trip plus live
Expand All @@ -8,7 +29,7 @@
API key; reads `STORAGEOPS_MODEL_KEY` or provider env vars when set.
- **`storageops doctor`** — adds `Live diagnosis` row and `live_diagnosis_available`
in `--json` output when install, Node, Pi, and API key are ready.
- **CI `diagnosis-smoke`** — calls `scripts/live_smoke.sh` (3 cases) when
- **CI `diagnosis-smoke`** — calls `scripts/live_smoke.sh` when
`STORAGEOPS_MODEL_KEY` is configured.
- **`.devcontainer/devcontainer.json`** — Python 3.12 + Node 22; postCreate runs
`dev_setup.sh --persist-path`.
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ help:
@echo " make extension-tests Run the TypeScript extension behavioral tests"
@echo " make install Install thin CLI shim (pip install -e .)"
@echo " make dev One-shot dev setup (scripts/dev_setup.sh; DEV_SETUP_FLAGS=...)"
@echo " make live-smoke Model smoke + 3 golden-case live diagnoses (needs API key)"
@echo " make live-smoke Model smoke + 5 golden-case live diagnoses (needs API key)"
@echo " make package-check Build wheel and run package_check.py (needs network once)"
@echo ""
@echo " Note: install-smoke and diagnosis-smoke run in CI when configured"
Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Architecture

StorageOps v0.7.2 is a Pi Coding Agent extension and skill pack.
StorageOps v0.8.0 is a Pi Coding Agent extension and skill pack.

## Components

Expand Down
2 changes: 1 addition & 1 deletion docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ from the older local package. Deployment provenance is written to
## `storageops --version`

```text
StorageOps v0.7.2 (pi: 0.78.0)
StorageOps v0.8.0 (pi: 0.78.0)
httpmon : /root/.storageops/bin/httpmon
api key : api-key file
independent install : yes (~/.storageops/agent)
Expand Down
4 changes: 2 additions & 2 deletions docs/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,6 @@ Locally (after `storageops configure --api-key` or exporting a provider env var)
make live-smoke
```

The script runs `storageops smoke` plus live `--print` diagnosis + eval for three
The script runs `storageops smoke` plus live `--print` diagnosis + eval for five
golden cases: `throttling-hot-prefix`, `access-denied-cross-account`,
`signature-clock-skew`.
`signature-clock-skew`, `cors-preflight-failed`, and `kms-denied-encrypt`.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "storageops"
version = "0.7.2"
version = "0.8.0"
description = "StorageOps — S3-compatible object storage diagnostic toolkit. A Pi Coding Agent extension + skill pack."
requires-python = ">=3.11"
readme = "README.md"
Expand Down
2 changes: 1 addition & 1 deletion scripts/coverage_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
BASELINES = CORPUS / "baseline-outputs"
TAXONOMY = ROOT / "docs" / "skill-taxonomy.json"

MIN_CASES = 2 # per baseline-enabled category
MIN_CASES = 3 # per baseline-enabled category


def main() -> int:
Expand Down
2 changes: 2 additions & 0 deletions scripts/live_smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ CASES=(
throttling-hot-prefix
access-denied-cross-account
signature-clock-skew
cors-preflight-failed
kms-denied-encrypt
)

for case_name in "${CASES[@]}"; do
Expand Down
10 changes: 8 additions & 2 deletions scripts/routing_contract_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@
REGISTRY = ROOT / "skill-registry.yaml"
CASES = ROOT / "skills" / "storageops-eval-golden-cases" / "cases"
BASELINES = ROOT / "skills" / "storageops-eval-golden-cases" / "baseline-outputs"
EXTENSION = ROOT / "storageops_cli" / "extensions" / "storageops.ts"
EXTENSION_DIR = ROOT / "storageops_cli" / "extensions"


def extension_source_text() -> str:
return "\n".join(
path.read_text(encoding="utf-8") for path in sorted(EXTENSION_DIR.glob("*.ts"))
)


def load_json(path: Path) -> dict:
Expand Down Expand Up @@ -90,7 +96,7 @@ def validate_baselines(taxonomy: dict, cases: dict[str, str]) -> list[str]:


def validate_extension_coverage(taxonomy: dict) -> list[str]:
text = EXTENSION.read_text(encoding="utf-8")
text = extension_source_text()
compact_extension = normalize(text)
errors = []
for category, entry in sorted(taxonomy.get("categories", {}).items()):
Expand Down
2 changes: 1 addition & 1 deletion skill-registry.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# StorageOps Skill Registry v0.7.2
# StorageOps Skill Registry v0.8.0

# Machine-readable metadata for all StorageOps skills.
# Skills are auto-discovered by Pi from the skills/ directory.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Summary

Category: cors_configuration
Route: storageops-s3-protocol-compatibility
Confidence: 0.86
Root Cause Type: cors_method_not_allowed

Primary Diagnosis: root_cause_type=cors_method_not_allowed, affected_layer=policy

Browser PUT preflight fails because the bucket CORS rule allows GET/HEAD but not PUT
for the app origin.

# Key Evidence

- Preflight reports `Method PUT is not allowed by Access-Control-Allow-Methods`.
- `Access-Control-Allow-Origin` is present for the app origin.
- Direct GET without an Origin header succeeds.

# Remediation

Update the bucket CORS configuration with `put-bucket-cors` to include `PUT` in `AllowedMethod` for the
narrow app origin. Retest with an OPTIONS preflight before retrying the browser upload.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# cors-method-put-blocked

Browser PUT upload fails CORS preflight because the bucket CORS rule allows GET
but not PUT for the app origin.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"expected_category": "cors_configuration",
"expected_root_cause_types": [
"cors_method_not_allowed",
"missing_allowed_method",
"cors_configuration_incomplete"
],
"expected_min_confidence": 0.85,
"must_include_evidence_keywords": [
"CORS",
"PUT",
"Access-Control-Allow-Methods",
"preflight"
],
"must_include_recommendation_keywords": [
"AllowedMethod",
"PUT",
"put-bucket-cors"
],
"must_not_include": [
"delete bucket",
"make bucket public",
"disable SSL"
],
"required_report_sections": [
"Summary",
"Key Evidence",
"Remediation"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Browser console (PUT upload):
Access to fetch at 'https://uploads.example-bucket.s3.amazonaws.com/incoming/report.csv'
from origin 'https://dashboard.example.com' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check: Method PUT is not
allowed by Access-Control-Allow-Methods in preflight response.

OPTIONS preflight response headers:
Access-Control-Allow-Origin: https://dashboard.example.com
Access-Control-Allow-Methods: GET, HEAD
Access-Control-Allow-Headers: content-type

Direct GET from curl without Origin succeeds (200 OK).

Current CORS configuration excerpt:
AllowedOrigin: https://dashboard.example.com
AllowedMethod: GET, HEAD
AllowedHeader: *

User goal: browser JavaScript PUT upload from dashboard.example.com
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# routing-triage-vague-evidence

Vague first-contact report with no error codes or logs yet. Triage should classify
the issue and list evidence gaps before routing to a specialist skill.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"case_type": "routing",
"expected_category": "triage",
"expected_root_cause_types": ["insufficient_evidence", "unknown_root_cause"],
"expected_min_confidence": 0.7,
"must_include_evidence_keywords": [
"evidence",
"logs",
"request"
],
"must_include_recommendation_keywords": [
"collect",
"error",
"route"
],
"must_not_include": [
"delete bucket",
"make public",
"disable TLS"
],
"required_report_sections": [
"Routing",
"Evidence Gaps"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Subject: object storage uploads failing intermittently

Our application team reports object storage upload problems this week from the
production dashboard. We do not have error logs, HTTP status codes, or request IDs
yet — only user reports that uploads sometimes fail mid-transfer.

We use a private bucket in us-east-1. No vendor support ticket has been opened.

Please classify the issue domain and tell us what evidence to collect next.
17 changes: 11 additions & 6 deletions storageops_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -737,12 +737,17 @@ def _write_install_marker(data: Path, target_agent: Path, merge: bool) -> None:


def _copy_extension(data: Path, dst_agent: Path) -> None:
"""Copy the StorageOps Pi extension to the target agent directory."""
ext_src = data / "extensions" / "storageops.ts"
ext_dst = dst_agent / "extensions" / "storageops.ts"
ext_dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(ext_src, ext_dst)
print(f" [ok] storageops.ts -> {ext_dst}")
"""Copy all StorageOps Pi extension modules to the target agent directory."""
ext_src = data / "extensions"
ext_dst = dst_agent / "extensions"
ext_dst.mkdir(parents=True, exist_ok=True)
ts_files = sorted(ext_src.glob("*.ts"))
if not any(p.name == "storageops.ts" for p in ts_files):
print(" [fail] storageops.ts not found in package extensions/")
sys.exit(1)
for src in ts_files:
shutil.copy2(src, ext_dst / src.name)
print(f" [ok] extension ({len(ts_files)} files) -> {ext_dst}")


def _copy_skills(data: Path, dst_agent: Path) -> None:
Expand Down
Loading
Loading