Skip to content

Commit d2ad280

Browse files
voidstackloopclaude
andcommitted
Add managed team-deployment platform: run orchestration, security hardening, and a Rust worker
Managed run system (buffdata/runs/, buffdata/server/): - Submit/claim/execute/finish lifecycle with immutable manifests and resumable/cancellable runs - Run lineage tracking and HTTPS webhook notifications (HMAC-signed, SSRF-guarded) - Worker pools: multiple projects sharing one worker token/claim queue, with fair randomized claim scanning so no pool member is structurally starved - Encryption at rest for managed artifacts: AES-GCM envelope encryption, per-project data keys wrapped by an operator-supplied master key, never persisted to disk unencrypted Security hardening: - Subprocess sandboxing for third-party validator/PII-recognizer plugins - Bounded-memory minhash deduplication (xxhash fingerprints instead of raw shingle strings) - Presidio dependency isolation into its own venv (unblocks a cryptography advisory fix in the main environment) - OS-keyring credential storage and file-permission hardening for local CLI use Team deployment: Docker Compose stack (API, worker, Postgres, nginx/squid egress proxy), OIDC browser auth, a reproducible synthetic-OIDC smoke test, and CI workflows. rust-worker/: an opt-in Rust reimplementation of the worker's claim loop and process supervisor -- identical wire protocol to buffdata/server/worker.py, still delegates the actual optimization work to the Python buffdata.runs.executor subprocess. Fix: the managed-run supply-chain identity fingerprint (runtime_identity) no longer depends on python-dotenv's ambient, invocation-mode-dependent .env discovery, which could make a submission and its execution disagree about the environment and falsely reject a valid run. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent df261cd commit d2ad280

85 files changed

Lines changed: 9838 additions & 119 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Build-context allowlist: datasets, credentials, history and local caches stay out.
2+
**
3+
!pyproject.toml
4+
!README.md
5+
!buffdata/
6+
!buffdata/**
7+
!deploy/
8+
!deploy/Dockerfile.team
9+
!deploy/requirements-cpu.lock
10+
!rust-worker/
11+
!rust-worker/Cargo.toml
12+
!rust-worker/Cargo.lock
13+
!rust-worker/src/
14+
!rust-worker/src/**
15+
**/.env
16+
**/.env.*
17+
**/__pycache__
18+
**/*.pyc
19+
rust-worker/target/
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Team deployment smoke
2+
on:
3+
workflow_dispatch:
4+
pull_request:
5+
paths:
6+
- 'deploy/**'
7+
- 'buffdata/server/**'
8+
- 'buffdata/runs/**'
9+
- 'buffdata/security/**'
10+
- 'buffdata/governance/oidc.py'
11+
- 'pyproject.toml'
12+
- '.dockerignore'
13+
- '.github/workflows/deployment-smoke.yml'
14+
permissions:
15+
contents: read
16+
jobs:
17+
compose:
18+
runs-on: ubuntu-latest
19+
timeout-minutes: 40
20+
steps:
21+
- uses: actions/checkout@v4
22+
- uses: actions/setup-python@v5
23+
with:
24+
python-version: '3.12'
25+
- uses: docker/setup-buildx-action@v3
26+
- uses: docker/build-push-action@v6
27+
with:
28+
context: .
29+
file: deploy/Dockerfile.team
30+
load: true
31+
tags: buffdata-team:local
32+
cache-from: type=gha
33+
cache-to: type=gha,mode=max
34+
- run: pip install httpx PyJWT cryptography PyYAML
35+
- run: python deploy/smoke_test.py
36+
- uses: actions/upload-artifact@v4
37+
if: always()
38+
with:
39+
name: compose-smoke-results
40+
# Never upload the generated secret files, JWT material, or database backup.
41+
path: |
42+
/tmp/buffdata-compose-smoke-*/results.json
43+
/tmp/buffdata-compose-smoke-*/sbom.json
44+
if-no-files-found: warn

.github/workflows/security.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Security and compatibility
2+
on: [push, pull_request]
3+
permissions:
4+
contents: read
5+
jobs:
6+
boundaries:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v4
10+
- uses: actions/setup-python@v5
11+
with:
12+
python-version: '3.12'
13+
- run: pip install torch --index-url https://download.pytorch.org/whl/cpu
14+
- run: pip install '.[server,dev]' pip-audit keyrings.alt
15+
- run: pytest -q tests/test_run_management.py tests/test_security_boundaries.py tests/test_team_server.py tests/test_adaptive_pipeline.py tests/test_accuracy_gate.py
16+
- run: pip-audit
17+
- run: buffdata sbom --output sbom.json
18+
- uses: actions/upload-artifact@v4
19+
with:
20+
name: dependency-sbom
21+
path: sbom.json
22+
secrets:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- uses: actions/checkout@v4
26+
with:
27+
fetch-depth: 0
28+
- uses: gitleaks/gitleaks-action@v2
29+
env:
30+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,28 @@ outputs/
1616
*.jsonl
1717
!examples/*.jsonl
1818
graphify-out/
19+
deploy/team/
20+
*.db
21+
*.db-journal
22+
*.db-wal
23+
*.db-shm
24+
benchmarks/results-managed-security/runs/
25+
.managed-test-artifacts/
26+
rust-worker/target/
27+
28+
# Generated benchmark run output, not source -- the benchmark scripts themselves are tracked.
29+
benchmarks/results-*/
30+
benchmarks/results-*.log
31+
benchmarks/_*.json
32+
33+
# Stray artifacts that have shown up in this working tree from local runs -- not part of the
34+
# project. The venv-shaped one (bin/lib/lib64/include/pyvenv.cfg at repo root) came from a
35+
# `python -m venv` accidentally targeted at the repo root instead of .venv/.
36+
/bin/
37+
/lib/
38+
/lib64
39+
/include/
40+
/pyvenv.cfg
41+
/.stress_output.checkpoint.json
42+
/stress_25k_out.report.json
43+
/train_25k_clean.report.json

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
# BuffData
22

3+
## Secure run management — optimization unchanged
4+
5+
Managed CLI/SDK runs now add immutable input snapshots, run history, verified artifact
6+
manifests, cancellation/resume, and original/generated comparisons. An optional internal-team
7+
API/dashboard adds OIDC login, project roles, bounded uploads, and project-specific workers.
8+
9+
Compatibility was checked on cached AG News data with the same PyTorch model, six epochs,
10+
and three seeds for each condition:
11+
12+
| Source / test rows | Original | Legacy optimizer | Managed optimizer |
13+
|---|---:|---:|---:|
14+
| 3,000 / 1,000 | 83.90% | 83.90% | 83.90% |
15+
| 20,000 / 4,000 | 87.80% | 87.80% | 87.80% |
16+
| 100,000 / 7,000 | 89.38% | 89.38% | 89.38% |
17+
18+
Ordered text/label sequences, accuracy, and macro-F1 matched. These are preservation
19+
checks, not additional accuracy gains. [Measured results and timing boundaries](benchmarks/results-managed-security/REPORT.md).
20+
21+
Start with `buffdata runs start input.parquet --config pipeline.yaml`.
22+
See [run management and security policy](docs/run-management.md) and
23+
[team deployment](docs/team-deployment.md). Docker Compose syntax and image digests are
24+
verified; live Compose/PostgreSQL/OIDC deployment checks still require a working Docker
25+
engine and organization identity configuration. Do not treat this as production validation.
26+
327
## Verified benchmark results
428

529
The latest accuracy gate directly compares matching original AG News records with

benchmarks/_verify_datasets.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#!/usr/bin/env python3
2+
"""Verify candidate Hugging Face datasets for the binary/multi-class/multi-label CLI
3+
classification benchmark, without downloading full data -- uses
4+
load_dataset_builder().info to inspect schema and split sizes.
5+
"""
6+
from __future__ import annotations
7+
8+
import json
9+
10+
from datasets import ClassLabel, Sequence, Value, load_dataset_builder
11+
12+
LABEL_COL_CANDIDATES = ("label", "labels", "topic", "answer", "category", "class")
13+
14+
# (hf_id, config_or_None, expected_type)
15+
CANDIDATES = [
16+
# --- binary (SetFit re-hosts + originals known to load) ---
17+
("stanfordnlp/imdb", None, "binary"),
18+
("fancyzhx/yelp_polarity", None, "binary"),
19+
("fancyzhx/amazon_polarity", None, "binary"),
20+
("stanfordnlp/sst2", None, "binary"),
21+
("SetFit/sst2", None, "binary"),
22+
("SetFit/imdb", None, "binary"),
23+
("SetFit/amazon_polarity", None, "binary"),
24+
("SetFit/enron_spam", None, "binary"),
25+
("SetFit/subj", None, "binary"),
26+
("SetFit/CR", None, "binary"),
27+
("SetFit/SentEval-CR", None, "binary"),
28+
("SetFit/mrpc", None, "binary"),
29+
("SetFit/qqp", None, "binary"),
30+
("SetFit/qnli", None, "binary"),
31+
("SetFit/rte", None, "binary"),
32+
("SetFit/wnli", None, "binary"),
33+
("SetFit/hate_speech18", None, "binary"),
34+
("SetFit/hate_speech_offensive", None, "binary"),
35+
("SetFit/ethos_binary", None, "binary"),
36+
("SetFit/toxic_conversations", None, "binary"),
37+
("SetFit/toxic_conversations_50k", None, "binary"),
38+
("SetFit/insincere-questions", None, "binary"),
39+
("SetFit/ade_corpus_v2_classification", None, "binary"),
40+
("SetFit/onestop_english", None, "multiclass"),
41+
("SetFit/wsc_fixed", None, "binary"),
42+
("nyu-mll/glue", "sst2", "binary"),
43+
("nyu-mll/glue", "qqp", "binary"),
44+
("cardiffnlp/tweet_eval", "offensive", "binary"),
45+
("cardiffnlp/tweet_eval", "emotion", "multiclass"),
46+
("cornell-movie-review-data/rotten_tomatoes", None, "binary"),
47+
# --- multi-class ---
48+
("fancyzhx/ag_news", None, "multiclass"),
49+
("fancyzhx/dbpedia_14", None, "multiclass"),
50+
("community-datasets/yahoo_answers_topics", None, "multiclass"),
51+
("dair-ai/emotion", None, "multiclass"),
52+
("SetFit/20_newsgroups", None, "multiclass"),
53+
("SetFit/emotion", None, "multiclass"),
54+
("SetFit/ag_news", None, "multiclass"),
55+
("SetFit/bbc-news", None, "multiclass"),
56+
("SetFit/sst5", None, "multiclass"),
57+
("SetFit/yelp_review_full", None, "multiclass"),
58+
("SetFit/TREC-QC", None, "multiclass"),
59+
("SetFit/student-question-categories", None, "multiclass"),
60+
("SetFit/tweet_eval_stance", None, "multiclass"),
61+
("SetFit/amazon_massive_scenario_en-US", None, "multiclass"),
62+
("SetFit/amazon_massive_intent_en-US", None, "multiclass"),
63+
("SetFit/amazon_reviews_multi_en", None, "multiclass"),
64+
("SetFit/ethos", None, "multiclass"),
65+
# --- multi-label ---
66+
("google-research-datasets/go_emotions", "simplified", "multilabel"),
67+
("google-research-datasets/go_emotions", "raw", "multilabel"),
68+
("SetFit/go_emotions", None, "multilabel"),
69+
("argilla/go_emotions_multi-label", None, "multilabel"),
70+
("owaiskha9654/PubMed_MultiLabel_Text_Classification_Dataset_MeSH", None, "multilabel"),
71+
("google/civil_comments", None, "multilabel"),
72+
("google/jigsaw_toxicity_pred", None, "multilabel"),
73+
("mteb/toxic_conversations_50k", None, "binary"),
74+
("Arsive/toxicity_classification_jigsaw", None, "multilabel"),
75+
("SetFit/ethos", "multilabel", "multilabel"),
76+
]
77+
78+
results = []
79+
for hf_id, config, expected in CANDIDATES:
80+
entry = {"hf_id": hf_id, "config": config, "expected": expected}
81+
try:
82+
builder = load_dataset_builder(hf_id, config) if config else load_dataset_builder(hf_id)
83+
info = builder.info
84+
features = info.features or {}
85+
splits = info.splits
86+
train_split = None
87+
if splits:
88+
for name in ("train", "training"):
89+
if name in splits:
90+
train_split = name
91+
break
92+
train_rows = splits[train_split].num_examples if train_split else None
93+
entry["train_rows"] = train_rows
94+
entry["columns"] = list(features.keys())
95+
96+
label_col = None
97+
label_type = None # "single" or "multi"
98+
num_classes = None
99+
for name in LABEL_COL_CANDIDATES:
100+
if name in features:
101+
feat = features[name]
102+
label_col = name
103+
if isinstance(feat, ClassLabel):
104+
label_type, num_classes = "single", feat.num_classes
105+
elif isinstance(feat, Sequence) and isinstance(feat.feature, ClassLabel):
106+
label_type, num_classes = "multi", feat.feature.num_classes
107+
elif isinstance(feat, Sequence):
108+
label_type = "multi"
109+
elif isinstance(feat, (Value,)) and feat.dtype in ("int64", "int32", "bool"):
110+
label_type = "single" # plain int/bool label column (e.g. SetFit style)
111+
break
112+
entry["label_col"] = label_col
113+
entry["label_type"] = label_type
114+
entry["num_classes"] = num_classes
115+
entry["ok"] = bool(train_rows and train_rows >= 10000 and label_col)
116+
entry["error"] = None
117+
except Exception as exc:
118+
entry["ok"] = False
119+
entry["error"] = f"{type(exc).__name__}: {exc}"[:150]
120+
results.append(entry)
121+
status = "OK" if entry.get("ok") else "FAIL"
122+
print(f"[{status}] {hf_id} ({config}) expected={expected} -> "
123+
f"rows={entry.get('train_rows')} label_col={entry.get('label_col')} "
124+
f"label_type={entry.get('label_type')} classes={entry.get('num_classes')} "
125+
f"err={entry.get('error')}", flush=True)
126+
127+
with open("benchmarks/_dataset_verification.json", "w") as f:
128+
json.dump(results, f, indent=2)
129+
130+
ok = [r for r in results if r["ok"]]
131+
print(f"\n{len(ok)}/{len(results)} candidates verified OK")
132+
for kind in ("binary", "multiclass", "multilabel"):
133+
matching = [r for r in ok if r["expected"] == kind]
134+
print(f" {kind}: {len(matching)} verified")

benchmarks/_verify_true_classes.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/env python3
2+
"""Get the TRUE full-dataset class count for every entry in DATASETS (benchmark_scale_matrix.py),
3+
using dataset.unique() on the full train split -- not a partial sample, which can miss rare
4+
classes (exactly what caused the amazon_massive_scenario bug: a 3000-row sample only showed
5+
14 of the real 17+ classes)."""
6+
import sys
7+
from pathlib import Path
8+
9+
sys.path.insert(0, str(Path(__file__).parent))
10+
from benchmark_scale_matrix import DATASETS, load_normalized
11+
12+
for name, spec in DATASETS.items():
13+
try:
14+
train, test = load_normalized(spec)
15+
train_labels = set(train.unique("label"))
16+
test_labels = set(test.unique("label"))
17+
all_labels = train_labels | test_labels
18+
true_n = len(all_labels)
19+
declared_n = spec["classes"]
20+
min_label, max_label = min(all_labels), max(all_labels)
21+
contiguous = all_labels == set(range(min_label, max_label + 1))
22+
status = "OK" if true_n == declared_n and min_label == 0 and contiguous else "MISMATCH"
23+
print(f"[{status}] {name}: declared={declared_n} true={true_n} range=[{min_label},{max_label}] contiguous={contiguous} train_rows={len(train)}", flush=True)
24+
except Exception as exc:
25+
print(f"[FAIL] {name}: {type(exc).__name__}: {str(exc)[:150]}", flush=True)

0 commit comments

Comments
 (0)