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.
Files changed (106) hide show
  1. package/DEPENDENCIES.md +58 -30
  2. package/INSTALL.md +4 -4
  3. package/README.md +105 -28
  4. package/claude/CLAUDE.md +1 -1
  5. package/claude/guides/claude-prompting.md +1 -1
  6. package/claude/guides/cli-multi-model-workflow.md +4 -4
  7. package/claude/guides/coding-staged-workflow.md +17 -0
  8. package/claude/guides/documentation-hygiene.md +3 -0
  9. package/claude/guides/gpt-prompting.md +1 -1
  10. package/claude/guides/korean-writing.md +153 -0
  11. package/claude/guides/learning-flow.md +4 -4
  12. package/claude/guides/llm-capability-boundary.md +7 -1
  13. package/claude/guides/session-distill-workflow.md +8 -8
  14. package/claude/guides/slide-writing/RUNBOOK.md +5 -5
  15. package/claude/guides/tooling-gotchas.md +20 -1
  16. package/claude/guides/ui-design/visual-direction.md +88 -0
  17. package/claude/guides/ui-design.md +90 -0
  18. package/claude/guides/verification-discipline.md +10 -1
  19. package/claude/hooks/tooling-gotchas-hook.py +41 -0
  20. package/claude/skills/repo-charter/SKILL.md +3 -3
  21. package/claude/skills/understand/SKILL.md +5 -5
  22. package/codex/AGENTS.md +1 -1
  23. package/codex/guides/claude-prompting.md +1 -1
  24. package/codex/guides/cli-multi-model-workflow.md +4 -4
  25. package/codex/guides/coding-staged-workflow.md +17 -0
  26. package/codex/guides/documentation-hygiene.md +3 -0
  27. package/codex/guides/gpt-prompting.md +1 -1
  28. package/codex/guides/korean-writing.md +153 -0
  29. package/codex/guides/learning-flow.md +4 -4
  30. package/codex/guides/llm-capability-boundary.md +7 -1
  31. package/codex/guides/session-distill-workflow.md +8 -8
  32. package/codex/guides/slide-writing/RUNBOOK.md +5 -5
  33. package/codex/guides/tooling-gotchas.md +20 -1
  34. package/codex/guides/ui-design/visual-direction.md +88 -0
  35. package/codex/guides/ui-design.md +90 -0
  36. package/codex/guides/verification-discipline.md +10 -1
  37. package/compose/app_bridge/SKILL.md +12 -12
  38. package/compose/app_bridge/scripts/bridge.py +35 -10
  39. package/compose/app_desktop/server.py +250 -0
  40. package/compose/assemble.py +5 -5
  41. package/compose/bootstrap/SKILL.md +18 -18
  42. package/compose/canary.sh +4 -4
  43. package/compose/check-domains.py +6 -6
  44. package/compose/corpus-state.py +16 -1168
  45. package/compose/corpus.py +13 -402
  46. package/compose/corpus_app.py +14 -450
  47. package/compose/corpus_catalog.py +15 -926
  48. package/compose/corpus_import.py +14 -523
  49. package/compose/corpus_install.py +14 -1847
  50. package/compose/corpus_session.py +16 -848
  51. package/compose/corpus_setup.py +16 -672
  52. package/compose/corpus_setup_cli.py +15 -580
  53. package/compose/corpus_setup_i18n.py +20 -324
  54. package/compose/corpus_setup_ui.py +18 -645
  55. package/compose/corpus_store.py +16 -1664
  56. package/compose/corpus_transaction.py +15 -284
  57. package/compose/corpus_ui.py +17 -972
  58. package/compose/corpus_ui_runtime.py +16 -274
  59. package/compose/corpus_understand.py +13 -671
  60. package/compose/domains.json +3 -1
  61. package/compose/host_platform.py +121 -0
  62. package/compose/instructions-state.py +1178 -0
  63. package/compose/instructions.py +409 -0
  64. package/compose/instructions_app.py +697 -0
  65. package/compose/instructions_catalog.py +931 -0
  66. package/compose/instructions_import.py +537 -0
  67. package/compose/instructions_install.py +1932 -0
  68. package/compose/instructions_session.py +852 -0
  69. package/compose/instructions_setup.py +713 -0
  70. package/compose/instructions_setup_cli.py +607 -0
  71. package/compose/instructions_setup_i18n.py +327 -0
  72. package/compose/instructions_setup_ui.py +647 -0
  73. package/compose/instructions_store.py +1668 -0
  74. package/compose/instructions_transaction.py +308 -0
  75. package/compose/instructions_ui.py +975 -0
  76. package/compose/instructions_ui_runtime.py +279 -0
  77. package/compose/instructions_understand.py +678 -0
  78. package/compose/native_cli.py +52 -0
  79. package/compose/register-hooks.py +1 -1
  80. package/compose/runtime_entry.py +58 -0
  81. package/compose/setup/START.md +11 -11
  82. package/compose/windows_deploy.py +719 -0
  83. package/docs/advanced-launch.md +11 -11
  84. package/docs/instructions-compatibility.md +86 -0
  85. package/docs/{corpus.md → instructions.md} +36 -8
  86. package/docs/recovery.md +10 -10
  87. package/docs/releases/0.19.2.md +38 -0
  88. package/docs/releases/0.19.3.md +107 -0
  89. package/docs/session-model.md +31 -20
  90. package/docs/setup.md +63 -26
  91. package/docs/understand.md +6 -6
  92. package/docs/windows.md +99 -0
  93. package/install.sh +71 -69
  94. package/launch/agent-launch.py +309 -293
  95. package/launch/agent-launch.toml +2 -2
  96. package/launch/agent-launch.zsh +11 -1
  97. package/launch/i18n/en.toml +55 -55
  98. package/launch/i18n/ja.toml +56 -56
  99. package/launch/i18n/ko.toml +56 -56
  100. package/launch/shell_integration.py +4 -4
  101. package/learn/collect-learning.py +10 -10
  102. package/learn/learning.schema.json +1 -1
  103. package/learn/migrate-learnings.py +51 -51
  104. package/package.json +33 -12
  105. package/provenance.json +1 -1
  106. /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())