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.
Files changed (35) hide show
  1. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/PKG-INFO +1 -1
  2. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/pyproject.toml +1 -1
  3. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode/session.py +18 -0
  4. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode.egg-info/PKG-INFO +1 -1
  5. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_session_blob_hooks.py +5 -0
  6. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_session_local.py +19 -1
  7. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_session_resume.py +44 -2
  8. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_session_seed.py +5 -0
  9. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/README.md +0 -0
  10. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/setup.cfg +0 -0
  11. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode/__init__.py +0 -0
  12. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode/host_actions.py +0 -0
  13. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode/prompt.py +0 -0
  14. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode/seed_manifest.py +0 -0
  15. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode/snapshots.py +0 -0
  16. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode/types.py +0 -0
  17. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode.egg-info/SOURCES.txt +0 -0
  18. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode.egg-info/dependency_links.txt +0 -0
  19. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode.egg-info/requires.txt +0 -0
  20. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/src/optio_opencode.egg-info/top_level.txt +0 -0
  21. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_host_actions.py +0 -0
  22. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_host_local.py +0 -0
  23. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_host_primitives_local.py +0 -0
  24. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_host_primitives_remote.py +0 -0
  25. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_host_remote_resume.py +0 -0
  26. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_host_resume.py +0 -0
  27. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_prompt.py +0 -0
  28. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_purge_seed.py +0 -0
  29. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_sanity.py +0 -0
  30. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_seed_config.py +0 -0
  31. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_session_hooks.py +0 -0
  32. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_session_remote.py +0 -0
  33. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_smart_install.py +0 -0
  34. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_snapshots.py +0 -0
  35. {optio_opencode-0.1.8 → optio_opencode-0.1.9}/tests/test_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: optio-opencode
3
- Version: 0.1.8
3
+ Version: 0.1.9
4
4
  Summary: Run opencode web as an optio task; local subprocess or remote via SSH.
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-opencode"
7
- version = "0.1.8"
7
+ version = "0.1.9"
8
8
  description = "Run opencode web as an optio task; local subprocess or remote via SSH."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.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,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: optio-opencode
3
- Version: 0.1.8
3
+ Version: 0.1.9
4
4
  Summary: Run opencode web as an optio task; local subprocess or remote via SSH.
5
5
  Author-email: Kristof Csillag <kristof.csillag@deai-labs.com>
6
6
  License-Expression: Apache-2.0
@@ -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
- cfg = _config("happy") # default supports_resume=True
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 _run_one_cycle(mongo_db, process_id: str, resume: bool) -> None:
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(consumer_instructions=f"(scenario: happy {process_id})")
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