agent-bios 0.19.2 → 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 (46) hide show
  1. package/DEPENDENCIES.md +32 -4
  2. package/README.md +54 -4
  3. package/claude/guides/cli-multi-model-workflow.md +1 -1
  4. package/claude/guides/coding-staged-workflow.md +17 -0
  5. package/claude/guides/llm-capability-boundary.md +7 -1
  6. package/claude/guides/tooling-gotchas.md +20 -1
  7. package/claude/guides/ui-design/visual-direction.md +88 -0
  8. package/claude/guides/ui-design.md +90 -0
  9. package/claude/guides/verification-discipline.md +10 -1
  10. package/claude/hooks/tooling-gotchas-hook.py +41 -0
  11. package/codex/guides/cli-multi-model-workflow.md +1 -1
  12. package/codex/guides/coding-staged-workflow.md +17 -0
  13. package/codex/guides/llm-capability-boundary.md +7 -1
  14. package/codex/guides/tooling-gotchas.md +20 -1
  15. package/codex/guides/ui-design/visual-direction.md +88 -0
  16. package/codex/guides/ui-design.md +90 -0
  17. package/codex/guides/verification-discipline.md +10 -1
  18. package/compose/app_bridge/scripts/bridge.py +23 -6
  19. package/compose/app_desktop/server.py +250 -0
  20. package/compose/domains.json +1 -0
  21. package/compose/host_platform.py +121 -0
  22. package/compose/instructions-state.py +5 -2
  23. package/compose/instructions_app.py +281 -48
  24. package/compose/instructions_catalog.py +2 -2
  25. package/compose/instructions_import.py +19 -11
  26. package/compose/instructions_install.py +77 -11
  27. package/compose/instructions_session.py +6 -6
  28. package/compose/instructions_setup.py +41 -4
  29. package/compose/instructions_setup_cli.py +31 -10
  30. package/compose/instructions_setup_i18n.py +3 -0
  31. package/compose/instructions_store.py +6 -6
  32. package/compose/instructions_transaction.py +8 -6
  33. package/compose/instructions_ui_runtime.py +2 -1
  34. package/compose/native_cli.py +52 -0
  35. package/compose/runtime_entry.py +58 -0
  36. package/compose/windows_deploy.py +719 -0
  37. package/docs/instructions.md +1 -0
  38. package/docs/releases/0.19.3.md +107 -0
  39. package/docs/session-model.md +8 -0
  40. package/docs/setup.md +36 -0
  41. package/docs/windows.md +99 -0
  42. package/install.sh +1 -0
  43. package/launch/agent-launch.py +12 -4
  44. package/launch/agent-launch.zsh +11 -1
  45. package/package.json +8 -3
  46. package/provenance.json +1 -1
@@ -1,18 +1,30 @@
1
1
  #!/usr/bin/env python3
2
- """Explicit Codex app discovery and per-task instructions context delivery."""
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
+ """
3
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
4
12
 
5
13
  import argparse
6
14
  from datetime import datetime, timezone
7
15
  import hashlib
16
+ import importlib.util
17
+ import io
8
18
  import json
9
19
  import os
10
20
  from pathlib import Path
11
21
  import re
22
+ import secrets
12
23
  import shutil
13
24
  import sys
14
25
  import tempfile
15
26
  from typing import Any
27
+ import zipfile
16
28
 
17
29
  try:
18
30
  from instructions_store import InstructionsStore, InstructionsStoreError
@@ -26,10 +38,23 @@ except ImportError:
26
38
 
27
39
  SCHEMA_VERSION = 1
28
40
  BRIDGE_MEMBERS = ("SKILL.md", "agents/openai.yaml", "scripts/bridge.py",
29
- "scripts/instructions_transaction.py", "bridge.json")
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")
30
43
  LEGACY_BRIDGE_MEMBERS = ("SKILL.md", "agents/openai.yaml", "scripts/bridge.py",
31
44
  "scripts/corpus_transaction.py", "bridge.json")
32
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")
33
58
 
34
59
 
35
60
  class AppError(RuntimeError):
@@ -106,6 +131,11 @@ class AppBridge:
106
131
  saved = _read_json(current / "bridge.json")
107
132
  if "launch_venv" in saved:
108
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
109
139
  return config
110
140
 
111
141
  def _source_members(self) -> dict[str, bytes]:
@@ -121,23 +151,36 @@ class AppBridge:
121
151
  members[name] = _json_bytes(self._config())
122
152
  continue
123
153
  path = release / "compose" / Path(name).name if name in {
124
- "scripts/instructions_transaction.py", "scripts/corpus_transaction.py"
154
+ "scripts/instructions_transaction.py", "scripts/corpus_transaction.py", "scripts/host_platform.py"
125
155
  } else source / name
126
156
  reject_symlink_ancestors(path)
127
157
  if not path.is_file():
128
158
  raise AppError(f"installed release has no app bridge member: {path}")
129
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")
130
167
  return members
131
168
 
132
169
  def _owned_target(self) -> Path | None:
133
170
  reject_symlink_ancestors(self.target.parent)
134
171
  if not os.path.lexists(self.target):
135
172
  return None
136
- if not self.target.is_symlink():
173
+ if not (self.target.is_symlink() or (os.name == "nt" and self.target.is_junction())):
137
174
  raise AppError(f"preserving unowned app skill: {self.target}")
138
- raw = Path(os.readlink(self.target))
139
- if not raw.is_absolute() or raw.parent != self.generations or not re.fullmatch(r"[a-f0-9]{64}", raw.name):
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):
140
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
141
184
  reject_symlink_ancestors(raw)
142
185
  if not raw.is_dir():
143
186
  raise AppError(f"owned app skill generation is unavailable: {raw}")
@@ -145,12 +188,12 @@ class AppBridge:
145
188
  if any(path.is_symlink() for path in paths):
146
189
  raise AppError(f"preserving redirected app skill generation: {raw}")
147
190
  files = {path.relative_to(raw).as_posix(): path.read_bytes() for path in paths if path.is_file()}
148
- if set(files) not in (set(BRIDGE_MEMBERS), set(LEGACY_BRIDGE_MEMBERS)) or _tree_digest(files) != raw.name:
191
+ if set(files) not in (set(BRIDGE_MEMBERS), set(PREVIOUS_BRIDGE_MEMBERS), set(LEGACY_BRIDGE_MEMBERS)) or _tree_digest(files) != raw.name:
149
192
  raise AppError(f"preserving changed app skill generation: {raw}")
150
193
  config = _read_json(raw / "bridge.json")
151
194
  base = self._base_config()
152
195
  if (any(config.get(key) != value for key, value in base.items())
153
- or set(config) - set(base) - {"launch_venv"}
196
+ or set(config) - set(base) - {"launch_venv", "python_binding"}
154
197
  or ("launch_venv" in config and (not isinstance(config["launch_venv"], str)
155
198
  or (config["launch_venv"] and not Path(config["launch_venv"]).is_absolute())))):
156
199
  raise AppError(f"app skill belongs to different private root settings: {self.target}")
@@ -170,10 +213,12 @@ class AppBridge:
170
213
  """Identify our link namespace without opening unrelated skill contents."""
171
214
  try:
172
215
  reject_symlink_ancestors(self.target.parent)
173
- if not self.target.is_symlink():
216
+ if not (self.target.is_symlink() or (os.name == "nt" and self.target.is_junction())):
174
217
  return False
175
- target = Path(os.readlink(self.target))
176
- return target.is_absolute() and target.parent == self.generations
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
177
222
  except (OSError, TransactionError):
178
223
  return False
179
224
 
@@ -228,11 +273,16 @@ class AppBridge:
228
273
  os.close(descriptor)
229
274
  os.unlink(temporary)
230
275
  try:
231
- os.symlink(str(generation), temporary)
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)
232
282
  os.replace(temporary, self.target)
233
283
  finally:
234
284
  if os.path.lexists(temporary):
235
- os.unlink(temporary)
285
+ os.rmdir(temporary) if os.name == "nt" and Path(temporary).is_junction() else os.unlink(temporary)
236
286
  return result
237
287
 
238
288
  def unregister(self, dry_run: bool = False) -> dict[str, Any]:
@@ -244,7 +294,7 @@ class AppBridge:
244
294
  with transaction_lock(self.state_root):
245
295
  before = self._owned_target()
246
296
  if before is not None:
247
- self.target.unlink()
297
+ self.target.rmdir() if os.name == "nt" else self.target.unlink()
248
298
  return {"registered": False, "dry_run": False, "changed": before is not None,
249
299
  "discovery_path": str(self.target), "retained_private_generations": True}
250
300
 
@@ -262,16 +312,37 @@ class AppBridge:
262
312
  class AppSessions:
263
313
  """Record returned context, separately from native launch pins and host proof."""
264
314
 
265
- def __init__(self, repo: Path, environ: dict[str, str] | None = None):
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}")
266
318
  self.repo = Path(repo).absolute()
319
+ self.host = host
267
320
  self.env, self.home, self.state_root, self.user_root = _roots(environ)
268
321
 
269
- def _session(self, session: str | None) -> str:
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
270
333
  value = session if session is not None else self.env.get("CODEX_THREAD_ID")
271
334
  if not isinstance(value, str) or not SESSION_ID.fullmatch(value):
272
335
  raise AppError("app session requires CODEX_THREAD_ID or --session with a valid task id")
273
336
  return value
274
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
+
275
346
  def _path(self, session: str) -> Path:
276
347
  return self.state_root / "sessions/app-context" / f"{session}.json"
277
348
 
@@ -279,31 +350,50 @@ class AppSessions:
279
350
  path = self._path(session)
280
351
  reject_symlink_ancestors(path)
281
352
  if not path.exists():
282
- return {"schema_version": SCHEMA_VERSION, "host": "codex", "session_id": session,
353
+ return {"schema_version": SCHEMA_VERSION, "host": self.host, "session_id": session,
283
354
  "enabled": False, "deliveries": []}
284
355
  record = _read_json(path)
285
- if (record.get("schema_version") != SCHEMA_VERSION or record.get("host") != "codex"
356
+ if (record.get("schema_version") != SCHEMA_VERSION or record.get("host") != self.host
286
357
  or record.get("session_id") != session or not isinstance(record.get("enabled"), bool)
287
358
  or not isinstance(record.get("deliveries"), list)):
288
359
  raise AppError(f"invalid app session receipt: {path}")
289
360
  for delivery in record["deliveries"]:
290
361
  if (not isinstance(delivery, dict) or delivery.get("delivery") != "returned-as-context"
291
362
  or not isinstance(delivery.get("content_ref"), str)
292
- or not re.fullmatch(r"[a-f0-9]{64}", delivery["content_ref"])):
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))):
293
367
  raise AppError(f"invalid app session delivery: {path}")
294
368
  return record
295
369
 
296
370
  @staticmethod
297
371
  def _view(record: dict[str, Any]) -> dict[str, Any]:
298
372
  delivered = bool(record["deliveries"])
299
- return {**record, "ever_delivered": delivered,
373
+ view = {**record, "ever_delivered": delivered,
300
374
  "active_content_ref": record["deliveries"][-1]["content_ref"] if delivered and record["enabled"] else None,
301
375
  "native_activation": False, "host_loading": "unverified",
302
376
  "context_retracted": False,
303
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
304
383
 
305
- def status(self, session: str | None = None) -> dict[str, Any]:
306
- return self._view(self._read(self._session(session)))
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."}
307
397
 
308
398
  def off(self, session: str | None = None) -> dict[str, Any]:
309
399
  session = self._session(session)
@@ -328,46 +418,55 @@ class AppSessions:
328
418
  store = InstructionsStore(release, self.state_root, self.user_root)
329
419
  if selection is not None and selection_mode is None:
330
420
  selection_mode = "selected"
331
- return store.snapshot(host="codex", selection=selection, selection_mode=selection_mode,
332
- cwd=Path(cwd or Path.cwd()).absolute(), dry_run=dry_run, native=False)
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)
333
423
 
334
424
  def _runtime_commands(self) -> dict[str, Any]:
335
425
  release = confirmed_release(self.state_root)
336
426
  bridge = AppBridge(self.repo, self.env)
337
427
  result = {"package_root": str(release),
338
- "learn_argv": ["/bin/bash", str(release / "install.sh"), "learn"],
428
+ "learn_argv": cli_argv(release, "learn"),
339
429
  "environment": {"AGENT_BIOS_PACKAGE_ROOT": str(release),
340
430
  "AGENT_BIOS_STATE_DIR": str(self.state_root),
341
431
  "AGENT_BIOS_INSTRUCTIONS_DIR": str(self.user_root),
342
432
  "AGENT_BIOS_CORPUS_DIR": str(self.user_root),
343
433
  "AGENT_BIOS_PRIVATE_CORPUS": "1",
344
434
  "AGENT_BIOS_PRIVATE_INSTRUCTIONS": "1", "AGENT_BIOS_LEGACY_INSTALL": "0"}}
435
+ result["environment"].update(runtime_environment())
345
436
  registered = bridge.managed_status().get("registered")
346
437
  if "AGENT_LAUNCH_VENV" in self.env or registered:
347
438
  config = bridge._config()
348
439
  if "launch_venv" in config:
349
440
  result["environment"]["AGENT_LAUNCH_VENV"] = config["launch_venv"]
350
441
  if registered:
351
- result["bridge_learn_argv"] = [sys.executable, str(bridge.target / "scripts/bridge.py"), "learn"]
442
+ result["bridge_learn_argv"] = python_argv(bridge.target / "scripts/bridge.py", "learn")
352
443
  return result
353
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
+
354
450
  def preview(self, session: str | None = None, *, selection: list[str] | None = None,
355
451
  selection_mode: str | None = None, cwd: Path | None = None) -> dict[str, Any]:
356
- session = self._session(session)
452
+ session = self._session(session, mint=True)
357
453
  if selection_mode == "none":
358
454
  return {"session_id": session, "selection_mode": "none", "content_ref": None,
359
455
  "instruction_characters": 0, "delivery": "preview-only", "native_activation": False}
360
456
  snapshot = self._snapshot(selection=selection, selection_mode=selection_mode, cwd=cwd, dry_run=True)
361
- return {"session_id": session, "content_ref": snapshot["content_ref"], "revision": snapshot["revision"],
362
- "instruction_characters": len(snapshot["instruction_text"]),
363
- "unavailable": snapshot.get("unavailable", []), "selection": selection,
364
- "selection_mode": snapshot.get("selection_mode", selection_mode),
365
- "delivery": "preview-only", "native_activation": False}
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
366
465
 
367
466
  def use(self, session: str | None = None, *, selection: list[str] | None = None,
368
467
  selection_mode: str | None = None, cwd: Path | None = None,
369
468
  expected_content_ref: str | None = None) -> dict[str, Any]:
370
- session = self._session(session)
469
+ session = self._session(session, mint=True)
371
470
  if selection_mode == "none":
372
471
  return self.off(session)
373
472
  reject_symlink_ancestors(self.state_root)
@@ -378,19 +477,144 @@ class AppSessions:
378
477
  raise AppError("instructions preview changed; preview again before using this session selection")
379
478
  if not snapshot["instruction_text"].strip():
380
479
  return self.off(session)
381
- delivery = {"at": datetime.now(timezone.utc).isoformat(), "content_ref": snapshot["content_ref"],
382
- "snapshot_path": snapshot["path"], "revision": snapshot["revision"],
383
- "cwd": str(Path(cwd or Path.cwd()).absolute()), "delivery": "returned-as-context",
384
- "instruction_sha256": hashlib.sha256(snapshot["instruction_text"].encode("utf-8")).hexdigest()}
385
- record["deliveries"].append(delivery)
386
- record["enabled"] = True
387
- runtime = self._runtime_commands()
388
- _write_json(self._path(session), record)
389
- return {**self._view(record), "delivery": "returned-as-context",
390
- "content_ref": snapshot["content_ref"], "instruction_text": snapshot["instruction_text"],
391
- "runtime": runtime,
392
- "unavailable": snapshot.get("unavailable", []),
393
- "message": "Returned the selected immutable instructions as task context. This receipt is not proof of model reading or native startup activation."}
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}
394
618
 
395
619
 
396
620
  def build_parser() -> argparse.ArgumentParser:
@@ -404,11 +628,16 @@ def build_parser() -> argparse.ArgumentParser:
404
628
  command = commands.add_parser(name)
405
629
  if name != "status":
406
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")
407
633
  session = commands.add_parser("session", help="explicitly preview, use or stop instructions delivery in one app task")
408
634
  operations = session.add_subparsers(dest="operation", required=True)
409
635
  for name in ("preview", "use", "off", "status"):
410
636
  command = operations.add_parser(name)
411
- command.add_argument("--session", help="defaults to CODEX_THREAD_ID")
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")
412
641
  if name in {"preview", "use"}:
413
642
  selection = command.add_mutually_exclusive_group()
414
643
  selection.add_argument("--domains", help="comma-separated qualified instructions selection")
@@ -430,8 +659,10 @@ def main(argv: list[str] | None = None) -> int:
430
659
  env["AGENT_BIOS_INSTRUCTIONS_DIR"] = str(args.user_dir)
431
660
  try:
432
661
  if args.command == "session":
433
- manager = AppSessions(args.repo, env)
662
+ manager = AppSessions(args.repo, env, host=args.host)
434
663
  options = {}
664
+ if args.operation == "status" and args.end_marker_seen is not None:
665
+ options["end_marker_seen"] = args.end_marker_seen
435
666
  if args.operation in {"preview", "use"}:
436
667
  selected = None
437
668
  if args.domains is not None:
@@ -444,6 +675,8 @@ def main(argv: list[str] | None = None) -> int:
444
675
  if args.operation == "use":
445
676
  options["expected_content_ref"] = args.expected_content_ref
446
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)
447
680
  else:
448
681
  manager = AppBridge(args.repo, env)
449
682
  options = {} if args.command == "status" else {"dry_run": args.dry_run}
@@ -573,7 +573,7 @@ def _write_private(path: pathlib.Path, content: str) -> None:
573
573
  raise CatalogError(f"compiler refuses symlink output: {path}")
574
574
  path.parent.mkdir(parents=True, exist_ok=True)
575
575
  try:
576
- with path.open('x', encoding='utf-8') as output:
576
+ with path.open('x', encoding='utf-8', newline='\n') as output:
577
577
  output.write(content)
578
578
  except FileExistsError as exc:
579
579
  raise CatalogError(f"compiler output already exists or aliases another member: {path}") from exc
@@ -586,7 +586,7 @@ def _replace_private_owned(path: pathlib.Path, content: str) -> None:
586
586
  descriptor, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
587
587
  temporary = pathlib.Path(name)
588
588
  try:
589
- with os.fdopen(descriptor, "w", encoding="utf-8") as output:
589
+ with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as output:
590
590
  output.write(content)
591
591
  output.flush()
592
592
  os.fsync(output.fileno())
@@ -5,6 +5,10 @@ meaning, wording and consumption surface; this module checks source provenance,
5
5
  evidence coverage and publication preconditions for InstructionsStore's transaction.
6
6
  """
7
7
  from __future__ import annotations
8
+ try:
9
+ from host_platform import sync_directory, cli_argv, redirected
10
+ except ImportError:
11
+ from .host_platform import sync_directory, cli_argv, redirected
8
12
 
9
13
  import copy
10
14
  import argparse
@@ -67,7 +71,7 @@ def _safe_source_path(source: dict[str, Any], *, inspect: bool = True) -> Path:
67
71
  raise ValidationError("only discovered instruction files may be captured")
68
72
  if inspect:
69
73
  for member in (root, *(root / Path(*relative.parts[:index]) for index in range(1, len(relative.parts) + 1))):
70
- if member.is_symlink():
74
+ if redirected(member):
71
75
  raise ValidationError(f"instruction source is symlinked: {member}")
72
76
  return path
73
77
 
@@ -125,12 +129,20 @@ def _read_source(source: dict[str, Any]) -> tuple[bytes, str]:
125
129
  directories = []
126
130
  try:
127
131
  flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
128
- directory = os.open(path.anchor, flags | os.O_DIRECTORY)
129
- directories.append(directory)
130
- for part in path.parts[1:-1]:
131
- directory = os.open(part, flags | os.O_DIRECTORY, dir_fd=directory)
132
+ if os.name == "nt":
133
+ # Reparse points are rejected before and after opening; bind the opened
134
+ # file identity to the selected source below.
135
+ from host_platform import redirected
136
+ if any(redirected(p) for p in (path, *path.parents)):
137
+ raise ValidationError("redirected instruction source")
138
+ descriptor = os.open(path, os.O_RDONLY | os.O_BINARY)
139
+ else:
140
+ directory = os.open(path.anchor, flags | os.O_DIRECTORY)
132
141
  directories.append(directory)
133
- descriptor = os.open(path.name, flags | os.O_NONBLOCK, dir_fd=directory)
142
+ for part in path.parts[1:-1]:
143
+ directory = os.open(part, flags | os.O_DIRECTORY, dir_fd=directory)
144
+ directories.append(directory)
145
+ descriptor = os.open(path.name, flags | os.O_NONBLOCK, dir_fd=directory)
134
146
  with os.fdopen(descriptor, "rb") as stream:
135
147
  before = os.fstat(stream.fileno())
136
148
  if not stat.S_ISREG(before.st_mode) or before.st_size > MAX_SOURCE_BYTES:
@@ -311,11 +323,7 @@ def capture(store, paths: list[str | Path], *, environ: dict[str, str] | None =
311
323
  os.link(temporary, path)
312
324
  except FileExistsError:
313
325
  record = load_capture(store, record["capture_id"])
314
- directory = os.open(path.parent, os.O_RDONLY)
315
- try:
316
- os.fsync(directory)
317
- finally:
318
- os.close(directory)
326
+ sync_directory(path.parent)
319
327
  finally:
320
328
  temporary.unlink(missing_ok=True)
321
329
  return {**record, "review_prompt": review_prompt(record)}