harbor-dsh-evolution 0.1.0__tar.gz
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.
- harbor_dsh_evolution-0.1.0/.gitignore +18 -0
- harbor_dsh_evolution-0.1.0/LICENSE +21 -0
- harbor_dsh_evolution-0.1.0/PKG-INFO +56 -0
- harbor_dsh_evolution-0.1.0/README.md +34 -0
- harbor_dsh_evolution-0.1.0/pyproject.toml +50 -0
- harbor_dsh_evolution-0.1.0/src/harbor_dsh_evolution/__init__.py +7 -0
- harbor_dsh_evolution-0.1.0/src/harbor_dsh_evolution/agent.py +151 -0
- harbor_dsh_evolution-0.1.0/src/harbor_dsh_evolution/candidate.py +176 -0
- harbor_dsh_evolution-0.1.0/src/harbor_dsh_evolution/cli.py +69 -0
- harbor_dsh_evolution-0.1.0/src/harbor_dsh_evolution/context.py +128 -0
- harbor_dsh_evolution-0.1.0/src/harbor_dsh_evolution/plugin.py +101 -0
- harbor_dsh_evolution-0.1.0/src/harbor_dsh_evolution/promotion.py +130 -0
- harbor_dsh_evolution-0.1.0/src/harbor_dsh_evolution/summary.py +101 -0
- harbor_dsh_evolution-0.1.0/tests/test_candidate.py +80 -0
- harbor_dsh_evolution-0.1.0/tests/test_context.py +36 -0
- harbor_dsh_evolution-0.1.0/tests/test_plugin.py +64 -0
- harbor_dsh_evolution-0.1.0/tests/test_promotion.py +79 -0
- harbor_dsh_evolution-0.1.0/tests/test_summary.py +46 -0
- harbor_dsh_evolution-0.1.0/uv.lock +2211 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.DS_Store
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
.pytest_cache/
|
|
5
|
+
.venv/
|
|
6
|
+
dist/
|
|
7
|
+
|
|
8
|
+
# Harbor execution results are local evidence, not template source.
|
|
9
|
+
jobs/*
|
|
10
|
+
!jobs/.gitkeep
|
|
11
|
+
|
|
12
|
+
# Local Docker/Buildx state.
|
|
13
|
+
.runtime/
|
|
14
|
+
|
|
15
|
+
# Generated Candidate snapshots and local package installs.
|
|
16
|
+
examples/**/candidate-manifest.json
|
|
17
|
+
examples/**/node_modules/
|
|
18
|
+
packages/**/node_modules/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 istarwyh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: harbor-dsh-evolution
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Candidate identity, evaluation summaries, and promotion gates for DeepSeek Harness agents evaluated with Harbor.
|
|
5
|
+
Project-URL: Homepage, https://github.com/istarwyh/harbor-self-evolving
|
|
6
|
+
Project-URL: Documentation, https://github.com/istarwyh/harbor-self-evolving#readme
|
|
7
|
+
Project-URL: Issues, https://github.com/istarwyh/harbor-self-evolving/issues
|
|
8
|
+
Project-URL: Repository, https://github.com/istarwyh/harbor-self-evolving.git
|
|
9
|
+
Author: istarwyh
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agent-evaluation,agent-evolution,deepseek-harness,harbor
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Requires-Dist: harbor<0.22,>=0.21.0
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# harbor-dsh-evolution
|
|
24
|
+
|
|
25
|
+
Harbor-side integration for DeepSeek Harness Candidate evolution.
|
|
26
|
+
|
|
27
|
+
It provides:
|
|
28
|
+
|
|
29
|
+
- `DshCandidateAgent`: verifies and uploads an immutable Candidate, installs its locked npm dependencies, and runs it through Harbor's ACP runner.
|
|
30
|
+
- `EvolutionPlugin`: records Candidate identity, evaluation-context identity, Trial events, and `evaluation-summary.json` for each Job.
|
|
31
|
+
- `harbor-dsh`: snapshots/verifies Candidates, fingerprints datasets, summarizes Jobs, and runs the deterministic Promotion Gate.
|
|
32
|
+
|
|
33
|
+
Install it into the same Python environment as Harbor so the plugin entry point is discoverable:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv venv .venv
|
|
37
|
+
uv pip install --python .venv/bin/python harbor-dsh-evolution
|
|
38
|
+
source .venv/bin/activate
|
|
39
|
+
harbor plugins list
|
|
40
|
+
harbor-dsh --help
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Development from this repository:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv sync
|
|
47
|
+
uv run harbor plugins list
|
|
48
|
+
uv run harbor-dsh --help
|
|
49
|
+
uv run harbor-dsh context ../../examples/deep-research/task
|
|
50
|
+
uv run pytest
|
|
51
|
+
uv build
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Harbor and this package must be installed into the same Python environment for the `dsh-evolution` entry point to appear in `harbor plugins list`.
|
|
55
|
+
|
|
56
|
+
`snapshot` derives Candidate id and version from `package.json` unless they are explicitly supplied. Promotion rejects Jobs whose `evaluation-context` digests are missing or different.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# harbor-dsh-evolution
|
|
2
|
+
|
|
3
|
+
Harbor-side integration for DeepSeek Harness Candidate evolution.
|
|
4
|
+
|
|
5
|
+
It provides:
|
|
6
|
+
|
|
7
|
+
- `DshCandidateAgent`: verifies and uploads an immutable Candidate, installs its locked npm dependencies, and runs it through Harbor's ACP runner.
|
|
8
|
+
- `EvolutionPlugin`: records Candidate identity, evaluation-context identity, Trial events, and `evaluation-summary.json` for each Job.
|
|
9
|
+
- `harbor-dsh`: snapshots/verifies Candidates, fingerprints datasets, summarizes Jobs, and runs the deterministic Promotion Gate.
|
|
10
|
+
|
|
11
|
+
Install it into the same Python environment as Harbor so the plugin entry point is discoverable:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv venv .venv
|
|
15
|
+
uv pip install --python .venv/bin/python harbor-dsh-evolution
|
|
16
|
+
source .venv/bin/activate
|
|
17
|
+
harbor plugins list
|
|
18
|
+
harbor-dsh --help
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Development from this repository:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv sync
|
|
25
|
+
uv run harbor plugins list
|
|
26
|
+
uv run harbor-dsh --help
|
|
27
|
+
uv run harbor-dsh context ../../examples/deep-research/task
|
|
28
|
+
uv run pytest
|
|
29
|
+
uv build
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Harbor and this package must be installed into the same Python environment for the `dsh-evolution` entry point to appear in `harbor plugins list`.
|
|
33
|
+
|
|
34
|
+
`snapshot` derives Candidate id and version from `package.json` unless they are explicitly supplied. Promotion rejects Jobs whose `evaluation-context` digests are missing or different.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "harbor-dsh-evolution"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Candidate identity, evaluation summaries, and promotion gates for DeepSeek Harness agents evaluated with Harbor."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "istarwyh" }]
|
|
10
|
+
keywords = [
|
|
11
|
+
"agent-evaluation",
|
|
12
|
+
"agent-evolution",
|
|
13
|
+
"deepseek-harness",
|
|
14
|
+
"harbor",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
]
|
|
24
|
+
dependencies = ["harbor>=0.21.0,<0.22"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/istarwyh/harbor-self-evolving"
|
|
28
|
+
Documentation = "https://github.com/istarwyh/harbor-self-evolving#readme"
|
|
29
|
+
Issues = "https://github.com/istarwyh/harbor-self-evolving/issues"
|
|
30
|
+
Repository = "https://github.com/istarwyh/harbor-self-evolving.git"
|
|
31
|
+
|
|
32
|
+
[project.entry-points."harbor.plugins"]
|
|
33
|
+
dsh-evolution = "harbor_dsh_evolution.plugin:EvolutionPlugin"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
harbor-dsh = "harbor_dsh_evolution.cli:main"
|
|
37
|
+
|
|
38
|
+
[dependency-groups]
|
|
39
|
+
dev = ["pytest>=8.4", "pytest-asyncio>=1.0"]
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["hatchling>=1.27"]
|
|
43
|
+
build-backend = "hatchling.build"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["src/harbor_dsh_evolution"]
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
testpaths = ["tests"]
|
|
50
|
+
addopts = "-q"
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shlex
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import override
|
|
7
|
+
|
|
8
|
+
from harbor.agents.installed.acp import AcpAgent, DistributionKind
|
|
9
|
+
from harbor.environments.base import BaseEnvironment
|
|
10
|
+
from harbor.models.agent.context import AgentContext
|
|
11
|
+
|
|
12
|
+
from harbor_dsh_evolution.candidate import CandidateManifest, verify_candidate
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DshCandidateAgent(AcpAgent):
|
|
16
|
+
"""Run one immutable DeepSeek Harness Candidate through Harbor's ACP runner."""
|
|
17
|
+
|
|
18
|
+
_REMOTE_ROOT = "/opt/harbor-dsh-candidate"
|
|
19
|
+
_DSH_ACP_PACKAGE = "@deepseek-ai/dsh-acp-demo@0.1.0-rc.6"
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
logs_dir: Path,
|
|
24
|
+
candidate_path: str,
|
|
25
|
+
candidate_digest: str,
|
|
26
|
+
candidate_version: str | None = None,
|
|
27
|
+
**kwargs,
|
|
28
|
+
) -> None:
|
|
29
|
+
self.candidate_path = Path(candidate_path).expanduser().resolve(strict=True)
|
|
30
|
+
self.manifest: CandidateManifest = verify_candidate(
|
|
31
|
+
self.candidate_path, expected_digest=candidate_digest
|
|
32
|
+
)
|
|
33
|
+
if candidate_version is not None and candidate_version != self.manifest.version:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
"Candidate version mismatch: "
|
|
36
|
+
f"requested={candidate_version}, manifest={self.manifest.version}"
|
|
37
|
+
)
|
|
38
|
+
self.candidate_digest = candidate_digest
|
|
39
|
+
|
|
40
|
+
registry_entry = {
|
|
41
|
+
"id": self.manifest.candidate_id,
|
|
42
|
+
"name": f"DeepSeek Harness Candidate {self.manifest.candidate_id}",
|
|
43
|
+
"version": self.manifest.version,
|
|
44
|
+
"description": "Immutable Cordis composition evaluated through ACP.",
|
|
45
|
+
"distribution": {
|
|
46
|
+
"npx": {
|
|
47
|
+
"package": self._DSH_ACP_PACKAGE,
|
|
48
|
+
"args": ["--config", f"{self._REMOTE_ROOT}/cordis.yml"],
|
|
49
|
+
"env": {
|
|
50
|
+
"DSH_SESSION_ROOT": f"{self._REMOTE_ROOT}/.sessions",
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
super().__init__(
|
|
56
|
+
logs_dir=logs_dir,
|
|
57
|
+
registry_entry=registry_entry,
|
|
58
|
+
distribution_preference="npx",
|
|
59
|
+
auth_policy="disabled",
|
|
60
|
+
# This adapter only exposes an architecture-neutral npx distribution.
|
|
61
|
+
# Supplying a supported platform avoids a best-effort uname probe on
|
|
62
|
+
# deliberately minimal task images where that probe can be unavailable.
|
|
63
|
+
target_platform="linux-x86_64",
|
|
64
|
+
**kwargs,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
@override
|
|
69
|
+
def name() -> str:
|
|
70
|
+
return "dsh-candidate"
|
|
71
|
+
|
|
72
|
+
@override
|
|
73
|
+
def version(self) -> str:
|
|
74
|
+
return self.manifest.version
|
|
75
|
+
|
|
76
|
+
@override
|
|
77
|
+
def _build_dependencies_command(self, kind: DistributionKind) -> str:
|
|
78
|
+
"""Avoid network installation when the Task image is ACP-ready.
|
|
79
|
+
|
|
80
|
+
Harbor's generic ACP installer supports arbitrary base images, but a
|
|
81
|
+
stable evaluation image should pin these dependencies at build time.
|
|
82
|
+
``stdbuf`` is included because Harbor's ACP log pipeline requires it.
|
|
83
|
+
"""
|
|
84
|
+
install = super()._build_dependencies_command(kind)
|
|
85
|
+
return f"""
|
|
86
|
+
set -euo pipefail
|
|
87
|
+
if command -v bash >/dev/null 2>&1 \
|
|
88
|
+
&& command -v python3 >/dev/null 2>&1 \
|
|
89
|
+
&& command -v curl >/dev/null 2>&1 \
|
|
90
|
+
&& command -v node >/dev/null 2>&1 \
|
|
91
|
+
&& command -v npm >/dev/null 2>&1 \
|
|
92
|
+
&& command -v stdbuf >/dev/null 2>&1 \
|
|
93
|
+
&& [ -x {self._RUNNER_VENV_PATH}/bin/python ] \
|
|
94
|
+
&& {self._RUNNER_VENV_PATH}/bin/python -c 'import acp' >/dev/null 2>&1; then
|
|
95
|
+
exit 0
|
|
96
|
+
fi
|
|
97
|
+
{install}
|
|
98
|
+
if ! command -v stdbuf >/dev/null 2>&1; then
|
|
99
|
+
if command -v apk >/dev/null 2>&1; then
|
|
100
|
+
apk add --no-cache coreutils
|
|
101
|
+
elif command -v apt-get >/dev/null 2>&1; then
|
|
102
|
+
apt-get install -y coreutils
|
|
103
|
+
fi
|
|
104
|
+
fi
|
|
105
|
+
""".strip()
|
|
106
|
+
|
|
107
|
+
@override
|
|
108
|
+
async def setup(self, environment: BaseEnvironment) -> None:
|
|
109
|
+
await super().setup(environment)
|
|
110
|
+
await environment.exec(
|
|
111
|
+
f"rm -rf {shlex.quote(self._REMOTE_ROOT)} && "
|
|
112
|
+
f"mkdir -p {shlex.quote(self._REMOTE_ROOT)}",
|
|
113
|
+
user="root",
|
|
114
|
+
)
|
|
115
|
+
await environment.upload_dir(self.candidate_path, self._REMOTE_ROOT)
|
|
116
|
+
|
|
117
|
+
package_command = (
|
|
118
|
+
"npm ci" if (self.candidate_path / "package-lock.json").is_file()
|
|
119
|
+
else "npm install"
|
|
120
|
+
)
|
|
121
|
+
install = await environment.exec(
|
|
122
|
+
f"{package_command} --omit=dev --ignore-scripts --no-audit --no-fund",
|
|
123
|
+
cwd=self._REMOTE_ROOT,
|
|
124
|
+
user="root",
|
|
125
|
+
timeout_sec=600,
|
|
126
|
+
)
|
|
127
|
+
if install.return_code != 0:
|
|
128
|
+
raise RuntimeError(
|
|
129
|
+
"Failed to install Candidate runtime dependencies: "
|
|
130
|
+
f"{install.stderr or install.stdout}"
|
|
131
|
+
)
|
|
132
|
+
await environment.exec(
|
|
133
|
+
f"chmod -R a+rX {shlex.quote(self._REMOTE_ROOT)}", user="root"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
@override
|
|
137
|
+
def populate_context_post_run(self, context: AgentContext) -> None:
|
|
138
|
+
super().populate_context_post_run(context)
|
|
139
|
+
metadata = dict(context.metadata or {})
|
|
140
|
+
metadata["candidate"] = {
|
|
141
|
+
"id": self.manifest.candidate_id,
|
|
142
|
+
"version": self.manifest.version,
|
|
143
|
+
"digest": self.candidate_digest,
|
|
144
|
+
"runtime": self.manifest.runtime,
|
|
145
|
+
}
|
|
146
|
+
context.metadata = metadata
|
|
147
|
+
|
|
148
|
+
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
(self.logs_dir / "candidate.json").write_text(
|
|
150
|
+
json.dumps(metadata["candidate"], ensure_ascii=False, indent=2) + "\n"
|
|
151
|
+
)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import asdict, dataclass
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
MANIFEST_NAME = "candidate-manifest.json"
|
|
12
|
+
_DIGEST_PREFIX = b"harbor-dsh-candidate-v1\0"
|
|
13
|
+
_EXCLUDED_DIRS = {".git", "node_modules", "__pycache__"}
|
|
14
|
+
_EXCLUDED_FILES = {MANIFEST_NAME, ".DS_Store"}
|
|
15
|
+
_DIGEST_PATTERN = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class CandidateFile:
|
|
20
|
+
path: str
|
|
21
|
+
size: int
|
|
22
|
+
sha256: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class CandidateManifest:
|
|
27
|
+
schema_version: int
|
|
28
|
+
candidate_id: str
|
|
29
|
+
version: str
|
|
30
|
+
digest: str
|
|
31
|
+
created_at: str
|
|
32
|
+
runtime: dict[str, str]
|
|
33
|
+
files: list[CandidateFile]
|
|
34
|
+
metadata: dict[str, Any]
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> dict[str, Any]:
|
|
37
|
+
return asdict(self)
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def from_dict(cls, value: dict[str, Any]) -> "CandidateManifest":
|
|
41
|
+
schema_version = int(value["schema_version"])
|
|
42
|
+
if schema_version != 1:
|
|
43
|
+
raise ValueError(f"Unsupported Candidate schema_version: {schema_version}")
|
|
44
|
+
candidate_id = str(value["candidate_id"])
|
|
45
|
+
version = str(value["version"])
|
|
46
|
+
digest = str(value["digest"])
|
|
47
|
+
if not candidate_id or not version:
|
|
48
|
+
raise ValueError("Candidate id and version must not be empty")
|
|
49
|
+
if not _DIGEST_PATTERN.fullmatch(digest):
|
|
50
|
+
raise ValueError(f"Invalid Candidate digest: {digest}")
|
|
51
|
+
return cls(
|
|
52
|
+
schema_version=schema_version,
|
|
53
|
+
candidate_id=candidate_id,
|
|
54
|
+
version=version,
|
|
55
|
+
digest=digest,
|
|
56
|
+
created_at=str(value["created_at"]),
|
|
57
|
+
runtime=dict(value["runtime"]),
|
|
58
|
+
files=[CandidateFile(**item) for item in value["files"]],
|
|
59
|
+
metadata=dict(value.get("metadata") or {}),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _candidate_files(candidate_dir: Path) -> list[Path]:
|
|
64
|
+
paths: list[Path] = []
|
|
65
|
+
for path in candidate_dir.rglob("*"):
|
|
66
|
+
relative = path.relative_to(candidate_dir)
|
|
67
|
+
if any(part in _EXCLUDED_DIRS for part in relative.parts):
|
|
68
|
+
continue
|
|
69
|
+
if path.name in _EXCLUDED_FILES:
|
|
70
|
+
continue
|
|
71
|
+
if path.is_symlink():
|
|
72
|
+
raise ValueError(f"Candidate must not contain symlinks: {relative}")
|
|
73
|
+
if path.is_file():
|
|
74
|
+
paths.append(path)
|
|
75
|
+
return sorted(paths, key=lambda item: item.relative_to(candidate_dir).as_posix())
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def compute_candidate(candidate_dir: Path) -> tuple[str, list[CandidateFile]]:
|
|
79
|
+
candidate_dir = candidate_dir.expanduser().resolve(strict=True)
|
|
80
|
+
if not candidate_dir.is_dir():
|
|
81
|
+
raise ValueError(f"Candidate path is not a directory: {candidate_dir}")
|
|
82
|
+
|
|
83
|
+
digest = hashlib.sha256()
|
|
84
|
+
digest.update(_DIGEST_PREFIX)
|
|
85
|
+
files: list[CandidateFile] = []
|
|
86
|
+
for path in _candidate_files(candidate_dir):
|
|
87
|
+
relative = path.relative_to(candidate_dir).as_posix()
|
|
88
|
+
content = path.read_bytes()
|
|
89
|
+
file_digest = hashlib.sha256(content).hexdigest()
|
|
90
|
+
files.append(
|
|
91
|
+
CandidateFile(path=relative, size=len(content), sha256=file_digest)
|
|
92
|
+
)
|
|
93
|
+
digest.update(relative.encode("utf-8"))
|
|
94
|
+
digest.update(b"\0")
|
|
95
|
+
digest.update(str(len(content)).encode("ascii"))
|
|
96
|
+
digest.update(b"\0")
|
|
97
|
+
digest.update(content)
|
|
98
|
+
digest.update(b"\0")
|
|
99
|
+
return f"sha256:{digest.hexdigest()}", files
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def snapshot_candidate(
|
|
103
|
+
candidate_dir: Path,
|
|
104
|
+
*,
|
|
105
|
+
candidate_id: str | None = None,
|
|
106
|
+
version: str | None = None,
|
|
107
|
+
runtime_version: str = "0.1.0-rc.6",
|
|
108
|
+
metadata: dict[str, Any] | None = None,
|
|
109
|
+
) -> CandidateManifest:
|
|
110
|
+
candidate_dir = candidate_dir.expanduser().resolve(strict=True)
|
|
111
|
+
required = [candidate_dir / "cordis.yml", candidate_dir / "package.json"]
|
|
112
|
+
missing = [path.name for path in required if not path.is_file()]
|
|
113
|
+
if missing:
|
|
114
|
+
raise ValueError(f"Candidate is missing required files: {', '.join(missing)}")
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
package = json.loads((candidate_dir / "package.json").read_text())
|
|
118
|
+
except json.JSONDecodeError as error:
|
|
119
|
+
raise ValueError("Candidate package.json is not valid JSON") from error
|
|
120
|
+
if candidate_id is None:
|
|
121
|
+
candidate_id = str(package.get("name") or "")
|
|
122
|
+
if version is None:
|
|
123
|
+
version = str(package.get("version") or "")
|
|
124
|
+
if not candidate_id or not version or not runtime_version:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
"Candidate id, version, and runtime version must not be empty; "
|
|
127
|
+
"set package.json name/version or pass explicit values"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
digest, files = compute_candidate(candidate_dir)
|
|
131
|
+
manifest = CandidateManifest(
|
|
132
|
+
schema_version=1,
|
|
133
|
+
candidate_id=candidate_id,
|
|
134
|
+
version=version,
|
|
135
|
+
digest=digest,
|
|
136
|
+
created_at=datetime.now(timezone.utc).isoformat(),
|
|
137
|
+
runtime={
|
|
138
|
+
"kind": "deepseek-harness",
|
|
139
|
+
"version": runtime_version,
|
|
140
|
+
"transport": "acp",
|
|
141
|
+
},
|
|
142
|
+
files=files,
|
|
143
|
+
metadata=metadata or {},
|
|
144
|
+
)
|
|
145
|
+
(candidate_dir / MANIFEST_NAME).write_text(
|
|
146
|
+
json.dumps(manifest.to_dict(), ensure_ascii=False, indent=2) + "\n"
|
|
147
|
+
)
|
|
148
|
+
return manifest
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def load_manifest(candidate_dir: Path) -> CandidateManifest:
|
|
152
|
+
path = candidate_dir.expanduser().resolve(strict=True) / MANIFEST_NAME
|
|
153
|
+
return CandidateManifest.from_dict(json.loads(path.read_text()))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def verify_candidate(
|
|
157
|
+
candidate_dir: Path,
|
|
158
|
+
*,
|
|
159
|
+
expected_digest: str | None = None,
|
|
160
|
+
) -> CandidateManifest:
|
|
161
|
+
candidate_dir = candidate_dir.expanduser().resolve(strict=True)
|
|
162
|
+
manifest = load_manifest(candidate_dir)
|
|
163
|
+
actual_digest, actual_files = compute_candidate(candidate_dir)
|
|
164
|
+
if actual_digest != manifest.digest:
|
|
165
|
+
raise ValueError(
|
|
166
|
+
"Candidate digest mismatch: "
|
|
167
|
+
f"manifest={manifest.digest}, actual={actual_digest}"
|
|
168
|
+
)
|
|
169
|
+
if expected_digest is not None and actual_digest != expected_digest:
|
|
170
|
+
raise ValueError(
|
|
171
|
+
"Candidate does not match requested digest: "
|
|
172
|
+
f"expected={expected_digest}, actual={actual_digest}"
|
|
173
|
+
)
|
|
174
|
+
if actual_files != manifest.files:
|
|
175
|
+
raise ValueError("Candidate file inventory does not match its manifest")
|
|
176
|
+
return manifest
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from harbor_dsh_evolution.candidate import snapshot_candidate, verify_candidate
|
|
8
|
+
from harbor_dsh_evolution.context import build_evaluation_context
|
|
9
|
+
from harbor_dsh_evolution.promotion import compare_jobs, write_report
|
|
10
|
+
from harbor_dsh_evolution.summary import load_or_create_summary
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _parser() -> argparse.ArgumentParser:
|
|
14
|
+
parser = argparse.ArgumentParser(prog="harbor-dsh")
|
|
15
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
16
|
+
|
|
17
|
+
snapshot = commands.add_parser("snapshot", help="Freeze a Candidate directory")
|
|
18
|
+
snapshot.add_argument("candidate_dir", type=Path)
|
|
19
|
+
snapshot.add_argument("--id", dest="candidate_id")
|
|
20
|
+
snapshot.add_argument("--version")
|
|
21
|
+
snapshot.add_argument("--runtime-version", default="0.1.0-rc.6")
|
|
22
|
+
|
|
23
|
+
verify = commands.add_parser("verify", help="Verify a Candidate digest")
|
|
24
|
+
verify.add_argument("candidate_dir", type=Path)
|
|
25
|
+
verify.add_argument("--digest")
|
|
26
|
+
|
|
27
|
+
summary = commands.add_parser("summarize", help="Summarize a Harbor Job")
|
|
28
|
+
summary.add_argument("job_dir", type=Path)
|
|
29
|
+
|
|
30
|
+
context = commands.add_parser("context", help="Fingerprint a Harbor dataset")
|
|
31
|
+
context.add_argument("dataset_dir", type=Path)
|
|
32
|
+
|
|
33
|
+
promote = commands.add_parser("promote", help="Apply a Promotion Gate")
|
|
34
|
+
promote.add_argument("baseline_job", type=Path)
|
|
35
|
+
promote.add_argument("candidate_job", type=Path)
|
|
36
|
+
promote.add_argument("--policy", required=True, type=Path)
|
|
37
|
+
promote.add_argument("--output", type=Path)
|
|
38
|
+
return parser
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main() -> int:
|
|
42
|
+
args = _parser().parse_args()
|
|
43
|
+
if args.command == "snapshot":
|
|
44
|
+
result = snapshot_candidate(
|
|
45
|
+
args.candidate_dir,
|
|
46
|
+
candidate_id=args.candidate_id,
|
|
47
|
+
version=args.version,
|
|
48
|
+
runtime_version=args.runtime_version,
|
|
49
|
+
).to_dict()
|
|
50
|
+
elif args.command == "verify":
|
|
51
|
+
result = verify_candidate(
|
|
52
|
+
args.candidate_dir, expected_digest=args.digest
|
|
53
|
+
).to_dict()
|
|
54
|
+
elif args.command == "summarize":
|
|
55
|
+
result = load_or_create_summary(args.job_dir)
|
|
56
|
+
elif args.command == "context":
|
|
57
|
+
result = build_evaluation_context(args.dataset_dir).to_dict()
|
|
58
|
+
else:
|
|
59
|
+
report = compare_jobs(args.baseline_job, args.candidate_job, args.policy)
|
|
60
|
+
output = args.output or args.candidate_job / "promotion-report.json"
|
|
61
|
+
write_report(report, output)
|
|
62
|
+
result = report
|
|
63
|
+
|
|
64
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
65
|
+
return 0 if result.get("decision") != "REJECT" else 1
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
raise SystemExit(main())
|