Skip to content
Closed
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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ docker compose up -d postgres
- `shared/` shared types + hashing utils
- `docs/` PRD + decisions
- `scripts/` helper scripts
- `strategies/` directory for submitted agent strategies
- `strategies/` directory for submitted agent strategies (content-addressed filenames)

## Notes
- Week-1 goal is reproducibility: **dataset_version + code_hash + config_hash**.
Expand Down Expand Up @@ -133,9 +133,10 @@ Submitted strategies run in a restricted environment:

- **Timeout**: 60 seconds maximum execution time
- **Memory**: 256 MB limit
- **Network**: Blocked (HTTP_PROXY, HTTPS_PROXY cleared)
- **Network**: Obvious proxy/env-based access is blocked, but this is not a real firewall
- **File I/O**: Blocked in sandbox mode
- **Isolation**: Process-level via multiprocessing
- **Storage**: Submitted code is persisted under `STRATEGIES_DIR` using `{safe_name}_{code_hash[:12]}.py`

For trusted local files, use `strategy_path` endpoints with `trusted=True` to skip sandbox.

Expand Down
19 changes: 10 additions & 9 deletions api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,23 +81,24 @@ def ensure_strategies_dir():
os.makedirs(STRATEGIES_DIR, exist_ok=True)


def save_strategy_code(code: str, name: str) -> str:
def save_strategy_code(code: str, name: str, code_hash: str) -> str:
"""
Save strategy code to the strategies directory.

Args:
code: Python source code for the strategy
name: Strategy name (used for filename)
code_hash: SHA-256 of the strategy code

Returns:
Absolute path to the saved strategy file
"""
ensure_strategies_dir()
# Sanitize filename
safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in name)
filename = f"{safe_name}.py"
safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in name).strip("_") or "strategy"
short_hash = code_hash[:12]
filename = f"{safe_name}_{short_hash}.py"
path = os.path.abspath(os.path.join(STRATEGIES_DIR, filename))
with open(path, "w") as f:
with open(path, "w", encoding="utf-8") as f:
f.write(code)
return path

Expand Down Expand Up @@ -166,12 +167,12 @@ def submit_strategy(req: StrategySubmit, db: Session = Depends(get_db)):
if "def simulate(" not in req.code:
raise HTTPException(status_code=400, detail="code must contain simulate(prices, params) function")

# Save code to disk
strategy_path = save_strategy_code(req.code, req.name)

# Compute hash and create version
# Compute hash before writing so filenames are content-addressed.
code_hash = sha256_bytes(req.code.encode("utf-8"))

# Save code to disk
strategy_path = save_strategy_code(req.code, req.name, code_hash)

# Check for existing version with same hash
existing = db.execute(
select(StrategyVersion).where(StrategyVersion.code_hash == code_hash)
Expand Down
4 changes: 2 additions & 2 deletions docs/PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Human-approved "default candidate" (manual promote) based on risk-adjusted score
Agents submit strategy code directly to the platform without needing file system access. The platform:
1. Validates the code structure
2. Computes a SHA-256 hash
3. Stores the code in the `STRATEGIES_DIR` (default: `./strategies/`)
3. Stores the code in the `STRATEGIES_DIR` (default: `./strategies/`) using a content-addressed filename
4. Creates a `StrategyVersion` record linking code_hash to the file path
5. Returns the `strategy_version_id` for subsequent runs

Expand Down Expand Up @@ -118,7 +118,7 @@ See `examples/strategy_template.py` for full documentation.
- Each submission's code is hashed (SHA-256)
- If an identical `code_hash` exists, the existing `StrategyVersion` is returned
- This enables agents to skip resubmission of unchanged strategies
- File storage: `STRATEGIES_DIR/{safe_name}.py`
- File storage: `STRATEGIES_DIR/{safe_name}_{code_hash[:12]}.py`

### Metrics Computed

Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ python-dotenv==1.0.1
numpy==2.2.3
pandas==2.2.3
SQLAlchemy==2.0.38
pytest==8.4.2
httpx==0.28.1
49 changes: 49 additions & 0 deletions tests/test_submission_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import os
from pathlib import Path

from fastapi.testclient import TestClient

os.environ.setdefault("DATASET_DIR", "/tmp/agenttest_dataset_tests")
os.environ.setdefault("DATASET_VERSION", "v1")
os.environ["DATABASE_URL"] = "sqlite:////tmp/agenttest_test.sqlite"
os.environ["STRATEGIES_DIR"] = "/tmp/agenttest_submitted_strategies"

Path(os.environ["DATASET_DIR"]).mkdir(parents=True, exist_ok=True)
Path(os.environ["STRATEGIES_DIR"]).mkdir(parents=True, exist_ok=True)
Path(os.environ["DATASET_DIR"]).joinpath("prices.csv").write_text("ts,price\n1,100\n2,101\n3,102\n", encoding="utf-8")
Path(os.environ["DATABASE_URL"].replace("sqlite:///", "")).unlink(missing_ok=True)
for existing in Path(os.environ["STRATEGIES_DIR"]).glob("*.py"):
existing.unlink()

from api.main import app # noqa: E402


def test_submit_strategy_persists_hashed_filename_and_reuses_duplicate_hash():
with TestClient(app) as client:
code = "def simulate(prices, params):\n return [1.0 for _ in prices] if prices else [1.0]\n"

response = client.post(
"/strategies/submit",
json={"code": code, "name": "mean reversion v1", "params": {}},
)
assert response.status_code == 200
payload = response.json()
code_hash = payload["code_hash"]

strategies_dir = Path(os.environ["STRATEGIES_DIR"])
expected_prefix = f"mean_reversion_v1_{code_hash[:12]}"
matches = list(strategies_dir.glob(f"{expected_prefix}.py"))
if not matches:
matches = list(strategies_dir.glob("*.py"))
assert len(matches) == 1
assert matches[0].name.startswith(expected_prefix)
assert matches[0].read_text(encoding="utf-8") == code

duplicate = client.post(
"/strategies/submit",
json={"code": code, "name": "mean reversion v1", "params": {}},
)
assert duplicate.status_code == 200
duplicate_payload = duplicate.json()
assert duplicate_payload["strategy_version_id"] == payload["strategy_version_id"]
assert duplicate_payload["code_hash"] == code_hash