agent-bios 0.18.0 → 0.19.0
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/DEPENDENCIES.md +236 -80
- package/INSTALL.md +112 -0
- package/README.md +184 -524
- package/claude/CLAUDE.md +1 -1
- package/claude/guides/cli-multi-model-workflow.md +1 -1
- package/claude/guides/learning-flow.md +23 -12
- package/claude/guides/session-distill-workflow.md +22 -12
- package/codex/AGENTS.md +1 -1
- package/codex/guides/cli-multi-model-workflow.md +1 -1
- package/codex/guides/learning-flow.md +23 -12
- package/codex/guides/session-distill-workflow.md +22 -12
- package/compose/app_bridge/SKILL.md +75 -0
- package/compose/app_bridge/agents/openai.yaml +2 -0
- package/compose/app_bridge/scripts/bridge.py +76 -0
- package/compose/bootstrap/SKILL.md +12 -1
- package/compose/corpus.py +31 -9
- package/compose/corpus_app.py +456 -0
- package/compose/corpus_import.py +529 -0
- package/compose/corpus_install.py +196 -18
- package/compose/corpus_session.py +27 -0
- package/compose/corpus_setup.py +674 -0
- package/compose/corpus_setup_cli.py +582 -0
- package/compose/corpus_setup_i18n.py +318 -0
- package/compose/corpus_setup_ui.py +633 -0
- package/compose/corpus_store.py +167 -29
- package/compose/corpus_transaction.py +43 -10
- package/compose/corpus_ui_runtime.py +278 -0
- package/compose/setup/START.md +147 -0
- package/compose/ui_runtime/linkify_it_py-2.2.0-py3-none-any.whl +0 -0
- package/compose/ui_runtime/manifest.json +238 -0
- package/compose/ui_runtime/markdown_it_py-4.2.0-py3-none-any.whl +0 -0
- package/compose/ui_runtime/mdit_py_plugins-0.6.1-py3-none-any.whl +0 -0
- package/compose/ui_runtime/mdurl-0.1.2-py3-none-any.whl +0 -0
- package/compose/ui_runtime/platformdirs-4.11.8-py3-none-any.whl +0 -0
- package/compose/ui_runtime/pygments-2.21.0-py3-none-any.whl +0 -0
- package/compose/ui_runtime/rich-15.0.0-py3-none-any.whl +0 -0
- package/compose/ui_runtime/textual-8.2.8-py3-none-any.whl +0 -0
- package/compose/ui_runtime/typing_extensions-4.16.0-py3-none-any.whl +0 -0
- package/docs/advanced-launch.md +131 -0
- package/docs/assets/corpus-studio.svg +227 -0
- package/docs/corpus.md +117 -0
- package/docs/recovery.md +201 -0
- package/docs/session-model.md +120 -0
- package/docs/setup.md +190 -0
- package/docs/understand.md +40 -0
- package/install.sh +75 -46
- package/launch/agent-launch.py +91 -47
- package/launch/provision-venv.sh +44 -13
- package/learn/collect-learning.py +14 -5
- package/learn/learning.schema.json +2 -2
- package/package.json +14 -2
- package/provenance.json +1 -1
- package/wrappers/claude-run.sh +10 -13
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
"""JSON setup adapter with read-only reviews and durable, non-replaying receipts."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import contextlib
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import platform
|
|
11
|
+
import re
|
|
12
|
+
import shutil
|
|
13
|
+
import socket
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
import time
|
|
18
|
+
from typing import Any, Callable
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
sys.dont_write_bytecode = True
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from corpus_install import CorpusInstaller
|
|
25
|
+
from corpus_setup import SetupController, SetupError, format_setup_result, review_summary
|
|
26
|
+
from corpus_setup_i18n import LANGUAGES, choice_label, dependency_display, detect_language, translate
|
|
27
|
+
from corpus_transaction import confirmed_release, reject_symlink_ancestors, transaction_lock, try_transaction_lock
|
|
28
|
+
except ImportError:
|
|
29
|
+
from .corpus_install import CorpusInstaller
|
|
30
|
+
from .corpus_setup import SetupController, SetupError, format_setup_result, review_summary
|
|
31
|
+
from .corpus_setup_i18n import LANGUAGES, choice_label, dependency_display, detect_language, translate
|
|
32
|
+
from .corpus_transaction import confirmed_release, reject_symlink_ancestors, transaction_lock, try_transaction_lock
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
SCHEMA_VERSION = 1
|
|
36
|
+
IDENTIFIER = re.compile(r"[0-9a-f]{64}\Z")
|
|
37
|
+
LANGUAGE_IDS = {identifier for _label, identifier in LANGUAGES}
|
|
38
|
+
INCIDENTAL_ENV = {
|
|
39
|
+
**dict.fromkeys(("_", "SHLVL"), "shell command bookkeeping does not select setup effects"),
|
|
40
|
+
**dict.fromkeys(("PWD", "OLDPWD"), "the actual working directory is bound separately in the review context"),
|
|
41
|
+
**dict.fromkeys(("TERM", "COLORTERM", "TERM_PROGRAM", "TERM_PROGRAM_VERSION", "TERM_SESSION_ID", "SHELL_SESSION_ID",
|
|
42
|
+
"COLUMNS", "LINES", "NO_COLOR", "FORCE_COLOR"),
|
|
43
|
+
"terminal presentation and terminal-session identity are not installation authority"),
|
|
44
|
+
**dict.fromkeys(("LC_ALL", "LC_MESSAGES", "LC_CTYPE", "LC_NUMERIC", "LC_TIME", "LC_COLLATE", "LC_MONETARY", "LANG", "LANGUAGE"),
|
|
45
|
+
"display locale may change between calls; the chosen review language is bound explicitly"),
|
|
46
|
+
**dict.fromkeys(("TMPDIR", "TMP", "TEMP"),
|
|
47
|
+
"scratch allocation may change between calls; package and private destinations are bound separately"),
|
|
48
|
+
**dict.fromkeys(("PYTHONDONTWRITEBYTECODE", "PYTHONUNBUFFERED"),
|
|
49
|
+
"Python cache and output policy do not select installation targets or recipes"),
|
|
50
|
+
**dict.fromkeys(("CODEX_THREAD_ID", "CODEX_TASK_ID", "CLAUDE_CODE_SESSION_ID", "CLAUDE_SESSION_ID"),
|
|
51
|
+
"setup does not activate a task and may continue from another task on the same machine and private roots"),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _canonical(value: Any) -> bytes:
|
|
56
|
+
return json.dumps(value, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _digest(value: Any) -> str:
|
|
60
|
+
return hashlib.sha256(_canonical(value)).hexdigest()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _identity() -> dict[str, Any]:
|
|
64
|
+
hardware = ""
|
|
65
|
+
method = "host-filesystem"
|
|
66
|
+
if sys.platform.startswith("linux"):
|
|
67
|
+
for path in (Path("/etc/machine-id"), Path("/var/lib/dbus/machine-id")):
|
|
68
|
+
try:
|
|
69
|
+
candidate = path.read_text(encoding="ascii").strip()
|
|
70
|
+
except (OSError, UnicodeError):
|
|
71
|
+
continue
|
|
72
|
+
if re.fullmatch(r"[a-fA-F0-9]{32}", candidate):
|
|
73
|
+
hardware, method = candidate, "machine-id"
|
|
74
|
+
break
|
|
75
|
+
elif sys.platform == "darwin":
|
|
76
|
+
try:
|
|
77
|
+
result = subprocess.run(["/usr/sbin/ioreg", "-rd1", "-c", "IOPlatformExpertDevice"],
|
|
78
|
+
stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=5)
|
|
79
|
+
match = re.search(r'"IOPlatformUUID"\s*=\s*"([A-Fa-f0-9-]+)"', result.stdout)
|
|
80
|
+
if result.returncode == 0 and match:
|
|
81
|
+
hardware, method = match[1], "platform-uuid"
|
|
82
|
+
except (OSError, subprocess.SubprocessError):
|
|
83
|
+
pass
|
|
84
|
+
host = socket.gethostname()
|
|
85
|
+
if not hardware:
|
|
86
|
+
root = Path("/").stat()
|
|
87
|
+
hardware = f"{host}:{root.st_dev}:{root.st_ino}"
|
|
88
|
+
return {"machine_id": hashlib.sha256(hardware.encode()).hexdigest(), "identity_method": method,
|
|
89
|
+
"hostname": host, "platform": platform.system(), "architecture": platform.machine(),
|
|
90
|
+
"uid": os.getuid() if hasattr(os, "getuid") else None}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _process_identity(pid: int) -> str | None:
|
|
94
|
+
if sys.platform.startswith("linux"):
|
|
95
|
+
try:
|
|
96
|
+
fields = (Path("/proc") / str(pid) / "stat").read_text().rsplit(")", 1)[1].split()
|
|
97
|
+
return fields[19]
|
|
98
|
+
except (OSError, IndexError):
|
|
99
|
+
return None
|
|
100
|
+
try:
|
|
101
|
+
result = subprocess.run(["/bin/ps", "-p", str(pid), "-o", "lstart="], stdin=subprocess.DEVNULL,
|
|
102
|
+
capture_output=True, text=True, timeout=3,
|
|
103
|
+
env={**os.environ, "LC_ALL": "C", "TZ": "UTC"})
|
|
104
|
+
return result.stdout.strip() if result.returncode == 0 and result.stdout.strip() else None
|
|
105
|
+
except (OSError, subprocess.SubprocessError):
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _read_json(path: Path) -> dict[str, Any]:
|
|
110
|
+
reject_symlink_ancestors(path)
|
|
111
|
+
if not path.is_file() or path.stat().st_size > 8 * 1024 * 1024:
|
|
112
|
+
raise SetupError(f"missing, unsafe, or oversized setup JSON: {path}")
|
|
113
|
+
try:
|
|
114
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
115
|
+
except (OSError, ValueError) as exc:
|
|
116
|
+
raise SetupError(f"cannot read setup JSON: {path}: {exc}") from exc
|
|
117
|
+
if not isinstance(value, dict):
|
|
118
|
+
raise SetupError("setup JSON must be an object")
|
|
119
|
+
return value
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _write_json(path: Path, value: dict[str, Any]) -> None:
|
|
123
|
+
reject_symlink_ancestors(path)
|
|
124
|
+
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
125
|
+
descriptor, raw = tempfile.mkstemp(prefix=".setup-receipt-", dir=path.parent)
|
|
126
|
+
temporary = Path(raw)
|
|
127
|
+
try:
|
|
128
|
+
with os.fdopen(descriptor, "wb") as output:
|
|
129
|
+
output.write(_canonical(value) + b"\n")
|
|
130
|
+
output.flush()
|
|
131
|
+
os.fsync(output.fileno())
|
|
132
|
+
temporary.chmod(0o600)
|
|
133
|
+
os.replace(temporary, path)
|
|
134
|
+
directory = os.open(path.parent, os.O_RDONLY)
|
|
135
|
+
try:
|
|
136
|
+
os.fsync(directory)
|
|
137
|
+
finally:
|
|
138
|
+
os.close(directory)
|
|
139
|
+
finally:
|
|
140
|
+
temporary.unlink(missing_ok=True)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _review_identity(review: dict[str, Any]) -> str:
|
|
144
|
+
return _digest({key: value for key, value in review.items() if key != "review_id"})
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _file_identity(path: Path) -> dict[str, Any]:
|
|
148
|
+
resolved = path.resolve()
|
|
149
|
+
if not resolved.is_file():
|
|
150
|
+
raise SetupError(f"reviewed dependency executable is unavailable: {path}")
|
|
151
|
+
digest = hashlib.sha256()
|
|
152
|
+
with resolved.open("rb") as source:
|
|
153
|
+
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
|
154
|
+
digest.update(chunk)
|
|
155
|
+
return {"path": str(resolved), "sha256": digest.hexdigest(), "size_bytes": resolved.stat().st_size}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class SetupService:
|
|
159
|
+
"""Bind shared-engine reviews to a machine, then record every effect boundary."""
|
|
160
|
+
|
|
161
|
+
def __init__(self, installer: Any, *, controller_factory: Callable | None = None,
|
|
162
|
+
identity: Callable[[], dict[str, Any]] = _identity,
|
|
163
|
+
progress_hook: Callable[[dict[str, Any]], None] | None = None):
|
|
164
|
+
self.installer = installer
|
|
165
|
+
self.controller_factory = controller_factory or (lambda: SetupController(installer))
|
|
166
|
+
self.identity = identity
|
|
167
|
+
self.progress_hook = progress_hook
|
|
168
|
+
|
|
169
|
+
def context(self) -> dict[str, Any]:
|
|
170
|
+
env = self.installer.env
|
|
171
|
+
home = Path(env.get("HOME", str(Path.home()))).expanduser().resolve()
|
|
172
|
+
state = Path(getattr(self.installer, "state_root", env.get("AGENT_BIOS_STATE_DIR", home / ".local/share/agent-bios"))).expanduser().resolve()
|
|
173
|
+
user = Path(getattr(self.installer, "user_root", env.get("AGENT_BIOS_CORPUS_DIR", home / ".config/agent-bios/corpus"))).expanduser().resolve()
|
|
174
|
+
return {**self.identity(), "home": str(home), "state_dir": str(state), "user_dir": str(user),
|
|
175
|
+
"codex_dir": str(Path(env.get("CODEX_HOME", home / ".codex")).expanduser().resolve()),
|
|
176
|
+
"claude_dir": str(Path(env.get("CLAUDE_CONFIG_DIR", home / ".claude")).expanduser().resolve()),
|
|
177
|
+
"package_root": str(Path(self.installer.repo).resolve()), "cwd": str(Path.cwd().resolve()),
|
|
178
|
+
"python": {"executable": str(Path(sys.executable).resolve()), "version": platform.python_version()},
|
|
179
|
+
"environment_digest": _digest({key: value for key, value in env.items() if key not in INCIDENTAL_ENV})}
|
|
180
|
+
|
|
181
|
+
def _language(self, language: str | None) -> str:
|
|
182
|
+
value = language or detect_language(self.installer.env)
|
|
183
|
+
if value not in LANGUAGE_IDS:
|
|
184
|
+
raise SetupError("language must be en, ko, or ja")
|
|
185
|
+
return value
|
|
186
|
+
|
|
187
|
+
def _assert_owner(self, review: dict[str, Any]) -> None:
|
|
188
|
+
current = self.context()
|
|
189
|
+
if any(review["context"].get(key) != current.get(key) for key in ("machine_id", "uid", "home", "state_dir", "user_dir")):
|
|
190
|
+
raise SetupError("review receipt belongs to another machine or private root context")
|
|
191
|
+
|
|
192
|
+
def start(self, language: str | None = None) -> dict[str, Any]:
|
|
193
|
+
package = Path(self.installer.repo).resolve()
|
|
194
|
+
cli = ["/bin/bash", str(package / "install.sh")]
|
|
195
|
+
return {"schema_version": SCHEMA_VERSION, "kind": "agent-bios-setup-start",
|
|
196
|
+
"languages": [{"label": label, "id": identifier} for label, identifier in LANGUAGES],
|
|
197
|
+
"suggested_language": detect_language(self.installer.env), "language": self._language(language),
|
|
198
|
+
"context": self.context(), "guide_path": str(package / "compose/setup/START.md"),
|
|
199
|
+
"cli_argv": cli, "setup_argv": [*cli, "setup"]}
|
|
200
|
+
|
|
201
|
+
def inspect(self, language: str | None = None) -> dict[str, Any]:
|
|
202
|
+
language = self._language(language)
|
|
203
|
+
controller = self.controller_factory()
|
|
204
|
+
return {"schema_version": SCHEMA_VERSION, "kind": "agent-bios-setup-inspection", "context": self.context(),
|
|
205
|
+
"language": language, "dependencies": controller.dependencies, "choices": controller.choices,
|
|
206
|
+
"default_plan": controller.default_plan(),
|
|
207
|
+
"display": {"dependencies": [dependency_display(language, row) for row in controller.dependencies],
|
|
208
|
+
"choices": [{**row, "label": choice_label(language, row)} for row in controller.choices]}}
|
|
209
|
+
|
|
210
|
+
def discover(self, roots: list[str]) -> dict[str, Any]:
|
|
211
|
+
if not isinstance(roots, list) or not all(isinstance(root, str) and Path(root).is_absolute() and Path(root).is_dir() for root in roots):
|
|
212
|
+
raise SetupError("project roots must be existing absolute directories")
|
|
213
|
+
found = self.installer.setup_discover(roots)
|
|
214
|
+
return {"schema_version": SCHEMA_VERSION, "kind": "agent-bios-setup-discovery", "context": self.context(),
|
|
215
|
+
"sources": found["sources"], "omitted": found.get("omitted", [])}
|
|
216
|
+
|
|
217
|
+
def _actions(self, preview: dict[str, Any]) -> list[dict[str, Any]]:
|
|
218
|
+
identities = []
|
|
219
|
+
for action in preview["plan"]["dependency_actions"]:
|
|
220
|
+
command = action["argv"][0]
|
|
221
|
+
path = shutil.which(command, path=self.installer.env.get("PATH", os.defpath))
|
|
222
|
+
if not path:
|
|
223
|
+
raise SetupError(f"reviewed dependency executable is unavailable: {command}")
|
|
224
|
+
identities.append({"id": action["id"], **_file_identity(Path(path))})
|
|
225
|
+
return identities
|
|
226
|
+
|
|
227
|
+
def plan(self, value: dict[str, Any], language: str | None = None, *, continuation: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
228
|
+
language = self._language(language)
|
|
229
|
+
controller = self.controller_factory()
|
|
230
|
+
if not isinstance(value, dict) or set(value) != controller.FIELDS:
|
|
231
|
+
raise SetupError("plan input must contain exactly the six setup choice fields; derived commands are not input")
|
|
232
|
+
preview = controller.preview(value)
|
|
233
|
+
review = {"schema_version": SCHEMA_VERSION, "kind": "agent-bios-setup-review", "context": self.context(),
|
|
234
|
+
"language": language, "preview": preview, "action_inputs": self._actions(preview),
|
|
235
|
+
"summary": review_summary(preview["plan"], controller.dependencies, controller.choices, language)}
|
|
236
|
+
if continuation is not None:
|
|
237
|
+
review["continuation"] = continuation
|
|
238
|
+
review["review_id"] = _review_identity(review)
|
|
239
|
+
return review
|
|
240
|
+
|
|
241
|
+
def _receipt_path(self, identifier: str) -> Path:
|
|
242
|
+
if not isinstance(identifier, str) or not IDENTIFIER.fullmatch(identifier):
|
|
243
|
+
raise SetupError("review id must be an engine-issued 64-character identifier")
|
|
244
|
+
return Path(self.context()["state_dir"]) / "setup/receipts" / f"{identifier}.json"
|
|
245
|
+
|
|
246
|
+
def _receipt(self, identifier: str) -> dict[str, Any] | None:
|
|
247
|
+
path = self._receipt_path(identifier)
|
|
248
|
+
reject_symlink_ancestors(path)
|
|
249
|
+
if not path.exists():
|
|
250
|
+
return None
|
|
251
|
+
receipt = _read_json(path)
|
|
252
|
+
fingerprint = _digest({key: value for key, value in receipt.items() if key != "receipt_revision"})
|
|
253
|
+
if receipt.get("schema_version") != SCHEMA_VERSION or receipt.get("review_id") != identifier \
|
|
254
|
+
or receipt.get("receipt_revision") != fingerprint or not isinstance(receipt.get("operations"), dict) \
|
|
255
|
+
or receipt.get("state") not in {"running", "complete", "partial", "unknown"}:
|
|
256
|
+
raise SetupError("setup receipt integrity check failed")
|
|
257
|
+
self._validate_envelope(receipt.get("review"), identifier)
|
|
258
|
+
return receipt
|
|
259
|
+
|
|
260
|
+
def _save(self, receipt: dict[str, Any]) -> None:
|
|
261
|
+
receipt["updated_at"] = time.time()
|
|
262
|
+
receipt["receipt_revision"] = _digest({key: value for key, value in receipt.items() if key != "receipt_revision"})
|
|
263
|
+
_write_json(self._receipt_path(receipt["review_id"]), receipt)
|
|
264
|
+
|
|
265
|
+
@staticmethod
|
|
266
|
+
def _validate_envelope(review: Any, expected: str) -> None:
|
|
267
|
+
fields = {"schema_version", "kind", "context", "language", "preview", "action_inputs", "summary", "review_id"}
|
|
268
|
+
if not isinstance(review, dict) or set(review) not in (fields, fields | {"continuation"}) \
|
|
269
|
+
or review.get("schema_version") != SCHEMA_VERSION or review.get("kind") != "agent-bios-setup-review":
|
|
270
|
+
raise SetupError("apply requires the entire engine-issued review envelope")
|
|
271
|
+
if not isinstance(review["context"], dict) or not isinstance(review["preview"], dict) \
|
|
272
|
+
or not isinstance(review["language"], str) or review["language"] not in LANGUAGE_IDS \
|
|
273
|
+
or not isinstance(review["summary"], str) or not isinstance(review["action_inputs"], list):
|
|
274
|
+
raise SetupError("review envelope fields have invalid types")
|
|
275
|
+
if not isinstance(expected, str) or not IDENTIFIER.fullmatch(expected) or review.get("review_id") != expected \
|
|
276
|
+
or _review_identity(review) != expected:
|
|
277
|
+
raise SetupError("review identity does not match the exact reviewed envelope")
|
|
278
|
+
|
|
279
|
+
def _current_review(self, review: dict[str, Any]) -> dict[str, Any]:
|
|
280
|
+
if "continuation" in review:
|
|
281
|
+
continuation = review["continuation"]
|
|
282
|
+
if not isinstance(continuation, dict) or set(continuation) != {"review_id", "receipt_revision", "reuse_installation"}:
|
|
283
|
+
raise SetupError("invalid setup continuation")
|
|
284
|
+
resumed = self.resume(continuation["review_id"], review["language"])
|
|
285
|
+
if resumed.get("review") is None:
|
|
286
|
+
raise SetupError("setup continuation requires inspection of prior effects")
|
|
287
|
+
return resumed["review"]
|
|
288
|
+
value = review.get("preview", {}).get("plan", {})
|
|
289
|
+
plan = {key: value[key] for key in SetupController.FIELDS if key in value}
|
|
290
|
+
return self.plan(plan, review["language"])
|
|
291
|
+
|
|
292
|
+
def handoff(self) -> dict[str, Any]:
|
|
293
|
+
context = self.context()
|
|
294
|
+
result = {"verification": "deferred", "runtime_verified": None, "package_verified": None, "package_root": None, "release_digest": None, "cli_argv": None, "setup_argv": None,
|
|
295
|
+
"guide_path": None, "helper_registered": None, "helper_verified": None, "helper_argv": None,
|
|
296
|
+
"helper_usable": None,
|
|
297
|
+
"native_discovery": "unverified", "task_activation": {"performed": False, "existing_tasks": "unchanged", "default": "not_requested"},
|
|
298
|
+
"environment": {"HOME": context["home"], "AGENT_BIOS_STATE_DIR": context["state_dir"],
|
|
299
|
+
"AGENT_BIOS_CORPUS_DIR": context["user_dir"], "CODEX_HOME": context["codex_dir"],
|
|
300
|
+
"CLAUDE_CONFIG_DIR": context["claude_dir"]}, "needs_action": []}
|
|
301
|
+
try:
|
|
302
|
+
with try_transaction_lock(Path(context["state_dir"])) as acquired:
|
|
303
|
+
if not acquired:
|
|
304
|
+
result["needs_action"].append("private state verification is deferred: another operation holds the lock or synchronization is unavailable; retry setup status")
|
|
305
|
+
return result
|
|
306
|
+
result.update(verification="checked", runtime_verified=False, package_verified=False,
|
|
307
|
+
helper_registered=False, helper_verified=False, helper_usable=False)
|
|
308
|
+
return self._verified_handoff(context, result)
|
|
309
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
310
|
+
result["needs_action"].append(str(exc))
|
|
311
|
+
return result
|
|
312
|
+
|
|
313
|
+
def _verified_handoff(self, context: dict[str, Any], result: dict[str, Any]) -> dict[str, Any]:
|
|
314
|
+
record = Path(context["state_dir"]) / "runtime/private-install.json"
|
|
315
|
+
pending = False
|
|
316
|
+
if record.is_file():
|
|
317
|
+
try:
|
|
318
|
+
reject_symlink_ancestors(record)
|
|
319
|
+
release = confirmed_release(Path(context["state_dir"]))
|
|
320
|
+
result.update(package_verified=True, package_root=str(release), release_digest=release.name,
|
|
321
|
+
cli_argv=["/bin/bash", str(release / "install.sh")],
|
|
322
|
+
setup_argv=["/bin/bash", str(release / "install.sh"), "setup"], guide_path=str(release / "compose/setup/START.md"))
|
|
323
|
+
result["environment"].update(AGENT_BIOS_PACKAGE_ROOT=str(release), AGENT_BIOS_PRIVATE_CORPUS="1", AGENT_BIOS_LEGACY_INSTALL="0")
|
|
324
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
325
|
+
result["needs_action"].append(str(exc))
|
|
326
|
+
try:
|
|
327
|
+
verification = self.installer.verify()
|
|
328
|
+
result["runtime_verified"] = verification.get("stored") is True
|
|
329
|
+
if not result["runtime_verified"]:
|
|
330
|
+
result["needs_action"].append("private runtime verification did not confirm installation")
|
|
331
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
332
|
+
result["needs_action"].append(str(exc))
|
|
333
|
+
try:
|
|
334
|
+
pending = bool(self.installer.status().get("transactions"))
|
|
335
|
+
if pending:
|
|
336
|
+
result["needs_action"].append("private installation has a pending transaction")
|
|
337
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
338
|
+
pending = True
|
|
339
|
+
result["needs_action"].append(str(exc))
|
|
340
|
+
try:
|
|
341
|
+
manager = self.installer._app_manager()
|
|
342
|
+
bridge = manager.managed_status()
|
|
343
|
+
result["helper_registered"] = bool(bridge.get("registered"))
|
|
344
|
+
result["needs_action"].extend(bridge.get("needs_action", []))
|
|
345
|
+
if bridge.get("registered") and not bridge.get("needs_action"):
|
|
346
|
+
helper = manager.target / "scripts/bridge.py"
|
|
347
|
+
result.update(helper_verified=True, helper_argv=[sys.executable, str(helper)])
|
|
348
|
+
if "AGENT_LAUNCH_VENV" in self.installer.env:
|
|
349
|
+
result["environment"]["AGENT_LAUNCH_VENV"] = self.installer.env["AGENT_LAUNCH_VENV"]
|
|
350
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
351
|
+
result["needs_action"].append(str(exc))
|
|
352
|
+
result["helper_usable"] = result["helper_verified"] and result["package_verified"] and not pending
|
|
353
|
+
return result
|
|
354
|
+
|
|
355
|
+
def status(self, identifier: str, language: str | None = None, *, replayed: bool = False) -> dict[str, Any]:
|
|
356
|
+
receipt = self._receipt(identifier)
|
|
357
|
+
result = receipt.get("result") or {} if receipt else {}
|
|
358
|
+
language = self._language(language or (receipt or {}).get("review", {}).get("language"))
|
|
359
|
+
state = receipt["state"] if receipt else "missing"
|
|
360
|
+
if state == "running":
|
|
361
|
+
owner = receipt.get("owner", {})
|
|
362
|
+
token = _process_identity(owner.get("pid", -1)) if isinstance(owner.get("pid"), int) else None
|
|
363
|
+
same_machine = receipt["review"]["context"].get("machine_id") == self.context()["machine_id"]
|
|
364
|
+
if not same_machine or token is None or token != owner.get("process_identity"):
|
|
365
|
+
state = "unknown"
|
|
366
|
+
return {"schema_version": SCHEMA_VERSION, "kind": "agent-bios-setup-status", "review_id": identifier,
|
|
367
|
+
"state": state, "receipt_path": str(self._receipt_path(identifier)), "receipt": receipt,
|
|
368
|
+
"result": result, "summary": format_setup_result(result, language, interface="conversation"), "handoff": self.handoff(),
|
|
369
|
+
"replayed": replayed, "context": self.context(), "language": language}
|
|
370
|
+
|
|
371
|
+
def resume(self, identifier: str, language: str | None = None) -> dict[str, Any]:
|
|
372
|
+
response = self.status(identifier, language)
|
|
373
|
+
response.update(remaining_plan=None, review=None, needs_action=[])
|
|
374
|
+
receipt = response["receipt"]
|
|
375
|
+
if receipt is None:
|
|
376
|
+
response["needs_action"] = ["setup receipt was not found"]
|
|
377
|
+
return response
|
|
378
|
+
self._assert_owner(receipt["review"])
|
|
379
|
+
chain = []
|
|
380
|
+
while receipt.get("continued_by"):
|
|
381
|
+
if receipt["review_id"] in chain or len(chain) >= 32:
|
|
382
|
+
raise SetupError("setup continuation chain is invalid")
|
|
383
|
+
chain.append(receipt["review_id"])
|
|
384
|
+
child = self._receipt(receipt["continued_by"])
|
|
385
|
+
if child is None or child["review"].get("continuation", {}).get("review_id") != receipt["review_id"]:
|
|
386
|
+
raise SetupError("setup continuation receipt is missing or inconsistent")
|
|
387
|
+
self._assert_owner(child["review"])
|
|
388
|
+
receipt = child
|
|
389
|
+
response = self.status(receipt["review_id"], language)
|
|
390
|
+
response.update(remaining_plan=None, review=None, needs_action=[], resumed_from=identifier)
|
|
391
|
+
if receipt["state"] == "complete":
|
|
392
|
+
return response
|
|
393
|
+
if receipt["state"] == "running":
|
|
394
|
+
response["needs_action"] = ["setup is running or its interruption is unconfirmed; inspect its recorded progress before retrying"]
|
|
395
|
+
return response
|
|
396
|
+
unsafe = [name for name, operation in receipt.get("operations", {}).items()
|
|
397
|
+
if operation.get("state") in {"running", "failed", "unknown"}]
|
|
398
|
+
if unsafe or receipt["state"] == "unknown":
|
|
399
|
+
response["needs_action"] = ["inspect effects before retrying: " + ", ".join(unsafe or ["unknown operation"])]
|
|
400
|
+
response["inspection"] = self.inspect(response["language"])
|
|
401
|
+
return response
|
|
402
|
+
original = receipt["review"]["preview"]["plan"]
|
|
403
|
+
plan = {key: original[key] for key in SetupController.FIELDS}
|
|
404
|
+
completed = {name for name, operation in receipt.get("operations", {}).items() if operation.get("state") == "completed"}
|
|
405
|
+
controller = self.controller_factory()
|
|
406
|
+
inventory = {row["id"]: row for row in controller.dependencies}
|
|
407
|
+
remaining = []
|
|
408
|
+
for name in plan["dependencies"]:
|
|
409
|
+
row = inventory.get(name)
|
|
410
|
+
if row is None:
|
|
411
|
+
response["needs_action"].append(f"requested dependency is absent from the current inventory: {name}")
|
|
412
|
+
elif "dependency:" + name in completed:
|
|
413
|
+
if row.get("status") != "available":
|
|
414
|
+
response["needs_action"].append(f"completed dependency is not currently available: {name}")
|
|
415
|
+
elif row.get("status") == "available":
|
|
416
|
+
continue
|
|
417
|
+
elif not row.get("install_argv"):
|
|
418
|
+
response["needs_action"].append(f"requested dependency is still missing and has no current installation recipe: {name}")
|
|
419
|
+
else:
|
|
420
|
+
remaining.append(name)
|
|
421
|
+
response["inspection"] = {"dependencies": controller.dependencies, "choices": controller.choices}
|
|
422
|
+
if response["needs_action"]:
|
|
423
|
+
return response
|
|
424
|
+
plan["dependencies"] = remaining
|
|
425
|
+
reuse = "install" in completed
|
|
426
|
+
if reuse:
|
|
427
|
+
if not response["handoff"]["runtime_verified"] or response["handoff"]["needs_action"]:
|
|
428
|
+
response["needs_action"] = ["verify the completed private installation before continuing"]
|
|
429
|
+
return response
|
|
430
|
+
plan["selection_mode"], plan["targets"] = None, None
|
|
431
|
+
if "extras" in completed:
|
|
432
|
+
plan["app_bridge"], plan["import_paths"], plan["project_roots"] = False, [], []
|
|
433
|
+
continuation = {"review_id": receipt["review_id"], "receipt_revision": receipt["receipt_revision"], "reuse_installation": reuse}
|
|
434
|
+
response["remaining_plan"] = plan
|
|
435
|
+
candidate = self.plan(plan, response["language"], continuation=continuation)
|
|
436
|
+
if reuse:
|
|
437
|
+
completed_install = receipt["operations"]["install"]["event"].get("installation", {})
|
|
438
|
+
completed_digest = completed_install.get("record", {}).get("release_digest") or completed_install.get("release_digest")
|
|
439
|
+
source_digest = candidate["preview"]["installation"].get("release_digest")
|
|
440
|
+
if source_digest != response["handoff"].get("release_digest") or completed_digest != source_digest:
|
|
441
|
+
response["needs_action"] = ["source or installed package changed after partial setup; create a fresh installation plan"]
|
|
442
|
+
return response
|
|
443
|
+
response["review"] = candidate
|
|
444
|
+
return response
|
|
445
|
+
|
|
446
|
+
def apply(self, review: dict[str, Any], expected_review_id: str, *, yes: bool = False) -> dict[str, Any]:
|
|
447
|
+
if yes is not True:
|
|
448
|
+
raise SetupError("apply requires explicit --yes acknowledgment of the exact reviewed plan")
|
|
449
|
+
review = json.loads(json.dumps(review, ensure_ascii=False))
|
|
450
|
+
self._validate_envelope(review, expected_review_id)
|
|
451
|
+
existing = self._receipt(expected_review_id)
|
|
452
|
+
if existing is not None:
|
|
453
|
+
if existing["review"] != review:
|
|
454
|
+
raise SetupError("review differs from the owned receipt")
|
|
455
|
+
self._assert_owner(review)
|
|
456
|
+
return self.status(expected_review_id, replayed=True)
|
|
457
|
+
if self._current_review(review) != review:
|
|
458
|
+
raise SetupError("review is stale or belongs to another execution context; create a fresh plan")
|
|
459
|
+
state_root = Path(self.context()["state_dir"])
|
|
460
|
+
with transaction_lock(state_root):
|
|
461
|
+
if self._receipt(expected_review_id) is not None:
|
|
462
|
+
return self.status(expected_review_id, replayed=True)
|
|
463
|
+
if self._current_review(review) != review:
|
|
464
|
+
raise SetupError("review changed while waiting for the setup lock; create a fresh plan")
|
|
465
|
+
receipt = {"schema_version": SCHEMA_VERSION, "kind": "agent-bios-setup-receipt", "review_id": expected_review_id,
|
|
466
|
+
"review": review, "state": "running", "created_at": time.time(), "operations": {}, "progress": [], "result": None,
|
|
467
|
+
"owner": {"pid": os.getpid(), "process_identity": _process_identity(os.getpid())}}
|
|
468
|
+
self._save(receipt)
|
|
469
|
+
if review.get("continuation"):
|
|
470
|
+
parent = self._receipt(review["continuation"]["review_id"])
|
|
471
|
+
if parent is None or parent["receipt_revision"] != review["continuation"]["receipt_revision"] or parent.get("continued_by"):
|
|
472
|
+
raise SetupError("setup continuation was consumed or changed before execution")
|
|
473
|
+
parent["continued_by"] = expected_review_id
|
|
474
|
+
self._save(parent)
|
|
475
|
+
def progress(event: dict[str, Any]) -> None:
|
|
476
|
+
stage, phase = event.get("stage"), event.get("phase")
|
|
477
|
+
key = "dependency:" + str(event.get("dependency", event.get("id", ""))) if stage == "dependency" else stage
|
|
478
|
+
if phase == "started" and stage == "dependency":
|
|
479
|
+
actual = self._actions({"plan": {"dependency_actions": [action for action in review["preview"]["plan"]["dependency_actions"] if action["id"] == event["dependency"]]}})
|
|
480
|
+
expected = [row for row in review["action_inputs"] if row["id"] == event["dependency"]]
|
|
481
|
+
if actual != expected:
|
|
482
|
+
raise SetupError("dependency executable changed after review")
|
|
483
|
+
if stage in {"dependency", "install", "extras"}:
|
|
484
|
+
receipt["operations"][key] = {"state": {"started": "running", "completed": "completed", "failed": "failed", "not_started": "not_started"}.get(phase, "not_started"),
|
|
485
|
+
"event": event, "at": time.time()}
|
|
486
|
+
receipt["progress"].append(event)
|
|
487
|
+
self._save(receipt)
|
|
488
|
+
if self.progress_hook:
|
|
489
|
+
self.progress_hook(event)
|
|
490
|
+
try:
|
|
491
|
+
controller = self.controller_factory()
|
|
492
|
+
plan = {key: review["preview"]["plan"][key] for key in SetupController.FIELDS}
|
|
493
|
+
result = controller.apply(plan, preview=review["preview"], progress=progress,
|
|
494
|
+
reuse_installation=bool(review.get("continuation", {}).get("reuse_installation")))
|
|
495
|
+
receipt["result"] = result
|
|
496
|
+
active = any(operation["state"] == "running" for operation in receipt["operations"].values())
|
|
497
|
+
receipt["state"] = "complete" if result.get("applied") else "unknown" if active else "partial"
|
|
498
|
+
except BaseException as exc:
|
|
499
|
+
active = any(operation["state"] == "running" for operation in receipt["operations"].values())
|
|
500
|
+
receipt["state"] = "unknown" if active else "partial"
|
|
501
|
+
receipt["error"] = {"type": type(exc).__name__, "message": str(exc)}
|
|
502
|
+
receipt["result"] = {"applied": False, "installation_error": str(exc) or type(exc).__name__,
|
|
503
|
+
"dependency_results": [operation["event"] for key, operation in receipt["operations"].items() if key.startswith("dependency:") and "returncode" in operation["event"]]}
|
|
504
|
+
self._save(receipt)
|
|
505
|
+
return self.status(expected_review_id)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
class _Parser(argparse.ArgumentParser):
|
|
509
|
+
def error(self, message):
|
|
510
|
+
raise SetupError(message)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _input(source: str) -> dict[str, Any]:
|
|
514
|
+
if source != "-":
|
|
515
|
+
return _read_json(Path(source).expanduser())
|
|
516
|
+
if sys.stdin.isatty():
|
|
517
|
+
raise SetupError("--input - requires piped JSON; use a review file in a terminal")
|
|
518
|
+
body = sys.stdin.read(8 * 1024 * 1024 + 1)
|
|
519
|
+
if len(body) > 8 * 1024 * 1024:
|
|
520
|
+
raise SetupError("setup input is too large")
|
|
521
|
+
value = json.loads(body)
|
|
522
|
+
if not isinstance(value, dict):
|
|
523
|
+
raise SetupError("setup input must be a JSON object")
|
|
524
|
+
return value
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def main(argv: list[str] | None = None) -> int:
|
|
528
|
+
args = None
|
|
529
|
+
presentation_language = None
|
|
530
|
+
try:
|
|
531
|
+
parser = _Parser(prog="agent-bios setup", add_help=False)
|
|
532
|
+
parser.add_argument("--repo", type=Path, default=Path(__file__).resolve().parents[1])
|
|
533
|
+
parser.add_argument("--help", action="store_true")
|
|
534
|
+
sub = parser.add_subparsers(dest="command")
|
|
535
|
+
for name in ("start", "inspect"):
|
|
536
|
+
command = sub.add_parser(name, add_help=False)
|
|
537
|
+
command.add_argument("--language", choices=sorted(LANGUAGE_IDS))
|
|
538
|
+
discover = sub.add_parser("discover", add_help=False)
|
|
539
|
+
discover.add_argument("--project-root", action="append", default=[])
|
|
540
|
+
plan = sub.add_parser("plan", add_help=False)
|
|
541
|
+
plan.add_argument("--input", required=True)
|
|
542
|
+
plan.add_argument("--language", choices=sorted(LANGUAGE_IDS))
|
|
543
|
+
apply = sub.add_parser("apply", add_help=False)
|
|
544
|
+
apply.add_argument("--input", required=True)
|
|
545
|
+
apply.add_argument("--review-id", required=True)
|
|
546
|
+
apply.add_argument("--yes", action="store_true")
|
|
547
|
+
for name in ("status", "resume"):
|
|
548
|
+
command = sub.add_parser(name, add_help=False)
|
|
549
|
+
command.add_argument("--review-id", required=True)
|
|
550
|
+
command.add_argument("--language", choices=sorted(LANGUAGE_IDS))
|
|
551
|
+
raw = list(sys.argv[1:] if argv is None else argv)
|
|
552
|
+
args = parser.parse_args([value for value in raw if value != "--json"])
|
|
553
|
+
if args.help:
|
|
554
|
+
result = {"schema_version": SCHEMA_VERSION, "commands": ["start", "inspect", "discover", "plan", "apply", "status", "resume"], "format": "JSON"}
|
|
555
|
+
else:
|
|
556
|
+
service = SetupService(CorpusInstaller(args.repo))
|
|
557
|
+
command = args.command or "start"
|
|
558
|
+
with contextlib.redirect_stdout(sys.stderr):
|
|
559
|
+
if command == "start": result = service.start(getattr(args, "language", None))
|
|
560
|
+
elif command == "inspect": result = service.inspect(args.language)
|
|
561
|
+
elif command == "discover": result = service.discover(args.project_root)
|
|
562
|
+
elif command == "plan": result = service.plan(_input(args.input), args.language)
|
|
563
|
+
elif command == "apply":
|
|
564
|
+
envelope = _input(args.input)
|
|
565
|
+
selected_language = envelope.get("language")
|
|
566
|
+
if isinstance(selected_language, str) and selected_language in LANGUAGE_IDS:
|
|
567
|
+
presentation_language = selected_language
|
|
568
|
+
result = service.apply(envelope, args.review_id, yes=args.yes)
|
|
569
|
+
elif command == "status": result = service.status(args.review_id, args.language)
|
|
570
|
+
else: result = service.resume(args.review_id, args.language)
|
|
571
|
+
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
|
|
572
|
+
return 1 if (args.command == "apply" and result.get("state") != "complete") else 0
|
|
573
|
+
except (OSError, RuntimeError, ValueError, TypeError, KeyError) as exc:
|
|
574
|
+
language = presentation_language or getattr(args, "language", None) or detect_language(os.environ)
|
|
575
|
+
print(json.dumps({"schema_version": SCHEMA_VERSION, "kind": "agent-bios-setup-error", "ok": False,
|
|
576
|
+
"error": {"type": type(exc).__name__, "message": str(exc)},
|
|
577
|
+
"summary": translate(language, "Setup could not finish. Details: {detail}", detail=str(exc))}, ensure_ascii=False))
|
|
578
|
+
return 2
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
if __name__ == "__main__":
|
|
582
|
+
raise SystemExit(main())
|