agent-apprenticeship 0.1.6 → 0.1.8
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.
- package/bin/agent-apprenticeship.js +85 -37
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/agent_apprenticeship_trace/__init__.py +1 -1
- package/src/agent_apprenticeship_trace/actual_outputs_normalizer.py +104 -28
- package/src/agent_apprenticeship_trace/apprentice_adapters.py +3 -1
- package/src/agent_apprenticeship_trace/artifact_resolver.py +6 -0
- package/src/agent_apprenticeship_trace/bundle_exporter.py +132 -2
- package/src/agent_apprenticeship_trace/cli.py +1171 -262
- package/src/agent_apprenticeship_trace/codex_runner.py +89 -6
- package/src/agent_apprenticeship_trace/config.py +251 -32
- package/src/agent_apprenticeship_trace/ecosystem_r2.py +515 -0
- package/src/agent_apprenticeship_trace/env.py +5 -5
- package/src/agent_apprenticeship_trace/evaluator.py +127 -11
- package/src/agent_apprenticeship_trace/grader.py +12 -11
- package/src/agent_apprenticeship_trace/learning.py +270 -1
- package/src/agent_apprenticeship_trace/llm_output_normalizer.py +8 -7
- package/src/agent_apprenticeship_trace/loop.py +103 -4
- package/src/agent_apprenticeship_trace/loop_review.py +686 -0
- package/src/agent_apprenticeship_trace/mentor_checkpoints.py +49 -21
- package/src/agent_apprenticeship_trace/openai_structured.py +84 -14
- package/src/agent_apprenticeship_trace/package_exporter.py +1 -1
- package/src/agent_apprenticeship_trace/progress.py +2 -1
- package/src/agent_apprenticeship_trace/public_run.py +369 -57
- package/src/agent_apprenticeship_trace/public_sanitizer.py +37 -1
- package/src/agent_apprenticeship_trace/rubric_generation.py +11 -10
- package/src/agent_apprenticeship_trace/schemas.py +378 -3
- package/src/agent_apprenticeship_trace/task_intake.py +43 -9
- package/src/agent_apprenticeship_trace/tdo.py +2304 -0
- package/src/agent_apprenticeship_trace/trace_normalizer.py +351 -17
- package/src/agent_apprenticeship_trace/trace_prompt.py +38 -11
- package/src/agent_apprenticeship_trace/training_signals.py +1 -1
- package/src/agent_apprenticeship_trace/verifier.py +12 -6
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import secrets
|
|
7
|
+
import shutil
|
|
8
|
+
import urllib.error
|
|
9
|
+
import urllib.parse
|
|
10
|
+
import urllib.request
|
|
11
|
+
import zipfile
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from .config import (
|
|
17
|
+
DEFAULT_R2_BUCKET,
|
|
18
|
+
DEFAULT_R2_CONTRIBUTION_PREFIX,
|
|
19
|
+
DEFAULT_R2_INDEX_PREFIX,
|
|
20
|
+
DEFAULT_R2_SEED_PREFIX,
|
|
21
|
+
Settings,
|
|
22
|
+
get_settings,
|
|
23
|
+
)
|
|
24
|
+
from .env import redact_secrets
|
|
25
|
+
from .io import append_jsonl, read_json, read_jsonl, write_json
|
|
26
|
+
from .public_sanitizer import sanitize_public_obj
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def utc_now() -> str:
|
|
30
|
+
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def safe_object_id(value: str) -> str:
|
|
34
|
+
raw = str(value or "").strip()
|
|
35
|
+
if "/" in raw or "\\" in raw or ".." in raw:
|
|
36
|
+
raise ValueError("Unsafe ecosystem package id for object storage.")
|
|
37
|
+
cleaned = re.sub(r"[^A-Za-z0-9_.-]+", "-", raw).strip(".-")
|
|
38
|
+
if not cleaned or cleaned in {".", ".."} or "/" in cleaned or "\\" in cleaned:
|
|
39
|
+
raise ValueError("Unsafe ecosystem package id for object storage.")
|
|
40
|
+
return cleaned[:160]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def identity_path(settings: Settings | None = None) -> Path:
|
|
44
|
+
s = settings or get_settings()
|
|
45
|
+
return s.app_home / "ecosystem_identity.json"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def shared_history_path(settings: Settings | None = None) -> Path:
|
|
49
|
+
s = settings or get_settings()
|
|
50
|
+
return s.app_home / "shared_contributions.jsonl"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_or_create_ecosystem_identity(settings: Settings | None = None) -> dict[str, Any]:
|
|
54
|
+
s = settings or get_settings()
|
|
55
|
+
path = identity_path(s)
|
|
56
|
+
if path.exists():
|
|
57
|
+
data = read_json(path)
|
|
58
|
+
if isinstance(data, dict) and data.get("ecosystem_user_id"):
|
|
59
|
+
return data
|
|
60
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
data = {
|
|
62
|
+
"ecosystem_user_id": f"aa-user-{secrets.token_hex(12)}",
|
|
63
|
+
"installation_id": f"aa-install-{secrets.token_hex(12)}",
|
|
64
|
+
"created_at": utc_now(),
|
|
65
|
+
"display_name": None,
|
|
66
|
+
"metadata_json": {},
|
|
67
|
+
}
|
|
68
|
+
write_json(path, data)
|
|
69
|
+
return data
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def shared_history(settings: Settings | None = None) -> list[dict[str, Any]]:
|
|
73
|
+
path = shared_history_path(settings)
|
|
74
|
+
if not path.exists():
|
|
75
|
+
return []
|
|
76
|
+
return [row for row in read_jsonl(path) if isinstance(row, dict)]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def append_shared_history(record: dict[str, Any], settings: Settings | None = None) -> None:
|
|
80
|
+
path = shared_history_path(settings)
|
|
81
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
append_jsonl(path, record)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def shared_count(settings: Settings | None = None) -> int:
|
|
86
|
+
return len(shared_history(settings))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def r2_api_url(settings: Settings | None = None) -> str | None:
|
|
90
|
+
s = settings or get_settings()
|
|
91
|
+
value = (s.r2_api_url or "").strip().rstrip("/")
|
|
92
|
+
return value or None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def r2_status(settings: Settings | None = None) -> dict[str, Any]:
|
|
96
|
+
s = settings or get_settings()
|
|
97
|
+
identity = get_or_create_ecosystem_identity(s)
|
|
98
|
+
return {
|
|
99
|
+
"storage_backend": s.ecosystem_storage_backend,
|
|
100
|
+
"r2_api_url": r2_api_url(s),
|
|
101
|
+
"r2_bucket": s.r2_bucket or DEFAULT_R2_BUCKET,
|
|
102
|
+
"r2_index_prefix": s.r2_index_prefix or DEFAULT_R2_INDEX_PREFIX,
|
|
103
|
+
"r2_contribution_prefix": s.r2_contribution_prefix or DEFAULT_R2_CONTRIBUTION_PREFIX,
|
|
104
|
+
"r2_seed_prefix": s.r2_seed_prefix or DEFAULT_R2_SEED_PREFIX,
|
|
105
|
+
"ecosystem_user_id": identity.get("ecosystem_user_id"),
|
|
106
|
+
"shared_count": shared_count(s),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def item_id_for_package(package_id: str) -> str:
|
|
111
|
+
return f"aa-item-{safe_object_id(package_id)}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _bundle_manifest(bundle: Path) -> dict[str, Any]:
|
|
115
|
+
manifest = bundle / "contribution_manifest.json"
|
|
116
|
+
if not manifest.exists():
|
|
117
|
+
raise FileNotFoundError(f"Experience Compilation manifest not found: {manifest}")
|
|
118
|
+
return read_json(manifest)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _first_text(*values: Any) -> str | None:
|
|
122
|
+
for value in values:
|
|
123
|
+
if isinstance(value, list):
|
|
124
|
+
for item in value:
|
|
125
|
+
if item not in (None, ""):
|
|
126
|
+
return str(item)
|
|
127
|
+
elif value not in (None, ""):
|
|
128
|
+
return str(value)
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _as_list(value: Any) -> list[Any]:
|
|
133
|
+
if value is None:
|
|
134
|
+
return []
|
|
135
|
+
if isinstance(value, list):
|
|
136
|
+
return value
|
|
137
|
+
if isinstance(value, tuple):
|
|
138
|
+
return list(value)
|
|
139
|
+
if value == "":
|
|
140
|
+
return []
|
|
141
|
+
return [value]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _first_list(*values: Any) -> list[str]:
|
|
145
|
+
for value in values:
|
|
146
|
+
items = [str(item) for item in _as_list(value) if item not in (None, "")]
|
|
147
|
+
if items:
|
|
148
|
+
return items
|
|
149
|
+
return []
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _read_optional_json(path: Path) -> Any:
|
|
153
|
+
if not path.exists():
|
|
154
|
+
return None
|
|
155
|
+
try:
|
|
156
|
+
return read_json(path)
|
|
157
|
+
except Exception:
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _economic_value_category(value: Any, rows_by_type: dict[str, Any], artifacts: int, traces: int) -> str:
|
|
162
|
+
raw = str(value or "").strip().lower()
|
|
163
|
+
if raw in {"low", "medium", "high", "unknown"}:
|
|
164
|
+
return raw
|
|
165
|
+
if raw and raw not in {"none", "null"}:
|
|
166
|
+
if any(token in raw for token in ["high", "strategic", "critical", "enterprise"]):
|
|
167
|
+
return "high"
|
|
168
|
+
if any(token in raw for token in ["low", "small", "minor"]):
|
|
169
|
+
return "low"
|
|
170
|
+
return "medium"
|
|
171
|
+
accepted = 0
|
|
172
|
+
for count in (rows_by_type or {}).values():
|
|
173
|
+
try:
|
|
174
|
+
accepted += int(count or 0)
|
|
175
|
+
except Exception:
|
|
176
|
+
pass
|
|
177
|
+
if accepted >= 12 or artifacts >= 4 or traces >= 3:
|
|
178
|
+
return "high"
|
|
179
|
+
if accepted >= 4 or artifacts >= 1 or traces >= 1:
|
|
180
|
+
return "medium"
|
|
181
|
+
return "unknown"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def ecosystem_item_from_bundle(
|
|
185
|
+
bundle: Path,
|
|
186
|
+
metadata: dict[str, Any],
|
|
187
|
+
*,
|
|
188
|
+
settings: Settings | None = None,
|
|
189
|
+
identity: dict[str, Any] | None = None,
|
|
190
|
+
source: str = "contribution",
|
|
191
|
+
) -> dict[str, Any]:
|
|
192
|
+
s = settings or get_settings()
|
|
193
|
+
identity = identity or get_or_create_ecosystem_identity(s)
|
|
194
|
+
manifest = _bundle_manifest(bundle)
|
|
195
|
+
package_id = safe_object_id(str(metadata.get("bundle_id") or manifest.get("bundle_id") or bundle.name))
|
|
196
|
+
item_id = item_id_for_package(package_id)
|
|
197
|
+
api_url = r2_api_url(s)
|
|
198
|
+
root_prefix = (
|
|
199
|
+
(os.environ.get("AA_R2_QA_PREFIX") or "qa").strip("/")
|
|
200
|
+
if source == "qa"
|
|
201
|
+
else (s.r2_contribution_prefix or DEFAULT_R2_CONTRIBUTION_PREFIX).strip("/")
|
|
202
|
+
)
|
|
203
|
+
prefix = f"{root_prefix}/{package_id}"
|
|
204
|
+
download_url = f"{api_url}/v1/ecosystem/packages/{item_id}" if api_url else None
|
|
205
|
+
compiler_dir = bundle / "experience_compiler"
|
|
206
|
+
skill_title = manifest.get("title") or metadata.get("title") or package_id
|
|
207
|
+
skill_summary = ""
|
|
208
|
+
skill_path = compiler_dir / "skill_pack" / "skill.md"
|
|
209
|
+
if skill_path.exists():
|
|
210
|
+
for line in skill_path.read_text(errors="ignore").splitlines():
|
|
211
|
+
clean = line.strip("# ").strip()
|
|
212
|
+
if clean:
|
|
213
|
+
skill_summary = clean[:240]
|
|
214
|
+
break
|
|
215
|
+
rows_by_type = manifest.get("experience_compiler_rows_by_type") or manifest.get("tdo_rows_by_type") or {}
|
|
216
|
+
env_spec = _read_optional_json(compiler_dir / "environment" / "environment_spec.json") or {}
|
|
217
|
+
skill_card = _read_optional_json(compiler_dir / "skill_pack" / "skill_card.json") or {}
|
|
218
|
+
retrieval_card = _read_optional_json(compiler_dir / "skill_pack" / "retrieval_card.json") or {}
|
|
219
|
+
loop_manifest = _read_optional_json(bundle / "loops" / "loop_manifest.json") or {}
|
|
220
|
+
artifact_count = int(metadata.get("artifact_count") or manifest.get("artifact_count") or 0)
|
|
221
|
+
trace_count = int(metadata.get("trace_count") or metadata.get("attempts") or manifest.get("trace_count") or manifest.get("attempts") or 0)
|
|
222
|
+
domains = _first_list(metadata.get("domains"), manifest.get("domains"), [env_spec.get("environment_domain")])
|
|
223
|
+
subdomains = _first_list(metadata.get("subdomains"), manifest.get("subdomains"))
|
|
224
|
+
domain = _first_text(domains) or "unknown"
|
|
225
|
+
subdomain = _first_text(subdomains) or "unknown"
|
|
226
|
+
tags = sorted(set(_first_list(metadata.get("tags"), manifest.get("tags"), skill_card.get("tags"), retrieval_card.get("tags"))))
|
|
227
|
+
surfaces = sorted(set(_first_list(env_spec.get("interaction_surfaces"), metadata.get("surfaces"), manifest.get("surfaces"))))
|
|
228
|
+
tools = sorted(set(_first_list(env_spec.get("required_tools"), metadata.get("tools"), manifest.get("tools"))))
|
|
229
|
+
expected_value = _economic_value_category(
|
|
230
|
+
metadata.get("expected_economic_value") or manifest.get("expected_economic_value"),
|
|
231
|
+
rows_by_type,
|
|
232
|
+
artifact_count,
|
|
233
|
+
trace_count,
|
|
234
|
+
)
|
|
235
|
+
economic_summary = (
|
|
236
|
+
str(metadata.get("economic_value_summary") or manifest.get("economic_value_summary") or "").strip()
|
|
237
|
+
or f"{expected_value.title()} reuse value based on trace evidence, artifact coverage, and generated training rows."
|
|
238
|
+
)
|
|
239
|
+
package_version = str(manifest.get("package_version") or manifest.get("agent_apprenticeship_version") or "0.1.7")
|
|
240
|
+
loop_count = int(loop_manifest.get("loop_iteration_count") or manifest.get("loop_iteration_count") or 0)
|
|
241
|
+
has_skill = (compiler_dir / "skill_pack").exists()
|
|
242
|
+
has_training = (compiler_dir / "training_time_package").exists()
|
|
243
|
+
has_eval = (compiler_dir / "eval_package").exists() or (compiler_dir / "evals").exists()
|
|
244
|
+
has_environment = (compiler_dir / "environment").exists()
|
|
245
|
+
return sanitize_public_obj(
|
|
246
|
+
{
|
|
247
|
+
"id": item_id,
|
|
248
|
+
"item_id": item_id,
|
|
249
|
+
"kind": "agent_experience_package",
|
|
250
|
+
"experience_compilation_id": package_id,
|
|
251
|
+
"title": metadata.get("title") or manifest.get("title") or package_id,
|
|
252
|
+
"task_title": manifest.get("title") or metadata.get("title") or package_id,
|
|
253
|
+
"summary": metadata.get("summary") or metadata.get("title") or manifest.get("title") or "",
|
|
254
|
+
"domain": domain,
|
|
255
|
+
"domains": domains or [domain],
|
|
256
|
+
"subdomain": subdomain,
|
|
257
|
+
"subdomains": subdomains or ([subdomain] if subdomain != "unknown" else []),
|
|
258
|
+
"expected_economic_value": expected_value,
|
|
259
|
+
"economic_value_summary": economic_summary,
|
|
260
|
+
"tags": tags,
|
|
261
|
+
"surfaces": surfaces,
|
|
262
|
+
"tools": tools,
|
|
263
|
+
"created_at": metadata.get("created_at") or utc_now(),
|
|
264
|
+
"updated_at": utc_now(),
|
|
265
|
+
"source": source,
|
|
266
|
+
"package_id": package_id,
|
|
267
|
+
"package_version": package_version,
|
|
268
|
+
"task_id": manifest.get("task_id"),
|
|
269
|
+
"apprenticeship_mode": manifest.get("apprenticeship_mode") or metadata.get("apprenticeship_mode"),
|
|
270
|
+
"mentor_mode": manifest.get("mentor_mode"),
|
|
271
|
+
"apprentice_agent": metadata.get("apprentice_agent"),
|
|
272
|
+
"mentor_model_provider": metadata.get("mentor_model_provider"),
|
|
273
|
+
"task_status": metadata.get("task_status") or manifest.get("task_status"),
|
|
274
|
+
"run_status": metadata.get("run_status") or manifest.get("run_status"),
|
|
275
|
+
"attempts": metadata.get("attempts") or manifest.get("attempts") or 0,
|
|
276
|
+
"traced_steps": metadata.get("traced_steps") or manifest.get("traced_steps") or 0,
|
|
277
|
+
"tdo_status": manifest.get("tdo_status"),
|
|
278
|
+
"trace_count": trace_count,
|
|
279
|
+
"artifact_count": artifact_count,
|
|
280
|
+
"loop_iteration_count": loop_count,
|
|
281
|
+
"tdo_rows_by_type": rows_by_type,
|
|
282
|
+
"training_row_counts": rows_by_type,
|
|
283
|
+
"has_full_experience_compilation": True,
|
|
284
|
+
"has_runtime_training": bool((compiler_dir / "skill_pack" / "skill.md").exists()),
|
|
285
|
+
"has_skill_pack": has_skill,
|
|
286
|
+
"has_training_time_package": has_training,
|
|
287
|
+
"has_eval_package": has_eval,
|
|
288
|
+
"has_environment_package": has_environment,
|
|
289
|
+
"available_outputs": {
|
|
290
|
+
"full_experience_compilation": True,
|
|
291
|
+
"full_training_package": True,
|
|
292
|
+
"runtime_training": bool((compiler_dir / "skill_pack" / "skill.md").exists()),
|
|
293
|
+
"skill_pack": has_skill,
|
|
294
|
+
"training_time_package": has_training,
|
|
295
|
+
"eval_package": has_eval,
|
|
296
|
+
"environment_package": has_environment,
|
|
297
|
+
},
|
|
298
|
+
"experience_compiler": {
|
|
299
|
+
"enabled": bool(manifest.get("experience_compiler_enabled") or compiler_dir.exists()),
|
|
300
|
+
"status": manifest.get("experience_compiler_status") or manifest.get("tdo_status"),
|
|
301
|
+
"skill_title": skill_title,
|
|
302
|
+
"skill_summary": skill_summary,
|
|
303
|
+
"runtime_training_summary": skill_summary or f"Runtime training compiled from {skill_title}.",
|
|
304
|
+
"training_data_counts": rows_by_type,
|
|
305
|
+
"environment_domain": metadata.get("environment_domain") or manifest.get("environment_domain") or _first_text(metadata.get("domains"), manifest.get("domains")),
|
|
306
|
+
},
|
|
307
|
+
"contributor": {
|
|
308
|
+
"ecosystem_user_id": identity.get("ecosystem_user_id"),
|
|
309
|
+
"display_name": identity.get("display_name"),
|
|
310
|
+
},
|
|
311
|
+
"storage": {
|
|
312
|
+
"backend": "cloudflare_r2",
|
|
313
|
+
"bucket": s.r2_bucket or DEFAULT_R2_BUCKET,
|
|
314
|
+
"path_prefix": prefix,
|
|
315
|
+
"manifest_path": f"{prefix}/manifest.json",
|
|
316
|
+
"package_path": f"{prefix}/contribution_package.zip",
|
|
317
|
+
"download_url": download_url,
|
|
318
|
+
},
|
|
319
|
+
}
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def storage_ref_for_item(
|
|
324
|
+
item: dict[str, Any],
|
|
325
|
+
*,
|
|
326
|
+
settings: Settings | None = None,
|
|
327
|
+
dry_run: bool,
|
|
328
|
+
uploaded_at: str | None = None,
|
|
329
|
+
url: str | None = None,
|
|
330
|
+
) -> dict[str, Any]:
|
|
331
|
+
s = settings or get_settings()
|
|
332
|
+
storage = item.get("storage") or {}
|
|
333
|
+
return sanitize_public_obj(
|
|
334
|
+
{
|
|
335
|
+
"storage_backend": "forsy-r2",
|
|
336
|
+
"bucket_backend": "cloudflare_r2",
|
|
337
|
+
"ingestion_api_url": r2_api_url(s),
|
|
338
|
+
"bucket": s.r2_bucket or DEFAULT_R2_BUCKET,
|
|
339
|
+
"path_prefix": storage.get("path_prefix"),
|
|
340
|
+
"manifest_path": storage.get("manifest_path"),
|
|
341
|
+
"package_path": storage.get("package_path"),
|
|
342
|
+
"index_item_path": f"{(s.r2_index_prefix or DEFAULT_R2_INDEX_PREFIX).strip('/')}/items/{item.get('id')}.json",
|
|
343
|
+
"item_id": item.get("id"),
|
|
344
|
+
"package_id": item.get("package_id"),
|
|
345
|
+
"ecosystem_user_id": (item.get("contributor") or {}).get("ecosystem_user_id"),
|
|
346
|
+
"url": url or (r2_api_url(s) + f"/v1/ecosystem/items/{item.get('id')}" if r2_api_url(s) else None),
|
|
347
|
+
"uploaded_at": uploaded_at,
|
|
348
|
+
"dry_run": dry_run,
|
|
349
|
+
"upload_mode": "dry_run" if dry_run else "r2_worker_ingestion",
|
|
350
|
+
}
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def write_storage_ref(bundle: Path, submission_dir: Path, storage_ref: dict[str, Any]) -> Path:
|
|
355
|
+
path = submission_dir / "storage_ref.json"
|
|
356
|
+
write_json(path, storage_ref)
|
|
357
|
+
write_json(bundle / "storage_ref.json", storage_ref)
|
|
358
|
+
copied_bundle = submission_dir / "contribution_bundle"
|
|
359
|
+
if copied_bundle.exists():
|
|
360
|
+
write_json(copied_bundle / "storage_ref.json", storage_ref)
|
|
361
|
+
return path
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _multipart_body(fields: dict[str, tuple[str, bytes, str] | str]) -> tuple[bytes, str]:
|
|
365
|
+
boundary = f"----aa-r2-{secrets.token_hex(12)}"
|
|
366
|
+
parts: list[bytes] = []
|
|
367
|
+
for name, value in fields.items():
|
|
368
|
+
parts.append(f"--{boundary}\r\n".encode())
|
|
369
|
+
if isinstance(value, tuple):
|
|
370
|
+
filename, content, content_type = value
|
|
371
|
+
parts.append(
|
|
372
|
+
f'Content-Disposition: form-data; name="{name}"; filename="{filename}"\r\n'
|
|
373
|
+
f"Content-Type: {content_type}\r\n\r\n".encode()
|
|
374
|
+
)
|
|
375
|
+
parts.append(content)
|
|
376
|
+
parts.append(b"\r\n")
|
|
377
|
+
else:
|
|
378
|
+
parts.append(f'Content-Disposition: form-data; name="{name}"\r\n\r\n{value}\r\n'.encode())
|
|
379
|
+
parts.append(f"--{boundary}--\r\n".encode())
|
|
380
|
+
return b"".join(parts), boundary
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def upload_contribution_to_worker(
|
|
384
|
+
*,
|
|
385
|
+
api_url: str,
|
|
386
|
+
package_zip: Path,
|
|
387
|
+
ecosystem_item: dict[str, Any],
|
|
388
|
+
manifest: dict[str, Any],
|
|
389
|
+
storage_ref: dict[str, Any],
|
|
390
|
+
qa: bool = False,
|
|
391
|
+
timeout: int = 60,
|
|
392
|
+
) -> dict[str, Any]:
|
|
393
|
+
api_url = api_url.rstrip("/")
|
|
394
|
+
body, boundary = _multipart_body(
|
|
395
|
+
{
|
|
396
|
+
"package_zip": (package_zip.name, package_zip.read_bytes(), "application/zip"),
|
|
397
|
+
"ecosystem_item_json": json.dumps(ecosystem_item, sort_keys=True),
|
|
398
|
+
"manifest_json": json.dumps(manifest, sort_keys=True),
|
|
399
|
+
"storage_ref_json": json.dumps(storage_ref, sort_keys=True),
|
|
400
|
+
"qa": "true" if qa else "false",
|
|
401
|
+
}
|
|
402
|
+
)
|
|
403
|
+
req = urllib.request.Request(
|
|
404
|
+
f"{api_url}/v1/ecosystem/contributions",
|
|
405
|
+
data=body,
|
|
406
|
+
method="POST",
|
|
407
|
+
headers={
|
|
408
|
+
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
|
409
|
+
"User-Agent": "agent-apprenticeship-cli",
|
|
410
|
+
},
|
|
411
|
+
)
|
|
412
|
+
try:
|
|
413
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
414
|
+
payload = response.read().decode("utf-8")
|
|
415
|
+
return json.loads(payload or "{}")
|
|
416
|
+
except urllib.error.HTTPError as exc:
|
|
417
|
+
detail = exc.read().decode("utf-8", errors="replace")
|
|
418
|
+
raise RuntimeError(redact_secrets(f"R2 ingestion failed: HTTP {exc.code} {detail}")) from exc
|
|
419
|
+
except urllib.error.URLError as exc:
|
|
420
|
+
raise RuntimeError(redact_secrets(f"R2 ingestion API unavailable: {exc}")) from exc
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _request_json(url: str, *, timeout: int = 30) -> dict[str, Any] | list[Any]:
|
|
424
|
+
req = urllib.request.Request(url, headers={"User-Agent": "agent-apprenticeship-cli"})
|
|
425
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
426
|
+
return json.loads(response.read().decode("utf-8") or "{}")
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _request_bytes(url: str, *, timeout: int = 60) -> bytes:
|
|
430
|
+
req = urllib.request.Request(url, headers={"User-Agent": "agent-apprenticeship-cli"})
|
|
431
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
432
|
+
return response.read()
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def r2_index_cache_path(settings: Settings | None = None) -> Path:
|
|
436
|
+
s = settings or get_settings()
|
|
437
|
+
return s.app_home / "ecosystem" / "r2_index_cache.json"
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def fetch_r2_index(settings: Settings | None = None, *, use_cache: bool = True) -> list[dict[str, Any]]:
|
|
441
|
+
s = settings or get_settings()
|
|
442
|
+
api_url = r2_api_url(s)
|
|
443
|
+
if not api_url:
|
|
444
|
+
if use_cache and r2_index_cache_path(s).exists():
|
|
445
|
+
data = read_json(r2_index_cache_path(s))
|
|
446
|
+
return [row for row in data.get("items", []) if isinstance(row, dict)]
|
|
447
|
+
raise FileNotFoundError("R2 ingestion API is not configured. Run `apprentice ecosystem configure --r2-api-url <url>` or use `--storage github`.")
|
|
448
|
+
data = _request_json(f"{api_url}/v1/ecosystem/index")
|
|
449
|
+
rows = data.get("items") if isinstance(data, dict) else data
|
|
450
|
+
rows = [row for row in rows or [] if isinstance(row, dict)]
|
|
451
|
+
cache = r2_index_cache_path(s)
|
|
452
|
+
cache.parent.mkdir(parents=True, exist_ok=True)
|
|
453
|
+
write_json(cache, {"cached_at": utc_now(), "api_url": api_url, "items": rows})
|
|
454
|
+
return rows
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def fetch_r2_item(item_id: str, settings: Settings | None = None) -> dict[str, Any]:
|
|
458
|
+
s = settings or get_settings()
|
|
459
|
+
api_url = r2_api_url(s)
|
|
460
|
+
if not api_url:
|
|
461
|
+
for row in fetch_r2_index(s, use_cache=True):
|
|
462
|
+
if row.get("id") == item_id or row.get("package_id") == item_id or row.get("bundle_id") == item_id:
|
|
463
|
+
return row
|
|
464
|
+
raise FileNotFoundError("R2 ingestion API is not configured and item is not in cache.")
|
|
465
|
+
data = _request_json(f"{api_url}/v1/ecosystem/items/{urllib.parse.quote(item_id)}")
|
|
466
|
+
return data if isinstance(data, dict) else {}
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def download_r2_package(item_id: str, dest: Path, settings: Settings | None = None) -> Path:
|
|
470
|
+
s = settings or get_settings()
|
|
471
|
+
api_url = r2_api_url(s)
|
|
472
|
+
if not api_url:
|
|
473
|
+
raise FileNotFoundError("R2 ingestion API is not configured. Use `--storage github` for legacy fallback.")
|
|
474
|
+
data = _request_bytes(f"{api_url}/v1/ecosystem/packages/{urllib.parse.quote(item_id)}")
|
|
475
|
+
zip_path = dest.parent / f"{safe_object_id(item_id)}.zip"
|
|
476
|
+
zip_path.parent.mkdir(parents=True, exist_ok=True)
|
|
477
|
+
zip_path.write_bytes(data)
|
|
478
|
+
if dest.exists():
|
|
479
|
+
shutil.rmtree(dest)
|
|
480
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
481
|
+
with zipfile.ZipFile(zip_path) as zf:
|
|
482
|
+
for info in zf.infolist():
|
|
483
|
+
target = dest / info.filename
|
|
484
|
+
if not target.resolve().is_relative_to(dest.resolve()):
|
|
485
|
+
raise RuntimeError("R2 package contains an unsafe path.")
|
|
486
|
+
zf.extractall(dest)
|
|
487
|
+
return dest
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def append_successful_share(
|
|
491
|
+
*,
|
|
492
|
+
item: dict[str, Any],
|
|
493
|
+
bundle: Path,
|
|
494
|
+
storage_backend: str,
|
|
495
|
+
url: str | None,
|
|
496
|
+
sharing_mode: str,
|
|
497
|
+
settings: Settings | None = None,
|
|
498
|
+
) -> dict[str, Any]:
|
|
499
|
+
s = settings or get_settings()
|
|
500
|
+
record = {
|
|
501
|
+
"item_id": item.get("id"),
|
|
502
|
+
"package_id": item.get("package_id"),
|
|
503
|
+
"ecosystem_user_id": (item.get("contributor") or {}).get("ecosystem_user_id"),
|
|
504
|
+
"uploaded_at": utc_now(),
|
|
505
|
+
"storage_backend": storage_backend,
|
|
506
|
+
"url": url,
|
|
507
|
+
"local_bundle_path": str(bundle),
|
|
508
|
+
"title": item.get("title"),
|
|
509
|
+
"domain": item.get("domain"),
|
|
510
|
+
"tdo_status": item.get("tdo_status"),
|
|
511
|
+
"apprenticeship_mode": item.get("apprenticeship_mode"),
|
|
512
|
+
"sharing_mode": sharing_mode,
|
|
513
|
+
}
|
|
514
|
+
append_shared_history(record, s)
|
|
515
|
+
return record
|
|
@@ -23,13 +23,13 @@ def load_local_env(root: Path | None=None) -> dict[str,str]:
|
|
|
23
23
|
return loaded
|
|
24
24
|
|
|
25
25
|
SECRET_PATTERNS=[
|
|
26
|
-
re.compile(r"sk-proj-[A-Za-z0-9_-]{20,}"),
|
|
27
|
-
re.compile(r"sk-ant-[A-Za-z0-9_-]{20,}"),
|
|
28
|
-
re.compile(r"sk-or-v1-[A-Za-z0-9_-]{20,}"),
|
|
29
|
-
re.compile(r"sk-[A-Za-z0-9_-]{20,}"),
|
|
26
|
+
re.compile(r"(?<![A-Za-z0-9])sk-proj-[A-Za-z0-9_-]{20,}"),
|
|
27
|
+
re.compile(r"(?<![A-Za-z0-9])sk-ant-[A-Za-z0-9_-]{20,}"),
|
|
28
|
+
re.compile(r"(?<![A-Za-z0-9])sk-or-v1-[A-Za-z0-9_-]{20,}"),
|
|
29
|
+
re.compile(r"(?<![A-Za-z0-9])sk-[A-Za-z0-9_-]{20,}"),
|
|
30
30
|
re.compile(r"AIza[A-Za-z0-9_-]{20,}"),
|
|
31
31
|
re.compile(r"dsk-[A-Za-z0-9_-]{20,}"),
|
|
32
|
-
re.compile(r"
|
|
32
|
+
re.compile(r"\buser_(?=[A-Za-z0-9]*\d)[A-Za-z0-9]{16,}\b"),
|
|
33
33
|
re.compile(r"(['\"]user_id['\"]\s*:\s*)['\"][^'\"]+['\"]"),
|
|
34
34
|
]
|
|
35
35
|
def contains_secret(text: str) -> bool:
|