agent-bios 0.18.0 → 0.19.1
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 +187 -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/claude/skills/understand/SKILL.md +52 -22
- 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 +202 -18
- package/compose/corpus_session.py +27 -0
- package/compose/corpus_setup.py +676 -0
- package/compose/corpus_setup_cli.py +585 -0
- package/compose/corpus_setup_i18n.py +324 -0
- package/compose/corpus_setup_ui.py +647 -0
- package/compose/corpus_store.py +213 -32
- package/compose/corpus_transaction.py +43 -10
- package/compose/corpus_ui_runtime.py +278 -0
- package/compose/corpus_understand.py +173 -22
- package/compose/setup/START.md +158 -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 +206 -0
- package/docs/understand.md +88 -0
- package/install.sh +75 -46
- package/launch/agent-launch.py +99 -52
- 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
package/compose/corpus.py
CHANGED
|
@@ -86,6 +86,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
86
86
|
snapshot_view.add_argument("--host", choices=("claude", "codex"))
|
|
87
87
|
snapshot_view.add_argument("--content-ref", help="read this stored snapshot without resolving current authoring")
|
|
88
88
|
snapshot.add_argument("--native", action="store_true", help="opt into selected native hooks on either host and Claude agents for this snapshot")
|
|
89
|
+
snapshot.add_argument("--selection-mode", choices=("default", "selected", "none"))
|
|
90
|
+
snapshot.add_argument("--select", action="append", help="one-session package/domain/item selection")
|
|
91
|
+
snapshot.add_argument("--cwd", type=Path, help="project scope for this snapshot")
|
|
89
92
|
|
|
90
93
|
install = sub.add_parser("install", help="record the current package as a private baseline")
|
|
91
94
|
install.add_argument("--domains", help="comma-separated qualified selection values")
|
|
@@ -177,10 +180,12 @@ def run_command(args: argparse.Namespace, store: CorpusStore) -> Any:
|
|
|
177
180
|
return store.status()
|
|
178
181
|
if command == "snapshot":
|
|
179
182
|
if args.content_ref:
|
|
180
|
-
if args.native:
|
|
181
|
-
raise CorpusStoreError("a stored snapshot is immutable;
|
|
183
|
+
if args.native or args.selection_mode or args.select or args.cwd:
|
|
184
|
+
raise CorpusStoreError("a stored snapshot is immutable; selection and scope options require --host")
|
|
182
185
|
return store.snapshot_inventory(args.content_ref)
|
|
183
|
-
return store.snapshot(args.host, native=args.native
|
|
186
|
+
return store.snapshot(args.host, selection=args.select, native=args.native,
|
|
187
|
+
selection_mode=args.selection_mode or ("selected" if args.select is not None else None),
|
|
188
|
+
cwd=args.cwd)
|
|
184
189
|
if command == "install":
|
|
185
190
|
domains = None
|
|
186
191
|
if args.domains is not None:
|
|
@@ -328,14 +333,16 @@ def _run_tui_or_fallback(args: argparse.Namespace, store: CorpusStore) -> int:
|
|
|
328
333
|
module_name = f"{__package__}.corpus_ui" if __package__ else "corpus_ui"
|
|
329
334
|
try:
|
|
330
335
|
module = importlib.import_module(module_name)
|
|
331
|
-
except
|
|
332
|
-
|
|
336
|
+
except ImportError as exc:
|
|
337
|
+
missing_ui_dependency = any(exc.name == name or (exc.name or "").startswith(name + ".")
|
|
338
|
+
for name in ("textual", "rich"))
|
|
339
|
+
if not missing_ui_dependency and exc.name not in {"corpus_ui", module_name}:
|
|
333
340
|
raise
|
|
334
341
|
interpreter = venv_python()
|
|
335
342
|
if (
|
|
336
|
-
|
|
343
|
+
missing_ui_dependency
|
|
337
344
|
and interpreter is not None
|
|
338
|
-
and Path(sys.executable).
|
|
345
|
+
and Path(sys.executable).absolute() != interpreter.absolute()
|
|
339
346
|
and os.environ.get("AGENT_BIOS_CORPUS_TUI_REEXEC") != "1"
|
|
340
347
|
):
|
|
341
348
|
env = os.environ.copy()
|
|
@@ -358,7 +365,20 @@ def _run_tui_or_fallback(args: argparse.Namespace, store: CorpusStore) -> int:
|
|
|
358
365
|
return 0
|
|
359
366
|
|
|
360
367
|
|
|
361
|
-
def
|
|
368
|
+
def _run_bundled_tui(store: CorpusStore) -> int:
|
|
369
|
+
try:
|
|
370
|
+
from corpus_ui_runtime import activate_ui_runtime
|
|
371
|
+
activate_ui_runtime(Path(__file__).resolve().parents[1])
|
|
372
|
+
module_name = f"{__package__}.corpus_ui" if __package__ else "corpus_ui"
|
|
373
|
+
module = importlib.import_module(module_name)
|
|
374
|
+
except (ImportError, OSError, RuntimeError) as exc:
|
|
375
|
+
raise CorpusStoreError(f"bundled terminal UI could not start: {exc}") from exc
|
|
376
|
+
app = module.CorpusStudio(store)
|
|
377
|
+
app.run()
|
|
378
|
+
return 0
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def main(argv: Sequence[str] | None = None, *, bundled_ui: bool = False) -> int:
|
|
362
382
|
parser = build_parser()
|
|
363
383
|
raw = list(sys.argv[1:] if argv is None else argv)
|
|
364
384
|
# Machine callers naturally put --json after the verb. argparse only accepts
|
|
@@ -372,6 +392,8 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
372
392
|
try:
|
|
373
393
|
if args.command is None:
|
|
374
394
|
if sys.stdin.isatty() and sys.stdout.isatty():
|
|
395
|
+
if bundled_ui:
|
|
396
|
+
return _run_bundled_tui(store)
|
|
375
397
|
return _run_tui_or_fallback(args, store)
|
|
376
398
|
args.command = "list"
|
|
377
399
|
args.active_only = False
|
|
@@ -384,4 +406,4 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
384
406
|
|
|
385
407
|
|
|
386
408
|
if __name__ == "__main__":
|
|
387
|
-
raise SystemExit(main())
|
|
409
|
+
raise SystemExit(main(bundled_ui=True))
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Explicit Codex app discovery and per-task corpus context delivery."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import re
|
|
12
|
+
import shutil
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from corpus_store import CorpusStore, CorpusStoreError
|
|
19
|
+
from corpus_transaction import (TransactionError, confirmed_release, guard_pending,
|
|
20
|
+
reject_symlink_ancestors, transaction_lock)
|
|
21
|
+
except ImportError:
|
|
22
|
+
from .corpus_store import CorpusStore, CorpusStoreError
|
|
23
|
+
from .corpus_transaction import (TransactionError, confirmed_release, guard_pending,
|
|
24
|
+
reject_symlink_ancestors, transaction_lock)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
SCHEMA_VERSION = 1
|
|
28
|
+
BRIDGE_MEMBERS = ("SKILL.md", "agents/openai.yaml", "scripts/bridge.py",
|
|
29
|
+
"scripts/corpus_transaction.py", "bridge.json")
|
|
30
|
+
SESSION_ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9_-]{0,127}\Z")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AppError(RuntimeError):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _json_bytes(value: Any) -> bytes:
|
|
38
|
+
return (json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True) + "\n").encode("utf-8")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _read_json(path: Path) -> dict[str, Any]:
|
|
42
|
+
reject_symlink_ancestors(path)
|
|
43
|
+
try:
|
|
44
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
45
|
+
except (OSError, ValueError) as exc:
|
|
46
|
+
raise AppError(f"unreadable app state: {path}") from exc
|
|
47
|
+
if not isinstance(value, dict):
|
|
48
|
+
raise AppError(f"invalid app state: {path}")
|
|
49
|
+
return value
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _write_json(path: Path, value: dict[str, Any]) -> None:
|
|
53
|
+
reject_symlink_ancestors(path)
|
|
54
|
+
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
55
|
+
descriptor, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
|
|
56
|
+
try:
|
|
57
|
+
with os.fdopen(descriptor, "wb") as handle:
|
|
58
|
+
handle.write(_json_bytes(value))
|
|
59
|
+
handle.flush()
|
|
60
|
+
os.fsync(handle.fileno())
|
|
61
|
+
os.replace(temporary, path)
|
|
62
|
+
finally:
|
|
63
|
+
if os.path.lexists(temporary):
|
|
64
|
+
os.unlink(temporary)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _tree_digest(members: dict[str, bytes]) -> str:
|
|
68
|
+
digest = hashlib.sha256()
|
|
69
|
+
for relative, data in sorted(members.items()):
|
|
70
|
+
digest.update(relative.encode("utf-8") + b"\0" + data + b"\0")
|
|
71
|
+
return digest.hexdigest()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _roots(environ: dict[str, str] | None) -> tuple[dict[str, str], Path, Path, Path]:
|
|
75
|
+
env = dict(os.environ if environ is None else environ)
|
|
76
|
+
home = Path(env.get("HOME", str(Path.home()))).expanduser().absolute()
|
|
77
|
+
state = Path(env.get("AGENT_BIOS_STATE_DIR", str(home / ".local/share/agent-bios"))).expanduser().absolute()
|
|
78
|
+
user = Path(env.get("AGENT_BIOS_CORPUS_DIR", str(home / ".config/agent-bios/corpus"))).expanduser().absolute()
|
|
79
|
+
return env, home, state, user
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class AppBridge:
|
|
83
|
+
"""Own one explicit-only skill symlink; retain immutable private generations."""
|
|
84
|
+
|
|
85
|
+
def __init__(self, repo: Path, environ: dict[str, str] | None = None):
|
|
86
|
+
self.repo = Path(repo).absolute()
|
|
87
|
+
self.env, self.home, self.state_root, self.user_root = _roots(environ)
|
|
88
|
+
self.target = self.home / ".agents/skills/agent-bios"
|
|
89
|
+
self.generations = self.state_root / "runtime/app-bridges"
|
|
90
|
+
|
|
91
|
+
def _base_config(self) -> dict[str, Any]:
|
|
92
|
+
return {"schema_version": SCHEMA_VERSION, "home": str(self.home),
|
|
93
|
+
"state_root": str(self.state_root), "user_root": str(self.user_root),
|
|
94
|
+
"discovery_path": str(self.target)}
|
|
95
|
+
|
|
96
|
+
def _config(self) -> dict[str, Any]:
|
|
97
|
+
config = self._base_config()
|
|
98
|
+
if "AGENT_LAUNCH_VENV" in self.env:
|
|
99
|
+
value = self.env["AGENT_LAUNCH_VENV"]
|
|
100
|
+
config["launch_venv"] = str(Path(value).expanduser().absolute()) if value else ""
|
|
101
|
+
else:
|
|
102
|
+
current = self._owned_target()
|
|
103
|
+
if current is not None:
|
|
104
|
+
saved = _read_json(current / "bridge.json")
|
|
105
|
+
if "launch_venv" in saved:
|
|
106
|
+
config["launch_venv"] = saved["launch_venv"]
|
|
107
|
+
return config
|
|
108
|
+
|
|
109
|
+
def _source_members(self) -> dict[str, bytes]:
|
|
110
|
+
reject_symlink_ancestors(self.state_root)
|
|
111
|
+
guard_pending(self.state_root)
|
|
112
|
+
release = confirmed_release(self.state_root)
|
|
113
|
+
source = release / "compose/app_bridge"
|
|
114
|
+
members = {}
|
|
115
|
+
for name in BRIDGE_MEMBERS:
|
|
116
|
+
if name == "bridge.json":
|
|
117
|
+
members[name] = _json_bytes(self._config())
|
|
118
|
+
continue
|
|
119
|
+
path = release / "compose/corpus_transaction.py" if name == "scripts/corpus_transaction.py" else source / name
|
|
120
|
+
reject_symlink_ancestors(path)
|
|
121
|
+
if not path.is_file():
|
|
122
|
+
raise AppError(f"installed release has no app bridge member: {path}")
|
|
123
|
+
members[name] = path.read_bytes()
|
|
124
|
+
return members
|
|
125
|
+
|
|
126
|
+
def _owned_target(self) -> Path | None:
|
|
127
|
+
reject_symlink_ancestors(self.target.parent)
|
|
128
|
+
if not os.path.lexists(self.target):
|
|
129
|
+
return None
|
|
130
|
+
if not self.target.is_symlink():
|
|
131
|
+
raise AppError(f"preserving unowned app skill: {self.target}")
|
|
132
|
+
raw = Path(os.readlink(self.target))
|
|
133
|
+
if not raw.is_absolute() or raw.parent != self.generations or not re.fullmatch(r"[a-f0-9]{64}", raw.name):
|
|
134
|
+
raise AppError(f"preserving unowned app skill link: {self.target}")
|
|
135
|
+
reject_symlink_ancestors(raw)
|
|
136
|
+
if not raw.is_dir():
|
|
137
|
+
raise AppError(f"owned app skill generation is unavailable: {raw}")
|
|
138
|
+
paths = list(raw.rglob("*"))
|
|
139
|
+
if any(path.is_symlink() for path in paths):
|
|
140
|
+
raise AppError(f"preserving redirected app skill generation: {raw}")
|
|
141
|
+
files = {path.relative_to(raw).as_posix(): path.read_bytes() for path in paths if path.is_file()}
|
|
142
|
+
if set(files) != set(BRIDGE_MEMBERS) or _tree_digest(files) != raw.name:
|
|
143
|
+
raise AppError(f"preserving changed app skill generation: {raw}")
|
|
144
|
+
config = _read_json(raw / "bridge.json")
|
|
145
|
+
base = self._base_config()
|
|
146
|
+
if (any(config.get(key) != value for key, value in base.items())
|
|
147
|
+
or set(config) - set(base) - {"launch_venv"}
|
|
148
|
+
or ("launch_venv" in config and (not isinstance(config["launch_venv"], str)
|
|
149
|
+
or (config["launch_venv"] and not Path(config["launch_venv"]).is_absolute())))):
|
|
150
|
+
raise AppError(f"app skill belongs to different private root settings: {self.target}")
|
|
151
|
+
return raw
|
|
152
|
+
|
|
153
|
+
def status(self) -> dict[str, Any]:
|
|
154
|
+
try:
|
|
155
|
+
target = self._owned_target()
|
|
156
|
+
return {"registered": target is not None, "discovery_path": str(self.target),
|
|
157
|
+
"generation": str(target) if target else None, "implicit_invocation": False,
|
|
158
|
+
"native_discovery": "unverified", "session_activation": "explicit-use-only"}
|
|
159
|
+
except (AppError, OSError, TransactionError) as exc:
|
|
160
|
+
return {"registered": False, "discovery_path": str(self.target), "needs_action": [str(exc)],
|
|
161
|
+
"native_discovery": "unverified", "session_activation": "explicit-use-only"}
|
|
162
|
+
|
|
163
|
+
def has_owned_registration(self) -> bool:
|
|
164
|
+
"""Identify our link namespace without opening unrelated skill contents."""
|
|
165
|
+
try:
|
|
166
|
+
reject_symlink_ancestors(self.target.parent)
|
|
167
|
+
if not self.target.is_symlink():
|
|
168
|
+
return False
|
|
169
|
+
target = Path(os.readlink(self.target))
|
|
170
|
+
return target.is_absolute() and target.parent == self.generations
|
|
171
|
+
except (OSError, TransactionError):
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
def managed_status(self) -> dict[str, Any]:
|
|
175
|
+
"""Lifecycle view: foreign native paths are outside this installer."""
|
|
176
|
+
if not self.has_owned_registration():
|
|
177
|
+
return {"registered": False, "managed": False, "discovery_path": str(self.target)}
|
|
178
|
+
return {**self.status(), "managed": True}
|
|
179
|
+
|
|
180
|
+
def register(self, dry_run: bool = False) -> dict[str, Any]:
|
|
181
|
+
reject_symlink_ancestors(self.state_root)
|
|
182
|
+
if dry_run:
|
|
183
|
+
before = self._owned_target()
|
|
184
|
+
members = self._source_members()
|
|
185
|
+
generation = self.generations / _tree_digest(members)
|
|
186
|
+
return {"registered": before is not None, "dry_run": True, "changed": before != generation,
|
|
187
|
+
"discovery_path": str(self.target), "generation": str(generation),
|
|
188
|
+
"implicit_invocation": False, "native_discovery": "unverified",
|
|
189
|
+
"session_activation": "explicit-use-only"}
|
|
190
|
+
with transaction_lock(self.state_root):
|
|
191
|
+
before = self._owned_target()
|
|
192
|
+
members = self._source_members()
|
|
193
|
+
generation = self.generations / _tree_digest(members)
|
|
194
|
+
result = {"registered": True, "dry_run": False, "changed": before != generation,
|
|
195
|
+
"discovery_path": str(self.target), "generation": str(generation),
|
|
196
|
+
"implicit_invocation": False, "native_discovery": "unverified",
|
|
197
|
+
"session_activation": "explicit-use-only"}
|
|
198
|
+
reject_symlink_ancestors(generation)
|
|
199
|
+
if generation.exists():
|
|
200
|
+
paths = list(generation.rglob("*"))
|
|
201
|
+
actual = {path.relative_to(generation).as_posix(): path.read_bytes()
|
|
202
|
+
for path in paths if path.is_file() and not path.is_symlink()}
|
|
203
|
+
if any(path.is_symlink() for path in paths) or actual != members:
|
|
204
|
+
raise AppError(f"app bridge generation collision: {generation}")
|
|
205
|
+
else:
|
|
206
|
+
self.generations.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
207
|
+
temporary = Path(tempfile.mkdtemp(prefix=".bridge-", dir=self.generations))
|
|
208
|
+
try:
|
|
209
|
+
for relative, data in members.items():
|
|
210
|
+
path = temporary / relative
|
|
211
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
212
|
+
path.write_bytes(data)
|
|
213
|
+
path.chmod(0o600)
|
|
214
|
+
os.replace(temporary, generation)
|
|
215
|
+
finally:
|
|
216
|
+
if temporary.exists():
|
|
217
|
+
shutil.rmtree(temporary)
|
|
218
|
+
self.target.parent.mkdir(parents=True, exist_ok=True)
|
|
219
|
+
if self._owned_target() != before:
|
|
220
|
+
raise AppError("app skill changed while preparing registration")
|
|
221
|
+
descriptor, temporary = tempfile.mkstemp(prefix=".agent-bios-link-", dir=self.target.parent)
|
|
222
|
+
os.close(descriptor)
|
|
223
|
+
os.unlink(temporary)
|
|
224
|
+
try:
|
|
225
|
+
os.symlink(str(generation), temporary)
|
|
226
|
+
os.replace(temporary, self.target)
|
|
227
|
+
finally:
|
|
228
|
+
if os.path.lexists(temporary):
|
|
229
|
+
os.unlink(temporary)
|
|
230
|
+
return result
|
|
231
|
+
|
|
232
|
+
def unregister(self, dry_run: bool = False) -> dict[str, Any]:
|
|
233
|
+
reject_symlink_ancestors(self.state_root)
|
|
234
|
+
if dry_run or not os.path.lexists(self.target):
|
|
235
|
+
before = self._owned_target()
|
|
236
|
+
return {"registered": before is not None, "dry_run": dry_run, "changed": before is not None,
|
|
237
|
+
"discovery_path": str(self.target), "retained_private_generations": True}
|
|
238
|
+
with transaction_lock(self.state_root):
|
|
239
|
+
before = self._owned_target()
|
|
240
|
+
if before is not None:
|
|
241
|
+
self.target.unlink()
|
|
242
|
+
return {"registered": False, "dry_run": False, "changed": before is not None,
|
|
243
|
+
"discovery_path": str(self.target), "retained_private_generations": True}
|
|
244
|
+
|
|
245
|
+
def refresh_registration(self) -> dict[str, Any]:
|
|
246
|
+
"""Refresh an existing owned registration; installation never opts in."""
|
|
247
|
+
status = self.managed_status()
|
|
248
|
+
if not status["registered"]:
|
|
249
|
+
return status
|
|
250
|
+
try:
|
|
251
|
+
return {**self.register(), "managed": True}
|
|
252
|
+
except (AppError, OSError, TransactionError) as exc:
|
|
253
|
+
return {**self.managed_status(), "needs_action": [str(exc)]}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class AppSessions:
|
|
257
|
+
"""Record returned context, separately from native launch pins and host proof."""
|
|
258
|
+
|
|
259
|
+
def __init__(self, repo: Path, environ: dict[str, str] | None = None):
|
|
260
|
+
self.repo = Path(repo).absolute()
|
|
261
|
+
self.env, self.home, self.state_root, self.user_root = _roots(environ)
|
|
262
|
+
|
|
263
|
+
def _session(self, session: str | None) -> str:
|
|
264
|
+
value = session if session is not None else self.env.get("CODEX_THREAD_ID")
|
|
265
|
+
if not isinstance(value, str) or not SESSION_ID.fullmatch(value):
|
|
266
|
+
raise AppError("app session requires CODEX_THREAD_ID or --session with a valid task id")
|
|
267
|
+
return value
|
|
268
|
+
|
|
269
|
+
def _path(self, session: str) -> Path:
|
|
270
|
+
return self.state_root / "sessions/app-context" / f"{session}.json"
|
|
271
|
+
|
|
272
|
+
def _read(self, session: str) -> dict[str, Any]:
|
|
273
|
+
path = self._path(session)
|
|
274
|
+
reject_symlink_ancestors(path)
|
|
275
|
+
if not path.exists():
|
|
276
|
+
return {"schema_version": SCHEMA_VERSION, "host": "codex", "session_id": session,
|
|
277
|
+
"enabled": False, "deliveries": []}
|
|
278
|
+
record = _read_json(path)
|
|
279
|
+
if (record.get("schema_version") != SCHEMA_VERSION or record.get("host") != "codex"
|
|
280
|
+
or record.get("session_id") != session or not isinstance(record.get("enabled"), bool)
|
|
281
|
+
or not isinstance(record.get("deliveries"), list)):
|
|
282
|
+
raise AppError(f"invalid app session receipt: {path}")
|
|
283
|
+
for delivery in record["deliveries"]:
|
|
284
|
+
if (not isinstance(delivery, dict) or delivery.get("delivery") != "returned-as-context"
|
|
285
|
+
or not isinstance(delivery.get("content_ref"), str)
|
|
286
|
+
or not re.fullmatch(r"[a-f0-9]{64}", delivery["content_ref"])):
|
|
287
|
+
raise AppError(f"invalid app session delivery: {path}")
|
|
288
|
+
return record
|
|
289
|
+
|
|
290
|
+
@staticmethod
|
|
291
|
+
def _view(record: dict[str, Any]) -> dict[str, Any]:
|
|
292
|
+
delivered = bool(record["deliveries"])
|
|
293
|
+
return {**record, "ever_delivered": delivered,
|
|
294
|
+
"active_content_ref": record["deliveries"][-1]["content_ref"] if delivered and record["enabled"] else None,
|
|
295
|
+
"native_activation": False, "host_loading": "unverified",
|
|
296
|
+
"context_retracted": False,
|
|
297
|
+
"clean_exclusion_requires_new_session": delivered and not record["enabled"]}
|
|
298
|
+
|
|
299
|
+
def status(self, session: str | None = None) -> dict[str, Any]:
|
|
300
|
+
return self._view(self._read(self._session(session)))
|
|
301
|
+
|
|
302
|
+
def off(self, session: str | None = None) -> dict[str, Any]:
|
|
303
|
+
session = self._session(session)
|
|
304
|
+
reject_symlink_ancestors(self.state_root)
|
|
305
|
+
with transaction_lock(self.state_root):
|
|
306
|
+
record = self._read(session)
|
|
307
|
+
record["enabled"] = False
|
|
308
|
+
_write_json(self._path(session), record)
|
|
309
|
+
result = self._view(record)
|
|
310
|
+
result["message"] = (
|
|
311
|
+
"Corpus delivery is off. Previously returned text remains in conversation context; "
|
|
312
|
+
"start a new session for clean exclusion."
|
|
313
|
+
if record["deliveries"] else "Corpus delivery is off; this bridge has returned no corpus text to this session."
|
|
314
|
+
)
|
|
315
|
+
return result
|
|
316
|
+
|
|
317
|
+
def _snapshot(self, *, selection: list[str] | None, selection_mode: str | None,
|
|
318
|
+
cwd: Path | None, dry_run: bool) -> dict[str, Any]:
|
|
319
|
+
reject_symlink_ancestors(self.state_root)
|
|
320
|
+
guard_pending(self.state_root)
|
|
321
|
+
release = confirmed_release(self.state_root)
|
|
322
|
+
store = CorpusStore(release, self.state_root, self.user_root)
|
|
323
|
+
if selection is not None and selection_mode is None:
|
|
324
|
+
selection_mode = "selected"
|
|
325
|
+
return store.snapshot(host="codex", selection=selection, selection_mode=selection_mode,
|
|
326
|
+
cwd=Path(cwd or Path.cwd()).absolute(), dry_run=dry_run, native=False)
|
|
327
|
+
|
|
328
|
+
def _runtime_commands(self) -> dict[str, Any]:
|
|
329
|
+
release = confirmed_release(self.state_root)
|
|
330
|
+
bridge = AppBridge(self.repo, self.env)
|
|
331
|
+
result = {"package_root": str(release),
|
|
332
|
+
"learn_argv": ["/bin/bash", str(release / "install.sh"), "learn"],
|
|
333
|
+
"environment": {"AGENT_BIOS_PACKAGE_ROOT": str(release),
|
|
334
|
+
"AGENT_BIOS_STATE_DIR": str(self.state_root),
|
|
335
|
+
"AGENT_BIOS_CORPUS_DIR": str(self.user_root),
|
|
336
|
+
"AGENT_BIOS_PRIVATE_CORPUS": "1", "AGENT_BIOS_LEGACY_INSTALL": "0"}}
|
|
337
|
+
registered = bridge.managed_status().get("registered")
|
|
338
|
+
if "AGENT_LAUNCH_VENV" in self.env or registered:
|
|
339
|
+
config = bridge._config()
|
|
340
|
+
if "launch_venv" in config:
|
|
341
|
+
result["environment"]["AGENT_LAUNCH_VENV"] = config["launch_venv"]
|
|
342
|
+
if registered:
|
|
343
|
+
result["bridge_learn_argv"] = [sys.executable, str(bridge.target / "scripts/bridge.py"), "learn"]
|
|
344
|
+
return result
|
|
345
|
+
|
|
346
|
+
def preview(self, session: str | None = None, *, selection: list[str] | None = None,
|
|
347
|
+
selection_mode: str | None = None, cwd: Path | None = None) -> dict[str, Any]:
|
|
348
|
+
session = self._session(session)
|
|
349
|
+
if selection_mode == "none":
|
|
350
|
+
return {"session_id": session, "selection_mode": "none", "content_ref": None,
|
|
351
|
+
"instruction_characters": 0, "delivery": "preview-only", "native_activation": False}
|
|
352
|
+
snapshot = self._snapshot(selection=selection, selection_mode=selection_mode, cwd=cwd, dry_run=True)
|
|
353
|
+
return {"session_id": session, "content_ref": snapshot["content_ref"], "revision": snapshot["revision"],
|
|
354
|
+
"instruction_characters": len(snapshot["instruction_text"]),
|
|
355
|
+
"unavailable": snapshot.get("unavailable", []), "selection": selection,
|
|
356
|
+
"selection_mode": snapshot.get("selection_mode", selection_mode),
|
|
357
|
+
"delivery": "preview-only", "native_activation": False}
|
|
358
|
+
|
|
359
|
+
def use(self, session: str | None = None, *, selection: list[str] | None = None,
|
|
360
|
+
selection_mode: str | None = None, cwd: Path | None = None,
|
|
361
|
+
expected_content_ref: str | None = None) -> dict[str, Any]:
|
|
362
|
+
session = self._session(session)
|
|
363
|
+
if selection_mode == "none":
|
|
364
|
+
return self.off(session)
|
|
365
|
+
reject_symlink_ancestors(self.state_root)
|
|
366
|
+
with transaction_lock(self.state_root):
|
|
367
|
+
record = self._read(session)
|
|
368
|
+
snapshot = self._snapshot(selection=selection, selection_mode=selection_mode, cwd=cwd, dry_run=False)
|
|
369
|
+
if expected_content_ref is not None and snapshot["content_ref"] != expected_content_ref:
|
|
370
|
+
raise AppError("corpus preview changed; preview again before using this session selection")
|
|
371
|
+
if not snapshot["instruction_text"].strip():
|
|
372
|
+
return self.off(session)
|
|
373
|
+
delivery = {"at": datetime.now(timezone.utc).isoformat(), "content_ref": snapshot["content_ref"],
|
|
374
|
+
"snapshot_path": snapshot["path"], "revision": snapshot["revision"],
|
|
375
|
+
"cwd": str(Path(cwd or Path.cwd()).absolute()), "delivery": "returned-as-context",
|
|
376
|
+
"instruction_sha256": hashlib.sha256(snapshot["instruction_text"].encode("utf-8")).hexdigest()}
|
|
377
|
+
record["deliveries"].append(delivery)
|
|
378
|
+
record["enabled"] = True
|
|
379
|
+
runtime = self._runtime_commands()
|
|
380
|
+
_write_json(self._path(session), record)
|
|
381
|
+
return {**self._view(record), "delivery": "returned-as-context",
|
|
382
|
+
"content_ref": snapshot["content_ref"], "instruction_text": snapshot["instruction_text"],
|
|
383
|
+
"runtime": runtime,
|
|
384
|
+
"unavailable": snapshot.get("unavailable", []),
|
|
385
|
+
"message": "Returned the selected immutable corpus as task context. This receipt is not proof of model reading or native startup activation."}
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
389
|
+
parser = argparse.ArgumentParser(prog="agent-bios app", description=__doc__)
|
|
390
|
+
parser.add_argument("--repo", type=Path, default=Path(__file__).resolve().parents[1])
|
|
391
|
+
parser.add_argument("--state-dir", type=Path)
|
|
392
|
+
parser.add_argument("--user-dir", type=Path)
|
|
393
|
+
parser.add_argument("--json", action="store_true")
|
|
394
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
395
|
+
for name in ("register", "unregister", "status"):
|
|
396
|
+
command = commands.add_parser(name)
|
|
397
|
+
if name != "status":
|
|
398
|
+
command.add_argument("--dry-run", action="store_true")
|
|
399
|
+
session = commands.add_parser("session", help="explicitly preview, use or stop corpus delivery in one app task")
|
|
400
|
+
operations = session.add_subparsers(dest="operation", required=True)
|
|
401
|
+
for name in ("preview", "use", "off", "status"):
|
|
402
|
+
command = operations.add_parser(name)
|
|
403
|
+
command.add_argument("--session", help="defaults to CODEX_THREAD_ID")
|
|
404
|
+
if name in {"preview", "use"}:
|
|
405
|
+
selection = command.add_mutually_exclusive_group()
|
|
406
|
+
selection.add_argument("--domains", help="comma-separated qualified corpus selection")
|
|
407
|
+
selection.add_argument("--no-corpus", action="store_true")
|
|
408
|
+
command.add_argument("--cwd", type=Path)
|
|
409
|
+
if name == "use":
|
|
410
|
+
command.add_argument("--expected-content-ref", help="require the exact previously previewed snapshot")
|
|
411
|
+
return parser
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def main(argv: list[str] | None = None) -> int:
|
|
415
|
+
raw = list(sys.argv[1:] if argv is None else argv)
|
|
416
|
+
as_json = "--json" in raw
|
|
417
|
+
args = build_parser().parse_args([token for token in raw if token != "--json"])
|
|
418
|
+
env = dict(os.environ)
|
|
419
|
+
if args.state_dir is not None:
|
|
420
|
+
env["AGENT_BIOS_STATE_DIR"] = str(args.state_dir)
|
|
421
|
+
if args.user_dir is not None:
|
|
422
|
+
env["AGENT_BIOS_CORPUS_DIR"] = str(args.user_dir)
|
|
423
|
+
try:
|
|
424
|
+
if args.command == "session":
|
|
425
|
+
manager = AppSessions(args.repo, env)
|
|
426
|
+
options = {}
|
|
427
|
+
if args.operation in {"preview", "use"}:
|
|
428
|
+
selected = None
|
|
429
|
+
if args.domains is not None:
|
|
430
|
+
selected = [value.strip() for value in args.domains.split(",") if value.strip()]
|
|
431
|
+
if not selected:
|
|
432
|
+
raise AppError("--domains requires a nonempty corpus selection; use --no-corpus to keep corpus off")
|
|
433
|
+
options = {"selection": selected,
|
|
434
|
+
"selection_mode": "none" if args.no_corpus else "selected" if selected is not None else None,
|
|
435
|
+
"cwd": args.cwd}
|
|
436
|
+
if args.operation == "use":
|
|
437
|
+
options["expected_content_ref"] = args.expected_content_ref
|
|
438
|
+
result = getattr(manager, args.operation)(session=args.session, **options)
|
|
439
|
+
else:
|
|
440
|
+
manager = AppBridge(args.repo, env)
|
|
441
|
+
options = {} if args.command == "status" else {"dry_run": args.dry_run}
|
|
442
|
+
result = getattr(manager, args.command)(**options)
|
|
443
|
+
if not as_json and isinstance(result.get("instruction_text"), str):
|
|
444
|
+
metadata = {key: value for key, value in result.items() if key != "instruction_text"}
|
|
445
|
+
print(json.dumps(metadata, ensure_ascii=False, indent=2, sort_keys=True), file=sys.stderr)
|
|
446
|
+
print(result["instruction_text"], end="")
|
|
447
|
+
else:
|
|
448
|
+
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
|
|
449
|
+
return 0
|
|
450
|
+
except (AppError, CorpusStoreError, TransactionError, OSError) as exc:
|
|
451
|
+
print(f"agent-bios app: {exc}", file=sys.stderr)
|
|
452
|
+
return 2
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
if __name__ == "__main__":
|
|
456
|
+
raise SystemExit(main())
|