cosmosforge 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,105 @@
1
+ """Client CLI (`cf`)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tarfile
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+ import typer
10
+
11
+ from cosmosforge.client import ssh
12
+ from cosmosforge.client.config import CONFIG_PATH, HostConfig, load_config
13
+ from cosmosforge.config import BUNDLES_DIR, RESULTS_DIR
14
+
15
+ app = typer.Typer(no_args_is_help=True)
16
+
17
+
18
+ @app.command()
19
+ def init(
20
+ host: str = typer.Option(..., help="Server hostname or IP."),
21
+ user: str = typer.Option(..., help="SSH user on the server."),
22
+ ):
23
+ """Write the server connection config."""
24
+ CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
25
+ CONFIG_PATH.write_text(f'host = "{host}"\nuser = "{user}"\n')
26
+ typer.echo(f"config written to {CONFIG_PATH}")
27
+
28
+
29
+ @app.command()
30
+ def submit(
31
+ task_dir: Path = typer.Argument(..., help="Local harbor task directory."),
32
+ host: str = typer.Option(None),
33
+ user: str = typer.Option(None),
34
+ ):
35
+ """Upload a task bundle to the server."""
36
+ cfg = load_config(host, user)
37
+ from cosmosforge.core.structure import check_structure
38
+
39
+ errors = check_structure(task_dir)
40
+ if errors:
41
+ for e in errors:
42
+ typer.echo(f"structure error: {e}", err=True)
43
+ raise typer.Exit(1)
44
+
45
+ bundle_id = task_dir.resolve().name
46
+ remote_tar = f"{BUNDLES_DIR}/incoming/{bundle_id}.tar.gz"
47
+ with tempfile.TemporaryDirectory(prefix="cf-submit-") as tmp:
48
+ tar_path = Path(tmp) / f"{bundle_id}.tar.gz"
49
+ with tarfile.open(tar_path, "w:gz") as tar:
50
+ tar.add(task_dir, arcname=bundle_id)
51
+ rc = ssh.upload(cfg, tar_path, remote_tar)
52
+ if rc != 0:
53
+ raise typer.Exit(rc)
54
+ rc = ssh.run_remote(
55
+ cfg, f"mkdir -p {BUNDLES_DIR}/incoming && cf-server receive {bundle_id} {remote_tar}"
56
+ )
57
+ if rc == 0:
58
+ typer.echo(f"submitted bundle: {bundle_id}")
59
+ raise typer.Exit(rc)
60
+
61
+
62
+ @app.command()
63
+ def eval(
64
+ bundle_id: str = typer.Argument(..., help="Bundle ID from `cf submit`."),
65
+ host: str = typer.Option(None),
66
+ user: str = typer.Option(None),
67
+ ):
68
+ """Run oracle/nop validation on the server and print the report."""
69
+ cfg = load_config(host, user)
70
+ rc = ssh.run_remote(cfg, f"cf-server eval {bundle_id}")
71
+ raise typer.Exit(rc)
72
+
73
+
74
+ @app.command()
75
+ def status(
76
+ host: str = typer.Option(None),
77
+ user: str = typer.Option(None),
78
+ ):
79
+ """List bundles and results on the server."""
80
+ cfg = load_config(host, user)
81
+ rc = ssh.run_remote(
82
+ cfg,
83
+ f"ls -1 {BUNDLES_DIR} 2>/dev/null; echo ---; ls -1 {RESULTS_DIR} 2>/dev/null",
84
+ )
85
+ raise typer.Exit(rc)
86
+
87
+
88
+ @app.command()
89
+ def results(
90
+ bundle_id: str = typer.Argument(...),
91
+ host: str = typer.Option(None),
92
+ user: str = typer.Option(None),
93
+ ):
94
+ """Fetch and print the stored result JSON for a bundle."""
95
+ cfg = load_config(host, user)
96
+ rc = ssh.run_remote(cfg, f"cat {RESULTS_DIR}/{bundle_id}.json")
97
+ raise typer.Exit(rc)
98
+
99
+
100
+ def main():
101
+ app()
102
+
103
+
104
+ if __name__ == "__main__":
105
+ main()
@@ -0,0 +1,37 @@
1
+ """Client-side connection config (~/.cosmosforge/config.toml)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ CONFIG_PATH = Path.home() / ".cosmosforge" / "config.toml"
10
+
11
+
12
+ @dataclass
13
+ class HostConfig:
14
+ host: str
15
+ user: str
16
+
17
+ def ssh_target(self) -> str:
18
+ return f"{self.user}@{self.host}"
19
+
20
+
21
+ def load_config(host: str | None = None, user: str | None = None) -> HostConfig:
22
+ host = host or os.environ.get("COSMOSFORGE_HOST")
23
+ user = user or os.environ.get("COSMOSFORGE_USER")
24
+ if (not host or not user) and CONFIG_PATH.is_file():
25
+ try:
26
+ import tomllib
27
+ data = tomllib.loads(CONFIG_PATH.read_text())
28
+ except Exception:
29
+ data = {}
30
+ host = host or data.get("host")
31
+ user = user or data.get("user")
32
+ if not host or not user:
33
+ raise SystemExit(
34
+ "No server configured. Run `cf init --host <host> --user <user>`, "
35
+ "or set COSMOSFORGE_HOST / COSMOSFORGE_USER."
36
+ )
37
+ return HostConfig(host=host, user=user)
@@ -0,0 +1,23 @@
1
+ """Thin ssh/scp wrappers. Password auth passes through interactively."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ from cosmosforge.client.config import HostConfig
9
+
10
+
11
+ def _run(cmd: list[str]) -> subprocess.CompletedProcess:
12
+ # stdin/stdout are inherited so ssh password prompts reach the user.
13
+ return subprocess.run(cmd, check=False)
14
+
15
+
16
+ def run_remote(cfg: HostConfig, command: str) -> int:
17
+ proc = _run(["ssh", "-o", "BatchMode=no", cfg.ssh_target(), command])
18
+ return proc.returncode
19
+
20
+
21
+ def upload(cfg: HostConfig, local_path: Path, remote_path: str) -> int:
22
+ proc = _run(["scp", str(local_path), f"{cfg.ssh_target()}:{remote_path}"])
23
+ return proc.returncode
cosmosforge/config.py ADDED
@@ -0,0 +1,14 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ SERVER_ROOT = Path(os.environ.get("COSMOSFORGE_ROOT", "/data/cosmosforge"))
5
+ BUNDLES_DIR = SERVER_ROOT / "bundles"
6
+ RESULTS_DIR = SERVER_ROOT / "results"
7
+
8
+
9
+ def bundle_dir(bundle_id: str) -> Path:
10
+ return BUNDLES_DIR / bundle_id
11
+
12
+
13
+ def result_path(bundle_id: str) -> Path:
14
+ return RESULTS_DIR / f"{bundle_id}.json"
File without changes
@@ -0,0 +1,108 @@
1
+ """Oracle/nop validation via the harbor framework.
2
+
3
+ Runs both agents in a single harbor job using a job config
4
+ (`harbor run -c`), then parses the job's result.json with harbor's own
5
+ models to get per-agent rewards.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import subprocess
11
+ from pathlib import Path
12
+
13
+ import yaml
14
+
15
+ from cosmosforge.core.report import RunResult
16
+ from cosmosforge.core.structure import check_structure
17
+
18
+ EXPECTED_REWARDS = {"oracle": 1.0, "nop": 0.0}
19
+
20
+
21
+ def _write_job_config(task_dir: Path, config_path: Path) -> None:
22
+ config = {
23
+ "path": str(task_dir),
24
+ "agents": [{"name": name} for name in EXPECTED_REWARDS],
25
+ "job_name": "cosmosforge-eval",
26
+ "n_concurrent": 1,
27
+ "quiet": True,
28
+ }
29
+ config_path.write_text(yaml.safe_dump(config))
30
+
31
+
32
+ def _mean_reward(trial_result) -> float | None:
33
+ rewards = trial_result.verifier_result.rewards if trial_result.verifier_result else None
34
+ if not rewards:
35
+ return None
36
+ return sum(float(v) for v in rewards.values()) / len(rewards)
37
+
38
+
39
+ def run_oracle_nop(task_dir: Path, jobs_dir: Path) -> tuple[list[RunResult], str | None]:
40
+ """Run oracle/nop in one harbor job. Returns (runs, error)."""
41
+ from harbor.models.job.result import JobResult
42
+
43
+ jobs_dir.mkdir(parents=True, exist_ok=True)
44
+ config_path = jobs_dir / "job-config.yaml"
45
+ _write_job_config(task_dir, config_path)
46
+
47
+ cmd = [
48
+ "harbor", "run",
49
+ "-c", str(config_path),
50
+ "-o", str(jobs_dir),
51
+ ]
52
+ proc = subprocess.run(cmd, capture_output=True, text=True, timeout=3600, check=False)
53
+
54
+ result_path = jobs_dir / "cosmosforge-eval" / "result.json"
55
+ if not result_path.exists():
56
+ return [], f"harbor run produced no result.json (rc={proc.returncode}): {proc.stderr[-2000:]}"
57
+
58
+ job_result = JobResult.model_validate_json(result_path.read_text())
59
+
60
+ runs: dict[str, RunResult] = {}
61
+ for trial in job_result.trial_results:
62
+ agent = trial.agent_info.name
63
+ reward = _mean_reward(trial)
64
+ error = None
65
+ if trial.exception_info is not None:
66
+ error = f"{trial.exception_info.exception_type}: {trial.exception_info.exception_message}"
67
+ entry = runs.setdefault(agent, RunResult(agent=agent, reward=None, passed=False))
68
+ if reward is not None:
69
+ entry.reward = reward
70
+ if error and not entry.error:
71
+ entry.error = error
72
+
73
+ missing = [a for a in EXPECTED_REWARDS if a not in runs]
74
+ for agent in missing:
75
+ runs[agent] = RunResult(agent=agent, reward=None, passed=False,
76
+ error="no trial result for this agent")
77
+
78
+ error = None
79
+ if proc.returncode != 0:
80
+ error = f"harbor run exited with rc={proc.returncode}"
81
+ return [runs[a] for a in EXPECTED_REWARDS], error
82
+
83
+
84
+ def evaluate_oracle_nop(bundle_id: str, task_dir: Path, jobs_dir: Path):
85
+ from cosmosforge.core.report import EvaluationReport
86
+
87
+ errors = check_structure(task_dir)
88
+ report = EvaluationReport(
89
+ bundle_id=bundle_id,
90
+ task_name=task_dir.name,
91
+ structure_ok=not errors,
92
+ structure_errors=errors,
93
+ )
94
+ if errors:
95
+ return report
96
+
97
+ runs, error = run_oracle_nop(task_dir, jobs_dir)
98
+ report.runs = runs
99
+ for run in runs:
100
+ expected = EXPECTED_REWARDS.get(run.agent)
101
+ if run.reward == expected and run.error is None:
102
+ run.passed = True
103
+ else:
104
+ run.passed = False
105
+ if run.reward != expected and run.error is None:
106
+ run.error = f"expected reward {expected}, got {run.reward}"
107
+ report.passed = error is None and all(r.passed for r in report.runs)
108
+ return report
@@ -0,0 +1,33 @@
1
+ """Evaluation report models shared by client and server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import asdict, dataclass, field
7
+
8
+
9
+ @dataclass
10
+ class RunResult:
11
+ agent: str # "oracle" | "nop"
12
+ reward: float | None
13
+ passed: bool
14
+ error: str | None = None
15
+
16
+
17
+ @dataclass
18
+ class EvaluationReport:
19
+ bundle_id: str
20
+ task_name: str
21
+ structure_ok: bool
22
+ structure_errors: list[str] = field(default_factory=list)
23
+ runs: list[RunResult] = field(default_factory=list)
24
+ passed: bool = False
25
+
26
+ def to_json(self) -> str:
27
+ return json.dumps(asdict(self), indent=2)
28
+
29
+ @classmethod
30
+ def from_json(cls, text: str) -> "EvaluationReport":
31
+ data = json.loads(text)
32
+ data["runs"] = [RunResult(**r) for r in data["runs"]]
33
+ return cls(**data)
@@ -0,0 +1,23 @@
1
+ """Harbor task structure validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ REQUIRED_FILES = [
8
+ "task.toml",
9
+ "instruction.md",
10
+ "environment/Dockerfile",
11
+ "solution/solve.sh",
12
+ "tests/test.sh",
13
+ ]
14
+
15
+
16
+ def check_structure(task_dir: Path) -> list[str]:
17
+ errors = []
18
+ if not task_dir.is_dir():
19
+ return [f"not a directory: {task_dir}"]
20
+ for rel in REQUIRED_FILES:
21
+ if not (task_dir / rel).is_file():
22
+ errors.append(f"missing required file: {rel}")
23
+ return errors
File without changes
@@ -0,0 +1,69 @@
1
+ """Server-side CLI (`cf-server`), invoked over SSH by the client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+ import typer
10
+
11
+ from cosmosforge import config
12
+ from cosmosforge.core.oracle_nop import evaluate_oracle_nop
13
+ from cosmosforge.server import store
14
+
15
+ app = typer.Typer(no_args_is_help=True)
16
+
17
+ INCOMING_DIR = config.BUNDLES_DIR / "incoming"
18
+
19
+
20
+ @app.command()
21
+ def receive(
22
+ bundle_id: str = typer.Argument(..., help="Bundle ID (directory name under bundles/)."),
23
+ tar_path: Path = typer.Argument(..., help="Path to uploaded tar.gz."),
24
+ ):
25
+ """Extract an uploaded bundle and validate its structure."""
26
+ task_dir = store.extract_bundle(tar_path, bundle_id)
27
+ from cosmosforge.core.structure import check_structure
28
+
29
+ errors = check_structure(task_dir)
30
+ if errors:
31
+ for e in errors:
32
+ typer.echo(f"structure error: {e}", err=True)
33
+ shutil.rmtree(config.bundle_dir(bundle_id))
34
+ raise typer.Exit(1)
35
+ typer.echo(f"bundle {bundle_id} received, task dir: {task_dir}")
36
+
37
+
38
+ @app.command()
39
+ def eval(
40
+ bundle_id: str = typer.Argument(..., help="Bundle ID to evaluate."),
41
+ ):
42
+ """Run oracle/nop validation and store the report JSON."""
43
+ task_dir = _find_task_dir(bundle_id)
44
+ with tempfile.TemporaryDirectory(prefix="cf-jobs-") as jobs_dir:
45
+ report = evaluate_oracle_nop(bundle_id, task_dir, Path(jobs_dir))
46
+ out = store.save_result(bundle_id, report.to_json())
47
+ typer.echo(report.to_json())
48
+ typer.echo(f"report saved to {out}", err=True)
49
+ if not report.passed:
50
+ raise typer.Exit(1)
51
+
52
+
53
+ def _find_task_dir(bundle_id: str) -> Path:
54
+ bundle = config.bundle_dir(bundle_id)
55
+ if not bundle.is_dir():
56
+ typer.echo(f"bundle not found: {bundle_id}", err=True)
57
+ raise typer.Exit(1)
58
+ entries = [p for p in bundle.iterdir() if p.is_dir()]
59
+ if len(entries) == 1:
60
+ return entries[0]
61
+ return bundle
62
+
63
+
64
+ def main():
65
+ app()
66
+
67
+
68
+ if __name__ == "__main__":
69
+ main()
@@ -0,0 +1,33 @@
1
+ """Server-side bundle/results storage."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tarfile
6
+ from pathlib import Path
7
+
8
+ from cosmosforge import config
9
+
10
+
11
+ def extract_bundle(tar_path: Path, bundle_id: str) -> Path:
12
+ dest = config.bundle_dir(bundle_id)
13
+ dest.mkdir(parents=True, exist_ok=True)
14
+ with tarfile.open(tar_path, "r:gz") as tar:
15
+ tar.extractall(dest, filter="data")
16
+ # Support both "bundle-id/task-dir/..." and bare "task-dir/..." layouts.
17
+ entries = [p for p in dest.iterdir() if not p.name.startswith(".")]
18
+ if len(entries) == 1 and entries[0].is_dir():
19
+ return entries[0]
20
+ return dest
21
+
22
+
23
+ def save_result(bundle_id: str, report_json: str) -> Path:
24
+ config.RESULTS_DIR.mkdir(parents=True, exist_ok=True)
25
+ out = config.result_path(bundle_id)
26
+ out.write_text(report_json)
27
+ return out
28
+
29
+
30
+ def list_bundles() -> list[str]:
31
+ if not config.BUNDLES_DIR.exists():
32
+ return []
33
+ return sorted(p.name for p in config.BUNDLES_DIR.iterdir() if p.is_dir())
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.5
2
+ Name: cosmosforge
3
+ Version: 0.1.0
4
+ Summary: CLI and evaluation service for Harbor-format terminal tasks.
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: pyyaml>=6.0
7
+ Requires-Dist: rich>=13.0
8
+ Requires-Dist: typer>=0.12
9
+ Description-Content-Type: text/markdown
10
+
11
+ # CosmosForge
@@ -0,0 +1,17 @@
1
+ cosmosforge/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
+ cosmosforge/config.py,sha256=wmUZnKCMJIPzve3BQuk6skRYCWXpj0jBOXjlaNYx0uo,353
3
+ cosmosforge/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ cosmosforge/client/cli.py,sha256=tZtjWCZ-mxudmFpCeXRSnOXWXbwabVHTmtpILpIDWzI,2974
5
+ cosmosforge/client/config.py,sha256=rcxM0DnQqtBmeVHDpOxpbs_5emPeV2A6yE1ST5A5Its,1088
6
+ cosmosforge/client/ssh.py,sha256=RQFwY0cBlA5oKbhy_L_F7HhTCU1E86fM-RAmid_1rpA,716
7
+ cosmosforge/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ cosmosforge/core/oracle_nop.py,sha256=xWq1RJtsmWUGzZL5Dm7S8ZCKy2zATYjrVzNsQAb7Dzk,3685
9
+ cosmosforge/core/report.py,sha256=weP8SpSqNNbz74lKh3Z7QFQ-_g1jLa9k0W6f1SW4WaY,812
10
+ cosmosforge/core/structure.py,sha256=CL_9Iu5ZPxbei1sh6nDbR-YL03snhWpxtI55KpPRQzU,536
11
+ cosmosforge/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ cosmosforge/server/cli.py,sha256=luTe8Z9bWHOXV0RDi7vxx5s6aKNeknYXqasNPiRQClM,2006
13
+ cosmosforge/server/store.py,sha256=_CaJui4cFrdKvCU8CaxyktgmOrrIpv5y4tNaOu4N03Y,1007
14
+ cosmosforge-0.1.0.dist-info/METADATA,sha256=VBE8X8iZ2uRe75GGbT5YH5LSSTj7Id0ItM8ezwI-4rs,284
15
+ cosmosforge-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
16
+ cosmosforge-0.1.0.dist-info/entry_points.txt,sha256=hAIdwh6vamn8V5VHJCZso8PlrcA6UR3sYww_xhmDx8Y,89
17
+ cosmosforge-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ cf = cosmosforge.client.cli:app
3
+ cf-server = cosmosforge.server.cli:app