agent-bios 0.19.1 → 0.19.3
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 +58 -30
- package/INSTALL.md +4 -4
- package/README.md +105 -28
- package/claude/CLAUDE.md +1 -1
- package/claude/guides/claude-prompting.md +1 -1
- package/claude/guides/cli-multi-model-workflow.md +4 -4
- package/claude/guides/coding-staged-workflow.md +17 -0
- package/claude/guides/documentation-hygiene.md +3 -0
- package/claude/guides/gpt-prompting.md +1 -1
- package/claude/guides/korean-writing.md +153 -0
- package/claude/guides/learning-flow.md +4 -4
- package/claude/guides/llm-capability-boundary.md +7 -1
- package/claude/guides/session-distill-workflow.md +8 -8
- package/claude/guides/slide-writing/RUNBOOK.md +5 -5
- package/claude/guides/tooling-gotchas.md +20 -1
- package/claude/guides/ui-design/visual-direction.md +88 -0
- package/claude/guides/ui-design.md +90 -0
- package/claude/guides/verification-discipline.md +10 -1
- package/claude/hooks/tooling-gotchas-hook.py +41 -0
- package/claude/skills/repo-charter/SKILL.md +3 -3
- package/claude/skills/understand/SKILL.md +5 -5
- package/codex/AGENTS.md +1 -1
- package/codex/guides/claude-prompting.md +1 -1
- package/codex/guides/cli-multi-model-workflow.md +4 -4
- package/codex/guides/coding-staged-workflow.md +17 -0
- package/codex/guides/documentation-hygiene.md +3 -0
- package/codex/guides/gpt-prompting.md +1 -1
- package/codex/guides/korean-writing.md +153 -0
- package/codex/guides/learning-flow.md +4 -4
- package/codex/guides/llm-capability-boundary.md +7 -1
- package/codex/guides/session-distill-workflow.md +8 -8
- package/codex/guides/slide-writing/RUNBOOK.md +5 -5
- package/codex/guides/tooling-gotchas.md +20 -1
- package/codex/guides/ui-design/visual-direction.md +88 -0
- package/codex/guides/ui-design.md +90 -0
- package/codex/guides/verification-discipline.md +10 -1
- package/compose/app_bridge/SKILL.md +12 -12
- package/compose/app_bridge/scripts/bridge.py +35 -10
- package/compose/app_desktop/server.py +250 -0
- package/compose/assemble.py +5 -5
- package/compose/bootstrap/SKILL.md +18 -18
- package/compose/canary.sh +4 -4
- package/compose/check-domains.py +6 -6
- package/compose/corpus-state.py +16 -1168
- package/compose/corpus.py +13 -402
- package/compose/corpus_app.py +14 -450
- package/compose/corpus_catalog.py +15 -926
- package/compose/corpus_import.py +14 -523
- package/compose/corpus_install.py +14 -1847
- package/compose/corpus_session.py +16 -848
- package/compose/corpus_setup.py +16 -672
- package/compose/corpus_setup_cli.py +15 -580
- package/compose/corpus_setup_i18n.py +20 -324
- package/compose/corpus_setup_ui.py +18 -645
- package/compose/corpus_store.py +16 -1664
- package/compose/corpus_transaction.py +15 -284
- package/compose/corpus_ui.py +17 -972
- package/compose/corpus_ui_runtime.py +16 -274
- package/compose/corpus_understand.py +13 -671
- package/compose/domains.json +3 -1
- package/compose/host_platform.py +121 -0
- package/compose/instructions-state.py +1178 -0
- package/compose/instructions.py +409 -0
- package/compose/instructions_app.py +697 -0
- package/compose/instructions_catalog.py +931 -0
- package/compose/instructions_import.py +537 -0
- package/compose/instructions_install.py +1932 -0
- package/compose/instructions_session.py +852 -0
- package/compose/instructions_setup.py +713 -0
- package/compose/instructions_setup_cli.py +607 -0
- package/compose/instructions_setup_i18n.py +327 -0
- package/compose/instructions_setup_ui.py +647 -0
- package/compose/instructions_store.py +1668 -0
- package/compose/instructions_transaction.py +308 -0
- package/compose/instructions_ui.py +975 -0
- package/compose/instructions_ui_runtime.py +279 -0
- package/compose/instructions_understand.py +678 -0
- package/compose/native_cli.py +52 -0
- package/compose/register-hooks.py +1 -1
- package/compose/runtime_entry.py +58 -0
- package/compose/setup/START.md +11 -11
- package/compose/windows_deploy.py +719 -0
- package/docs/advanced-launch.md +11 -11
- package/docs/instructions-compatibility.md +86 -0
- package/docs/{corpus.md → instructions.md} +36 -8
- package/docs/recovery.md +10 -10
- package/docs/releases/0.19.2.md +38 -0
- package/docs/releases/0.19.3.md +107 -0
- package/docs/session-model.md +31 -20
- package/docs/setup.md +63 -26
- package/docs/understand.md +6 -6
- package/docs/windows.md +99 -0
- package/install.sh +71 -69
- package/launch/agent-launch.py +309 -293
- package/launch/agent-launch.toml +2 -2
- package/launch/agent-launch.zsh +11 -1
- package/launch/i18n/en.toml +55 -55
- package/launch/i18n/ja.toml +56 -56
- package/launch/i18n/ko.toml +56 -56
- package/launch/shell_integration.py +4 -4
- package/learn/collect-learning.py +10 -10
- package/learn/learning.schema.json +1 -1
- package/learn/migrate-learnings.py +51 -51
- package/package.json +33 -12
- package/provenance.json +1 -1
- /package/docs/assets/{corpus-studio.svg → instructions-studio.svg} +0 -0
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Explicit app discovery and per-task instructions context delivery.
|
|
3
|
+
|
|
4
|
+
The Codex app pulls through a registered skill; Claude Desktop pulls through a
|
|
5
|
+
generated `.mcpb` bundle whose server calls the session operations here.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
try:
|
|
9
|
+
from host_platform import cli_argv, create_junction, python_argv, runtime_environment
|
|
10
|
+
except ImportError:
|
|
11
|
+
from .host_platform import cli_argv, create_junction, python_argv, runtime_environment
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
import hashlib
|
|
16
|
+
import importlib.util
|
|
17
|
+
import io
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
import re
|
|
22
|
+
import secrets
|
|
23
|
+
import shutil
|
|
24
|
+
import sys
|
|
25
|
+
import tempfile
|
|
26
|
+
from typing import Any
|
|
27
|
+
import zipfile
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from instructions_store import InstructionsStore, InstructionsStoreError
|
|
31
|
+
from instructions_transaction import (environment_value, TransactionError, confirmed_release, guard_pending,
|
|
32
|
+
reject_symlink_ancestors, transaction_lock)
|
|
33
|
+
except ImportError:
|
|
34
|
+
from .instructions_store import InstructionsStore, InstructionsStoreError
|
|
35
|
+
from .instructions_transaction import (environment_value, TransactionError, confirmed_release, guard_pending,
|
|
36
|
+
reject_symlink_ancestors, transaction_lock)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
SCHEMA_VERSION = 1
|
|
40
|
+
BRIDGE_MEMBERS = ("SKILL.md", "agents/openai.yaml", "scripts/bridge.py",
|
|
41
|
+
"scripts/instructions_transaction.py", "scripts/host_platform.py", "bridge.json")
|
|
42
|
+
PREVIOUS_BRIDGE_MEMBERS = tuple(x for x in BRIDGE_MEMBERS if x != "scripts/host_platform.py")
|
|
43
|
+
LEGACY_BRIDGE_MEMBERS = ("SKILL.md", "agents/openai.yaml", "scripts/bridge.py",
|
|
44
|
+
"scripts/corpus_transaction.py", "bridge.json")
|
|
45
|
+
SESSION_ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9_-]{0,127}\Z")
|
|
46
|
+
# Desktop sends no conversation identity with a tool call (2.9939.4 and 2.16120.0,
|
|
47
|
+
# design/app-session-reach/2026-09-30T0645--20084c8--desktop-mcpb-qualification.md),
|
|
48
|
+
# so AppSessions mints one; the prefix keeps it apart from Codex task ids.
|
|
49
|
+
DESKTOP_SESSION_ID = re.compile(r"desktop-[a-f0-9]{32}\Z")
|
|
50
|
+
END_MARKER = re.compile(r"agent-bios end [a-f0-9]{12}\Z")
|
|
51
|
+
# Receipt host -> the snapshot host whose items it receives.
|
|
52
|
+
APP_HOSTS = {"codex": "codex", "claude-desktop": "claude"}
|
|
53
|
+
# The smallest inline boundary Desktop has shown (2.9939.4). 2.16120.0 inlined
|
|
54
|
+
# more, so crossing it is disclosed, never refused.
|
|
55
|
+
DESKTOP_INLINE_FLOOR = 49_152
|
|
56
|
+
DESKTOP_BUNDLE_MEMBERS = ("manifest.json", "server/desktop.json", "server/host_platform.py",
|
|
57
|
+
"server/instructions_transaction.py", "server/server.py")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class AppError(RuntimeError):
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _json_bytes(value: Any) -> bytes:
|
|
65
|
+
return (json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True) + "\n").encode("utf-8")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _read_json(path: Path) -> dict[str, Any]:
|
|
69
|
+
reject_symlink_ancestors(path)
|
|
70
|
+
try:
|
|
71
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
72
|
+
except (OSError, ValueError) as exc:
|
|
73
|
+
raise AppError(f"unreadable app state: {path}") from exc
|
|
74
|
+
if not isinstance(value, dict):
|
|
75
|
+
raise AppError(f"invalid app state: {path}")
|
|
76
|
+
return value
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _write_json(path: Path, value: dict[str, Any]) -> None:
|
|
80
|
+
reject_symlink_ancestors(path)
|
|
81
|
+
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
82
|
+
descriptor, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
|
|
83
|
+
try:
|
|
84
|
+
with os.fdopen(descriptor, "wb") as handle:
|
|
85
|
+
handle.write(_json_bytes(value))
|
|
86
|
+
handle.flush()
|
|
87
|
+
os.fsync(handle.fileno())
|
|
88
|
+
os.replace(temporary, path)
|
|
89
|
+
finally:
|
|
90
|
+
if os.path.lexists(temporary):
|
|
91
|
+
os.unlink(temporary)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _tree_digest(members: dict[str, bytes]) -> str:
|
|
95
|
+
digest = hashlib.sha256()
|
|
96
|
+
for relative, data in sorted(members.items()):
|
|
97
|
+
digest.update(relative.encode("utf-8") + b"\0" + data + b"\0")
|
|
98
|
+
return digest.hexdigest()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _roots(environ: dict[str, str] | None) -> tuple[dict[str, str], Path, Path, Path]:
|
|
102
|
+
env = dict(os.environ if environ is None else environ)
|
|
103
|
+
home = Path(env.get("HOME", str(Path.home()))).expanduser().absolute()
|
|
104
|
+
state = Path(env.get("AGENT_BIOS_STATE_DIR", str(home / ".local/share/agent-bios"))).expanduser().absolute()
|
|
105
|
+
user = Path(environment_value(env, "AGENT_BIOS_INSTRUCTIONS_DIR", "AGENT_BIOS_CORPUS_DIR", str(home / ".config/agent-bios/corpus"))).expanduser().absolute()
|
|
106
|
+
return env, home, state, user
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class AppBridge:
|
|
110
|
+
"""Own one explicit-only skill symlink; retain immutable private generations."""
|
|
111
|
+
|
|
112
|
+
def __init__(self, repo: Path, environ: dict[str, str] | None = None):
|
|
113
|
+
self.repo = Path(repo).absolute()
|
|
114
|
+
self.env, self.home, self.state_root, self.user_root = _roots(environ)
|
|
115
|
+
self.target = self.home / ".agents/skills/agent-bios"
|
|
116
|
+
self.generations = self.state_root / "runtime/app-bridges"
|
|
117
|
+
|
|
118
|
+
def _base_config(self) -> dict[str, Any]:
|
|
119
|
+
return {"schema_version": SCHEMA_VERSION, "home": str(self.home),
|
|
120
|
+
"state_root": str(self.state_root), "user_root": str(self.user_root),
|
|
121
|
+
"discovery_path": str(self.target)}
|
|
122
|
+
|
|
123
|
+
def _config(self) -> dict[str, Any]:
|
|
124
|
+
config = self._base_config()
|
|
125
|
+
if "AGENT_LAUNCH_VENV" in self.env:
|
|
126
|
+
value = self.env["AGENT_LAUNCH_VENV"]
|
|
127
|
+
config["launch_venv"] = str(Path(value).expanduser().absolute()) if value else ""
|
|
128
|
+
else:
|
|
129
|
+
current = self._owned_target()
|
|
130
|
+
if current is not None:
|
|
131
|
+
saved = _read_json(current / "bridge.json")
|
|
132
|
+
if "launch_venv" in saved:
|
|
133
|
+
config["launch_venv"] = saved["launch_venv"]
|
|
134
|
+
if "python_binding" in saved:
|
|
135
|
+
config["python_binding"] = saved["python_binding"]
|
|
136
|
+
binding = runtime_environment(self.env)
|
|
137
|
+
if binding:
|
|
138
|
+
config["python_binding"] = binding
|
|
139
|
+
return config
|
|
140
|
+
|
|
141
|
+
def _source_members(self) -> dict[str, bytes]:
|
|
142
|
+
reject_symlink_ancestors(self.state_root)
|
|
143
|
+
guard_pending(self.state_root)
|
|
144
|
+
release = confirmed_release(self.state_root)
|
|
145
|
+
source = release / "compose/app_bridge"
|
|
146
|
+
members = {}
|
|
147
|
+
canonical = (release / "compose/instructions_transaction.py").is_file()
|
|
148
|
+
inventory = BRIDGE_MEMBERS if canonical else LEGACY_BRIDGE_MEMBERS
|
|
149
|
+
for name in inventory:
|
|
150
|
+
if name == "bridge.json":
|
|
151
|
+
members[name] = _json_bytes(self._config())
|
|
152
|
+
continue
|
|
153
|
+
path = release / "compose" / Path(name).name if name in {
|
|
154
|
+
"scripts/instructions_transaction.py", "scripts/corpus_transaction.py", "scripts/host_platform.py"
|
|
155
|
+
} else source / name
|
|
156
|
+
reject_symlink_ancestors(path)
|
|
157
|
+
if not path.is_file():
|
|
158
|
+
raise AppError(f"installed release has no app bridge member: {path}")
|
|
159
|
+
members[name] = path.read_bytes()
|
|
160
|
+
if os.name == "nt" and name == "SKILL.md":
|
|
161
|
+
interpreter = sys.executable.replace("'", "''")
|
|
162
|
+
command = python_argv(Path('$BRIDGE'))
|
|
163
|
+
prefix = '& ' + ' '.join(('"$BRIDGE"' if word == '$BRIDGE' else "'" + word.replace("'", "''") + "'") for word in command)
|
|
164
|
+
text = members[name].decode("utf-8").replace('python3 "$BRIDGE"', prefix)
|
|
165
|
+
text += "\nOn Windows use PowerShell and the bundled interpreter shown above. Set $BRIDGE to the absolute scripts/bridge.py path beside this skill. Follow returned command argument arrays for setup; do not translate them into Bash commands.\n"
|
|
166
|
+
members[name] = text.encode("utf-8")
|
|
167
|
+
return members
|
|
168
|
+
|
|
169
|
+
def _owned_target(self) -> Path | None:
|
|
170
|
+
reject_symlink_ancestors(self.target.parent)
|
|
171
|
+
if not os.path.lexists(self.target):
|
|
172
|
+
return None
|
|
173
|
+
if not (self.target.is_symlink() or (os.name == "nt" and self.target.is_junction())):
|
|
174
|
+
raise AppError(f"preserving unowned app skill: {self.target}")
|
|
175
|
+
raw = self.target.resolve() if os.name == "nt" else Path(os.readlink(self.target))
|
|
176
|
+
reject_symlink_ancestors(self.generations)
|
|
177
|
+
same_parent = raw.parent.resolve() == self.generations.resolve() if os.name == "nt" else raw.parent == self.generations
|
|
178
|
+
if not raw.is_absolute() or not same_parent or not re.fullmatch(r"[a-f0-9]{64}", raw.name):
|
|
179
|
+
raise AppError(f"preserving unowned app skill link: {self.target}")
|
|
180
|
+
if os.name == "nt":
|
|
181
|
+
# Keep the saved root spelling: canonicalizing the record itself would
|
|
182
|
+
# invalidate its exact ownership/context checks. Only compare locations.
|
|
183
|
+
raw = self.generations / raw.name
|
|
184
|
+
reject_symlink_ancestors(raw)
|
|
185
|
+
if not raw.is_dir():
|
|
186
|
+
raise AppError(f"owned app skill generation is unavailable: {raw}")
|
|
187
|
+
paths = list(raw.rglob("*"))
|
|
188
|
+
if any(path.is_symlink() for path in paths):
|
|
189
|
+
raise AppError(f"preserving redirected app skill generation: {raw}")
|
|
190
|
+
files = {path.relative_to(raw).as_posix(): path.read_bytes() for path in paths if path.is_file()}
|
|
191
|
+
if set(files) not in (set(BRIDGE_MEMBERS), set(PREVIOUS_BRIDGE_MEMBERS), set(LEGACY_BRIDGE_MEMBERS)) or _tree_digest(files) != raw.name:
|
|
192
|
+
raise AppError(f"preserving changed app skill generation: {raw}")
|
|
193
|
+
config = _read_json(raw / "bridge.json")
|
|
194
|
+
base = self._base_config()
|
|
195
|
+
if (any(config.get(key) != value for key, value in base.items())
|
|
196
|
+
or set(config) - set(base) - {"launch_venv", "python_binding"}
|
|
197
|
+
or ("launch_venv" in config and (not isinstance(config["launch_venv"], str)
|
|
198
|
+
or (config["launch_venv"] and not Path(config["launch_venv"]).is_absolute())))):
|
|
199
|
+
raise AppError(f"app skill belongs to different private root settings: {self.target}")
|
|
200
|
+
return raw
|
|
201
|
+
|
|
202
|
+
def status(self) -> dict[str, Any]:
|
|
203
|
+
try:
|
|
204
|
+
target = self._owned_target()
|
|
205
|
+
return {"registered": target is not None, "discovery_path": str(self.target),
|
|
206
|
+
"generation": str(target) if target else None, "implicit_invocation": False,
|
|
207
|
+
"native_discovery": "unverified", "session_activation": "explicit-use-only"}
|
|
208
|
+
except (AppError, OSError, TransactionError) as exc:
|
|
209
|
+
return {"registered": False, "discovery_path": str(self.target), "needs_action": [str(exc)],
|
|
210
|
+
"native_discovery": "unverified", "session_activation": "explicit-use-only"}
|
|
211
|
+
|
|
212
|
+
def has_owned_registration(self) -> bool:
|
|
213
|
+
"""Identify our link namespace without opening unrelated skill contents."""
|
|
214
|
+
try:
|
|
215
|
+
reject_symlink_ancestors(self.target.parent)
|
|
216
|
+
if not (self.target.is_symlink() or (os.name == "nt" and self.target.is_junction())):
|
|
217
|
+
return False
|
|
218
|
+
target = self.target.resolve() if os.name == "nt" else Path(os.readlink(self.target))
|
|
219
|
+
reject_symlink_ancestors(self.generations)
|
|
220
|
+
same_parent = target.parent.resolve() == self.generations.resolve() if os.name == "nt" else target.parent == self.generations
|
|
221
|
+
return target.is_absolute() and same_parent
|
|
222
|
+
except (OSError, TransactionError):
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
def managed_status(self) -> dict[str, Any]:
|
|
226
|
+
"""Lifecycle view: foreign native paths are outside this installer."""
|
|
227
|
+
if not self.has_owned_registration():
|
|
228
|
+
return {"registered": False, "managed": False, "discovery_path": str(self.target)}
|
|
229
|
+
return {**self.status(), "managed": True}
|
|
230
|
+
|
|
231
|
+
def register(self, dry_run: bool = False) -> dict[str, Any]:
|
|
232
|
+
reject_symlink_ancestors(self.state_root)
|
|
233
|
+
if dry_run:
|
|
234
|
+
before = self._owned_target()
|
|
235
|
+
members = self._source_members()
|
|
236
|
+
generation = self.generations / _tree_digest(members)
|
|
237
|
+
return {"registered": before is not None, "dry_run": True, "changed": before != generation,
|
|
238
|
+
"discovery_path": str(self.target), "generation": str(generation),
|
|
239
|
+
"implicit_invocation": False, "native_discovery": "unverified",
|
|
240
|
+
"session_activation": "explicit-use-only"}
|
|
241
|
+
with transaction_lock(self.state_root):
|
|
242
|
+
before = self._owned_target()
|
|
243
|
+
members = self._source_members()
|
|
244
|
+
generation = self.generations / _tree_digest(members)
|
|
245
|
+
result = {"registered": True, "dry_run": False, "changed": before != generation,
|
|
246
|
+
"discovery_path": str(self.target), "generation": str(generation),
|
|
247
|
+
"implicit_invocation": False, "native_discovery": "unverified",
|
|
248
|
+
"session_activation": "explicit-use-only"}
|
|
249
|
+
reject_symlink_ancestors(generation)
|
|
250
|
+
if generation.exists():
|
|
251
|
+
paths = list(generation.rglob("*"))
|
|
252
|
+
actual = {path.relative_to(generation).as_posix(): path.read_bytes()
|
|
253
|
+
for path in paths if path.is_file() and not path.is_symlink()}
|
|
254
|
+
if any(path.is_symlink() for path in paths) or actual != members:
|
|
255
|
+
raise AppError(f"app bridge generation collision: {generation}")
|
|
256
|
+
else:
|
|
257
|
+
self.generations.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
258
|
+
temporary = Path(tempfile.mkdtemp(prefix=".bridge-", dir=self.generations))
|
|
259
|
+
try:
|
|
260
|
+
for relative, data in members.items():
|
|
261
|
+
path = temporary / relative
|
|
262
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
263
|
+
path.write_bytes(data)
|
|
264
|
+
path.chmod(0o600)
|
|
265
|
+
os.replace(temporary, generation)
|
|
266
|
+
finally:
|
|
267
|
+
if temporary.exists():
|
|
268
|
+
shutil.rmtree(temporary)
|
|
269
|
+
self.target.parent.mkdir(parents=True, exist_ok=True)
|
|
270
|
+
if self._owned_target() != before:
|
|
271
|
+
raise AppError("app skill changed while preparing registration")
|
|
272
|
+
descriptor, temporary = tempfile.mkstemp(prefix=".agent-bios-link-", dir=self.target.parent)
|
|
273
|
+
os.close(descriptor)
|
|
274
|
+
os.unlink(temporary)
|
|
275
|
+
try:
|
|
276
|
+
if os.name == "nt":
|
|
277
|
+
create_junction(Path(temporary), generation)
|
|
278
|
+
if before is not None:
|
|
279
|
+
self.target.rmdir()
|
|
280
|
+
else:
|
|
281
|
+
os.symlink(str(generation), temporary)
|
|
282
|
+
os.replace(temporary, self.target)
|
|
283
|
+
finally:
|
|
284
|
+
if os.path.lexists(temporary):
|
|
285
|
+
os.rmdir(temporary) if os.name == "nt" and Path(temporary).is_junction() else os.unlink(temporary)
|
|
286
|
+
return result
|
|
287
|
+
|
|
288
|
+
def unregister(self, dry_run: bool = False) -> dict[str, Any]:
|
|
289
|
+
reject_symlink_ancestors(self.state_root)
|
|
290
|
+
if dry_run or not os.path.lexists(self.target):
|
|
291
|
+
before = self._owned_target()
|
|
292
|
+
return {"registered": before is not None, "dry_run": dry_run, "changed": before is not None,
|
|
293
|
+
"discovery_path": str(self.target), "retained_private_generations": True}
|
|
294
|
+
with transaction_lock(self.state_root):
|
|
295
|
+
before = self._owned_target()
|
|
296
|
+
if before is not None:
|
|
297
|
+
self.target.rmdir() if os.name == "nt" else self.target.unlink()
|
|
298
|
+
return {"registered": False, "dry_run": False, "changed": before is not None,
|
|
299
|
+
"discovery_path": str(self.target), "retained_private_generations": True}
|
|
300
|
+
|
|
301
|
+
def refresh_registration(self) -> dict[str, Any]:
|
|
302
|
+
"""Refresh an existing owned registration; installation never opts in."""
|
|
303
|
+
status = self.managed_status()
|
|
304
|
+
if not status["registered"]:
|
|
305
|
+
return status
|
|
306
|
+
try:
|
|
307
|
+
return {**self.register(), "managed": True}
|
|
308
|
+
except (AppError, OSError, TransactionError) as exc:
|
|
309
|
+
return {**self.managed_status(), "needs_action": [str(exc)]}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class AppSessions:
|
|
313
|
+
"""Record returned context, separately from native launch pins and host proof."""
|
|
314
|
+
|
|
315
|
+
def __init__(self, repo: Path, environ: dict[str, str] | None = None, host: str = "codex"):
|
|
316
|
+
if host not in APP_HOSTS:
|
|
317
|
+
raise AppError(f"unsupported app host: {host}")
|
|
318
|
+
self.repo = Path(repo).absolute()
|
|
319
|
+
self.host = host
|
|
320
|
+
self.env, self.home, self.state_root, self.user_root = _roots(environ)
|
|
321
|
+
|
|
322
|
+
@property
|
|
323
|
+
def desktop(self) -> bool:
|
|
324
|
+
return self.host == "claude-desktop"
|
|
325
|
+
|
|
326
|
+
def _session(self, session: str | None, *, mint: bool = False) -> str:
|
|
327
|
+
if self.desktop:
|
|
328
|
+
if session is None and mint:
|
|
329
|
+
return "desktop-" + secrets.token_hex(16)
|
|
330
|
+
if not isinstance(session, str) or not DESKTOP_SESSION_ID.fullmatch(session):
|
|
331
|
+
raise AppError("Desktop session requires the session_id that preview or use returned in this conversation")
|
|
332
|
+
return session
|
|
333
|
+
value = session if session is not None else self.env.get("CODEX_THREAD_ID")
|
|
334
|
+
if not isinstance(value, str) or not SESSION_ID.fullmatch(value):
|
|
335
|
+
raise AppError("app session requires CODEX_THREAD_ID or --session with a valid task id")
|
|
336
|
+
return value
|
|
337
|
+
|
|
338
|
+
def _cwd(self, cwd: Path | None) -> Path:
|
|
339
|
+
if not self.desktop:
|
|
340
|
+
return Path(cwd or Path.cwd()).absolute()
|
|
341
|
+
if cwd is not None:
|
|
342
|
+
raise AppError("Desktop delivery has no project scope; it does not take a working directory")
|
|
343
|
+
# Desktop names no project: the filesystem root lies inside no imported project scope.
|
|
344
|
+
return Path(os.path.abspath(os.sep))
|
|
345
|
+
|
|
346
|
+
def _path(self, session: str) -> Path:
|
|
347
|
+
return self.state_root / "sessions/app-context" / f"{session}.json"
|
|
348
|
+
|
|
349
|
+
def _read(self, session: str) -> dict[str, Any]:
|
|
350
|
+
path = self._path(session)
|
|
351
|
+
reject_symlink_ancestors(path)
|
|
352
|
+
if not path.exists():
|
|
353
|
+
return {"schema_version": SCHEMA_VERSION, "host": self.host, "session_id": session,
|
|
354
|
+
"enabled": False, "deliveries": []}
|
|
355
|
+
record = _read_json(path)
|
|
356
|
+
if (record.get("schema_version") != SCHEMA_VERSION or record.get("host") != self.host
|
|
357
|
+
or record.get("session_id") != session or not isinstance(record.get("enabled"), bool)
|
|
358
|
+
or not isinstance(record.get("deliveries"), list)):
|
|
359
|
+
raise AppError(f"invalid app session receipt: {path}")
|
|
360
|
+
for delivery in record["deliveries"]:
|
|
361
|
+
if (not isinstance(delivery, dict) or delivery.get("delivery") != "returned-as-context"
|
|
362
|
+
or not isinstance(delivery.get("content_ref"), str)
|
|
363
|
+
or not re.fullmatch(r"[a-f0-9]{64}", delivery["content_ref"])
|
|
364
|
+
or ("end_marker" in delivery and not (isinstance(delivery["end_marker"], str)
|
|
365
|
+
and END_MARKER.fullmatch(delivery["end_marker"])))
|
|
366
|
+
or ("confirmed_at" in delivery and not isinstance(delivery["confirmed_at"], str))):
|
|
367
|
+
raise AppError(f"invalid app session delivery: {path}")
|
|
368
|
+
return record
|
|
369
|
+
|
|
370
|
+
@staticmethod
|
|
371
|
+
def _view(record: dict[str, Any]) -> dict[str, Any]:
|
|
372
|
+
delivered = bool(record["deliveries"])
|
|
373
|
+
view = {**record, "ever_delivered": delivered,
|
|
374
|
+
"active_content_ref": record["deliveries"][-1]["content_ref"] if delivered and record["enabled"] else None,
|
|
375
|
+
"native_activation": False, "host_loading": "unverified",
|
|
376
|
+
"context_retracted": False,
|
|
377
|
+
"clean_exclusion_requires_new_session": delivered and not record["enabled"]}
|
|
378
|
+
if record["host"] == "claude-desktop":
|
|
379
|
+
# A marker reported back proves the end was read, never inline delivery.
|
|
380
|
+
view["latest_end_confirmed"] = bool(delivered and record["deliveries"][-1].get("confirmed_at"))
|
|
381
|
+
view["project_scope"] = "none"
|
|
382
|
+
return view
|
|
383
|
+
|
|
384
|
+
def status(self, session: str | None = None, *, end_marker_seen: str | None = None) -> dict[str, Any]:
|
|
385
|
+
session = self._session(session)
|
|
386
|
+
if end_marker_seen is None:
|
|
387
|
+
return self._view(self._read(session))
|
|
388
|
+
reject_symlink_ancestors(self.state_root)
|
|
389
|
+
with transaction_lock(self.state_root):
|
|
390
|
+
record = self._read(session)
|
|
391
|
+
latest = record["deliveries"][-1] if record["deliveries"] else None
|
|
392
|
+
if latest is None or latest.get("end_marker") != end_marker_seen.strip():
|
|
393
|
+
raise AppError("that end marker is not the latest delivery's in this session; read the delivered text to its last line")
|
|
394
|
+
latest.setdefault("confirmed_at", datetime.now(timezone.utc).isoformat())
|
|
395
|
+
_write_json(self._path(session), record)
|
|
396
|
+
return {**self._view(record), "message": "The end of the latest delivery was read. This does not show whether the host kept it inline or saved it to a file."}
|
|
397
|
+
|
|
398
|
+
def off(self, session: str | None = None) -> dict[str, Any]:
|
|
399
|
+
session = self._session(session)
|
|
400
|
+
reject_symlink_ancestors(self.state_root)
|
|
401
|
+
with transaction_lock(self.state_root):
|
|
402
|
+
record = self._read(session)
|
|
403
|
+
record["enabled"] = False
|
|
404
|
+
_write_json(self._path(session), record)
|
|
405
|
+
result = self._view(record)
|
|
406
|
+
result["message"] = (
|
|
407
|
+
"Instructions delivery is off. Previously returned text remains in conversation context; "
|
|
408
|
+
"start a new session for clean exclusion."
|
|
409
|
+
if record["deliveries"] else "Instructions delivery is off; this bridge has returned no instructions text to this session."
|
|
410
|
+
)
|
|
411
|
+
return result
|
|
412
|
+
|
|
413
|
+
def _snapshot(self, *, selection: list[str] | None, selection_mode: str | None,
|
|
414
|
+
cwd: Path | None, dry_run: bool) -> dict[str, Any]:
|
|
415
|
+
reject_symlink_ancestors(self.state_root)
|
|
416
|
+
guard_pending(self.state_root)
|
|
417
|
+
release = confirmed_release(self.state_root)
|
|
418
|
+
store = InstructionsStore(release, self.state_root, self.user_root)
|
|
419
|
+
if selection is not None and selection_mode is None:
|
|
420
|
+
selection_mode = "selected"
|
|
421
|
+
return store.snapshot(host=APP_HOSTS[self.host], selection=selection, selection_mode=selection_mode,
|
|
422
|
+
cwd=self._cwd(cwd), dry_run=dry_run, native=False)
|
|
423
|
+
|
|
424
|
+
def _runtime_commands(self) -> dict[str, Any]:
|
|
425
|
+
release = confirmed_release(self.state_root)
|
|
426
|
+
bridge = AppBridge(self.repo, self.env)
|
|
427
|
+
result = {"package_root": str(release),
|
|
428
|
+
"learn_argv": cli_argv(release, "learn"),
|
|
429
|
+
"environment": {"AGENT_BIOS_PACKAGE_ROOT": str(release),
|
|
430
|
+
"AGENT_BIOS_STATE_DIR": str(self.state_root),
|
|
431
|
+
"AGENT_BIOS_INSTRUCTIONS_DIR": str(self.user_root),
|
|
432
|
+
"AGENT_BIOS_CORPUS_DIR": str(self.user_root),
|
|
433
|
+
"AGENT_BIOS_PRIVATE_CORPUS": "1",
|
|
434
|
+
"AGENT_BIOS_PRIVATE_INSTRUCTIONS": "1", "AGENT_BIOS_LEGACY_INSTALL": "0"}}
|
|
435
|
+
result["environment"].update(runtime_environment())
|
|
436
|
+
registered = bridge.managed_status().get("registered")
|
|
437
|
+
if "AGENT_LAUNCH_VENV" in self.env or registered:
|
|
438
|
+
config = bridge._config()
|
|
439
|
+
if "launch_venv" in config:
|
|
440
|
+
result["environment"]["AGENT_LAUNCH_VENV"] = config["launch_venv"]
|
|
441
|
+
if registered:
|
|
442
|
+
result["bridge_learn_argv"] = python_argv(bridge.target / "scripts/bridge.py", "learn")
|
|
443
|
+
return result
|
|
444
|
+
|
|
445
|
+
def _desktop_size(self, text: str) -> dict[str, Any]:
|
|
446
|
+
size = len(text.encode("utf-8"))
|
|
447
|
+
return {"instruction_bytes": size, "project_scope": "none",
|
|
448
|
+
"host_may_save_to_file": size > DESKTOP_INLINE_FLOOR}
|
|
449
|
+
|
|
450
|
+
def preview(self, session: str | None = None, *, selection: list[str] | None = None,
|
|
451
|
+
selection_mode: str | None = None, cwd: Path | None = None) -> dict[str, Any]:
|
|
452
|
+
session = self._session(session, mint=True)
|
|
453
|
+
if selection_mode == "none":
|
|
454
|
+
return {"session_id": session, "selection_mode": "none", "content_ref": None,
|
|
455
|
+
"instruction_characters": 0, "delivery": "preview-only", "native_activation": False}
|
|
456
|
+
snapshot = self._snapshot(selection=selection, selection_mode=selection_mode, cwd=cwd, dry_run=True)
|
|
457
|
+
result = {"session_id": session, "content_ref": snapshot["content_ref"], "revision": snapshot["revision"],
|
|
458
|
+
"instruction_characters": len(snapshot["instruction_text"]),
|
|
459
|
+
"unavailable": snapshot.get("unavailable", []), "selection": selection,
|
|
460
|
+
"selection_mode": snapshot.get("selection_mode", selection_mode),
|
|
461
|
+
"delivery": "preview-only", "native_activation": False}
|
|
462
|
+
if self.desktop:
|
|
463
|
+
result.update(self._desktop_size(snapshot["instruction_text"]))
|
|
464
|
+
return result
|
|
465
|
+
|
|
466
|
+
def use(self, session: str | None = None, *, selection: list[str] | None = None,
|
|
467
|
+
selection_mode: str | None = None, cwd: Path | None = None,
|
|
468
|
+
expected_content_ref: str | None = None) -> dict[str, Any]:
|
|
469
|
+
session = self._session(session, mint=True)
|
|
470
|
+
if selection_mode == "none":
|
|
471
|
+
return self.off(session)
|
|
472
|
+
reject_symlink_ancestors(self.state_root)
|
|
473
|
+
with transaction_lock(self.state_root):
|
|
474
|
+
record = self._read(session)
|
|
475
|
+
snapshot = self._snapshot(selection=selection, selection_mode=selection_mode, cwd=cwd, dry_run=False)
|
|
476
|
+
if expected_content_ref is not None and snapshot["content_ref"] != expected_content_ref:
|
|
477
|
+
raise AppError("instructions preview changed; preview again before using this session selection")
|
|
478
|
+
if not snapshot["instruction_text"].strip():
|
|
479
|
+
return self.off(session)
|
|
480
|
+
latest = record["deliveries"][-1] if record["deliveries"] else None
|
|
481
|
+
# Desktop retries a failing call without telling the model, and a retry exists
|
|
482
|
+
# exactly when the first result may not have arrived: return the same text and
|
|
483
|
+
# receipt again rather than recording a second delivery.
|
|
484
|
+
repeat = (self.desktop and record["enabled"] and latest is not None
|
|
485
|
+
and latest["content_ref"] == snapshot["content_ref"])
|
|
486
|
+
runtime = None if self.desktop else self._runtime_commands()
|
|
487
|
+
if not repeat:
|
|
488
|
+
delivery = {"at": datetime.now(timezone.utc).isoformat(), "content_ref": snapshot["content_ref"],
|
|
489
|
+
"snapshot_path": snapshot["path"], "revision": snapshot["revision"],
|
|
490
|
+
"cwd": str(self._cwd(cwd)), "delivery": "returned-as-context",
|
|
491
|
+
"instruction_sha256": hashlib.sha256(snapshot["instruction_text"].encode("utf-8")).hexdigest()}
|
|
492
|
+
if self.desktop:
|
|
493
|
+
delivery["end_marker"] = "agent-bios end " + secrets.token_hex(6)
|
|
494
|
+
record["deliveries"].append(delivery)
|
|
495
|
+
record["enabled"] = True
|
|
496
|
+
_write_json(self._path(session), record)
|
|
497
|
+
latest = delivery
|
|
498
|
+
result = {**self._view(record), "delivery": "returned-as-context",
|
|
499
|
+
"content_ref": snapshot["content_ref"], "instruction_text": snapshot["instruction_text"],
|
|
500
|
+
"unavailable": snapshot.get("unavailable", []),
|
|
501
|
+
"message": "Returned the selected immutable instructions as task context. This receipt is not proof of model reading or native startup activation."}
|
|
502
|
+
if self.desktop:
|
|
503
|
+
result.update(self._desktop_size(snapshot["instruction_text"]))
|
|
504
|
+
result.update({"repeat": repeat, "end_marker": latest["end_marker"]})
|
|
505
|
+
else:
|
|
506
|
+
result["runtime"] = runtime
|
|
507
|
+
return result
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _zip_bytes(members: dict[str, bytes]) -> bytes:
|
|
511
|
+
"""Byte-stable archive: fixed order, times and modes, so equal members give equal files."""
|
|
512
|
+
buffer = io.BytesIO()
|
|
513
|
+
with zipfile.ZipFile(buffer, "w") as archive:
|
|
514
|
+
for name in sorted(members):
|
|
515
|
+
info = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
|
|
516
|
+
info.external_attr = 0o644 << 16
|
|
517
|
+
info.compress_type = zipfile.ZIP_DEFLATED
|
|
518
|
+
archive.writestr(info, members[name])
|
|
519
|
+
return buffer.getvalue()
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
class DesktopBundle:
|
|
523
|
+
"""Write one content-addressed Claude Desktop bundle; installing it is the user's step.
|
|
524
|
+
|
|
525
|
+
Desktop resolves a bare `python3` through the user's login-shell PATH, which on
|
|
526
|
+
stock macOS reaches 3.9, so the manifest names the interpreter running agent-bios.
|
|
527
|
+
Nothing here writes into Desktop's own directories.
|
|
528
|
+
"""
|
|
529
|
+
|
|
530
|
+
def __init__(self, repo: Path, environ: dict[str, str] | None = None):
|
|
531
|
+
self.repo = Path(repo).absolute()
|
|
532
|
+
self.env, self.home, self.state_root, self.user_root = _roots(environ)
|
|
533
|
+
self.bundles = self.state_root / "runtime/desktop-bundles"
|
|
534
|
+
|
|
535
|
+
def _interpreter(self, binding: dict[str, str]) -> str:
|
|
536
|
+
if binding:
|
|
537
|
+
return binding["AGENT_BIOS_PYTHON_EXECUTABLE"]
|
|
538
|
+
if sys.version_info < (3, 11):
|
|
539
|
+
raise AppError(f"the Desktop bundle needs Python 3.11 or newer; this is {sys.version.split()[0]} at {sys.executable}")
|
|
540
|
+
if not sys.executable or not Path(sys.executable).is_absolute():
|
|
541
|
+
raise AppError("cannot name an absolute Python interpreter for the Desktop bundle")
|
|
542
|
+
return sys.executable
|
|
543
|
+
|
|
544
|
+
@staticmethod
|
|
545
|
+
def _tools(server: Path) -> list[dict[str, str]]:
|
|
546
|
+
spec = importlib.util.spec_from_file_location("agent_bios_desktop_server", server)
|
|
547
|
+
if spec is None or spec.loader is None:
|
|
548
|
+
raise AppError(f"cannot read the Desktop server tools: {server}")
|
|
549
|
+
module = importlib.util.module_from_spec(spec)
|
|
550
|
+
spec.loader.exec_module(module)
|
|
551
|
+
return [{"name": tool["name"], "description": tool["description"]} for tool in module.TOOLS]
|
|
552
|
+
|
|
553
|
+
def _members(self) -> tuple[dict[str, bytes], str]:
|
|
554
|
+
reject_symlink_ancestors(self.state_root)
|
|
555
|
+
guard_pending(self.state_root)
|
|
556
|
+
release = confirmed_release(self.state_root)
|
|
557
|
+
binding = runtime_environment(self.env)
|
|
558
|
+
interpreter = self._interpreter(binding)
|
|
559
|
+
sources = {"server/server.py": release / "compose/app_desktop/server.py",
|
|
560
|
+
"server/instructions_transaction.py": release / "compose/instructions_transaction.py",
|
|
561
|
+
"server/host_platform.py": release / "compose/host_platform.py"}
|
|
562
|
+
members: dict[str, bytes] = {}
|
|
563
|
+
for name, source in sources.items():
|
|
564
|
+
reject_symlink_ancestors(source)
|
|
565
|
+
if not source.is_file():
|
|
566
|
+
raise AppError(f"installed release has no Desktop bundle member: {source}")
|
|
567
|
+
members[name] = source.read_bytes()
|
|
568
|
+
config: dict[str, Any] = {"schema_version": SCHEMA_VERSION, "home": str(self.home),
|
|
569
|
+
"state_root": str(self.state_root), "user_root": str(self.user_root)}
|
|
570
|
+
if binding:
|
|
571
|
+
config["python_binding"] = binding
|
|
572
|
+
members["server/desktop.json"] = _json_bytes(config)
|
|
573
|
+
version = _read_json(release / "package.json").get("version")
|
|
574
|
+
if not isinstance(version, str):
|
|
575
|
+
raise AppError("installed release has no package version")
|
|
576
|
+
manifest = {
|
|
577
|
+
"manifest_version": "0.3", "name": "agent-bios", "display_name": "agent-bios", "version": version,
|
|
578
|
+
"description": "Pull your selected agent-bios work environment into a conversation when you ask for it.",
|
|
579
|
+
"author": {"name": "agent-bios"},
|
|
580
|
+
"server": {"type": "python", "entry_point": "server/server.py",
|
|
581
|
+
"mcp_config": {"command": interpreter, "args": ["-I", "${__dirname}/server/server.py"]}},
|
|
582
|
+
"tools": self._tools(release / "compose/app_desktop/server.py"),
|
|
583
|
+
"compatibility": {"platforms": ["darwin"]},
|
|
584
|
+
}
|
|
585
|
+
members["manifest.json"] = _json_bytes(manifest)
|
|
586
|
+
if set(members) != set(DESKTOP_BUNDLE_MEMBERS):
|
|
587
|
+
raise AppError("Desktop bundle member inventory changed")
|
|
588
|
+
return members, interpreter
|
|
589
|
+
|
|
590
|
+
def build(self, dry_run: bool = False) -> dict[str, Any]:
|
|
591
|
+
members, interpreter = self._members()
|
|
592
|
+
path = self.bundles / _tree_digest(members) / "agent-bios.mcpb"
|
|
593
|
+
result = {"path": str(path), "dry_run": dry_run, "interpreter": interpreter,
|
|
594
|
+
"desktop_installation": "unverified",
|
|
595
|
+
"next_step": "Open this file with Claude Desktop and confirm its installation dialog; "
|
|
596
|
+
"agent-bios does not write into Desktop."}
|
|
597
|
+
reject_symlink_ancestors(path)
|
|
598
|
+
if dry_run:
|
|
599
|
+
return {**result, "changed": not path.exists()}
|
|
600
|
+
with transaction_lock(self.state_root):
|
|
601
|
+
if path.exists():
|
|
602
|
+
with zipfile.ZipFile(path) as archive:
|
|
603
|
+
if {name: archive.read(name) for name in archive.namelist()} != members:
|
|
604
|
+
raise AppError(f"Desktop bundle collision: {path}")
|
|
605
|
+
return {**result, "changed": False}
|
|
606
|
+
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
607
|
+
descriptor, temporary = tempfile.mkstemp(prefix=".agent-bios-", suffix=".mcpb", dir=path.parent)
|
|
608
|
+
try:
|
|
609
|
+
with os.fdopen(descriptor, "wb") as handle:
|
|
610
|
+
handle.write(_zip_bytes(members))
|
|
611
|
+
handle.flush()
|
|
612
|
+
os.fsync(handle.fileno())
|
|
613
|
+
os.replace(temporary, path)
|
|
614
|
+
finally:
|
|
615
|
+
if os.path.lexists(temporary):
|
|
616
|
+
os.unlink(temporary)
|
|
617
|
+
return {**result, "changed": True}
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
621
|
+
parser = argparse.ArgumentParser(prog="agent-bios app", description=__doc__)
|
|
622
|
+
parser.add_argument("--repo", type=Path, default=Path(__file__).resolve().parents[1])
|
|
623
|
+
parser.add_argument("--state-dir", type=Path)
|
|
624
|
+
parser.add_argument("--user-dir", type=Path)
|
|
625
|
+
parser.add_argument("--json", action="store_true")
|
|
626
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
627
|
+
for name in ("register", "unregister", "status"):
|
|
628
|
+
command = commands.add_parser(name)
|
|
629
|
+
if name != "status":
|
|
630
|
+
command.add_argument("--dry-run", action="store_true")
|
|
631
|
+
desktop = commands.add_parser("desktop", help="write the Claude Desktop bundle; installing it stays a Desktop step")
|
|
632
|
+
desktop.add_argument("--dry-run", action="store_true")
|
|
633
|
+
session = commands.add_parser("session", help="explicitly preview, use or stop instructions delivery in one app task")
|
|
634
|
+
operations = session.add_subparsers(dest="operation", required=True)
|
|
635
|
+
for name in ("preview", "use", "off", "status"):
|
|
636
|
+
command = operations.add_parser(name)
|
|
637
|
+
command.add_argument("--session", help="defaults to CODEX_THREAD_ID; Desktop preview and use mint one")
|
|
638
|
+
command.add_argument("--host", choices=sorted(APP_HOSTS), default="codex")
|
|
639
|
+
if name == "status":
|
|
640
|
+
command.add_argument("--end-marker-seen", help="Desktop: the delivery's last line, as the model read it")
|
|
641
|
+
if name in {"preview", "use"}:
|
|
642
|
+
selection = command.add_mutually_exclusive_group()
|
|
643
|
+
selection.add_argument("--domains", help="comma-separated qualified instructions selection")
|
|
644
|
+
selection.add_argument("--no-instructions", "--no-corpus", action="store_true")
|
|
645
|
+
command.add_argument("--cwd", type=Path)
|
|
646
|
+
if name == "use":
|
|
647
|
+
command.add_argument("--expected-content-ref", help="require the exact previously previewed snapshot")
|
|
648
|
+
return parser
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def main(argv: list[str] | None = None) -> int:
|
|
652
|
+
raw = list(sys.argv[1:] if argv is None else argv)
|
|
653
|
+
as_json = "--json" in raw
|
|
654
|
+
args = build_parser().parse_args([token for token in raw if token != "--json"])
|
|
655
|
+
env = dict(os.environ)
|
|
656
|
+
if args.state_dir is not None:
|
|
657
|
+
env["AGENT_BIOS_STATE_DIR"] = str(args.state_dir)
|
|
658
|
+
if args.user_dir is not None:
|
|
659
|
+
env["AGENT_BIOS_INSTRUCTIONS_DIR"] = str(args.user_dir)
|
|
660
|
+
try:
|
|
661
|
+
if args.command == "session":
|
|
662
|
+
manager = AppSessions(args.repo, env, host=args.host)
|
|
663
|
+
options = {}
|
|
664
|
+
if args.operation == "status" and args.end_marker_seen is not None:
|
|
665
|
+
options["end_marker_seen"] = args.end_marker_seen
|
|
666
|
+
if args.operation in {"preview", "use"}:
|
|
667
|
+
selected = None
|
|
668
|
+
if args.domains is not None:
|
|
669
|
+
selected = [value.strip() for value in args.domains.split(",") if value.strip()]
|
|
670
|
+
if not selected:
|
|
671
|
+
raise AppError("--domains requires a nonempty instructions selection; use --no-instructions to keep instructions off")
|
|
672
|
+
options = {"selection": selected,
|
|
673
|
+
"selection_mode": "none" if args.no_instructions else "selected" if selected is not None else None,
|
|
674
|
+
"cwd": args.cwd}
|
|
675
|
+
if args.operation == "use":
|
|
676
|
+
options["expected_content_ref"] = args.expected_content_ref
|
|
677
|
+
result = getattr(manager, args.operation)(session=args.session, **options)
|
|
678
|
+
elif args.command == "desktop":
|
|
679
|
+
result = DesktopBundle(args.repo, env).build(dry_run=args.dry_run)
|
|
680
|
+
else:
|
|
681
|
+
manager = AppBridge(args.repo, env)
|
|
682
|
+
options = {} if args.command == "status" else {"dry_run": args.dry_run}
|
|
683
|
+
result = getattr(manager, args.command)(**options)
|
|
684
|
+
if not as_json and isinstance(result.get("instruction_text"), str):
|
|
685
|
+
metadata = {key: value for key, value in result.items() if key != "instruction_text"}
|
|
686
|
+
print(json.dumps(metadata, ensure_ascii=False, indent=2, sort_keys=True), file=sys.stderr)
|
|
687
|
+
print(result["instruction_text"], end="")
|
|
688
|
+
else:
|
|
689
|
+
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
|
|
690
|
+
return 0
|
|
691
|
+
except (AppError, InstructionsStoreError, TransactionError, OSError) as exc:
|
|
692
|
+
print(f"agent-bios app: {exc}", file=sys.stderr)
|
|
693
|
+
return 2
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
if __name__ == "__main__":
|
|
697
|
+
raise SystemExit(main())
|