optio-codex 0.4.1__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.
Files changed (63) hide show
  1. {optio_codex-0.4.1 → optio_codex-0.4.2}/PKG-INFO +1 -1
  2. {optio_codex-0.4.1 → optio_codex-0.4.2}/pyproject.toml +1 -1
  3. {optio_codex-0.4.1 → optio_codex-0.4.2}/src/optio_codex/conversation.py +3 -1
  4. optio_codex-0.4.2/src/optio_codex/prompt.py +48 -0
  5. {optio_codex-0.4.1 → optio_codex-0.4.2}/src/optio_codex/session.py +3 -0
  6. {optio_codex-0.4.1 → optio_codex-0.4.2}/src/optio_codex.egg-info/PKG-INFO +1 -1
  7. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_config_hooks.py +9 -3
  8. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_prompt.py +10 -0
  9. optio_codex-0.4.1/src/optio_codex/prompt.py +0 -184
  10. {optio_codex-0.4.1 → optio_codex-0.4.2}/README.md +0 -0
  11. {optio_codex-0.4.1 → optio_codex-0.4.2}/setup.cfg +0 -0
  12. {optio_codex-0.4.1 → optio_codex-0.4.2}/src/optio_codex/__init__.py +0 -0
  13. {optio_codex-0.4.1 → optio_codex-0.4.2}/src/optio_codex/account.py +0 -0
  14. {optio_codex-0.4.1 → optio_codex-0.4.2}/src/optio_codex/conversation_listener.py +0 -0
  15. {optio_codex-0.4.1 → optio_codex-0.4.2}/src/optio_codex/cred_watcher.py +0 -0
  16. {optio_codex-0.4.1 → optio_codex-0.4.2}/src/optio_codex/fs_allowlist.py +0 -0
  17. {optio_codex-0.4.1 → optio_codex-0.4.2}/src/optio_codex/host_actions.py +0 -0
  18. {optio_codex-0.4.1 → optio_codex-0.4.2}/src/optio_codex/info.py +0 -0
  19. {optio_codex-0.4.1 → optio_codex-0.4.2}/src/optio_codex/models.py +0 -0
  20. {optio_codex-0.4.1 → optio_codex-0.4.2}/src/optio_codex/rollout.py +0 -0
  21. {optio_codex-0.4.1 → optio_codex-0.4.2}/src/optio_codex/seed_manifest.py +0 -0
  22. {optio_codex-0.4.1 → optio_codex-0.4.2}/src/optio_codex/snapshots.py +0 -0
  23. {optio_codex-0.4.1 → optio_codex-0.4.2}/src/optio_codex/types.py +0 -0
  24. {optio_codex-0.4.1 → optio_codex-0.4.2}/src/optio_codex/verify.py +0 -0
  25. {optio_codex-0.4.1 → optio_codex-0.4.2}/src/optio_codex.egg-info/SOURCES.txt +0 -0
  26. {optio_codex-0.4.1 → optio_codex-0.4.2}/src/optio_codex.egg-info/dependency_links.txt +0 -0
  27. {optio_codex-0.4.1 → optio_codex-0.4.2}/src/optio_codex.egg-info/requires.txt +0 -0
  28. {optio_codex-0.4.1 → optio_codex-0.4.2}/src/optio_codex.egg-info/top_level.txt +0 -0
  29. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_account.py +0 -0
  30. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_agent_info.py +0 -0
  31. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_await_codex_gone.py +0 -0
  32. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_claustrum.py +0 -0
  33. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_codex_cache.py +0 -0
  34. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_config.py +0 -0
  35. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_conversation.py +0 -0
  36. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_conversation_controls.py +0 -0
  37. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_conversation_listener.py +0 -0
  38. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_cred_watcher.py +0 -0
  39. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_file_download.py +0 -0
  40. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_file_upload.py +0 -0
  41. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_fs_allowlist.py +0 -0
  42. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_host_actions.py +0 -0
  43. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_import.py +0 -0
  44. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_input_wiring.py +0 -0
  45. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_kill_ttyd_by_socket.py +0 -0
  46. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_models.py +0 -0
  47. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_real_codex_conversation.py +0 -0
  48. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_real_codex_seed_resume.py +0 -0
  49. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_real_codex_session.py +0 -0
  50. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_rollout.py +0 -0
  51. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_sandbox_enforce.py +0 -0
  52. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_seed_manifest.py +0 -0
  53. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_session_conversation.py +0 -0
  54. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_session_lease.py +0 -0
  55. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_session_local.py +0 -0
  56. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_session_remote.py +0 -0
  57. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_session_resume.py +0 -0
  58. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_session_sandbox.py +0 -0
  59. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_session_seed.py +0 -0
  60. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_snapshots.py +0 -0
  61. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_teardown_session_tree.py +0 -0
  62. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_verify.py +0 -0
  63. {optio_codex-0.4.1 → optio_codex-0.4.2}/tests/test_workdir_trust.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: optio-codex
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: Run OpenAI Codex as an optio task; local subprocess; ttyd-served TUI iframe.
5
5
  Author-email: Kristof Csillag <kristof.csillag@deai-labs.com>
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "optio-codex"
7
- version = "0.4.1"
7
+ version = "0.4.2"
8
8
  description = "Run OpenAI Codex as an optio task; local subprocess; ttyd-served TUI iframe."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.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:
@@ -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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: optio-codex
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: Run OpenAI Codex as an optio task; local subprocess; ttyd-served TUI iframe.
5
5
  Author-email: Kristof Csillag <kristof.csillag@deai-labs.com>
6
6
  License-Expression: Apache-2.0
@@ -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