optio-opencode 0.1.8__tar.gz → 0.1.9__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_opencode-0.1.8 → optio_opencode-0.1.9}/PKG-INFO +1 -1
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/pyproject.toml +1 -1
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode/session.py +18 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode.egg-info/PKG-INFO +1 -1
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_session_blob_hooks.py +5 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_session_local.py +19 -1
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_session_resume.py +44 -2
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_session_seed.py +5 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/README.md +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/setup.cfg +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode/__init__.py +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode/host_actions.py +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode/prompt.py +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode/seed_manifest.py +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode/snapshots.py +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode/types.py +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode.egg-info/SOURCES.txt +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode.egg-info/dependency_links.txt +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode.egg-info/requires.txt +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode.egg-info/top_level.txt +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_host_actions.py +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_host_local.py +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_host_primitives_local.py +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_host_primitives_remote.py +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_host_remote_resume.py +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_host_resume.py +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_prompt.py +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_purge_seed.py +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_sanity.py +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_seed_config.py +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_session_hooks.py +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_session_remote.py +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_smart_install.py +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_snapshots.py +0 -0
- {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_types.py +0 -0
|
@@ -483,6 +483,24 @@ async def _capture_snapshot(
|
|
|
483
483
|
opencode_executable: str = "opencode",
|
|
484
484
|
session_blob_encrypt: "Callable[[bytes], bytes] | None" = None,
|
|
485
485
|
) -> None:
|
|
486
|
+
# Defense-in-depth (guard #2): refuse to capture a resumable snapshot
|
|
487
|
+
# unless opencode's auth.json exists and is non-empty on the host. A
|
|
488
|
+
# credential-less workdir is degenerate — restoring it would relaunch
|
|
489
|
+
# opencode with no auth, so marking it resumable is worse than useless.
|
|
490
|
+
# (Live-reach is already covered by the session_id is not None gate at
|
|
491
|
+
# the call site; this covers the bad/empty-seed edge.)
|
|
492
|
+
workdir = host.workdir.rstrip("/")
|
|
493
|
+
chk = await host.run_command(
|
|
494
|
+
f"test -s {shlex.quote(workdir)}/home/.local/share/opencode/auth.json "
|
|
495
|
+
f"&& echo OK || true"
|
|
496
|
+
)
|
|
497
|
+
if "OK" not in chk.stdout:
|
|
498
|
+
_LOG.warning(
|
|
499
|
+
"snapshot capture skipped: opencode auth.json absent/empty; "
|
|
500
|
+
"refusing to mark resumable"
|
|
501
|
+
)
|
|
502
|
+
return
|
|
503
|
+
|
|
486
504
|
session_json = await host_actions.opencode_export(
|
|
487
505
|
host, opencode_db, session_id,
|
|
488
506
|
opencode_executable=opencode_executable,
|
|
@@ -99,6 +99,11 @@ async def test_capture_writes_through_session_blob_encrypt(monkeypatch):
|
|
|
99
99
|
yield b"workdir-bytes"
|
|
100
100
|
fake_host = MagicMock()
|
|
101
101
|
fake_host.archive_workdir = _fake_archive
|
|
102
|
+
# Satisfy the snapshot-capture defense-in-depth guard: it runs a
|
|
103
|
+
# `test -s .../auth.json && echo OK` probe on the host and refuses to
|
|
104
|
+
# capture unless the output contains "OK".
|
|
105
|
+
fake_host.workdir = "/work"
|
|
106
|
+
fake_host.run_command = AsyncMock(return_value=MagicMock(stdout="OK\n"))
|
|
102
107
|
|
|
103
108
|
await _capture_snapshot(
|
|
104
109
|
fake_ctx, fake_host,
|
|
@@ -92,6 +92,21 @@ def _config(scenario: str, deliverable_cb=None, raises: bool = False) -> Opencod
|
|
|
92
92
|
)
|
|
93
93
|
|
|
94
94
|
|
|
95
|
+
async def _plant_auth_json(hook_ctx) -> None:
|
|
96
|
+
"""before_execute hook: plant a non-empty opencode auth.json in the workdir.
|
|
97
|
+
|
|
98
|
+
The snapshot-capture defense-in-depth guard refuses to mark a session
|
|
99
|
+
resumable unless ``home/.local/share/opencode/auth.json`` exists and is
|
|
100
|
+
non-empty on the host, so capture-expecting tests must plant credentials
|
|
101
|
+
the same way a real seeded launch would.
|
|
102
|
+
"""
|
|
103
|
+
await hook_ctx.run_on_host(
|
|
104
|
+
"mkdir -p home/.local/share/opencode && "
|
|
105
|
+
"printf '{\"anthropic\": {\"type\": \"api\", \"key\": \"sk-test\"}}' "
|
|
106
|
+
"> home/.local/share/opencode/auth.json"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
95
110
|
@pytest.fixture(autouse=True)
|
|
96
111
|
def _supply_scenario(monkeypatch):
|
|
97
112
|
"""Substitute fake_opencode.py for the real opencode binary.
|
|
@@ -596,11 +611,14 @@ async def test_session_local_supports_resume_true_captures_snapshot(
|
|
|
596
611
|
ctx_and_captures, _supply_scenario, tmp_workdir,
|
|
597
612
|
):
|
|
598
613
|
"""With supports_resume=True (default), a snapshot IS captured."""
|
|
614
|
+
import dataclasses
|
|
599
615
|
from optio_opencode.snapshots import SESSION_SNAPSHOT_COLLECTION_SUFFIX
|
|
600
616
|
ctx, _, _ = ctx_and_captures
|
|
601
617
|
_supply_scenario["name"] = "happy"
|
|
602
618
|
|
|
603
|
-
|
|
619
|
+
# Plant auth.json so the snapshot-capture defense-in-depth guard permits
|
|
620
|
+
# capture; without credentials it would refuse to mark resumable.
|
|
621
|
+
cfg = dataclasses.replace(_config("happy"), before_execute=_plant_auth_json)
|
|
604
622
|
await run_opencode_session(ctx, cfg)
|
|
605
623
|
|
|
606
624
|
coll_name = f"{ctx._prefix}{SESSION_SNAPSHOT_COLLECTION_SUFFIX}"
|
|
@@ -121,9 +121,30 @@ async def _make_ctx(mongo_db, process_id: str, *, resume: bool):
|
|
|
121
121
|
return ctx, proc["_id"]
|
|
122
122
|
|
|
123
123
|
|
|
124
|
-
async def
|
|
124
|
+
async def _plant_auth_json(hook_ctx) -> None:
|
|
125
|
+
"""before_execute hook: plant a non-empty opencode auth.json in the workdir.
|
|
126
|
+
|
|
127
|
+
The snapshot-capture defense-in-depth guard refuses to mark a session
|
|
128
|
+
resumable unless ``home/.local/share/opencode/auth.json`` exists and is
|
|
129
|
+
non-empty on the host. Capture-expecting tests must therefore plant a
|
|
130
|
+
credentials file the same way a real seeded launch would, mirroring how
|
|
131
|
+
claudecode tests plant ``.credentials.json``.
|
|
132
|
+
"""
|
|
133
|
+
await hook_ctx.run_on_host(
|
|
134
|
+
"mkdir -p home/.local/share/opencode && "
|
|
135
|
+
"printf '{\"anthropic\": {\"type\": \"api\", \"key\": \"sk-test\"}}' "
|
|
136
|
+
"> home/.local/share/opencode/auth.json"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
async def _run_one_cycle(
|
|
141
|
+
mongo_db, process_id: str, resume: bool, *, plant_auth: bool = True,
|
|
142
|
+
) -> None:
|
|
125
143
|
ctx, _ = await _make_ctx(mongo_db, process_id, resume=resume)
|
|
126
|
-
cfg = OpencodeTaskConfig(
|
|
144
|
+
cfg = OpencodeTaskConfig(
|
|
145
|
+
consumer_instructions=f"(scenario: happy {process_id})",
|
|
146
|
+
before_execute=_plant_auth_json if plant_auth else None,
|
|
147
|
+
)
|
|
127
148
|
await run_opencode_session(ctx, cfg)
|
|
128
149
|
|
|
129
150
|
|
|
@@ -142,6 +163,27 @@ async def test_terminal_flow_captures_snapshot_and_wipes_workdir(mongo_db, task_
|
|
|
142
163
|
assert not wd.exists() or not any(wd.iterdir())
|
|
143
164
|
|
|
144
165
|
|
|
166
|
+
async def test_no_auth_json_refuses_to_capture_snapshot(mongo_db, task_root):
|
|
167
|
+
"""Defense-in-depth: a normal launch (session_id created) but with NO
|
|
168
|
+
opencode auth.json on the host must NOT capture a snapshot or mark the
|
|
169
|
+
process resumable. Guards against marking a degenerate, credential-less
|
|
170
|
+
workdir as resumable.
|
|
171
|
+
"""
|
|
172
|
+
pid = "oc_no_auth_1"
|
|
173
|
+
await _run_one_cycle(mongo_db, pid, resume=False, plant_auth=False)
|
|
174
|
+
|
|
175
|
+
snap = await load_latest_snapshot(mongo_db, prefix="test", process_id=pid)
|
|
176
|
+
assert snap is None
|
|
177
|
+
|
|
178
|
+
count = await mongo_db[f"test{SESSION_SNAPSHOT_COLLECTION_SUFFIX}"].count_documents(
|
|
179
|
+
{"processId": pid}
|
|
180
|
+
)
|
|
181
|
+
assert count == 0
|
|
182
|
+
|
|
183
|
+
proc = await mongo_db["test_processes"].find_one({"processId": pid})
|
|
184
|
+
assert proc.get("hasSavedState") is not True
|
|
185
|
+
|
|
186
|
+
|
|
145
187
|
async def test_resume_creates_second_snapshot(mongo_db, task_root):
|
|
146
188
|
pid = "oc_resume_1"
|
|
147
189
|
await _run_one_cycle(mongo_db, pid, resume=False)
|
|
@@ -330,6 +330,10 @@ async def test_auto_start_posts_on_fresh_and_not_on_resume(
|
|
|
330
330
|
await run_opencode_session(ctx_fresh, OpencodeTaskConfig(
|
|
331
331
|
consumer_instructions="(scenario: happy)",
|
|
332
332
|
auto_start=True,
|
|
333
|
+
# Plant auth.json so the snapshot-capture defense-in-depth guard does
|
|
334
|
+
# not refuse to mark this resumable; otherwise the resume leg would
|
|
335
|
+
# fall back to a fresh launch and POST the kickoff prompt again.
|
|
336
|
+
before_execute=_plant_env,
|
|
333
337
|
))
|
|
334
338
|
assert len(posts) == 1
|
|
335
339
|
posted_session_id, posted_message = posts[0]
|
|
@@ -341,6 +345,7 @@ async def test_auto_start_posts_on_fresh_and_not_on_resume(
|
|
|
341
345
|
await run_opencode_session(ctx_resume, OpencodeTaskConfig(
|
|
342
346
|
consumer_instructions="(scenario: happy)",
|
|
343
347
|
auto_start=True,
|
|
348
|
+
before_execute=_plant_env,
|
|
344
349
|
))
|
|
345
350
|
assert len(posts) == 1, posts
|
|
346
351
|
|
|
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
|
{optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode.egg-info/dependency_links.txt
RENAMED
|
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
|