From d3ea48c87be42015b19f27a456adf60f52f46aea Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 14 Jun 2026 01:55:52 +0900 Subject: [PATCH 1/6] ticket: note e2e runtime isolation concern --- .yoi/tickets/00001KV0TJVN5/item.md | 2 +- .yoi/tickets/00001KV0TJVN5/thread.md | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.yoi/tickets/00001KV0TJVN5/item.md b/.yoi/tickets/00001KV0TJVN5/item.md index 7854996d..7a519c5d 100644 --- a/.yoi/tickets/00001KV0TJVN5/item.md +++ b/.yoi/tickets/00001KV0TJVN5/item.md @@ -2,7 +2,7 @@ title: 'E2E harness が最新 yoi binary を自動 build して使うようにする' state: 'done' created_at: '2026-06-13T15:46:07Z' -updated_at: '2026-06-13T16:09:29Z' +updated_at: '2026-06-13T16:53:48Z' assignee: null readiness: 'ready' queued_by: 'yoi ticket' diff --git a/.yoi/tickets/00001KV0TJVN5/thread.md b/.yoi/tickets/00001KV0TJVN5/thread.md index 6505c0d3..04d3a058 100644 --- a/.yoi/tickets/00001KV0TJVN5/thread.md +++ b/.yoi/tickets/00001KV0TJVN5/thread.md @@ -268,3 +268,25 @@ Next: E2E binary provider follow-up was reviewed, approved, merged into the Orchestrator branch as `8abc2b7f`, and validated in the Orchestrator worktree. Default E2E runs now build the current `yoi` binary before direct PTY spawn, `YOI_E2E_BIN` override remains available, and tested `yoi` subprocesses are isolated with `env_clear()` plus allowlist so host provider credentials are not inherited. Ticket implementation work is done; closure remains separate. --- + + + +## Comment + +Post-merge concern from user: E2E isolation may still have leaked or left visible Pod/worktree artifacts. + +Observed concern: +- User reports Pods named `yoi-orchestrator-orchestrator` and `workspace-orchestrator` appeared. + +Current assessment: +- Earlier E2E runs before the env isolation follow-up inherited host environment, including likely `XDG_RUNTIME_DIR`, so `yoi panel` could observe the host/global runtime Pod registry under `/run/user/...` even though `HOME`/`XDG_DATA_HOME` were fixture paths。 +- The fixture also intentionally writes blocking Pod metadata for `workspace` and `workspace-orchestrator` under fixture `XDG_DATA_HOME` to drive panel rows. That should be fixture-local, but if runtime/data isolation is wrong it can become visible outside the intended fixture。 +- The later `env_clear()` + allowlist fix prevents host env credential leak and likely prevents inheriting `XDG_RUNTIME_DIR`, causing runtime fallback to fixture HOME; however, no explicit regression assertion currently proves that E2E cannot see/create global runtime Pod state or workspace-orchestrator worktrees。 + +Required follow-up direction: +- Add explicit runtime isolation to E2E (`XDG_RUNTIME_DIR` or equivalent controlled fixture runtime path, or an assertion that fallback runtime is fixture-local)。 +- Add regression assertions/artifacts proving tested `yoi panel` sees only fixture Pod metadata/runtime state and does not observe host live Pods。 +- Ensure E2E cleanup removes any fixture Pod metadata/runtime/worktree artifacts it creates。 +- Investigate and clean any residual `yoi-orchestrator-orchestrator` / `workspace-orchestrator` artifacts only after confirming whether they are live Pods, fixture artifacts, or prior Panel-created worktrees。 + +--- From f467a77f6efdeb64b8c2e345401276ddb0caf886 Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 14 Jun 2026 01:56:31 +0900 Subject: [PATCH 2/6] ticket: create e2e tmp isolation followup --- .yoi/tickets/00001KV0YK5S0/artifacts/.gitkeep | 0 .../00001KV0YK5S0/artifacts/relations.json | 21 +++++++++ .yoi/tickets/00001KV0YK5S0/item.md | 43 +++++++++++++++++++ .yoi/tickets/00001KV0YK5S0/thread.md | 33 ++++++++++++++ 4 files changed, 97 insertions(+) create mode 100644 .yoi/tickets/00001KV0YK5S0/artifacts/.gitkeep create mode 100644 .yoi/tickets/00001KV0YK5S0/artifacts/relations.json create mode 100644 .yoi/tickets/00001KV0YK5S0/item.md create mode 100644 .yoi/tickets/00001KV0YK5S0/thread.md diff --git a/.yoi/tickets/00001KV0YK5S0/artifacts/.gitkeep b/.yoi/tickets/00001KV0YK5S0/artifacts/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.yoi/tickets/00001KV0YK5S0/artifacts/relations.json b/.yoi/tickets/00001KV0YK5S0/artifacts/relations.json new file mode 100644 index 00000000..e0f100ea --- /dev/null +++ b/.yoi/tickets/00001KV0YK5S0/artifacts/relations.json @@ -0,0 +1,21 @@ +{ + "version": 1, + "relations": [ + { + "ticket_id": "00001KV0YK5S0", + "kind": "related", + "target": "00001KSKBP9YG", + "note": "E2E harness first slice の runtime/tmp isolation と cleanup follow-up。", + "author": "orchestrator", + "at": "2026-06-13T16:56:22Z" + }, + { + "ticket_id": "00001KV0YK5S0", + "kind": "related", + "target": "00001KV0TJVN5", + "note": "E2E binary/env isolation follow-up の残課題(runtime/data/workspace isolation and cleanup)を補う。", + "author": "orchestrator", + "at": "2026-06-13T16:56:22Z" + } + ] +} diff --git a/.yoi/tickets/00001KV0YK5S0/item.md b/.yoi/tickets/00001KV0YK5S0/item.md new file mode 100644 index 00000000..94970c9d --- /dev/null +++ b/.yoi/tickets/00001KV0YK5S0/item.md @@ -0,0 +1,43 @@ +--- +title: 'E2E harness を完全な tmp runtime/data/workspace 隔離と cleanup に対応させる' +state: 'queued' +created_at: '2026-06-13T16:56:11Z' +updated_at: '2026-06-13T16:56:31Z' +assignee: null +readiness: 'ready' +queued_by: 'yoi ticket' +queued_at: '2026-06-13T16:56:31Z' +--- + +## 背景 + +E2E harness は `00001KSKBP9YG` / `00001KV0TJVN5` で Panel PTY E2E、最新 `yoi` binary build、tested subprocess の env isolation を導入した。しかし、ユーザーから `yoi-orchestrator-orchestrator` / `workspace-orchestrator` などの Pod/worktree artifact が出現したとの報告があり、host runtime / Pod registry / worktree artifact isolation と cleanup がまだ十分に証明されていない。 + +既知の問題: +- 初期 E2E は `env_clear()` 前に `XDG_RUNTIME_DIR` など host env を継承し得た。 +- Fixture は `workspace` / `workspace-orchestrator` の Pod metadata を作るが、これは fixture-local でなければならない。 +- 現在の env isolation は host env leak を防ぐが、E2E が完全に clean な tmp runtime/data/workspace で動き、実行後に cleanup することを明示的に保証・検証していない。 + +## 要件 + +- E2E は毎回完全に clean な temporary environment を作って実行する。 +- Workspace / HOME / XDG_DATA_HOME / XDG_STATE_HOME / XDG_CONFIG_HOME / runtime dir / artifacts root を fixture ごとに分離する。 +- Tested `yoi` subprocess は host runtime / Pod registry / session / worktree / data dir を見ない。 +- Fixture で作る Pod metadata(例: `workspace`, `workspace-orchestrator`)は fixture-local であり、host/global registry に出ない。 +- 実行後、fixture runtime/data/workspace/temp dirs は成功・失敗に関係なく cleanup される。失敗時に必要な artifact は `target/e2e-artifacts/...` にコピーしてから cleanup する。 +- Cleanup policy / fixture root / runtime dir / data dir / removed paths を artifact に記録する。 +- 既存の binary provider、env credential isolation、mouse capture tracking、quit pending barrier を壊さない。 + +## 受け入れ条件 + +- `cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` が clean tmp env を使い、終了後に fixture temp root を残さない。 +- E2E artifact から fixture workspace/data/runtime paths と cleanup result が確認できる。 +- Test または assertion により、Panel が host live Pods / host runtime registry を見ていないことを確認する。 +- Fixture-created `workspace-orchestrator` 等が fixture-local であり、cleanup 後に temp root ごと消えることを確認する。 +- Host `XDG_RUNTIME_DIR` などを設定した状態でも tested `yoi` は fixture runtime だけを見る。 +- `cargo fmt --check`、`git diff --check`、関連 `cargo check` / E2E tests が pass する。 + +## 関連 + +- `00001KSKBP9YG`: E2E harness first slice。 +- `00001KV0TJVN5`: E2E binary provider / env isolation follow-up。 diff --git a/.yoi/tickets/00001KV0YK5S0/thread.md b/.yoi/tickets/00001KV0YK5S0/thread.md new file mode 100644 index 00000000..4431322b --- /dev/null +++ b/.yoi/tickets/00001KV0YK5S0/thread.md @@ -0,0 +1,33 @@ + + +## 作成 + +LocalTicketBackend によって作成されました。 + +--- + + + +## Intake summary + +ユーザーが E2E を完全に clean な tmp environment で実行し、毎回 cleanup することを明示した。要件は workspace/HOME/XDG/runtime/artifacts の fixture 分離、host Pod/runtime registry 非参照、fixture-created Pod metadata の fixture-local 化、成功/失敗時 cleanup と artifact 記録。 + +--- + + + +## State changed + +Ticket planning が完了しました。state planning -> ready。 + + +--- + + + +## State changed + +Ticket を `yoi ticket` が queued にしました。 + + +--- From eb29b63aa16b39b1bdf0ad5b3614aeed41146125 Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 14 Jun 2026 01:57:03 +0900 Subject: [PATCH 3/6] ticket: accept e2e tmp isolation --- .yoi/tickets/00001KV0YK5S0/item.md | 4 +- .yoi/tickets/00001KV0YK5S0/thread.md | 56 ++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/.yoi/tickets/00001KV0YK5S0/item.md b/.yoi/tickets/00001KV0YK5S0/item.md index 94970c9d..7cc4438b 100644 --- a/.yoi/tickets/00001KV0YK5S0/item.md +++ b/.yoi/tickets/00001KV0YK5S0/item.md @@ -1,8 +1,8 @@ --- title: 'E2E harness を完全な tmp runtime/data/workspace 隔離と cleanup に対応させる' -state: 'queued' +state: 'inprogress' created_at: '2026-06-13T16:56:11Z' -updated_at: '2026-06-13T16:56:31Z' +updated_at: '2026-06-13T16:56:58Z' assignee: null readiness: 'ready' queued_by: 'yoi ticket' diff --git a/.yoi/tickets/00001KV0YK5S0/thread.md b/.yoi/tickets/00001KV0YK5S0/thread.md index 4431322b..a5a4eb29 100644 --- a/.yoi/tickets/00001KV0YK5S0/thread.md +++ b/.yoi/tickets/00001KV0YK5S0/thread.md @@ -30,4 +30,60 @@ Ticket planning が完了しました。state planning -> ready。 Ticket を `yoi ticket` が queued にしました。 +--- + + + +## Decision + +Routing decision: implementation_ready + +Reason: +- ユーザーが E2E harness の clean tmp environment / cleanup を明示した。 +- Ticket は `queued` で、要件・受け入れ条件は具体的。既存 `00001KSKBP9YG` / `00001KV0TJVN5` の follow-up で、主な変更面は `tests/e2e` に bounded される。 +- Blocking relation はなく、既存 queued Tickets とは変更面が異なる。Orchestrator worktree は clean。 +- Risk は test/runtime isolation だが、binding invariants(host runtime registry 非参照、fixture-local Pod metadata、cleanup artifact 記録)が明確なため実装可能。 + +Evidence checked: +- 新規 Ticket body / related records。 +- `00001KV0TJVN5` の post-merge concern: host/global runtime registry を見た可能性、fixture `workspace-orchestrator` metadata、runtime isolation assertion 不足。 +- Existing E2E code map: `tests/e2e/src/lib.rs` の fixture setup / `PanelHarness::spawn` / `run_yoi_capture` / artifact handling。 +- Orchestrator workspace state: clean。 + +IntentPacket: + +Intent: +- E2E harness が毎回 clean な tmp workspace/data/runtime environment で実行され、host Pod/runtime registry を見ず、成功・失敗に関係なく fixture temp を cleanup するようにする。 + +Binding decisions / invariants: +- Tested `yoi` process は host runtime/data/session/worktree/Pod registry を見ない。 +- Fixture-created Pods (`workspace`, `workspace-orchestrator` 等) は fixture-local。 +- Failure diagnostics は `target/e2e-artifacts` にコピーして残し、fixture temp root は cleanup する。 +- Existing binary provider、env credential isolation、mouse capture guard、quit pending barrier を壊さない。 +- Root/original workspace では作業しない。 + +Requirements / acceptance criteria: +- `cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` が clean tmp env を使い、終了後に fixture temp root を残さない。 +- Artifact に fixture workspace/data/runtime paths、cleanup result、env/runtime policy が残る。 +- Test/assertion で Panel が host live Pods / host runtime registry を見ないことを確認する。 +- Host `XDG_RUNTIME_DIR` 等が設定されていても tested `yoi` は fixture runtime だけを見る。 +- `cargo fmt --check` / `git diff --check` / relevant checks / E2E tests が pass。 + +Implementation latitude: +- `tempfile::TempDir` / custom guard / artifact snapshot strategy の具体設計は Coder が選んでよい。 +- Runtime dir は explicit `XDG_RUNTIME_DIR` fixture path でも、Yoi-specific fixture runtime env でもよいが、host fallback を使わないことを artifact/assertion で示す。 +- Failure artifact は fixture tree 全体ではなく必要 subset copy でよい。 + +Escalate if: +- Yoi runtime path discovery に explicit test runtime dir を渡す public/production surface 変更が必要になる場合。 +- Cleanup と artifact preservation が両立しない design constraint がある場合。 + +--- + + + +## State changed + +Ticket evidence、related E2E tickets、existing code map、Orchestrator worktree clean state を確認した。ユーザーが明示的に修正を依頼しており、blocking relation はない。実装 side effect の前に inprogress acceptance を記録する。 + --- From 07e754ce4b28ef9e6e6ed2026fd433d94b215a1f Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 14 Jun 2026 02:07:07 +0900 Subject: [PATCH 4/6] test: isolate e2e tmp runtime fixtures --- .yoi/tickets/00001KV0YK5S0/item.md | 2 +- .yoi/tickets/00001KV0YK5S0/thread.md | 35 ++++ tests/e2e/src/lib.rs | 298 +++++++++++++++++++++------ tests/e2e/tests/panel.rs | 103 ++++++++- 4 files changed, 374 insertions(+), 64 deletions(-) diff --git a/.yoi/tickets/00001KV0YK5S0/item.md b/.yoi/tickets/00001KV0YK5S0/item.md index 7cc4438b..a6b50fbf 100644 --- a/.yoi/tickets/00001KV0YK5S0/item.md +++ b/.yoi/tickets/00001KV0YK5S0/item.md @@ -2,7 +2,7 @@ title: 'E2E harness を完全な tmp runtime/data/workspace 隔離と cleanup に対応させる' state: 'inprogress' created_at: '2026-06-13T16:56:11Z' -updated_at: '2026-06-13T16:56:58Z' +updated_at: '2026-06-13T17:06:29Z' assignee: null readiness: 'ready' queued_by: 'yoi ticket' diff --git a/.yoi/tickets/00001KV0YK5S0/thread.md b/.yoi/tickets/00001KV0YK5S0/thread.md index a5a4eb29..142eac21 100644 --- a/.yoi/tickets/00001KV0YK5S0/thread.md +++ b/.yoi/tickets/00001KV0YK5S0/thread.md @@ -86,4 +86,39 @@ Escalate if: Ticket evidence、related E2E tickets、existing code map、Orchestrator worktree clean state を確認した。ユーザーが明示的に修正を依頼しており、blocking relation はない。実装 side effect の前に inprogress acceptance を記録する。 +--- + + + +## Implementation report + +Implemented scoped E2E tmp/runtime isolation for Ticket 00001KV0YK5S0. + +Files changed: +- `tests/e2e/src/lib.rs` + - `FixtureWorkspace` now owns a `TempDir` under `target/e2e-tmp` and writes persistent scenario artifacts under `target/e2e-artifacts/`. + - Each fixture creates isolated workspace, HOME, XDG data/state/config/runtime dirs and passes `XDG_RUNTIME_DIR` explicitly to all tested `yoi` subprocesses under `env_clear`. + - Fixture metadata records fixture root, workspace/home/XDG paths, runtime policy, env policy, and cleanup result. + - Cleanup snapshots the fixture tree into persistent artifacts, closes/removes the temp root, writes `cleanup.json`, and reports cleanup failure. +- `tests/e2e/tests/panel.rs` + - Existing two Panel E2E tests now assert fixture path isolation, absence of host/runtime Pod marker leakage in rendered rows, and explicit fixture cleanup/removal after each scenario. + +Isolation/cleanup policy: +- Tested `yoi` never inherits host env wholesale; fixture setup and Panel subprocesses use `env_clear` with an allowlist that includes only fixture HOME/XDG data/state/config/runtime plus test/runtime-specific variables. +- `XDG_RUNTIME_DIR` is always a fixture-local runtime dir under the temporary fixture root. +- Fixture-local Pod metadata markers (`workspace`, `workspace-orchestrator`) are created only inside fixture data home and panel rows assert no host/fixture runtime markers leak. +- Artifacts persist outside the temp root in `target/e2e-artifacts`; cleanup snapshots selected fixture contents there before removing the temp root. + +Validation: +- `cargo check -p yoi-e2e --all-targets --features e2e` — passed. +- `cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` — passed (2 tests). +- `XDG_RUNTIME_DIR=/tmp/yoi-e2e-host-runtime-leak-test OPENAI_API_KEY=host-secret cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` — passed (2 tests); artifacts show fixture `xdg_runtime_dir`, `host_xdg_runtime_dir_present: true`, and `cleanup_success: true`. +- `YOI_E2E_BIN=target/debug/yoi cargo test -p yoi-e2e --features e2e --test panel -- --nocapture` — passed (2 tests). +- `cargo fmt --check` — passed. +- `git diff --check` — passed. + +Remaining gaps: +- No known gaps within the delegated scope. + + --- diff --git a/tests/e2e/src/lib.rs b/tests/e2e/src/lib.rs index 7d866ed9..9157e586 100644 --- a/tests/e2e/src/lib.rs +++ b/tests/e2e/src/lib.rs @@ -16,6 +16,7 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use tempfile::TempDir; const DEFAULT_WAIT: Duration = Duration::from_secs(5); const DEFAULT_EXIT_WAIT: Duration = Duration::from_millis(1500); @@ -96,9 +97,10 @@ fn fixture_setup_env_policy() -> EnvPolicy { "XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_CONFIG_HOME", + "XDG_RUNTIME_DIR", "YOI_POD_RUNTIME_COMMAND", ], - "tested yoi fixture setup commands use env_clear and receive only fixture data/config homes plus the explicit runtime binary override", + "tested yoi fixture setup commands use env_clear and receive only fixture HOME, XDG data/state/config/runtime dirs, and the explicit runtime binary override", ) } @@ -108,6 +110,7 @@ fn panel_env_policy(include_hold_background_task: bool) -> EnvPolicy { "XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_CONFIG_HOME", + "XDG_RUNTIME_DIR", "TERM", "YOI_TUI_TEST_EVENTS", "YOI_POD_RUNTIME_COMMAND", @@ -117,7 +120,7 @@ fn panel_env_policy(include_hold_background_task: bool) -> EnvPolicy { } env_policy( &allowlist, - "tested yoi panel subprocess uses env_clear and receives only fixture homes, terminal/test-observer variables, and the explicit runtime binary override", + "tested yoi panel subprocess uses env_clear and receives only fixture HOME, XDG data/state/config/runtime dirs, terminal/test-observer variables, and the explicit runtime binary override", ) } @@ -208,6 +211,8 @@ pub struct PanelHarnessConfig { pub xdg_data_home: PathBuf, pub xdg_state_home: PathBuf, pub xdg_config_home: PathBuf, + pub xdg_runtime_dir: PathBuf, + pub fixture_root: PathBuf, pub terminal_size: (u16, u16), pub hold_background_task: Option, pub artifacts_dir: PathBuf, @@ -304,6 +309,13 @@ impl PanelHarness { "xdg_data_home": config.xdg_data_home, "xdg_state_home": config.xdg_state_home, "xdg_config_home": config.xdg_config_home, + "xdg_runtime_dir": config.xdg_runtime_dir, + "fixture_root": config.fixture_root, + "runtime_policy": { + "host_runtime_inherited": false, + "host_xdg_runtime_dir_present": std::env::var_os("XDG_RUNTIME_DIR").is_some(), + "tested_yoi_runtime_source": "fixture XDG_RUNTIME_DIR" + }, "terminal_size": { "columns": config.terminal_size.0, "rows": config.terminal_size.1, @@ -329,6 +341,7 @@ impl PanelHarness { .env("XDG_DATA_HOME", &config.xdg_data_home) .env("XDG_STATE_HOME", &config.xdg_state_home) .env("XDG_CONFIG_HOME", &config.xdg_config_home) + .env("XDG_RUNTIME_DIR", &config.xdg_runtime_dir) .env("TERM", "xterm-256color") .stdin(Stdio::from(slave_for_stdin)) .stdout(Stdio::from(slave_for_stdout)) @@ -593,92 +606,130 @@ impl Drop for PanelHarness { } } +#[derive(Debug, Clone, Serialize)] +pub struct FixtureCleanupReport { + pub fixture_root: PathBuf, + pub artifacts_dir: PathBuf, + pub snapshot_dir: PathBuf, + pub cleanup_attempted: bool, + pub cleanup_success: bool, + pub fixture_root_exists_after: bool, + pub cleanup_error: Option, + pub report_path: PathBuf, +} + #[derive(Debug)] pub struct FixtureWorkspace { + temp_root: Option, pub root: PathBuf, pub workspace: PathBuf, pub home: PathBuf, pub xdg_data_home: PathBuf, pub xdg_state_home: PathBuf, pub xdg_config_home: PathBuf, + pub xdg_runtime_dir: PathBuf, pub artifacts_dir: PathBuf, } impl FixtureWorkspace { pub fn new(binary: &Path) -> Result { let workspace_root = workspace_root()?; - let root = workspace_root - .join("target") - .join("e2e-artifacts") - .join(format!( - "{}-{}-{}", - std::process::id(), - now_ms(), - FIXTURE_COUNTER.fetch_add(1, Ordering::Relaxed) - )); + let target_dir = workspace_root.join("target"); + let temp_parent = target_dir.join("e2e-tmp"); + let artifact_parent = target_dir.join("e2e-artifacts"); + fs::create_dir_all(&temp_parent)?; + fs::create_dir_all(&artifact_parent)?; + + let fixture_id = format!( + "{}-{}-{}", + std::process::id(), + now_ms(), + FIXTURE_COUNTER.fetch_add(1, Ordering::Relaxed) + ); + let temp_root = tempfile::Builder::new() + .prefix(&format!("yoi-e2e-{fixture_id}-")) + .tempdir_in(&temp_parent)?; + let root = temp_root.path().to_path_buf(); + let artifacts_dir = artifact_parent.join(fixture_id); let workspace = root.join("workspace"); let home = root.join("home"); let xdg_data_home = root.join("data"); let xdg_state_home = root.join("state"); let xdg_config_home = root.join("config"); - let artifacts_dir = root.join("artifacts"); + let xdg_runtime_dir = root.join("runtime"); for dir in [ &workspace, &home, &xdg_data_home, &xdg_state_home, &xdg_config_home, + &xdg_runtime_dir, &artifacts_dir, ] { fs::create_dir_all(dir)?; } - write_blocking_pod_metadata(&xdg_data_home, "workspace")?; - write_blocking_pod_metadata(&xdg_data_home, "workspace-orchestrator")?; - run_yoi( - binary, - &workspace, - &home, - &xdg_data_home, - &xdg_state_home, - &xdg_config_home, - &["ticket", "init"], - )?; - let first = create_ticket( - binary, - &workspace, - &home, - &xdg_data_home, - &xdg_state_home, - &xdg_config_home, - "Ready E2E Ticket", - )?; - run_yoi( - binary, - &workspace, - &home, - &xdg_data_home, - &xdg_state_home, - &xdg_config_home, - &["ticket", "state", &first, "ready"], - )?; - let _second = create_ticket( - binary, - &workspace, - &home, - &xdg_data_home, - &xdg_state_home, - &xdg_config_home, - "Planning E2E Ticket", - )?; - Ok(Self { + + let fixture = Self { + temp_root: Some(temp_root), root, workspace, home, xdg_data_home, xdg_state_home, xdg_config_home, + xdg_runtime_dir, artifacts_dir, - }) + }; + fixture.write_fixture_metadata("created", None)?; + + write_blocking_pod_metadata(&fixture.xdg_data_home, "workspace")?; + write_blocking_pod_metadata(&fixture.xdg_data_home, "workspace-orchestrator")?; + run_yoi( + binary, + &fixture.workspace, + &fixture.home, + &fixture.xdg_data_home, + &fixture.xdg_state_home, + &fixture.xdg_config_home, + &fixture.xdg_runtime_dir, + &fixture.artifacts_dir, + &["ticket", "init"], + )?; + let first = create_ticket( + binary, + &fixture.workspace, + &fixture.home, + &fixture.xdg_data_home, + &fixture.xdg_state_home, + &fixture.xdg_config_home, + &fixture.xdg_runtime_dir, + &fixture.artifacts_dir, + "Ready E2E Ticket", + )?; + run_yoi( + binary, + &fixture.workspace, + &fixture.home, + &fixture.xdg_data_home, + &fixture.xdg_state_home, + &fixture.xdg_config_home, + &fixture.xdg_runtime_dir, + &fixture.artifacts_dir, + &["ticket", "state", &first, "ready"], + )?; + let _second = create_ticket( + binary, + &fixture.workspace, + &fixture.home, + &fixture.xdg_data_home, + &fixture.xdg_state_home, + &fixture.xdg_config_home, + &fixture.xdg_runtime_dir, + &fixture.artifacts_dir, + "Planning E2E Ticket", + )?; + fixture.write_fixture_metadata("ready", None)?; + Ok(fixture) } pub fn panel_config(&self, binary: PathBuf) -> PanelHarnessConfig { @@ -689,6 +740,8 @@ impl FixtureWorkspace { xdg_data_home: self.xdg_data_home.clone(), xdg_state_home: self.xdg_state_home.clone(), xdg_config_home: self.xdg_config_home.clone(), + xdg_runtime_dir: self.xdg_runtime_dir.clone(), + fixture_root: self.root.clone(), terminal_size: (100, 32), hold_background_task: None, artifacts_dir: self.artifacts_dir.clone(), @@ -704,6 +757,88 @@ impl FixtureWorkspace { config.hold_background_task = Some(task.into()); config } + + pub fn cleanup(mut self) -> Result { + self.cleanup_inner(true) + } + + fn cleanup_inner(&mut self, strict: bool) -> Result { + let snapshot_dir = self.artifacts_dir.join("fixture-snapshot"); + if snapshot_dir.exists() { + fs::remove_dir_all(&snapshot_dir)?; + } + copy_dir_recursive(&self.root, &snapshot_dir)?; + + let mut cleanup_error = None; + if let Some(temp_root) = self.temp_root.take() { + if let Err(err) = temp_root.close() { + cleanup_error = Some(err.to_string()); + } + } + let fixture_root_exists_after = self.root.exists(); + let cleanup_success = cleanup_error.is_none() && !fixture_root_exists_after; + let report = FixtureCleanupReport { + fixture_root: self.root.clone(), + artifacts_dir: self.artifacts_dir.clone(), + snapshot_dir, + cleanup_attempted: true, + cleanup_success, + fixture_root_exists_after, + cleanup_error, + report_path: self.artifacts_dir.join("cleanup.json"), + }; + fs::write(&report.report_path, serde_json::to_vec_pretty(&report)?)?; + self.write_fixture_metadata("cleaned", Some(&report))?; + if strict && !report.cleanup_success { + return Err(HarnessError::Protocol(format!( + "fixture cleanup failed for {}; see {}", + report.fixture_root.display(), + report.report_path.display() + ))); + } + Ok(report) + } + + fn write_fixture_metadata( + &self, + phase: &str, + cleanup: Option<&FixtureCleanupReport>, + ) -> Result<()> { + fs::create_dir_all(&self.artifacts_dir)?; + fs::write( + self.artifacts_dir.join("fixture.json"), + serde_json::to_vec_pretty(&serde_json::json!({ + "phase": phase, + "fixture_root": &self.root, + "workspace": &self.workspace, + "home": &self.home, + "xdg_data_home": &self.xdg_data_home, + "xdg_state_home": &self.xdg_state_home, + "xdg_config_home": &self.xdg_config_home, + "xdg_runtime_dir": &self.xdg_runtime_dir, + "artifacts_dir": &self.artifacts_dir, + "env_runtime_policy": { + "tested_yoi_uses_env_clear": true, + "host_runtime_inherited": false, + "host_xdg_runtime_dir_present": std::env::var_os("XDG_RUNTIME_DIR").is_some(), + "tested_yoi_runtime_source": "fixture XDG_RUNTIME_DIR", + "tested_yoi_pod_registry": self.xdg_runtime_dir.join("yoi").join("pods.json"), + "fixture_pod_metadata_root": self.xdg_data_home.join("yoi").join("pods") + }, + "tested_yoi_env_policy": tested_yoi_env_policy_overview(), + "cleanup": cleanup, + }))?, + )?; + Ok(()) + } +} + +impl Drop for FixtureWorkspace { + fn drop(&mut self) { + if self.temp_root.is_some() { + let _ = self.cleanup_inner(false); + } + } } pub fn yoi_binary() -> Result { @@ -882,6 +1017,8 @@ fn create_ticket( data: &Path, state: &Path, config: &Path, + runtime: &Path, + artifacts_dir: &Path, title: &str, ) -> Result { let output = run_yoi_capture( @@ -891,6 +1028,8 @@ fn create_ticket( data, state, config, + runtime, + artifacts_dir, &["ticket", "create", "--title", title], )?; output @@ -907,9 +1046,21 @@ fn run_yoi( data: &Path, state: &Path, config: &Path, + runtime: &Path, + artifacts_dir: &Path, args: &[&str], ) -> Result<()> { - let output = run_yoi_capture(binary, workspace, home, data, state, config, args)?; + let output = run_yoi_capture( + binary, + workspace, + home, + data, + state, + config, + runtime, + artifacts_dir, + args, + )?; drop(output); Ok(()) } @@ -921,10 +1072,12 @@ fn run_yoi_capture( data: &Path, state: &Path, config: &Path, + runtime: &Path, + artifacts_dir: &Path, args: &[&str], ) -> Result { let env_policy = fixture_setup_env_policy(); - append_fixture_command_artifact(workspace, binary, args, &env_policy)?; + append_fixture_command_artifact(artifacts_dir, workspace, binary, args, &env_policy)?; let mut command = Command::new(binary); command @@ -935,6 +1088,7 @@ fn run_yoi_capture( .env("XDG_DATA_HOME", data) .env("XDG_STATE_HOME", state) .env("XDG_CONFIG_HOME", config) + .env("XDG_RUNTIME_DIR", runtime) .env("YOI_POD_RUNTIME_COMMAND", binary); let output = command.output()?; @@ -953,19 +1107,13 @@ fn run_yoi_capture( } fn append_fixture_command_artifact( + artifacts_dir: &Path, workspace: &Path, binary: &Path, args: &[&str], env_policy: &EnvPolicy, ) -> Result<()> { - let fixture_root = workspace.parent().ok_or_else(|| { - HarnessError::Protocol(format!( - "fixture workspace {} has no parent for artifacts", - workspace.display() - )) - })?; - let artifacts_dir = fixture_root.join("artifacts"); - fs::create_dir_all(&artifacts_dir)?; + fs::create_dir_all(artifacts_dir)?; let mut file = OpenOptions::new() .append(true) .create(true) @@ -975,6 +1123,7 @@ fn append_fixture_command_artifact( &serde_json::json!({ "ts_ms": now_ms(), "binary": binary, + "workspace": workspace, "args": args, "tested_yoi_env_policy": env_policy, }), @@ -983,6 +1132,31 @@ fn append_fixture_command_artifact( Ok(()) } +fn copy_dir_recursive(source: &Path, destination: &Path) -> Result<()> { + if !source.exists() { + return Ok(()); + } + fs::create_dir_all(destination)?; + for entry in fs::read_dir(source)? { + let entry = entry?; + let file_type = entry.file_type()?; + let target = destination.join(entry.file_name()); + if file_type.is_dir() { + copy_dir_recursive(&entry.path(), &target)?; + } else if file_type.is_file() { + fs::copy(entry.path(), target)?; + } else if file_type.is_symlink() { + // Preserve enough diagnostics for E2E artifacts without following links out of + // the fixture temp root. + fs::write( + target, + format!("symlink -> {}\n", fs::read_link(entry.path())?.display()), + )?; + } + } + Ok(()) +} + fn write_blocking_pod_metadata(data_home: &Path, pod_name: &str) -> Result<()> { let dir = data_home.join("yoi").join("pods").join(pod_name); fs::create_dir_all(&dir)?; diff --git a/tests/e2e/tests/panel.rs b/tests/e2e/tests/panel.rs index bf4d492a..5ae4cf45 100644 --- a/tests/e2e/tests/panel.rs +++ b/tests/e2e/tests/panel.rs @@ -1,15 +1,23 @@ use std::time::Duration; -use yoi_e2e::{FixtureWorkspace, KeyPress, PanelHarness, yoi_binary}; +use yoi_e2e::{ + FixtureCleanupReport, FixtureWorkspace, KeyPress, PanelHarness, RenderedPanelRow, yoi_binary, +}; #[test] fn panel_mouse_click_selects_row_without_dispatching_action() -> yoi_e2e::Result<()> { let binary = yoi_binary()?; let fixture = FixtureWorkspace::new(&binary)?; + assert_fixture_paths_are_isolated(&fixture); let mut panel = PanelHarness::spawn(fixture.panel_config(binary))?; panel.expect_mouse_capture_enabled()?; let rows = panel.wait_for_rows(2)?; + assert_no_runtime_or_host_pod_leak( + &fixture, + &rows.rows, + panel.artifacts().dir.display().to_string().as_str(), + ); let selected = rows.selected.clone(); let target = rows .rows @@ -34,6 +42,8 @@ fn panel_mouse_click_selects_row_without_dispatching_action() -> yoi_e2e::Result panel.press(KeyPress::CtrlC)?; let status = panel.expect_exit_within(PanelHarness::default_exit_wait())?; assert!(status.success(), "panel should exit cleanly with Ctrl+C"); + drop(panel); + assert_fixture_cleanup(fixture.cleanup()?); Ok(()) } @@ -41,6 +51,7 @@ fn panel_mouse_click_selects_row_without_dispatching_action() -> yoi_e2e::Result fn panel_ctrl_c_exits_promptly_after_background_barrier() -> yoi_e2e::Result<()> { let binary = yoi_binary()?; let fixture = FixtureWorkspace::new(&binary)?; + assert_fixture_paths_are_isolated(&fixture); let mut panel = PanelHarness::spawn(fixture.panel_config_holding_background_task(binary, "reload"))?; @@ -68,5 +79,95 @@ fn panel_ctrl_c_exits_promptly_after_background_barrier() -> yoi_e2e::Result<()> "quit_requested observability event missing; artifacts at {}", panel.artifacts().dir.display() ); + drop(panel); + assert_fixture_cleanup(fixture.cleanup()?); Ok(()) } + +fn assert_fixture_paths_are_isolated(fixture: &FixtureWorkspace) { + assert!( + fixture.root.exists(), + "fixture temp root should exist during scenario" + ); + assert!(fixture.workspace.starts_with(&fixture.root)); + assert!(fixture.home.starts_with(&fixture.root)); + assert!(fixture.xdg_data_home.starts_with(&fixture.root)); + assert!(fixture.xdg_state_home.starts_with(&fixture.root)); + assert!(fixture.xdg_config_home.starts_with(&fixture.root)); + assert!(fixture.xdg_runtime_dir.starts_with(&fixture.root)); + assert!( + !fixture.artifacts_dir.starts_with(&fixture.root), + "persistent artifacts must live outside the temp root so cleanup can remove the fixture" + ); + if let Some(host_runtime) = std::env::var_os("XDG_RUNTIME_DIR") { + assert_ne!( + fixture.xdg_runtime_dir, + std::path::PathBuf::from(host_runtime), + "tested yoi must not reuse host XDG_RUNTIME_DIR" + ); + } +} + +fn assert_no_runtime_or_host_pod_leak( + fixture: &FixtureWorkspace, + rows: &[RenderedPanelRow], + artifacts: &str, +) { + let rendered = rows + .iter() + .map(|row| { + format!( + "{} {} {} {}", + row.key.kind, + row.key.id, + row.title, + row.status.as_deref().unwrap_or_default() + ) + }) + .collect::>() + .join("\n"); + for marker in [ + "workspace-orchestrator", + "yoi-orchestrator-orchestrator", + "host-runtime-leak", + ] { + assert!( + !rendered.contains(marker), + "host/fixture runtime Pod marker {marker:?} leaked into panel rows; artifacts at {artifacts}\n{rendered}" + ); + } + if let Some(host_runtime) = std::env::var_os("XDG_RUNTIME_DIR") { + let host_runtime = host_runtime.to_string_lossy(); + assert!( + !rendered.contains(host_runtime.as_ref()), + "host XDG_RUNTIME_DIR leaked into panel rows; artifacts at {artifacts}\n{rendered}" + ); + } + assert!( + rendered.contains("E2E Ticket"), + "panel should be observing fixture-local Ticket data; artifacts at {artifacts}\n{rendered}" + ); + assert!(fixture.xdg_runtime_dir.starts_with(&fixture.root)); +} + +fn assert_fixture_cleanup(report: FixtureCleanupReport) { + assert!( + report.cleanup_success, + "fixture cleanup failed; report at {}: {:?}", + report.report_path.display(), + report.cleanup_error + ); + assert!( + !report.fixture_root.exists(), + "fixture temp root should be removed after scenario: {}", + report.fixture_root.display() + ); + assert!( + report.report_path.exists(), + "cleanup artifact should persist" + ); + assert!( + report.snapshot_dir.exists(), + "fixture snapshot should persist under target/e2e-artifacts before temp cleanup" + ); +} From 39f5fffb2b32987f765594b1381d2c7a7634483f Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 14 Jun 2026 02:32:17 +0900 Subject: [PATCH 5/6] ticket: approve e2e tmp isolation --- .yoi/tickets/00001KV0YK5S0/item.md | 2 +- .yoi/tickets/00001KV0YK5S0/thread.md | 32 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/.yoi/tickets/00001KV0YK5S0/item.md b/.yoi/tickets/00001KV0YK5S0/item.md index 7cc4438b..dbc488ed 100644 --- a/.yoi/tickets/00001KV0YK5S0/item.md +++ b/.yoi/tickets/00001KV0YK5S0/item.md @@ -2,7 +2,7 @@ title: 'E2E harness を完全な tmp runtime/data/workspace 隔離と cleanup に対応させる' state: 'inprogress' created_at: '2026-06-13T16:56:11Z' -updated_at: '2026-06-13T16:56:58Z' +updated_at: '2026-06-13T17:31:57Z' assignee: null readiness: 'ready' queued_by: 'yoi ticket' diff --git a/.yoi/tickets/00001KV0YK5S0/thread.md b/.yoi/tickets/00001KV0YK5S0/thread.md index a5a4eb29..86c02857 100644 --- a/.yoi/tickets/00001KV0YK5S0/thread.md +++ b/.yoi/tickets/00001KV0YK5S0/thread.md @@ -87,3 +87,35 @@ Escalate if: Ticket evidence、related E2E tickets、existing code map、Orchestrator worktree clean state を確認した。ユーザーが明示的に修正を依頼しており、blocking relation はない。実装 side effect の前に inprogress acceptance を記録する。 --- + + + +## Review: approve + +## Review: approve + +外部 Reviewer Pod 起動を試みたが、この Orchestrator の現在の delegation scope では child launch validation が要求する root workspace read grant を再委譲できず、Reviewer Pod を起動できなかった。そのため Orchestrator が実装 diff・validation・artifact を直接確認した。 + +確認した実装: +- `tests/e2e/src/lib.rs` に per-scenario `FixtureWorkspace` / `TempDir` 管理を追加。 +- fixture root 配下に `workspace`、`home`、`data`、`state`、`config`、`runtime` を作り、tested `yoi` には `env_clear()` + fixture-local `XDG_RUNTIME_DIR` を渡す。 +- `PanelHarness::spawn` と fixture setup command は host `XDG_RUNTIME_DIR` / host provider env を継承しない。 +- fixture snapshot / cleanup report / fixture metadata を `target/e2e-artifacts//` に保存し、fixture temp root は cleanup 後に存在しないことを記録する。 +- tests は hostile host `XDG_RUNTIME_DIR` を設定した状態でも fixture runtime policy を使う。 +- existing binary provider、mouse capture guard、quit pending barrier、env credential isolation は維持されている。 + +Validation performed by Orchestrator: +- `cargo fmt --check`: PASS +- `git diff --check`: PASS +- `cargo check -p yoi-e2e --all-targets --features e2e`: PASS +- `XDG_RUNTIME_DIR=/tmp/yoi-e2e-host-runtime-leak-test OPENAI_API_KEY=host-secret ANTHROPIC_API_KEY=host-secret GEMINI_API_KEY=host-secret cargo test -p yoi-e2e --features e2e --test panel -- --nocapture`: PASS +- artifact spot-check: recent `cleanup.json` records `cleanup_success: true` and `fixture_root_exists_after: false`。 +- `target/e2e-tmp` の直下に residual fixture temp root が残っていないことを確認。 + +Notes: +- Failure artifact は fixture snapshot を保存するため、host への leak ではなく `target/e2e-artifacts` に診断情報が残る。 +- Fixture-created `workspace` / `workspace-orchestrator` Pod metadata は fixture data dir / snapshot に閉じる。host runtime/data registry を読む経路は fixture `XDG_RUNTIME_DIR` + env clear により遮断されている。 + +Decision: approve. + +--- From 6aa7c650e8d0a4f3874d70936a1d267ac33b457b Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 14 Jun 2026 02:34:03 +0900 Subject: [PATCH 6/6] ticket: complete e2e tmp isolation --- .yoi/tickets/00001KV0YK5S0/item.md | 4 +-- .yoi/tickets/00001KV0YK5S0/thread.md | 43 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/.yoi/tickets/00001KV0YK5S0/item.md b/.yoi/tickets/00001KV0YK5S0/item.md index dbc488ed..ac642456 100644 --- a/.yoi/tickets/00001KV0YK5S0/item.md +++ b/.yoi/tickets/00001KV0YK5S0/item.md @@ -1,8 +1,8 @@ --- title: 'E2E harness を完全な tmp runtime/data/workspace 隔離と cleanup に対応させる' -state: 'inprogress' +state: 'done' created_at: '2026-06-13T16:56:11Z' -updated_at: '2026-06-13T17:31:57Z' +updated_at: '2026-06-13T17:33:53Z' assignee: null readiness: 'ready' queued_by: 'yoi ticket' diff --git a/.yoi/tickets/00001KV0YK5S0/thread.md b/.yoi/tickets/00001KV0YK5S0/thread.md index 7b38561c..e2e712eb 100644 --- a/.yoi/tickets/00001KV0YK5S0/thread.md +++ b/.yoi/tickets/00001KV0YK5S0/thread.md @@ -150,3 +150,46 @@ Notes: Decision: approve. --- + + + +## Implementation report + +Integration outcome: merged and validated. + +- Implementation branch: `ticket-00001KV0YK5S0-e2e-tmp-isolation` +- Implementation commit: `07e754ce test: isolate e2e tmp runtime fixtures` +- Review: Orchestrator direct review `approve`(Reviewer Pod は現 delegated scope の child launch validation 制約で起動できなかったため、Orchestrator が diff / validation / artifact を確認) +- Orchestrator merge commit: `20184eeb merge: e2e tmp isolation` + +Implemented: +- E2E fixture now creates per-scenario clean temp root under `target/e2e-tmp` with isolated workspace, HOME, XDG data/state/config, and explicit fixture `XDG_RUNTIME_DIR`。 +- Tested `yoi` subprocesses continue to use `env_clear()` and now receive fixture-local runtime dir, preventing host runtime / Pod registry observation。 +- Fixture metadata, run metadata, fixture snapshot, and cleanup report are persisted under `target/e2e-artifacts/` before temp cleanup。 +- Fixture temp roots are removed after scenario completion, and cleanup reports record `cleanup_success` plus `fixture_root_exists_after`。 +- Existing binary provider, `YOI_E2E_BIN` override, credential env isolation, mouse capture guard, and quit pending barrier remain intact。 + +Orchestrator validation after merge: +- `cargo fmt --check`: PASS +- `git diff --check`: PASS +- `cargo check -p yoi-e2e --all-targets --features e2e`: PASS +- `XDG_RUNTIME_DIR=/tmp/yoi-e2e-host-runtime-leak-test OPENAI_API_KEY=host-secret ANTHROPIC_API_KEY=host-secret GEMINI_API_KEY=host-secret cargo test -p yoi-e2e --features e2e --test panel -- --nocapture`: PASS(2 tests) +- Artifact spot-check: latest `cleanup.json` files record `cleanup_success: true` and `fixture_root_exists_after: false`。 +- `target/e2e-tmp` direct child count after validation: 0。 + +Residual notes: +- Persistent diagnostic artifacts intentionally remain under `target/e2e-artifacts`。 +- Fixture snapshots may contain fixture-local `workspace` / `workspace-orchestrator` metadata, but those live under the copied artifact snapshot, not host runtime/data state。 + +Next: +- Mark Ticket `done` and clean up implementation worktree/branch. + +--- + + + +## State changed + +E2E tmp/runtime isolation follow-up was reviewed, merged into the Orchestrator branch as `20184eeb`, and validated in the Orchestrator worktree. Panel E2E now uses clean per-scenario tmp workspace/data/runtime fixtures, preserves artifacts under `target/e2e-artifacts`, removes fixture temp roots after runs, and does not inherit host runtime/credential environment. Ticket implementation work is done; closure remains separate. + +---