gitinject 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.
- gitinject/__init__.py +1 -0
- gitinject/__main__.py +5 -0
- gitinject/analyzer.py +67 -0
- gitinject/attacks/__init__.py +27 -0
- gitinject/attacks/autoinject.py +165 -0
- gitinject/attacks/base.py +33 -0
- gitinject/attacks/static.py +25 -0
- gitinject/cli.py +768 -0
- gitinject/data/research/scenarios/claude_skills_injection.md +96 -0
- gitinject/data/research/scenarios/cline_issue_body_injection.md +93 -0
- gitinject/data/research/scenarios/codex_agents_md_injection.md +109 -0
- gitinject/data/research/scenarios/dos_request_flood.md +133 -0
- gitinject/data/research/scenarios/dropped/ci_log_injection_workflow_poisoning.md +138 -0
- gitinject/data/research/scenarios/dropped/claude_md_instructions_injection.md +158 -0
- gitinject/data/research/scenarios/dropped/supply_chain_token_pivot.md +101 -0
- gitinject/data/research/scenarios/gemini_api_key_exfiltration.md +68 -0
- gitinject/data/research/scenarios/gemini_api_key_exfiltration_replication.md +0 -0
- gitinject/data/research/scenarios/gemini_md_instructions_injection.md +131 -0
- gitinject/data/research/scenarios/nsfw_api_key_block.md +114 -0
- gitinject/data/research/scenarios/pr_token_exfiltration_via_git_config.md +117 -0
- gitinject/data/research/scenarios/supply_chain_malicious_code.md +135 -0
- gitinject/evaluators.py +169 -0
- gitinject/evidence.py +72 -0
- gitinject/gl_runner.py +163 -0
- gitinject/resources.py +30 -0
- gitinject/run_context.py +72 -0
- gitinject/run_record.py +113 -0
- gitinject/runner.py +1024 -0
- gitinject/scanner/__init__.py +22 -0
- gitinject/scanner/baselines/__init__.py +0 -0
- gitinject/scanner/baselines/actionlint_runner.py +44 -0
- gitinject/scanner/baselines/zizmor_runner.py +35 -0
- gitinject/scanner/diagnostics.py +174 -0
- gitinject/scanner/hypothesis_generator.py +164 -0
- gitinject/scanner/live_validator.py +284 -0
- gitinject/scanner/llm_ranker.py +120 -0
- gitinject/scanner/memory.py +239 -0
- gitinject/scanner/primitives.py +236 -0
- gitinject/scanner/prompt_extractor.py +134 -0
- gitinject/scanner/recipe_scenario.py +203 -0
- gitinject/scanner/report_generator.py +230 -0
- gitinject/scanner/types.py +179 -0
- gitinject/scenario_base.py +147 -0
- gitinject/scenario_loader.py +77 -0
- gitinject/scenarios/__init__.py +0 -0
- gitinject/scenarios/benign/documentation_out_of_sync/contents/math_utils.py +3 -0
- gitinject/scenarios/benign/documentation_out_of_sync/contents/pydantic/v1/utils.py +21 -0
- gitinject/scenarios/benign/documentation_out_of_sync/scenario.py +57 -0
- gitinject/scenarios/benign/issue_deduplication_utility/scenario.py +76 -0
- gitinject/scenarios/benign/repository_onboarding_qa/contents/README.md +3 -0
- gitinject/scenarios/benign/repository_onboarding_qa/scenario.py +52 -0
- gitinject/scenarios/benign/secrets_false_positive/contents/tests/test_auth.py +14 -0
- gitinject/scenarios/benign/secrets_false_positive/scenario.py +58 -0
- gitinject/scenarios/benign/test_coverage_gap/contents/src/requests/auth_extra.py +33 -0
- gitinject/scenarios/benign/test_coverage_gap/scenario.py +56 -0
- gitinject/scenarios/benign/vulnerability_fix_verification/contents/src/flask/utils.py +15 -0
- gitinject/scenarios/benign/vulnerability_fix_verification/scenario.py +57 -0
- gitinject/scenarios/benign/vulnerable_code_review/contents/examples/tutorial/flaskr/db_utils.py +18 -0
- gitinject/scenarios/benign/vulnerable_code_review/scenario.py +55 -0
- gitinject/scenarios/benign/weekly_activity_summary/scenario.py +83 -0
- gitinject/scenarios/malicious/agents_md_approval_manipulation/contents/AGENTS.md +25 -0
- gitinject/scenarios/malicious/agents_md_approval_manipulation/contents/src/flask/csrf.py +151 -0
- gitinject/scenarios/malicious/agents_md_approval_manipulation/contents/tests/test_csrf.py +71 -0
- gitinject/scenarios/malicious/agents_md_approval_manipulation/scenario.py +121 -0
- gitinject/scenarios/malicious/claude_md_instructions_injection/contents/CLAUDE.md +23 -0
- gitinject/scenarios/malicious/claude_md_instructions_injection/contents/scripts/deploy_check.sh +29 -0
- gitinject/scenarios/malicious/claude_md_instructions_injection/scenario.py +93 -0
- gitinject/scenarios/malicious/cline_issue_body_injection/scenario.py +132 -0
- gitinject/scenarios/malicious/codex_agents_md_injection/contents/AGENTS.md +36 -0
- gitinject/scenarios/malicious/codex_agents_md_injection/contents/scripts/utils.py +12 -0
- gitinject/scenarios/malicious/codex_agents_md_injection/scenario.py +128 -0
- gitinject/scenarios/malicious/dos_request_flood/scenario.py +80 -0
- gitinject/scenarios/malicious/gemini_md_approval_manipulation/contents/src/flask/csrf.py +151 -0
- gitinject/scenarios/malicious/gemini_md_approval_manipulation/contents/tests/test_csrf.py +71 -0
- gitinject/scenarios/malicious/gemini_md_approval_manipulation/scenario.py +121 -0
- gitinject/scenarios/malicious/gemini_md_instructions_injection/contents/scripts/deploy_check.sh +38 -0
- gitinject/scenarios/malicious/gemini_md_instructions_injection/scenario.py +105 -0
- gitinject/scenarios/malicious/issue_onboarding_secret_exfiltration/scenario.py +76 -0
- gitinject/scenarios/malicious/pr_token_exfiltration_via_git_config/contents/scripts/deploy_check.sh +7 -0
- gitinject/scenarios/malicious/pr_token_exfiltration_via_git_config/scenario.py +113 -0
- gitinject/scenarios/malicious/supply_chain_malicious_code/contents/CLAUDE.md +25 -0
- gitinject/scenarios/malicious/supply_chain_malicious_code/contents/src/flask/csrf.py +151 -0
- gitinject/scenarios/malicious/supply_chain_malicious_code/contents/tests/test_csrf.py +71 -0
- gitinject/scenarios/malicious/supply_chain_malicious_code/scenario.py +123 -0
- gitinject/scenarios/malicious/unauthorized_pr_approval/contents/django/utils/crypto.py +13 -0
- gitinject/scenarios/malicious/unauthorized_pr_approval/scenario.py +57 -0
- gitinject/simulator.py +89 -0
- gitinject/utils/__init__.py +0 -0
- gitinject/utils/gh_client.py +628 -0
- gitinject/utils/gl_client.py +132 -0
- gitinject/utils/gl_provisioner.py +83 -0
- gitinject/utils/llm.py +205 -0
- gitinject/utils/provisioner.py +114 -0
- gitinject/utils/scenario_resources.py +33 -0
- gitinject/utils/types.py +49 -0
- gitinject/workflows/__init__.py +0 -0
- gitinject/workflows/claude-ci-auto-fix/contents/.github/workflows/main.yml +107 -0
- gitinject/workflows/claude-ci-auto-fix/metadata.json +10 -0
- gitinject/workflows/claude-general/contents/.github/workflows/main.yml +58 -0
- gitinject/workflows/claude-general/metadata.json +10 -0
- gitinject/workflows/claude-gitlab-mr-review/contents/.gitlab-ci.yml +36 -0
- gitinject/workflows/claude-gitlab-mr-review/metadata.json +11 -0
- gitinject/workflows/claude-issue-deduplication/contents/.github/workflows/main.yml +66 -0
- gitinject/workflows/claude-issue-deduplication/metadata.json +10 -0
- gitinject/workflows/claude-issue-triage/contents/.github/workflows/main.yml +34 -0
- gitinject/workflows/claude-issue-triage/metadata.json +10 -0
- gitinject/workflows/claude-manual-analysis/contents/.github/workflows/main.yml +42 -0
- gitinject/workflows/claude-manual-analysis/metadata.json +10 -0
- gitinject/workflows/claude-pr-review/contents/.github/workflows/main.yml +77 -0
- gitinject/workflows/claude-pr-review/metadata.json +10 -0
- gitinject/workflows/claude-pr-review-authors/contents/.github/workflows/main.yml +48 -0
- gitinject/workflows/claude-pr-review-authors/metadata.json +10 -0
- gitinject/workflows/claude-pr-review-paths/contents/.github/workflows/main.yml +49 -0
- gitinject/workflows/claude-pr-review-paths/metadata.json +10 -0
- gitinject/workflows/claude-test-analysis/contents/.github/workflows/main.yml +114 -0
- gitinject/workflows/claude-test-analysis/metadata.json +10 -0
- gitinject/workflows/cline-assistant/contents/.github/workflows/main.yml +87 -0
- gitinject/workflows/cline-assistant/contents/git-scripts/analyze-issue.sh +43 -0
- gitinject/workflows/cline-assistant/metadata.json +10 -0
- gitinject/workflows/codex-pr-review/contents/.github/workflows/main.yml +73 -0
- gitinject/workflows/codex-pr-review/metadata.json +10 -0
- gitinject/workflows/copilot-ci-doctor/contents/.github/workflows/ci-doctor.yml +1161 -0
- gitinject/workflows/copilot-ci-doctor/metadata.json +10 -0
- gitinject/workflows/copilot-lean-squad/contents/.github/workflows/lean-squad.yml +1313 -0
- gitinject/workflows/copilot-lean-squad/metadata.json +10 -0
- gitinject/workflows/copilot-malicious-scan/contents/.github/workflows/daily-malicious-code-scan.yml +899 -0
- gitinject/workflows/copilot-malicious-scan/metadata.json +10 -0
- gitinject/workflows/copilot-repo-assist/contents/.github/workflows/repo-assist.yml +1503 -0
- gitinject/workflows/copilot-repo-assist/metadata.json +10 -0
- gitinject/workflows/copilot-wiki-writer/contents/.github/workflows/agentic-wiki-writer.yml +1316 -0
- gitinject/workflows/copilot-wiki-writer/metadata.json +10 -0
- gitinject/workflows/gemini-assistant/contents/.github/workflows/gemini-invoke.yml +122 -0
- gitinject/workflows/gemini-assistant/contents/.github/workflows/gemini-plan-execute.yml +130 -0
- gitinject/workflows/gemini-assistant/contents/.github/workflows/gemini-review.yml +118 -0
- gitinject/workflows/gemini-assistant/contents/.github/workflows/gemini-scheduled-triage.yml +220 -0
- gitinject/workflows/gemini-assistant/contents/.github/workflows/gemini-triage.yml +160 -0
- gitinject/workflows/gemini-assistant/contents/.github/workflows/main.yml +220 -0
- gitinject/workflows/gemini-assistant/metadata.json +10 -0
- gitinject/workflows/gemini-assistant-original/AWESOME.md +118 -0
- gitinject/workflows/gemini-assistant-original/CONFIGURATION.md +162 -0
- gitinject/workflows/gemini-assistant-original/README.md +93 -0
- gitinject/workflows/gemini-assistant-original/gemini-assistant/README.md +192 -0
- gitinject/workflows/gemini-assistant-original/gemini-assistant/gemini-invoke.toml +94 -0
- gitinject/workflows/gemini-assistant-original/gemini-assistant/gemini-invoke.yml +131 -0
- gitinject/workflows/gemini-assistant-original/gemini-assistant/gemini-plan-execute.toml +100 -0
- gitinject/workflows/gemini-assistant-original/gemini-assistant/gemini-plan-execute.yml +139 -0
- gitinject/workflows/gemini-assistant-original/gemini-dispatch/README.md +49 -0
- gitinject/workflows/gemini-assistant-original/gemini-dispatch/gemini-dispatch.yml +221 -0
- gitinject/workflows/gemini-assistant-original/issue-triage/README.md +190 -0
- gitinject/workflows/gemini-assistant-original/issue-triage/gemini-scheduled-triage.toml +96 -0
- gitinject/workflows/gemini-assistant-original/issue-triage/gemini-scheduled-triage.yml +223 -0
- gitinject/workflows/gemini-assistant-original/issue-triage/gemini-triage.toml +32 -0
- gitinject/workflows/gemini-assistant-original/issue-triage/gemini-triage.yml +167 -0
- gitinject/workflows/gemini-assistant-original/metadata.json +10 -0
- gitinject/workflows/gemini-assistant-original/pr-review/README.md +337 -0
- gitinject/workflows/gemini-assistant-original/pr-review/gemini-review.toml +176 -0
- gitinject/workflows/gemini-assistant-original/pr-review/gemini-review.yml +119 -0
- gitinject/workflows/opencode-pr-review/contents/.github/workflows/main.yml +28 -0
- gitinject/workflows/opencode-pr-review/metadata.json +10 -0
- gitinject-0.1.0.dist-info/METADATA +128 -0
- gitinject-0.1.0.dist-info/RECORD +164 -0
- gitinject-0.1.0.dist-info/WHEEL +4 -0
- gitinject-0.1.0.dist-info/entry_points.txt +2 -0
- gitinject-0.1.0.dist-info/licenses/LICENSE +202 -0
gitinject/resources.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Locate read-only datasets separately from the writable experiment workspace."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
PACKAGE_DIR = Path(__file__).resolve().parent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def dataset_dir(name: str, workspace_dir: str | Path | None = None) -> Path:
|
|
9
|
+
"""Use a workspace dataset when present, otherwise use the bundled dataset."""
|
|
10
|
+
if name not in {"workflows", "scenarios"}:
|
|
11
|
+
raise ValueError(f"Unknown dataset: {name}")
|
|
12
|
+
workspace = Path(workspace_dir) if workspace_dir is not None else Path.cwd()
|
|
13
|
+
for candidate in (workspace / name, workspace / "src/gitinject" / name, workspace / "src/benchmark" / name):
|
|
14
|
+
if candidate.is_dir():
|
|
15
|
+
return candidate.resolve()
|
|
16
|
+
return PACKAGE_DIR / name
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def research_dir(workspace_dir: str | Path | None = None) -> Path:
|
|
20
|
+
"""Locate the scanner warm-start notes in a workspace, wheel, or source tree."""
|
|
21
|
+
workspace = Path(workspace_dir) if workspace_dir is not None else Path.cwd()
|
|
22
|
+
candidates = (
|
|
23
|
+
workspace / "research/scenarios",
|
|
24
|
+
PACKAGE_DIR / "data/research/scenarios",
|
|
25
|
+
PACKAGE_DIR.parent.parent / "research/scenarios",
|
|
26
|
+
)
|
|
27
|
+
for candidate in candidates:
|
|
28
|
+
if candidate.is_dir():
|
|
29
|
+
return candidate.resolve()
|
|
30
|
+
raise FileNotFoundError("Scanner warm-start research notes are unavailable")
|
gitinject/run_context.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""The runtime capabilities supplied to Python scenarios."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Callable, Literal
|
|
6
|
+
|
|
7
|
+
from .run_record import RunRecord, RunSpec
|
|
8
|
+
from .utils.gh_client import GitHubClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class TriggerReceipt:
|
|
13
|
+
event_type: str | None = None
|
|
14
|
+
subject_kind: Literal["pr", "issue"] | None = None
|
|
15
|
+
subject_number: int | None = None
|
|
16
|
+
workflow_run_id: int | None = None
|
|
17
|
+
|
|
18
|
+
def __post_init__(self):
|
|
19
|
+
if self.subject_kind not in {None, "pr", "issue"}:
|
|
20
|
+
raise ValueError("Subject kind must be pr or issue")
|
|
21
|
+
if (self.subject_kind is None) != (self.subject_number is None):
|
|
22
|
+
raise ValueError("Subject kind and number must be supplied together")
|
|
23
|
+
for value in (self.subject_number, self.workflow_run_id):
|
|
24
|
+
if value is not None and (type(value) is not int or value <= 0):
|
|
25
|
+
raise ValueError("GitHub identifiers must be positive integers")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class RunContext:
|
|
30
|
+
spec: RunSpec
|
|
31
|
+
record: RunRecord
|
|
32
|
+
state: dict
|
|
33
|
+
actors: dict[str, GitHubClient]
|
|
34
|
+
default_trigger: Callable[[], TriggerReceipt]
|
|
35
|
+
collect_target: Callable[[], dict]
|
|
36
|
+
_repositories: list[tuple[str, str, int]] = field(default_factory=list)
|
|
37
|
+
|
|
38
|
+
def __post_init__(self):
|
|
39
|
+
self.rng = random.Random(self.spec.seed)
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def parameters(self) -> dict:
|
|
43
|
+
return self.spec.parameters
|
|
44
|
+
|
|
45
|
+
def github(self, actor: str) -> GitHubClient:
|
|
46
|
+
if actor not in self.actors:
|
|
47
|
+
raise ValueError(f"Required GitHub actor is unavailable: {actor}")
|
|
48
|
+
return self.actors[actor]
|
|
49
|
+
|
|
50
|
+
def save_artifact(self, name: str, value):
|
|
51
|
+
return self.record.artifact(name, value)
|
|
52
|
+
|
|
53
|
+
def track_repository(self, actor: str, name: str, repository_id: int) -> None:
|
|
54
|
+
"""Register a repository immediately after creating it through the raw API."""
|
|
55
|
+
self.github(actor)
|
|
56
|
+
if not name or type(repository_id) is not int or repository_id <= 0:
|
|
57
|
+
raise ValueError("Repository ownership requires a name and immutable ID")
|
|
58
|
+
identity = (actor, name, repository_id)
|
|
59
|
+
if identity not in self._repositories:
|
|
60
|
+
self._repositories.append(identity)
|
|
61
|
+
self.record.event("resource", actor=actor, name=name, id=repository_id, state="created")
|
|
62
|
+
|
|
63
|
+
def cleanup_repositories(self) -> list[str]:
|
|
64
|
+
errors = []
|
|
65
|
+
for actor, name, repository_id in self._repositories[:]:
|
|
66
|
+
try:
|
|
67
|
+
self.github(actor)._delete_repository(name, repository_id)
|
|
68
|
+
self.record.event("resource", actor=actor, name=name, id=repository_id, state="deleted")
|
|
69
|
+
self._repositories.remove((actor, name, repository_id))
|
|
70
|
+
except Exception as exc:
|
|
71
|
+
errors.append(str(exc))
|
|
72
|
+
return errors
|
gitinject/run_record.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Local attempt manifests and append-only execution journals."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import uuid
|
|
8
|
+
from dataclasses import asdict, dataclass, field
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class RunSpec:
|
|
15
|
+
workflow: str
|
|
16
|
+
scenario: str
|
|
17
|
+
parameters: dict = field(default_factory=dict)
|
|
18
|
+
seed: int | None = None
|
|
19
|
+
parent_attempt_id: str | None = None
|
|
20
|
+
attack: str | None = None
|
|
21
|
+
cleanup: bool = True
|
|
22
|
+
|
|
23
|
+
def __post_init__(self):
|
|
24
|
+
if not isinstance(self.parameters, dict):
|
|
25
|
+
raise ValueError("Scenario parameters must be a JSON object")
|
|
26
|
+
if self.seed is not None and type(self.seed) is not int:
|
|
27
|
+
raise ValueError("Scenario seed must be an integer")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def write_json(path: Path, value) -> None:
|
|
31
|
+
temporary = path.with_name(path.name + ".tmp")
|
|
32
|
+
with temporary.open("w") as handle:
|
|
33
|
+
json.dump(value, handle, indent=2, allow_nan=False)
|
|
34
|
+
handle.flush()
|
|
35
|
+
os.fsync(handle.fileno())
|
|
36
|
+
temporary.replace(path)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RunRecord:
|
|
40
|
+
def __init__(self, workspace_dir: str, spec: RunSpec):
|
|
41
|
+
self.attempt_id = uuid.uuid4().hex
|
|
42
|
+
self.timestamp = datetime.now(timezone.utc).isoformat()
|
|
43
|
+
self.directory = Path(workspace_dir) / "runs" / self.attempt_id
|
|
44
|
+
self.directory.mkdir(parents=True)
|
|
45
|
+
self.manifest = {
|
|
46
|
+
"schema_version": 1,
|
|
47
|
+
"attempt_id": self.attempt_id,
|
|
48
|
+
"timestamp": self.timestamp,
|
|
49
|
+
"spec": asdict(spec),
|
|
50
|
+
"inputs": {},
|
|
51
|
+
}
|
|
52
|
+
try:
|
|
53
|
+
revision = subprocess.run(
|
|
54
|
+
["git", "rev-parse", "HEAD"],
|
|
55
|
+
cwd=workspace_dir,
|
|
56
|
+
capture_output=True,
|
|
57
|
+
text=True,
|
|
58
|
+
check=True,
|
|
59
|
+
).stdout.strip()
|
|
60
|
+
self.manifest["source_revision"] = revision
|
|
61
|
+
status = subprocess.run(
|
|
62
|
+
["git", "status", "--porcelain"],
|
|
63
|
+
cwd=workspace_dir,
|
|
64
|
+
capture_output=True,
|
|
65
|
+
text=True,
|
|
66
|
+
check=True,
|
|
67
|
+
).stdout
|
|
68
|
+
self.manifest["source_dirty"] = bool(status.strip())
|
|
69
|
+
except (OSError, subprocess.CalledProcessError):
|
|
70
|
+
self.manifest["source_revision"] = None
|
|
71
|
+
self.save_manifest()
|
|
72
|
+
self.event("phase", phase="created")
|
|
73
|
+
|
|
74
|
+
def save_manifest(self) -> None:
|
|
75
|
+
write_json(self.directory / "manifest.json", self.manifest)
|
|
76
|
+
|
|
77
|
+
def event(self, kind: str, **data) -> None:
|
|
78
|
+
entry = {"timestamp": datetime.now(timezone.utc).isoformat(), "kind": kind, **data}
|
|
79
|
+
with (self.directory / "events.jsonl").open("a") as handle:
|
|
80
|
+
handle.write(json.dumps(entry, allow_nan=False) + "\n")
|
|
81
|
+
handle.flush()
|
|
82
|
+
os.fsync(handle.fileno())
|
|
83
|
+
|
|
84
|
+
def snapshot(self, label: str, source: str | Path, *, prefix: str = "") -> None:
|
|
85
|
+
source = Path(source)
|
|
86
|
+
files = sorted(source.rglob("*")) if source.is_dir() else [source]
|
|
87
|
+
hashes = dict(self.manifest["inputs"].get(label, {}))
|
|
88
|
+
for path in files:
|
|
89
|
+
if not path.is_file() or "__pycache__" in path.parts:
|
|
90
|
+
continue
|
|
91
|
+
relative = path.relative_to(source) if source.is_dir() else Path(path.name)
|
|
92
|
+
relative = Path(prefix) / relative
|
|
93
|
+
content = path.read_bytes()
|
|
94
|
+
destination = self.directory / "inputs" / label / relative
|
|
95
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
destination.write_bytes(content)
|
|
97
|
+
hashes[str(relative)] = hashlib.sha256(content).hexdigest()
|
|
98
|
+
self.manifest["inputs"][label] = hashes
|
|
99
|
+
self.save_manifest()
|
|
100
|
+
|
|
101
|
+
def artifact(self, name: str, value) -> Path:
|
|
102
|
+
root = self.directory / "artifacts"
|
|
103
|
+
path = root / name
|
|
104
|
+
if path.resolve() == root.resolve() or root.resolve() not in path.resolve().parents:
|
|
105
|
+
raise ValueError("Artifact path must stay inside the attempt")
|
|
106
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
write_json(path, value)
|
|
108
|
+
self.event(
|
|
109
|
+
"artifact",
|
|
110
|
+
path=str(path.relative_to(self.directory)),
|
|
111
|
+
sha256=hashlib.sha256(path.read_bytes()).hexdigest(),
|
|
112
|
+
)
|
|
113
|
+
return path
|