optio-codex 0.4.0__tar.gz → 0.4.2__tar.gz
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.
- {optio_codex-0.4.0 → optio_codex-0.4.2}/PKG-INFO +1 -1
- {optio_codex-0.4.0 → optio_codex-0.4.2}/pyproject.toml +1 -1
- {optio_codex-0.4.0 → optio_codex-0.4.2}/src/optio_codex/conversation.py +3 -1
- {optio_codex-0.4.0 → optio_codex-0.4.2}/src/optio_codex/host_actions.py +143 -3
- optio_codex-0.4.2/src/optio_codex/prompt.py +48 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/src/optio_codex/session.py +3 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/src/optio_codex.egg-info/PKG-INFO +1 -1
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_codex_cache.py +225 -2
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_config_hooks.py +9 -3
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_prompt.py +10 -0
- optio_codex-0.4.0/src/optio_codex/prompt.py +0 -184
- {optio_codex-0.4.0 → optio_codex-0.4.2}/README.md +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/setup.cfg +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/src/optio_codex/__init__.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/src/optio_codex/account.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/src/optio_codex/conversation_listener.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/src/optio_codex/cred_watcher.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/src/optio_codex/fs_allowlist.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/src/optio_codex/info.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/src/optio_codex/models.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/src/optio_codex/rollout.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/src/optio_codex/seed_manifest.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/src/optio_codex/snapshots.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/src/optio_codex/types.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/src/optio_codex/verify.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/src/optio_codex.egg-info/SOURCES.txt +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/src/optio_codex.egg-info/dependency_links.txt +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/src/optio_codex.egg-info/requires.txt +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/src/optio_codex.egg-info/top_level.txt +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_account.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_agent_info.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_await_codex_gone.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_claustrum.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_config.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_conversation.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_conversation_controls.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_conversation_listener.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_cred_watcher.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_file_download.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_file_upload.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_fs_allowlist.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_host_actions.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_import.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_input_wiring.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_kill_ttyd_by_socket.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_models.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_real_codex_conversation.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_real_codex_seed_resume.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_real_codex_session.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_rollout.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_sandbox_enforce.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_seed_manifest.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_session_conversation.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_session_lease.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_session_local.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_session_remote.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_session_resume.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_session_sandbox.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_session_seed.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_snapshots.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_teardown_session_tree.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_verify.py +0 -0
- {optio_codex-0.4.0 → optio_codex-0.4.2}/tests/test_workdir_trust.py +0 -0
|
@@ -496,7 +496,9 @@ class CodexConversation:
|
|
|
496
496
|
|
|
497
497
|
# -- Conversation protocol surface --------------------------------------
|
|
498
498
|
|
|
499
|
-
async def send(self, text: str) -> None:
|
|
499
|
+
async def send(self, text: str, *, uuid: str | None = None) -> None:
|
|
500
|
+
"""``uuid`` is an advisory message id (Fix 13a/Fix 23): this backend
|
|
501
|
+
has no message identity of its own, so it is accepted and ignored."""
|
|
500
502
|
if self._closed.is_set():
|
|
501
503
|
raise ConversationClosed(self._close_reason or "conversation closed")
|
|
502
504
|
if self.thread_id is None:
|
|
@@ -63,6 +63,24 @@ _CODEX_RELEASE_BASE = (
|
|
|
63
63
|
f"https://github.com/openai/codex/releases/download/rust-v{_CODEX_VERSION}"
|
|
64
64
|
)
|
|
65
65
|
|
|
66
|
+
# Version stamp recording which codex version the cache holds, written by
|
|
67
|
+
# BOTH install tiers (pinned download stamps _CODEX_VERSION; host-copy
|
|
68
|
+
# stamps the copied binary's probed version). The cache-hit path compares
|
|
69
|
+
# it against _CODEX_VERSION so a pin bump actually reaches warm caches —
|
|
70
|
+
# the cache is claustrum-``--rox`` to sessions, so freshness is owned
|
|
71
|
+
# entirely by this unconfined provisioning path (agent-binary-freshness
|
|
72
|
+
# plan, 2026-08-13).
|
|
73
|
+
_CODEX_VERSION_STAMP_NAME = "codex.version"
|
|
74
|
+
|
|
75
|
+
# ``codex --version`` output shape, verified against the REAL binary
|
|
76
|
+
# (2026-08-13, ~/.cache/optio-codex/bin/codex, v0.142.5):
|
|
77
|
+
# $ env HOME=<empty-dir> ~/.cache/optio-codex/bin/codex --version
|
|
78
|
+
# stdout: ``codex-cli 0.142.5`` (exactly; version only on stdout)
|
|
79
|
+
# stderr: PATH-alias WARNING when $HOME/.codex is absent (harmless)
|
|
80
|
+
# exit 0; no files written under HOME.
|
|
81
|
+
# Parse by regex, never by line position — stderr noise must not matter.
|
|
82
|
+
_CODEX_VERSION_OUTPUT_RE = re.compile(r"codex-cli\s+(\S+)")
|
|
83
|
+
|
|
66
84
|
|
|
67
85
|
async def _expand_user_path(host: "Host", path: str) -> str:
|
|
68
86
|
"""Expand a leading ``~``/``~/`` against the HOST's home directory.
|
|
@@ -207,6 +225,73 @@ async def claustrum_newer_tag() -> str | None:
|
|
|
207
225
|
return newest if key(newest) > key(claustrum.CLAUSTRUM_PINNED_TAG) else None
|
|
208
226
|
|
|
209
227
|
|
|
228
|
+
async def _probe_codex_version(host: "Host", codex_path: str) -> str | None:
|
|
229
|
+
"""Best-effort: the version string a codex binary reports, or ``None``.
|
|
230
|
+
|
|
231
|
+
Runs ``<codex_path> --version`` with ``HOME`` pointed at the cache ROOT
|
|
232
|
+
(grok's ``_grok_update_target`` pattern) so the probe can never touch
|
|
233
|
+
the operator's ``~/.codex``. Real-binary evidence (2026-08-13, v0.142.5):
|
|
234
|
+
the probe writes nothing under HOME and prints ``codex-cli <semver>`` on
|
|
235
|
+
stdout (stderr may carry a PATH-alias warning) — parsed via
|
|
236
|
+
:data:`_CODEX_VERSION_OUTPUT_RE`. Non-zero exit / unmatched output →
|
|
237
|
+
``None`` (never raises; a stamp-less cache just refreshes to the pin on
|
|
238
|
+
the next launch).
|
|
239
|
+
"""
|
|
240
|
+
# HOME = the parent of the cache dir (…/optio-codex for …/optio-codex/bin).
|
|
241
|
+
cache_root = os.path.dirname(os.path.dirname(codex_path.rstrip("/"))) or "/"
|
|
242
|
+
r = await host.run_command(
|
|
243
|
+
f"env HOME={shlex.quote(cache_root)} {shlex.quote(codex_path)} --version"
|
|
244
|
+
)
|
|
245
|
+
if r.exit_code != 0:
|
|
246
|
+
_LOG.warning(
|
|
247
|
+
"codex --version probe failed (exit %s): %s",
|
|
248
|
+
r.exit_code, (r.stderr or "").strip()[:200],
|
|
249
|
+
)
|
|
250
|
+
return None
|
|
251
|
+
m = _CODEX_VERSION_OUTPUT_RE.search(r.stdout or "")
|
|
252
|
+
if m is None:
|
|
253
|
+
_LOG.warning(
|
|
254
|
+
"codex --version output unparseable; no version stamp: %r",
|
|
255
|
+
(r.stdout or "")[:200],
|
|
256
|
+
)
|
|
257
|
+
return None
|
|
258
|
+
return m.group(1)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
async def _read_version_stamp(host: "Host", *, cache_dir: str) -> str | None:
|
|
262
|
+
"""The cache's ``codex.version`` stamp content, or ``None`` when absent
|
|
263
|
+
(legacy warm cache filled before stamping existed)."""
|
|
264
|
+
stamp = f"{cache_dir}/{_CODEX_VERSION_STAMP_NAME}"
|
|
265
|
+
r = await host.run_command(f"cat {shlex.quote(stamp)} 2>/dev/null || true")
|
|
266
|
+
content = (r.stdout or "").strip()
|
|
267
|
+
return content or None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
async def _write_version_stamp(
|
|
271
|
+
host: "Host", *, cache_dir: str, version: str,
|
|
272
|
+
) -> None:
|
|
273
|
+
"""Record ``version`` as ``<cache>/codex.version``, atomically.
|
|
274
|
+
|
|
275
|
+
Written AFTER the binary lands, via a per-invocation temp + ``mv -f``
|
|
276
|
+
(same concurrency discipline as the binary install): a concurrent
|
|
277
|
+
launch never reads a half-written stamp, and racers writing identical
|
|
278
|
+
content are last-writer-wins. Best-effort — a failed stamp write only
|
|
279
|
+
costs a redundant pinned re-download on the next launch, never a launch.
|
|
280
|
+
"""
|
|
281
|
+
stamp = f"{cache_dir}/{_CODEX_VERSION_STAMP_NAME}"
|
|
282
|
+
tmp = f"{stamp}.{os.getpid()}-{uuid.uuid4().hex}"
|
|
283
|
+
r = await host.run_command(
|
|
284
|
+
f"printf %s {shlex.quote(version)} > {shlex.quote(tmp)} "
|
|
285
|
+
f"&& mv -f {shlex.quote(tmp)} {shlex.quote(stamp)}"
|
|
286
|
+
)
|
|
287
|
+
if r.exit_code != 0:
|
|
288
|
+
_LOG.warning(
|
|
289
|
+
"writing codex version stamp %r failed (exit %s): %s",
|
|
290
|
+
stamp, r.exit_code, (r.stderr or "").strip()[:200],
|
|
291
|
+
)
|
|
292
|
+
await host.run_command(f"rm -f {shlex.quote(tmp)}")
|
|
293
|
+
|
|
294
|
+
|
|
210
295
|
async def ensure_codex_installed(
|
|
211
296
|
hook_ctx: "HookContextProtocol",
|
|
212
297
|
*,
|
|
@@ -219,13 +304,25 @@ async def ensure_codex_installed(
|
|
|
219
304
|
any task workdir and never the operator's autoupdating ``~/.codex`` — so
|
|
220
305
|
it stays shared, evictable, and unsnapshotted. Resolution order:
|
|
221
306
|
|
|
222
|
-
- **cache hit** — ``<cache>/codex`` is
|
|
307
|
+
- **cache hit, stamp == pin** — ``<cache>/codex`` is executable and its
|
|
308
|
+
``codex.version`` stamp matches :data:`_CODEX_VERSION`.
|
|
309
|
+
- **cache hit, stamp missing/stale** — the ``_CODEX_VERSION`` pin is
|
|
310
|
+
authoritative: re-download the pinned release into the cache (never
|
|
311
|
+
host-copy on a refresh) and stamp it. Best-effort: a failed download
|
|
312
|
+
with an executable cached binary logs and uses the cached one (a
|
|
313
|
+
stale pin beats no engine — offline workers must still launch).
|
|
314
|
+
Gated on ``install_if_missing`` (an offline/pinned worker keeps the
|
|
315
|
+
binary it has; mirrors grok's update-probe gate).
|
|
223
316
|
- **cache miss** — seed the cache from the resolved host ``codex``
|
|
224
317
|
(login-shell ``command -v codex`` via :func:`resolve_codex`), copying
|
|
225
318
|
it into ``<cache>/codex`` (``cp -L`` deref + chmod + re-verify).
|
|
226
319
|
- **no host codex** — download the pinned GitHub-release tarball
|
|
227
320
|
(:func:`_download_codex_into_cache`) and install it into the cache.
|
|
228
321
|
|
|
322
|
+
Both install tiers write the ``codex.version`` stamp, so pin bumps
|
|
323
|
+
become EFFECTIVE on warm caches (the cache is claustrum-``--rox`` to
|
|
324
|
+
confined sessions; freshness is owned here, by unconfined provisioning).
|
|
325
|
+
|
|
229
326
|
Whatever fills the cache, the RETURNED path is always the per-task
|
|
230
327
|
``<workdir>/home/.local/bin/codex`` symlink (via
|
|
231
328
|
:func:`_provision_task_home`) so teardown's anchored pkill stays scoped
|
|
@@ -243,7 +340,38 @@ async def ensure_codex_installed(
|
|
|
243
340
|
f"[ -x {shlex.quote(cached)} ] && echo OK || true"
|
|
244
341
|
)
|
|
245
342
|
if "OK" in (probe.stdout or ""):
|
|
246
|
-
|
|
343
|
+
stamp = await _read_version_stamp(host, cache_dir=cache_dir)
|
|
344
|
+
if stamp == _CODEX_VERSION:
|
|
345
|
+
_LOG.info("ensure_codex_installed: cache HIT (%s, %s)", cached, stamp)
|
|
346
|
+
return await _provision_task_home(host, shared_codex_path=cached)
|
|
347
|
+
if not install_if_missing:
|
|
348
|
+
_LOG.warning(
|
|
349
|
+
"ensure_codex_installed: cache stamp %r != pin %r but "
|
|
350
|
+
"install_if_missing=False; keeping cached binary (%s)",
|
|
351
|
+
stamp, _CODEX_VERSION, cached,
|
|
352
|
+
)
|
|
353
|
+
return await _provision_task_home(host, shared_codex_path=cached)
|
|
354
|
+
# Stamp missing (legacy warm cache) or != pin: the pin is
|
|
355
|
+
# authoritative on refresh — always the pinned release, never a
|
|
356
|
+
# host-copy (an operator's codex tracks upstream, not the pin).
|
|
357
|
+
_LOG.info(
|
|
358
|
+
"ensure_codex_installed: cache stamp %r != pin %r -> "
|
|
359
|
+
"re-downloading pinned release (%s)",
|
|
360
|
+
stamp, _CODEX_VERSION, cached,
|
|
361
|
+
)
|
|
362
|
+
hook_ctx.report_progress(None, f"Updating codex to {_CODEX_VERSION}…")
|
|
363
|
+
try:
|
|
364
|
+
await _download_codex_into_cache(
|
|
365
|
+
hook_ctx, cache_dir=cache_dir, cached=cached,
|
|
366
|
+
)
|
|
367
|
+
except Exception: # noqa: BLE001
|
|
368
|
+
# Offline fallback: the cached binary is known-executable
|
|
369
|
+
# (probe above), so a failed refresh logs and launches on it.
|
|
370
|
+
_LOG.warning(
|
|
371
|
+
"ensure_codex_installed: pinned refresh to %s failed; "
|
|
372
|
+
"keeping cached binary (%s)",
|
|
373
|
+
_CODEX_VERSION, cached, exc_info=True,
|
|
374
|
+
)
|
|
247
375
|
return await _provision_task_home(host, shared_codex_path=cached)
|
|
248
376
|
|
|
249
377
|
if not install_if_missing:
|
|
@@ -301,6 +429,12 @@ async def _install_into_cache_from_host(
|
|
|
301
429
|
f"codex cache seed completed but {cached!r} is still not "
|
|
302
430
|
f"executable on the host. Check the seed source {source!r}."
|
|
303
431
|
)
|
|
432
|
+
# Stamp the COPIED binary's own reported version (a host codex tracks
|
|
433
|
+
# upstream, so it need not equal the pin). Unprobeable → no stamp,
|
|
434
|
+
# and the next launch refreshes the cache to the authoritative pin.
|
|
435
|
+
version = await _probe_codex_version(host, cached)
|
|
436
|
+
if version is not None:
|
|
437
|
+
await _write_version_stamp(host, cache_dir=cache_dir, version=version)
|
|
304
438
|
_LOG.info("ensure_codex_installed: cache MISS -> seeded from %s", source)
|
|
305
439
|
|
|
306
440
|
|
|
@@ -411,8 +545,14 @@ async def _download_codex_into_cache(
|
|
|
411
545
|
f"codex download completed but {cached!r} is still not "
|
|
412
546
|
f"executable on the host."
|
|
413
547
|
)
|
|
548
|
+
# Stamp AFTER the binary: a stamp must never claim a version the
|
|
549
|
+
# cached binary is not. This tier only ever installs the pin.
|
|
550
|
+
await _write_version_stamp(
|
|
551
|
+
host, cache_dir=cache_dir, version=_CODEX_VERSION,
|
|
552
|
+
)
|
|
553
|
+
# Serves both the cold-cache fill and the stale-stamp pinned refresh.
|
|
414
554
|
_LOG.info(
|
|
415
|
-
"ensure_codex_installed:
|
|
555
|
+
"ensure_codex_installed: downloaded pinned release %s", url,
|
|
416
556
|
)
|
|
417
557
|
finally:
|
|
418
558
|
await host.run_command(
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""AGENTS.md composition for optio-codex: the profile, plus a thin wrapper over
|
|
2
|
+
the shared composer (``optio_agents.prompt.compose_instructions_file``), which
|
|
3
|
+
owns all of the text. The exclude list is resolved with the same function the
|
|
4
|
+
snapshot archive uses, so the section's preservation claims never drift."""
|
|
5
|
+
|
|
6
|
+
from optio_agents.prompt import AgentPromptProfile, compose_instructions_file
|
|
7
|
+
|
|
8
|
+
from optio_codex.snapshots import effective_workdir_exclude
|
|
9
|
+
|
|
10
|
+
__all__ = ["PROFILE", "compose_agents_md"]
|
|
11
|
+
|
|
12
|
+
PROFILE = AgentPromptProfile(
|
|
13
|
+
state_dir="home/.codex/",
|
|
14
|
+
state_dir_contents=(
|
|
15
|
+
"the codex session store (rollout files under `home/.codex/sessions`, "
|
|
16
|
+
"auth, config)"
|
|
17
|
+
),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def compose_agents_md(
|
|
22
|
+
consumer_instructions: str,
|
|
23
|
+
*,
|
|
24
|
+
workdir_exclude: list[str] | None = None,
|
|
25
|
+
documentation: str | None = None,
|
|
26
|
+
supports_resume: bool = True,
|
|
27
|
+
host_protocol: bool = True,
|
|
28
|
+
omit_task_framing: bool = False,
|
|
29
|
+
fs_isolation_dirs: list[tuple[str, str]] | None = None,
|
|
30
|
+
file_download: bool = False,
|
|
31
|
+
check_resume_log_every_message: bool = True,
|
|
32
|
+
) -> str:
|
|
33
|
+
"""Render <workdir>/AGENTS.md for an optio-codex task.
|
|
34
|
+
|
|
35
|
+
``workdir_exclude=None`` means the codex defaults
|
|
36
|
+
(``snapshots.effective_workdir_exclude``), not the bare framework ones."""
|
|
37
|
+
return compose_instructions_file(
|
|
38
|
+
consumer_instructions,
|
|
39
|
+
profile=PROFILE,
|
|
40
|
+
workdir_exclude=effective_workdir_exclude(workdir_exclude),
|
|
41
|
+
documentation=documentation,
|
|
42
|
+
supports_resume=supports_resume,
|
|
43
|
+
host_protocol=host_protocol,
|
|
44
|
+
omit_task_framing=omit_task_framing,
|
|
45
|
+
fs_isolation_dirs=fs_isolation_dirs,
|
|
46
|
+
file_download=file_download,
|
|
47
|
+
check_resume_log_every_message=check_resume_log_every_message,
|
|
48
|
+
)
|
|
@@ -16,6 +16,7 @@ from optio_core.models import BasicAuth, TaskInstance
|
|
|
16
16
|
|
|
17
17
|
from optio_agents import HookContext, RESUME_NOTICE, SYSTEM_MESSAGE_PREFIX, get_protocol
|
|
18
18
|
from optio_agents import seeds as _seeds
|
|
19
|
+
from optio_agents.fs_grants import fs_isolation_dirs
|
|
19
20
|
from optio_agents.input_listener import serialized, start_input_listener
|
|
20
21
|
from optio_agents.protocol.session import _SessionFailed, run_log_protocol_session
|
|
21
22
|
from optio_agents.account import EMPTY, accounts_to_metadata
|
|
@@ -303,6 +304,7 @@ async def run_codex_session(ctx: ProcessContext, config: CodexTaskConfig) -> Non
|
|
|
303
304
|
workdir_exclude=config.workdir_exclude,
|
|
304
305
|
supports_resume=config.supports_resume,
|
|
305
306
|
file_download=config.file_download,
|
|
307
|
+
fs_isolation_dirs=fs_isolation_dirs(config, host.workdir),
|
|
306
308
|
),
|
|
307
309
|
)
|
|
308
310
|
else:
|
|
@@ -891,6 +893,7 @@ async def _maybe_refresh_on_resume(
|
|
|
891
893
|
workdir_exclude=new_config.workdir_exclude,
|
|
892
894
|
supports_resume=new_config.supports_resume,
|
|
893
895
|
file_download=new_config.file_download,
|
|
896
|
+
fs_isolation_dirs=fs_isolation_dirs(new_config, host.workdir),
|
|
894
897
|
)
|
|
895
898
|
try:
|
|
896
899
|
existing = await hook_ctx.read_text_from_host("AGENTS.md", silent=True)
|
|
@@ -53,10 +53,17 @@ def _per_task_path(ctx: _FakeHookCtx) -> str:
|
|
|
53
53
|
return f"{ctx._host.workdir}/home/.local/bin/codex"
|
|
54
54
|
|
|
55
55
|
|
|
56
|
+
def _stamp(cache: pathlib.Path, version: str | None = None) -> None:
|
|
57
|
+
"""Plant a ``codex.version`` stamp (defaults to the current pin)."""
|
|
58
|
+
cache.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
(cache / "codex.version").write_text(version or host_actions._CODEX_VERSION)
|
|
60
|
+
|
|
61
|
+
|
|
56
62
|
@pytest.mark.asyncio
|
|
57
63
|
async def test_cache_hit_returns_per_task_symlink_into_cache(tmp_path, monkeypatch):
|
|
58
64
|
cache = tmp_path / "cache"
|
|
59
65
|
_write_exe(cache / "codex")
|
|
66
|
+
_stamp(cache) # stamp == pin → plain hit
|
|
60
67
|
|
|
61
68
|
# A cache hit must not consult the host codex at all.
|
|
62
69
|
async def _boom(*a, **k): # noqa: ANN002, ANN003
|
|
@@ -117,6 +124,7 @@ async def test_default_cache_dir_from_env(tmp_path, monkeypatch):
|
|
|
117
124
|
cache dir — never the workdir, never the operator's ~/.codex."""
|
|
118
125
|
cache = tmp_path / "oai-cache" / "bin"
|
|
119
126
|
_write_exe(cache / "codex")
|
|
127
|
+
_stamp(cache)
|
|
120
128
|
monkeypatch.setenv("OPTIO_CODEX_CACHE_DIR", str(cache))
|
|
121
129
|
ctx = await _local_ctx(tmp_path)
|
|
122
130
|
|
|
@@ -181,10 +189,15 @@ async def test_cache_miss_no_host_codex_downloads_release(tmp_path, monkeypatch)
|
|
|
181
189
|
assert (cache / "codex").is_file()
|
|
182
190
|
assert os.access(cache / "codex", os.X_OK)
|
|
183
191
|
assert (cache / "codex").read_bytes().startswith(b"#!/bin/bash")
|
|
192
|
+
# …the pinned version was stamped alongside it…
|
|
193
|
+
assert (cache / "codex.version").read_text() == host_actions._CODEX_VERSION
|
|
184
194
|
# …behind the per-task launch symlink, and no temp litter remains.
|
|
185
195
|
assert result == _per_task_path(ctx)
|
|
186
196
|
assert os.path.realpath(result) == os.path.realpath(str(cache / "codex"))
|
|
187
|
-
leftovers = [
|
|
197
|
+
leftovers = [
|
|
198
|
+
p.name for p in cache.iterdir()
|
|
199
|
+
if p.name not in {"codex", "codex.version"}
|
|
200
|
+
]
|
|
188
201
|
assert leftovers == [], leftovers
|
|
189
202
|
|
|
190
203
|
|
|
@@ -258,7 +271,11 @@ async def test_concurrent_cold_cache_downloads_do_not_corrupt(
|
|
|
258
271
|
assert (cache / "codex").is_file()
|
|
259
272
|
assert os.access(cache / "codex", os.X_OK)
|
|
260
273
|
assert (cache / "codex").read_bytes().startswith(b"#!/bin/bash")
|
|
261
|
-
|
|
274
|
+
assert (cache / "codex.version").read_text() == host_actions._CODEX_VERSION
|
|
275
|
+
leftovers = [
|
|
276
|
+
p.name for p in cache.iterdir()
|
|
277
|
+
if p.name not in {"codex", "codex.version"}
|
|
278
|
+
]
|
|
262
279
|
assert leftovers == [], leftovers
|
|
263
280
|
|
|
264
281
|
|
|
@@ -322,3 +339,209 @@ async def test_download_multi_member_tarball_rejected(tmp_path, monkeypatch):
|
|
|
322
339
|
|
|
323
340
|
with pytest.raises(RuntimeError, match="exactly one"):
|
|
324
341
|
await host_actions.ensure_codex_installed(ctx, install_dir=str(cache))
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# --- pin-stamp freshness (agent-binary-freshness plan Task 4) ---------------
|
|
345
|
+
#
|
|
346
|
+
# The cache is claustrum---rox to confined sessions, so freshness is owned by
|
|
347
|
+
# this unconfined provisioning path: both install tiers stamp
|
|
348
|
+
# <cache>/codex.version, and a cache hit compares the stamp against the
|
|
349
|
+
# _CODEX_VERSION pin — a pin bump becomes effective on warm caches.
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class _NoDownloadCtx(_FakeHookCtx):
|
|
353
|
+
"""Fake hook_ctx that must never be asked to download."""
|
|
354
|
+
|
|
355
|
+
async def download_file(self, url: str, dest: str) -> None:
|
|
356
|
+
raise AssertionError(f"download_file must not be called (url={url})")
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class _FailingDownloadCtx(_FakeHookCtx):
|
|
360
|
+
"""Fake hook_ctx whose download always fails (offline worker)."""
|
|
361
|
+
|
|
362
|
+
def __init__(self, host) -> None: # noqa: ANN001
|
|
363
|
+
super().__init__(host)
|
|
364
|
+
self.attempts = 0
|
|
365
|
+
|
|
366
|
+
async def download_file(self, url: str, dest: str) -> None:
|
|
367
|
+
self.attempts += 1
|
|
368
|
+
raise RuntimeError("network down")
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@pytest.mark.asyncio
|
|
372
|
+
async def test_cache_hit_matching_stamp_skips_download(tmp_path, monkeypatch):
|
|
373
|
+
"""Warm cache whose stamp equals the pin → no download, no host-copy."""
|
|
374
|
+
cache = tmp_path / "cache"
|
|
375
|
+
_write_exe(cache / "codex")
|
|
376
|
+
_stamp(cache)
|
|
377
|
+
|
|
378
|
+
async def _boom(*a, **k): # noqa: ANN002, ANN003
|
|
379
|
+
raise AssertionError("resolve_codex must not be called on a stamped hit")
|
|
380
|
+
|
|
381
|
+
monkeypatch.setattr(host_actions, "resolve_codex", _boom)
|
|
382
|
+
host = LocalHost(taskdir=str(tmp_path / "task"))
|
|
383
|
+
await host.setup_workdir()
|
|
384
|
+
ctx = _NoDownloadCtx(host)
|
|
385
|
+
|
|
386
|
+
result = await host_actions.ensure_codex_installed(ctx, install_dir=str(cache))
|
|
387
|
+
assert result == _per_task_path(ctx)
|
|
388
|
+
assert os.path.realpath(result) == os.path.realpath(str(cache / "codex"))
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@pytest.mark.asyncio
|
|
392
|
+
@pytest.mark.parametrize("stale_stamp", [None, "0.1.0"])
|
|
393
|
+
async def test_cache_hit_stale_or_missing_stamp_redownloads_pin(
|
|
394
|
+
tmp_path, monkeypatch, stale_stamp,
|
|
395
|
+
):
|
|
396
|
+
"""Stamp missing (legacy warm cache) or != pin → the pinned release is
|
|
397
|
+
re-downloaded into the cache (never host-copied: the pin is
|
|
398
|
+
authoritative on refresh) and the stamp is rewritten to the pin."""
|
|
399
|
+
cache = tmp_path / "cache"
|
|
400
|
+
_write_exe(cache / "codex", body="#!/bin/bash\necho stale-codex\n")
|
|
401
|
+
if stale_stamp is not None:
|
|
402
|
+
_stamp(cache, stale_stamp)
|
|
403
|
+
|
|
404
|
+
async def _boom(*a, **k): # noqa: ANN002, ANN003
|
|
405
|
+
raise AssertionError("refresh must never host-copy; pin is authoritative")
|
|
406
|
+
|
|
407
|
+
monkeypatch.setattr(host_actions, "resolve_codex", _boom)
|
|
408
|
+
host = LocalHost(taskdir=str(tmp_path / "task"))
|
|
409
|
+
await host.setup_workdir()
|
|
410
|
+
ctx = _DownloadingHookCtx(host, _fake_release_tarball())
|
|
411
|
+
|
|
412
|
+
result = await host_actions.ensure_codex_installed(ctx, install_dir=str(cache))
|
|
413
|
+
|
|
414
|
+
# The pinned release was fetched and installed over the stale binary…
|
|
415
|
+
assert len(ctx.urls) == 1
|
|
416
|
+
assert host_actions._CODEX_VERSION in ctx.urls[0]
|
|
417
|
+
assert (cache / "codex").read_bytes().startswith(b"#!/bin/bash")
|
|
418
|
+
assert b"downloaded-codex" in (cache / "codex").read_bytes()
|
|
419
|
+
# …the stamp now records the pin…
|
|
420
|
+
assert (cache / "codex.version").read_text() == host_actions._CODEX_VERSION
|
|
421
|
+
# …and the per-task symlink resolves into the refreshed cache.
|
|
422
|
+
assert result == _per_task_path(ctx)
|
|
423
|
+
assert os.path.realpath(result) == os.path.realpath(str(cache / "codex"))
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@pytest.mark.asyncio
|
|
427
|
+
async def test_cache_hit_refresh_download_failure_falls_back_to_cached(
|
|
428
|
+
tmp_path, monkeypatch,
|
|
429
|
+
):
|
|
430
|
+
"""Offline fallback: a failed pinned refresh with an executable cached
|
|
431
|
+
binary logs and launches on the cached one (stale pin beats no engine)."""
|
|
432
|
+
cache = tmp_path / "cache"
|
|
433
|
+
_write_exe(cache / "codex", body="#!/bin/bash\necho stale-codex\n")
|
|
434
|
+
_stamp(cache, "0.1.0")
|
|
435
|
+
|
|
436
|
+
async def _boom(*a, **k): # noqa: ANN002, ANN003
|
|
437
|
+
raise AssertionError("refresh must never host-copy")
|
|
438
|
+
|
|
439
|
+
monkeypatch.setattr(host_actions, "resolve_codex", _boom)
|
|
440
|
+
host = LocalHost(taskdir=str(tmp_path / "task"))
|
|
441
|
+
await host.setup_workdir()
|
|
442
|
+
ctx = _FailingDownloadCtx(host)
|
|
443
|
+
|
|
444
|
+
result = await host_actions.ensure_codex_installed(ctx, install_dir=str(cache))
|
|
445
|
+
|
|
446
|
+
assert ctx.attempts == 1 # the refresh WAS attempted…
|
|
447
|
+
# …but the launch proceeds on the untouched cached binary,
|
|
448
|
+
assert result == _per_task_path(ctx)
|
|
449
|
+
assert b"stale-codex" in (cache / "codex").read_bytes()
|
|
450
|
+
# and the stamp stays stale so the NEXT launch retries the refresh.
|
|
451
|
+
assert (cache / "codex.version").read_text() == "0.1.0"
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
@pytest.mark.asyncio
|
|
455
|
+
async def test_cache_hit_stale_stamp_install_if_missing_false_keeps_cached(
|
|
456
|
+
tmp_path, monkeypatch,
|
|
457
|
+
):
|
|
458
|
+
"""install_if_missing=False means no network ever: a stale stamp keeps
|
|
459
|
+
the cached binary without attempting the pinned refresh (mirrors grok's
|
|
460
|
+
install_if_missing gate on its update probe)."""
|
|
461
|
+
cache = tmp_path / "cache"
|
|
462
|
+
_write_exe(cache / "codex", body="#!/bin/bash\necho stale-codex\n")
|
|
463
|
+
_stamp(cache, "0.1.0")
|
|
464
|
+
|
|
465
|
+
async def _boom(*a, **k): # noqa: ANN002, ANN003
|
|
466
|
+
raise AssertionError("must not resolve host codex")
|
|
467
|
+
|
|
468
|
+
monkeypatch.setattr(host_actions, "resolve_codex", _boom)
|
|
469
|
+
host = LocalHost(taskdir=str(tmp_path / "task"))
|
|
470
|
+
await host.setup_workdir()
|
|
471
|
+
ctx = _NoDownloadCtx(host)
|
|
472
|
+
|
|
473
|
+
result = await host_actions.ensure_codex_installed(
|
|
474
|
+
ctx, install_dir=str(cache), install_if_missing=False,
|
|
475
|
+
)
|
|
476
|
+
assert result == _per_task_path(ctx)
|
|
477
|
+
assert b"stale-codex" in (cache / "codex").read_bytes()
|
|
478
|
+
assert (cache / "codex.version").read_text() == "0.1.0"
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
# A fake host codex whose --version mimics the REAL binary's output shape
|
|
482
|
+
# (verified 2026-08-13 against ~/.cache/optio-codex/bin/codex: stdout is
|
|
483
|
+
# exactly ``codex-cli <semver>``; stderr may carry a PATH-alias warning).
|
|
484
|
+
_VERSIONED_FAKE = (
|
|
485
|
+
"#!/bin/bash\n"
|
|
486
|
+
'if [ "$1" = "--version" ]; then\n'
|
|
487
|
+
' echo "WARNING: could not create PATH aliases" >&2\n'
|
|
488
|
+
' echo "codex-cli 9.9.9"\n'
|
|
489
|
+
" exit 0\n"
|
|
490
|
+
"fi\n"
|
|
491
|
+
"echo fake-codex\n"
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
@pytest.mark.asyncio
|
|
496
|
+
async def test_host_copy_seed_stamps_probed_version(tmp_path, monkeypatch):
|
|
497
|
+
"""The host-copy tier stamps the COPIED binary's own reported version
|
|
498
|
+
(which need not equal the pin — a host codex tracks upstream)."""
|
|
499
|
+
cache = tmp_path / "cache"
|
|
500
|
+
cache.mkdir() # empty → miss
|
|
501
|
+
source = _write_exe(tmp_path / "hostbin" / "codex", body=_VERSIONED_FAKE)
|
|
502
|
+
|
|
503
|
+
async def _resolve(host, *, install_dir=None, install_if_missing=True): # noqa: ANN001
|
|
504
|
+
return str(source)
|
|
505
|
+
|
|
506
|
+
monkeypatch.setattr(host_actions, "resolve_codex", _resolve)
|
|
507
|
+
ctx = await _local_ctx(tmp_path)
|
|
508
|
+
|
|
509
|
+
result = await host_actions.ensure_codex_installed(ctx, install_dir=str(cache))
|
|
510
|
+
assert result == _per_task_path(ctx)
|
|
511
|
+
assert (cache / "codex.version").read_text() == "9.9.9"
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
@pytest.mark.asyncio
|
|
515
|
+
async def test_host_copy_unprobeable_version_writes_no_stamp(
|
|
516
|
+
tmp_path, monkeypatch,
|
|
517
|
+
):
|
|
518
|
+
"""A copied binary whose --version output does not parse gets NO stamp —
|
|
519
|
+
the next launch then refreshes the cache to the authoritative pin."""
|
|
520
|
+
cache = tmp_path / "cache"
|
|
521
|
+
cache.mkdir()
|
|
522
|
+
source = _write_exe(tmp_path / "hostbin" / "codex") # echoes fake-codex
|
|
523
|
+
|
|
524
|
+
async def _resolve(host, *, install_dir=None, install_if_missing=True): # noqa: ANN001
|
|
525
|
+
return str(source)
|
|
526
|
+
|
|
527
|
+
monkeypatch.setattr(host_actions, "resolve_codex", _resolve)
|
|
528
|
+
ctx = await _local_ctx(tmp_path)
|
|
529
|
+
|
|
530
|
+
result = await host_actions.ensure_codex_installed(ctx, install_dir=str(cache))
|
|
531
|
+
assert result == _per_task_path(ctx)
|
|
532
|
+
assert not (cache / "codex.version").exists()
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def test_version_output_regex_matches_real_binary_shape():
|
|
536
|
+
"""Anchor the parse to the REAL ``codex --version`` stdout (v0.142.5)."""
|
|
537
|
+
m = host_actions._CODEX_VERSION_OUTPUT_RE.search("codex-cli 0.142.5\n")
|
|
538
|
+
assert m is not None and m.group(1) == "0.142.5"
|
|
539
|
+
assert host_actions._CODEX_VERSION_OUTPUT_RE.search("fake-codex\n") is None
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def test_sshd_mount_stamp_tracks_pin():
|
|
543
|
+
"""tests/codex.version is compose-mounted beside the remote shim as its
|
|
544
|
+
cache stamp; it must track the _CODEX_VERSION pin, or the remote suite
|
|
545
|
+
would attempt a real pinned re-download. Update it on every pin bump."""
|
|
546
|
+
stamp = pathlib.Path(__file__).parent / "codex.version"
|
|
547
|
+
assert stamp.read_text().strip() == host_actions._CODEX_VERSION
|
|
@@ -16,6 +16,7 @@ from unittest.mock import AsyncMock, MagicMock
|
|
|
16
16
|
import pytest
|
|
17
17
|
|
|
18
18
|
from optio_agents import get_protocol
|
|
19
|
+
from optio_agents.fs_grants import fs_isolation_dirs
|
|
19
20
|
from optio_codex.prompt import compose_agents_md
|
|
20
21
|
from optio_codex.types import CodexTaskConfig
|
|
21
22
|
|
|
@@ -239,7 +240,7 @@ async def test_restore_workdir_blob_decrypts(monkeypatch):
|
|
|
239
240
|
# --- P2 unit: _maybe_refresh_on_resume -------------------------------------
|
|
240
241
|
|
|
241
242
|
|
|
242
|
-
def _resume_fakes(existing_agents_md: str):
|
|
243
|
+
def _resume_fakes(existing_agents_md: str, workdir: str = "/workdir"):
|
|
243
244
|
"""Build (host, hook_ctx) fakes whose read returns ``existing_agents_md``
|
|
244
245
|
and whose write records the rewritten body."""
|
|
245
246
|
written: dict[str, str] = {}
|
|
@@ -249,6 +250,7 @@ def _resume_fakes(existing_agents_md: str):
|
|
|
249
250
|
|
|
250
251
|
fake_host = MagicMock()
|
|
251
252
|
fake_host.write_text = _write_text
|
|
253
|
+
fake_host.workdir = workdir
|
|
252
254
|
|
|
253
255
|
fake_hook = MagicMock()
|
|
254
256
|
fake_hook.read_text_from_host = AsyncMock(return_value=existing_agents_md)
|
|
@@ -260,6 +262,7 @@ async def test_maybe_refresh_identity_unchanged_is_noop():
|
|
|
260
262
|
|
|
261
263
|
protocol = get_protocol(browser="redirect")
|
|
262
264
|
cfg = _cfg(consumer_instructions="orig")
|
|
265
|
+
workdir = "/workdir"
|
|
263
266
|
existing = compose_agents_md(
|
|
264
267
|
cfg.consumer_instructions,
|
|
265
268
|
documentation=protocol.documentation if cfg.host_protocol else None,
|
|
@@ -267,8 +270,9 @@ async def test_maybe_refresh_identity_unchanged_is_noop():
|
|
|
267
270
|
workdir_exclude=cfg.workdir_exclude,
|
|
268
271
|
supports_resume=cfg.supports_resume,
|
|
269
272
|
file_download=cfg.file_download,
|
|
273
|
+
fs_isolation_dirs=fs_isolation_dirs(cfg, workdir),
|
|
270
274
|
)
|
|
271
|
-
host, hook, written = _resume_fakes(existing)
|
|
275
|
+
host, hook, written = _resume_fakes(existing, workdir=workdir)
|
|
272
276
|
|
|
273
277
|
refreshed = await _maybe_refresh_on_resume(host, hook, cfg, protocol)
|
|
274
278
|
assert refreshed == []
|
|
@@ -285,12 +289,14 @@ async def test_maybe_refresh_mutating_hook_rewrites_agents_md():
|
|
|
285
289
|
return dataclasses.replace(c, consumer_instructions="UPDATED INSTRUCTIONS")
|
|
286
290
|
|
|
287
291
|
cfg = _cfg(consumer_instructions="orig", on_resume_refresh=_bump)
|
|
292
|
+
workdir = "/workdir"
|
|
288
293
|
stale = compose_agents_md(
|
|
289
294
|
"orig",
|
|
290
295
|
documentation=protocol.documentation,
|
|
291
296
|
host_protocol=True,
|
|
297
|
+
fs_isolation_dirs=fs_isolation_dirs(cfg, workdir),
|
|
292
298
|
)
|
|
293
|
-
host, hook, written = _resume_fakes(stale)
|
|
299
|
+
host, hook, written = _resume_fakes(stale, workdir=workdir)
|
|
294
300
|
|
|
295
301
|
refreshed = await _maybe_refresh_on_resume(host, hook, cfg, protocol)
|
|
296
302
|
assert refreshed == ["AGENTS.md"]
|
|
@@ -84,3 +84,13 @@ def test_host_protocol_false_keeps_resume_section_and_explainer():
|
|
|
84
84
|
# passing; same caveat as test_host_protocol_false_adds_system_explainer.)
|
|
85
85
|
assert get_protocol(browser="redirect").documentation not in md
|
|
86
86
|
assert "## Log channel" not in md
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_sandbox_note_and_no_prompt_text_of_its_own():
|
|
90
|
+
import inspect
|
|
91
|
+
import optio_codex.prompt as m
|
|
92
|
+
out = compose_agents_md("x", fs_isolation_dirs=[("/wd", "rwx")])
|
|
93
|
+
assert "**Filesystem access:**" in out
|
|
94
|
+
src = inspect.getsource(m)
|
|
95
|
+
assert "This harness may pause your session" not in src
|
|
96
|
+
assert "originate from the harness" not in src
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
"""AGENTS.md composition for optio-codex.
|
|
2
|
-
|
|
3
|
-
Codex reads an ``AGENTS.md`` file in its workdir. The shared framing and
|
|
4
|
-
the keyword-protocol documentation are owned by ``optio-agents`` (the
|
|
5
|
-
prompt SSOT); this module threads codex's protocol mode through and owns
|
|
6
|
-
the codex-specific resume-awareness section (Stage 2), rendered from the
|
|
7
|
-
EFFECTIVE snapshot exclude list so its preservation claims never drift
|
|
8
|
-
from what the snapshot actually keeps.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from optio_agents.prompt import compose_agents_md as _compose_agents_md_host
|
|
12
|
-
from optio_agents.prompt import downloadables_block
|
|
13
|
-
from optio_agents.protocol import ProtocolFeatures, build_log_channel_prompt
|
|
14
|
-
|
|
15
|
-
from optio_codex.snapshots import effective_workdir_exclude
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
# Self-contained System: explainer for sessions without the keyword-protocol
|
|
19
|
-
# docs (which normally explain the convention). Per-wrapper copy is the
|
|
20
|
-
# established pattern (claudecode/opencode/grok each carry their own).
|
|
21
|
-
_SYSTEM_PREFIX_EXPLAINER = """\
|
|
22
|
-
(Messages prefixed `System:` on your input channel originate from the
|
|
23
|
-
harness coordinating this session, not from the human user.)
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
RESUME_SECTION_TEMPLATE = """## Resumes
|
|
28
|
-
|
|
29
|
-
This harness may pause your session, save your context to a database,
|
|
30
|
-
terminate the underlying process, and later rehydrate it. From your
|
|
31
|
-
point of view the conversation is fully continuous — you keep your
|
|
32
|
-
prior context and will not "notice" the resume.
|
|
33
|
-
|
|
34
|
-
**A resume can happen at any point, not only at the start.** The host
|
|
35
|
-
environment may have changed across a resume — different host,
|
|
36
|
-
different running processes, files outside this workdir gone — even
|
|
37
|
-
though your context remembers everything as alive and well.
|
|
38
|
-
|
|
39
|
-
**The workdir (this directory) is preserved across resumes, with two
|
|
40
|
-
caveats:**
|
|
41
|
-
|
|
42
|
-
- {excludes_clause}
|
|
43
|
-
- **Anything outside the workdir is not preserved.**
|
|
44
|
-
|
|
45
|
-
- **Your `home/.codex/` directory — the codex session store (rollout
|
|
46
|
-
files under `home/.codex/sessions`, auth, config) — IS preserved
|
|
47
|
-
across resumes** (minus the excluded paths above), so your history
|
|
48
|
-
travels with you even when the underlying process and host change.
|
|
49
|
-
|
|
50
|
-
{outside_clause}
|
|
51
|
-
|
|
52
|
-
### Detecting a resume: `resume.log`
|
|
53
|
-
|
|
54
|
-
Each session start (fresh or resumed) appends one line to
|
|
55
|
-
`./resume.log`. Line format:
|
|
56
|
-
|
|
57
|
-
```
|
|
58
|
-
<ISO 8601 UTC timestamp>[ REFRESHED:<comma-separated filenames>]
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
The very first line is the original launch timestamp; each subsequent
|
|
62
|
-
line is a resume. The optional `REFRESHED:` suffix signals that the
|
|
63
|
-
harness rewrote the listed files on that resume (e.g.
|
|
64
|
-
`2026-05-28T13:15:42Z REFRESHED:AGENTS.md`) — your in-memory copy of
|
|
65
|
-
those files is stale and must be re-read before continuing.
|
|
66
|
-
|
|
67
|
-
**At the start of every new incoming user message, read
|
|
68
|
-
`./resume.log` first.** Compare the latest line to the value you
|
|
69
|
-
remembered last time you checked. If a new line has appeared, treat
|
|
70
|
-
the situation as a resume:
|
|
71
|
-
|
|
72
|
-
- Verify any tools, processes, or files you previously gathered
|
|
73
|
-
outside the workdir are still where you left them.
|
|
74
|
-
- Re-establish anything that's gone (re-launch a server, re-fetch a
|
|
75
|
-
file, etc.) before continuing.
|
|
76
|
-
- **If the latest line carries a `REFRESHED:` suffix, re-read each
|
|
77
|
-
listed file** (e.g. `cat ./AGENTS.md`) — the harness updated it
|
|
78
|
-
since your last context snapshot and the version you remember is
|
|
79
|
-
out of date.
|
|
80
|
-
- Then resume the work you were doing.
|
|
81
|
-
|
|
82
|
-
If a resume slips past unnoticed, a failing tool call is the
|
|
83
|
-
next-best signal — re-check `./resume.log` then.
|
|
84
|
-
|
|
85
|
-
You may also be notified of a resume by a `System:` message on your input
|
|
86
|
-
channel; when you see one, follow the `resume.log` procedure above.
|
|
87
|
-
"""
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def _render_resume_section(workdir_exclude: list[str] | None) -> str:
|
|
91
|
-
"""Render RESUME_SECTION_TEMPLATE with the EFFECTIVE exclude list.
|
|
92
|
-
|
|
93
|
-
``effective_workdir_exclude`` is the same resolver the snapshot archive
|
|
94
|
-
uses (``None`` → the codex defaults), so what this section claims is
|
|
95
|
-
preserved is exactly what the snapshot preserves.
|
|
96
|
-
"""
|
|
97
|
-
effective = effective_workdir_exclude(workdir_exclude)
|
|
98
|
-
if not effective:
|
|
99
|
-
excludes_clause = (
|
|
100
|
-
"**No paths are excluded** — every file in the workdir is preserved."
|
|
101
|
-
)
|
|
102
|
-
outside_clause = (
|
|
103
|
-
"If you need to stash large data, place it outside the workdir "
|
|
104
|
-
"(e.g. `/tmp/`) — but remember it may be missing when you next look."
|
|
105
|
-
)
|
|
106
|
-
else:
|
|
107
|
-
excludes_str = ", ".join(f"`{p}`" for p in effective)
|
|
108
|
-
excludes_clause = (
|
|
109
|
-
f"**Paths matching the snapshot exclude list are NOT preserved**, "
|
|
110
|
-
f"even inside the workdir. The current exclude list is: {excludes_str}."
|
|
111
|
-
)
|
|
112
|
-
outside_clause = (
|
|
113
|
-
"If you need to stash large data, place it outside the workdir "
|
|
114
|
-
"(e.g. `/tmp/`) or inside an excluded subdirectory — but remember "
|
|
115
|
-
"any such location may be missing when you next look."
|
|
116
|
-
)
|
|
117
|
-
return RESUME_SECTION_TEMPLATE.format(
|
|
118
|
-
excludes_clause=excludes_clause,
|
|
119
|
-
outside_clause=outside_clause,
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def compose_agents_md(
|
|
124
|
-
consumer_instructions: str,
|
|
125
|
-
*,
|
|
126
|
-
documentation: str | None = None,
|
|
127
|
-
host_protocol: bool = True,
|
|
128
|
-
workdir_exclude: list[str] | None = None,
|
|
129
|
-
supports_resume: bool = True,
|
|
130
|
-
file_download: bool = False,
|
|
131
|
-
) -> str:
|
|
132
|
-
"""Build the full AGENTS.md body.
|
|
133
|
-
|
|
134
|
-
``documentation`` is the keyword-protocol block; the session passes
|
|
135
|
-
``get_protocol(browser="redirect").documentation``. Defaults (for unit
|
|
136
|
-
tests / standalone callers) to codex's ``redirect`` docs. It must
|
|
137
|
-
always come from the session's ``Protocol`` where one exists — never
|
|
138
|
-
rebuild features at a second site.
|
|
139
|
-
|
|
140
|
-
``host_protocol=False`` omits the keyword-protocol documentation and
|
|
141
|
-
instead includes a self-contained ``System:`` message explainer
|
|
142
|
-
(guide Part 2D); iframe mode always runs with ``host_protocol=True``
|
|
143
|
-
(validated in ``CodexTaskConfig``), the False branch serves
|
|
144
|
-
conversation mode in a later stage.
|
|
145
|
-
|
|
146
|
-
``supports_resume=True`` (default) appends the resume-awareness section
|
|
147
|
-
so the agent watches ``resume.log`` and knows ``home/.codex`` (minus
|
|
148
|
-
``workdir_exclude``) survives across resumes. ``workdir_exclude`` is
|
|
149
|
-
this task's snapshot exclude list (None → the codex defaults), used to
|
|
150
|
-
keep the section's claims in sync with what is actually preserved.
|
|
151
|
-
|
|
152
|
-
``file_download=True`` appends the downloadables instruction block so
|
|
153
|
-
codex offers files to the human via the ``optio-file:`` sentinel link
|
|
154
|
-
(conversation_ui file-download feature); the wording is comparative when
|
|
155
|
-
the keyword protocol is active (``host_protocol``).
|
|
156
|
-
"""
|
|
157
|
-
if file_download:
|
|
158
|
-
consumer_instructions = (
|
|
159
|
-
consumer_instructions.rstrip()
|
|
160
|
-
+ downloadables_block(comparative=host_protocol)
|
|
161
|
-
)
|
|
162
|
-
if host_protocol:
|
|
163
|
-
if documentation is None:
|
|
164
|
-
documentation = build_log_channel_prompt(
|
|
165
|
-
ProtocolFeatures(browser="redirect")
|
|
166
|
-
)
|
|
167
|
-
else:
|
|
168
|
-
documentation = None
|
|
169
|
-
resume_section: str | None = (
|
|
170
|
-
_render_resume_section(workdir_exclude) if supports_resume else None
|
|
171
|
-
)
|
|
172
|
-
if not host_protocol:
|
|
173
|
-
# The protocol docs normally explain the `System:` convention;
|
|
174
|
-
# without them the composed prompt carries its own explainer.
|
|
175
|
-
resume_section = (
|
|
176
|
-
resume_section + _SYSTEM_PREFIX_EXPLAINER
|
|
177
|
-
if resume_section
|
|
178
|
-
else _SYSTEM_PREFIX_EXPLAINER
|
|
179
|
-
)
|
|
180
|
-
return _compose_agents_md_host(
|
|
181
|
-
consumer_instructions,
|
|
182
|
-
documentation=documentation,
|
|
183
|
-
resume_section=resume_section,
|
|
184
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|