optio-codex 0.1.0__py3-none-any.whl
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/__init__.py +66 -0
- optio_codex/conversation.py +553 -0
- optio_codex/conversation_listener.py +322 -0
- optio_codex/cred_watcher.py +138 -0
- optio_codex/fs_allowlist.py +149 -0
- optio_codex/host_actions.py +1070 -0
- optio_codex/models.py +68 -0
- optio_codex/prompt.py +184 -0
- optio_codex/seed_manifest.py +91 -0
- optio_codex/session.py +731 -0
- optio_codex/snapshots.py +147 -0
- optio_codex/types.py +325 -0
- optio_codex/verify.py +352 -0
- optio_codex-0.1.0.dist-info/METADATA +220 -0
- optio_codex-0.1.0.dist-info/RECORD +17 -0
- optio_codex-0.1.0.dist-info/WHEEL +5 -0
- optio_codex-0.1.0.dist-info/top_level.txt +1 -0
optio_codex/models.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Available-model list for the codex conversation widget's model picker.
|
|
2
|
+
|
|
3
|
+
============================================================================
|
|
4
|
+
MODEL-SWITCH MECHANISM — pinned from the app-server contract (codex-cli
|
|
5
|
+
0.142.5 schema dump + upstream README; see the conversation docstring).
|
|
6
|
+
============================================================================
|
|
7
|
+
|
|
8
|
+
Decision: **INLINE** (opencode/grok-style), NOT restart (claudecode-style) —
|
|
9
|
+
and codex needs NO dedicated set-model request at all: a ``model`` override
|
|
10
|
+
on ``turn/start`` "become[s] the default for subsequent turns" (README,
|
|
11
|
+
turn/start). So ``CodexConversation.request_model_change()`` just pins the
|
|
12
|
+
model sent with the next ``turn/start``; the session body needs no
|
|
13
|
+
model_change_requested restart loop.
|
|
14
|
+
|
|
15
|
+
MODEL LIST source: the ``model/list`` request, answered in-session
|
|
16
|
+
(``{data:[Model], nextCursor}``; ``Model`` carries ``id``, ``displayName``,
|
|
17
|
+
``hidden``, ``isDefault``). The conversation captures the raw result at
|
|
18
|
+
bootstrap; this module maps it to the widget shape. There is no CLI listing
|
|
19
|
+
tier (unlike grok) — live result → static fallback, nothing in between.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
# Shown when the live model/list is unavailable (fake agents, offline, error
|
|
24
|
+
# response). Version-sensitive vendor strings; update alongside the pinned
|
|
25
|
+
# codex-cli version.
|
|
26
|
+
FALLBACK_MODELS: dict = {
|
|
27
|
+
"models": [
|
|
28
|
+
{"id": "gpt-5.5", "label": "GPT-5.5", "disabled": False},
|
|
29
|
+
{"id": "gpt-5.4-mini", "label": "GPT-5.4 Mini", "disabled": False},
|
|
30
|
+
],
|
|
31
|
+
"default": "gpt-5.5",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_model_list(result: "dict | None") -> dict:
|
|
36
|
+
"""Map a raw ``model/list`` result to the widget shape
|
|
37
|
+
``{models:[{id,label,disabled}], default}``.
|
|
38
|
+
|
|
39
|
+
Hidden models are omitted; ``default`` is the ``isDefault`` entry's id
|
|
40
|
+
(None when absent). Missing / malformed input returns the static
|
|
41
|
+
fallback (never raises, never falsely empties the picker).
|
|
42
|
+
"""
|
|
43
|
+
if not isinstance(result, dict):
|
|
44
|
+
return _copy_fallback()
|
|
45
|
+
data = result.get("data")
|
|
46
|
+
if not isinstance(data, list):
|
|
47
|
+
return _copy_fallback()
|
|
48
|
+
out: list[dict] = []
|
|
49
|
+
default: str | None = None
|
|
50
|
+
for m in data:
|
|
51
|
+
if not isinstance(m, dict):
|
|
52
|
+
continue
|
|
53
|
+
mid = m.get("id")
|
|
54
|
+
if not isinstance(mid, str) or not mid or m.get("hidden"):
|
|
55
|
+
continue
|
|
56
|
+
out.append({"id": mid, "label": m.get("displayName") or mid, "disabled": False})
|
|
57
|
+
if m.get("isDefault"):
|
|
58
|
+
default = mid
|
|
59
|
+
if not out:
|
|
60
|
+
return _copy_fallback()
|
|
61
|
+
return {"models": out, "default": default}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _copy_fallback() -> dict:
|
|
65
|
+
return {
|
|
66
|
+
"models": [dict(m) for m in FALLBACK_MODELS["models"]],
|
|
67
|
+
"default": FALLBACK_MODELS["default"],
|
|
68
|
+
}
|
optio_codex/prompt.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
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
|
+
)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""codex adopter of the generic optio-agents seed engine.
|
|
2
|
+
|
|
3
|
+
Defines the codex seed manifest (HOME layout + capture-time include triage),
|
|
4
|
+
the Mongo collection suffix, and ergonomic ``delete_seed`` / ``list_seeds`` /
|
|
5
|
+
``purge_seed`` wrappers that bind the suffix for consuming apps.
|
|
6
|
+
|
|
7
|
+
A codex *seed* carries the logged-in identity that lives under ``CODEX_HOME``
|
|
8
|
+
(``<workdir>/home/.codex``): ``auth.json`` (ChatGPT mode: ``auth_mode`` +
|
|
9
|
+
``tokens{id_token, access_token, refresh_token}`` + ``last_refresh``; API-key
|
|
10
|
+
mode: ``OPENAI_API_KEY``) plus ``config.toml``. Replanting it into a fresh
|
|
11
|
+
workdir is the answer to headless login.
|
|
12
|
+
|
|
13
|
+
The include list is an allowlist, which is also the exclusion mechanism:
|
|
14
|
+
``packages/`` (the ~286MB binary cache), ``*.sqlite*`` (absolute
|
|
15
|
+
rollout-path poison; rebuilt from rollouts), ``sessions/``, ``cache/``,
|
|
16
|
+
``tmp/``, logs etc. are simply never members.
|
|
17
|
+
|
|
18
|
+
Like grok/opencode (and unlike claudecode), codex needs no consume-time
|
|
19
|
+
rekey: auth/config are cwd-independent, so ``consume_transform`` is None.
|
|
20
|
+
The one cwd-dependent consume step — pre-trusting the new workdir via a
|
|
21
|
+
``[projects."<workdir>"]`` entry in config.toml — is deliberately a
|
|
22
|
+
post-merge edit in the session's ``_prepare`` (see
|
|
23
|
+
``host_actions.ensure_workdir_trusted``), NOT a manifest transform: codex
|
|
24
|
+
rewrites config.toml itself at runtime, so optio's edit must stay a
|
|
25
|
+
minimal, idempotent append against the *planted* file, applied exactly at
|
|
26
|
+
the point the workdir is known.
|
|
27
|
+
|
|
28
|
+
Path note: the engine roots capture/extract at ``host.workdir + "/" +
|
|
29
|
+
home_subdir`` (see ``SeedManifest.home_subdir``). CODEX_HOME is
|
|
30
|
+
``<workdir>/home/.codex``, so the manifest uses ``home_subdir="home"`` with
|
|
31
|
+
``.codex/`` prefixes on the include paths (mirroring grok's ``.grok/…``).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
from optio_agents import seeds
|
|
37
|
+
|
|
38
|
+
CODEX_SEED_SUFFIX = "_codex_seeds"
|
|
39
|
+
CODEX_SEED_MANIFEST_VERSION = 1
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Credential-only manifest for in-session save-back (the write-back analog
|
|
43
|
+
# of the full CODEX_SEED_MANIFEST; mirrors grok's GROK_CRED_MANIFEST and
|
|
44
|
+
# opencode's OPENCODE_CRED_MANIFEST). Only auth.json is re-captured — the
|
|
45
|
+
# seed's config.toml is never touched by save-back.
|
|
46
|
+
CODEX_CRED_MANIFEST = seeds.SeedManifest(
|
|
47
|
+
home_subdir="home",
|
|
48
|
+
include=[".codex/auth.json"],
|
|
49
|
+
version=CODEX_SEED_MANIFEST_VERSION,
|
|
50
|
+
consume_transform=None,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
CODEX_SEED_MANIFEST = seeds.SeedManifest(
|
|
55
|
+
home_subdir="home",
|
|
56
|
+
include=CODEX_CRED_MANIFEST.include + [
|
|
57
|
+
".codex/config.toml",
|
|
58
|
+
],
|
|
59
|
+
version=CODEX_SEED_MANIFEST_VERSION,
|
|
60
|
+
consume_transform=None, # no cwd-rekey for codex (pre-trust is in _prepare)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def delete_seed(store, seed_id: str):
|
|
65
|
+
"""Delete a codex seed doc; returns its GridFS blobId (or None).
|
|
66
|
+
|
|
67
|
+
Takes an optio store binding (``optio.mongo_store`` — exposes ``db`` and
|
|
68
|
+
``prefix``) as-is, so consuming apps hand over the whole namespace handle
|
|
69
|
+
instead of threading db+prefix (or knowing the collection suffix). The
|
|
70
|
+
caller still removes the returned blob from GridFS.
|
|
71
|
+
"""
|
|
72
|
+
return await seeds.delete_seed(
|
|
73
|
+
store.db, prefix=store.prefix, suffix=CODEX_SEED_SUFFIX, seed_id=seed_id,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def list_seeds(store) -> list[dict]:
|
|
78
|
+
"""List codex seeds as [{seedId, createdAt}, ...]. Takes an optio store
|
|
79
|
+
binding (``optio.mongo_store``) as-is."""
|
|
80
|
+
return await seeds.list_seeds(store.db, prefix=store.prefix, suffix=CODEX_SEED_SUFFIX)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def purge_seed(store, seed_id: str) -> None:
|
|
84
|
+
"""Fully expunge a codex seed (doc + its GridFS blob); raises KeyError if
|
|
85
|
+
absent. Takes an optio store binding (``optio.mongo_store``) as-is.
|
|
86
|
+
|
|
87
|
+
Mirrors ``optio_grok.purge_seed`` / ``optio_claudecode.purge_seed``; a
|
|
88
|
+
thin re-export of the ``optio_agents.seeds.purge_seed`` engine."""
|
|
89
|
+
return await seeds.purge_seed(
|
|
90
|
+
store.db, prefix=store.prefix, suffix=CODEX_SEED_SUFFIX, seed_id=seed_id,
|
|
91
|
+
)
|