merge: orchestration
This commit is contained in:
commit
b6bb22f956
|
|
@ -1,2 +1,3 @@
|
||||||
{"id":"orch-plan-20260710-220020-1","ticket_id":"00001KX6WVNPD","kind":"after","related_ticket":"00001KX6Y2A9Q","note":"Ticket has explicit typed `depends_on` relation to `00001KX6Y2A9Q`. That prerequisite is currently `inprogress` implementing the WorkerFilesystemAuthority boundary this Ticket relies on. Start only after `00001KX6Y2A9Q` is approved, merged, and closed or an explicit combined-work decision is made.","author":"orchestrator","at":"2026-07-10T22:00:20Z"}
|
{"id":"orch-plan-20260710-220020-1","ticket_id":"00001KX6WVNPD","kind":"after","related_ticket":"00001KX6Y2A9Q","note":"Ticket has explicit typed `depends_on` relation to `00001KX6Y2A9Q`. That prerequisite is currently `inprogress` implementing the WorkerFilesystemAuthority boundary this Ticket relies on. Start only after `00001KX6Y2A9Q` is approved, merged, and closed or an explicit combined-work decision is made.","author":"orchestrator","at":"2026-07-10T22:00:20Z"}
|
||||||
{"id":"orch-plan-20260710-220024-2","ticket_id":"00001KX6WVNPD","kind":"waiting_capacity_note","note":"Dashboard queue routing inspected. This Ticket is otherwise concrete, but typed relation metadata reports blocker `00001KX6Y2A9Q` in state `inprogress`. That prerequisite introduces/removes the Worker filesystem-authority/cwd boundary used by this Ticket. Starting now would duplicate or conflict with that refactor. Leave queued and re-route after prerequisite merge/close.","author":"orchestrator","at":"2026-07-10T22:00:24Z"}
|
{"id":"orch-plan-20260710-220024-2","ticket_id":"00001KX6WVNPD","kind":"waiting_capacity_note","note":"Dashboard queue routing inspected. This Ticket is otherwise concrete, but typed relation metadata reports blocker `00001KX6Y2A9Q` in state `inprogress`. That prerequisite introduces/removes the Worker filesystem-authority/cwd boundary used by this Ticket. Starting now would duplicate or conflict with that refactor. Leave queued and re-route after prerequisite merge/close.","author":"orchestrator","at":"2026-07-10T22:00:24Z"}
|
||||||
|
{"id":"orch-plan-20260710-233758-3","ticket_id":"00001KX6WVNPD","kind":"accepted_plan","accepted_plan":{"summary":"Implement embedded Runtime no-workdir Worker creation policy using the merged `WorkerFilesystemAuthority` boundary: embedded Runtime may create Workers without selected workdir using `WorkerFilesystemAuthority::None`, non-embedded/local-authority paths preserve workdir requirements, no-workdir Workers expose no filesystem/Bash tool surface, and UI/API make workdir optional only for embedded Runtime.","branch":"work/00001KX6WVNPD-embedded-no-workdir-policy","worktree":"/home/hare/Projects/yoi/.worktree/00001KX6WVNPD-embedded-no-workdir-policy","role_plan":"Orchestrator accepts queued Ticket after prerequisite `00001KX6Y2A9Q` was approved, merged, validated, and closed. Use sibling Coder for implementation in a dedicated worktree, then sibling Reviewer for read-only review. Orchestrator retains merge/final-validation/close/worktree cleanup authority. Per current user instruction, do not call StopPod during cleanup."},"author":"orchestrator","at":"2026-07-10T23:37:58Z"}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
---
|
---
|
||||||
title: 'Embedded no-workdir Worker authority policy'
|
title: 'Embedded no-workdir Worker authority policy'
|
||||||
state: 'queued'
|
state: 'closed'
|
||||||
priority: 'P1'
|
priority: 'P1'
|
||||||
created_at: '2026-07-10T20:52:43Z'
|
created_at: '2026-07-10T20:52:43Z'
|
||||||
updated_at: '2026-07-10T22:00:33Z'
|
updated_at: '2026-07-11T00:13:51Z'
|
||||||
assignee: null
|
assignee: null
|
||||||
queued_by: 'workspace-panel'
|
queued_by: 'workspace-panel'
|
||||||
queued_at: '2026-07-10T22:00:07Z'
|
queued_at: '2026-07-10T22:00:07Z'
|
||||||
|
|
|
||||||
28
.yoi/tickets/00001KX6WVNPD/resolution.md
Normal file
28
.yoi/tickets/00001KX6WVNPD/resolution.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
Embedded no-workdir Worker authority policy を実装・レビュー・merge・検証した。
|
||||||
|
|
||||||
|
実装内容:
|
||||||
|
- Runtime launch metadata に `working_directory_required` を追加し、embedded Runtime のみ workdir optional として扱うようにした。
|
||||||
|
- Workspace/API Worker create path で、embedded Runtime では no-workdir Worker creation を許可し、non-embedded Runtime では no-workdir creation を typed diagnostic で拒否するようにした。
|
||||||
|
- Embedded no-workdir launch は Runtime へ working directory なしで渡され、merged `WorkerFilesystemAuthority` 境界により `WorkerFilesystemAuthority::None` になる。
|
||||||
|
- Workdir-present embedded Worker は materialized binding を `WorkerFilesystemAuthority::Local` として保持する既存 behavior を維持。
|
||||||
|
- Browser Worker creation UI で embedded Runtime の場合だけ “No working directory — guidance-only Worker; filesystem tools are disabled” を選べるようにした。
|
||||||
|
- No-workdir launch では relative cwd input を隠し、filesystem tools / Bash が disabled であることを明示。
|
||||||
|
- Tests で launch metadata、non-embedded no-workdir rejection、embedded no-workdir request construction、および existing runtime tests による no-workdir tool omission/no cwd fallback を確認。
|
||||||
|
|
||||||
|
Review:
|
||||||
|
- Reviewer approved with no blockers。
|
||||||
|
- Evidence included API optionality bounds, non-embedded rejection, authority construction path, no filesystem/Bash tool surface coverage, workdir-present behavior, and Browser labeling。
|
||||||
|
|
||||||
|
Merge / validation:
|
||||||
|
- Merge commit: `2f7b8094 merge: embedded no-workdir worker policy`。
|
||||||
|
- Final validation passed:
|
||||||
|
- `rg 'worker\\.cwd\\(' . || true`
|
||||||
|
- `git diff --check`
|
||||||
|
- `cargo test -p worker-runtime --features ws-server,fs-store`
|
||||||
|
- `cargo test -p yoi-workspace-server --lib`
|
||||||
|
- `cargo check -p yoi`
|
||||||
|
- `cd web/workspace && deno task check`
|
||||||
|
- `cd web/workspace && deno task test`
|
||||||
|
- `yoi ticket doctor`
|
||||||
|
- `nix build .#yoi --no-link`
|
||||||
|
- Validation log: `/run/user/1000/yoi/yoi-orchestrator/bash-output/embedded-no-workdir-final-validation-1783728763.txt`
|
||||||
|
|
@ -88,3 +88,223 @@ Next action:
|
||||||
- Escalate only if a human explicitly requests combined implementation in the same branch/worktree。
|
- Escalate only if a human explicitly requests combined implementation in the same branch/worktree。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: orchestrator at: 2026-07-10T23:38:28Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: implementation_ready
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- 前提 Ticket `00001KX6Y2A9Q` は approved / merged / final-validated / closed 済みで、`WorkerFilesystemAuthority::{None, Local}` と no-workdir core filesystem/Bash tool omission boundary が利用可能になった。
|
||||||
|
- この Ticket は embedded Runtime の workdir 未指定 Worker 作成 policy と UI/API 制約に集中しており、目的・binding decision・受け入れ条件が具体的である。
|
||||||
|
- `TicketRelationQuery(00001KX6WVNPD)` の blocker view は空で、残る outgoing `depends_on 00001KX6Y2A9Q` は closed prerequisite になっている。
|
||||||
|
- `TicketOrchestrationPlanQuery(00001KX6WVNPD)` の既存 waiting note は prerequisite inprogress を理由にしたものだったため、現在は解消済み。
|
||||||
|
- Orchestrator worktree は clean。既存 child implementation branch/worktree は見当たらない。
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- Ticket body / thread / relations / orchestration plan。
|
||||||
|
- Related closed Ticket `00001KX6Y2A9Q` resolution/review/validation evidence。
|
||||||
|
- `TicketList(inprogress)`: 0 件。
|
||||||
|
- `git status --short --branch` and `git worktree list` in `/home/hare/Projects/yoi/.worktree/orchestration`。
|
||||||
|
- Current queue also contains related `00001KX6Y6ZEA`, but its Worker workspace-backend refactor touches overlapping Worker context/constructor surfaces; it will be held separately with a conflict/migration-boundary reason rather than started in parallel。
|
||||||
|
|
||||||
|
IntentPacket:
|
||||||
|
|
||||||
|
Intent:
|
||||||
|
- Embedded Runtime 選択時に、workdir 未指定の guidance/conversation-oriented Worker を作成できるようにする。
|
||||||
|
- Embedded no-workdir Worker は `WorkerFilesystemAuthority::None` として作られ、repository filesystem authority / cwd fallback / filesystem tools / Bash を持たない。
|
||||||
|
|
||||||
|
Binding decisions / invariants:
|
||||||
|
- no-workdir authority は merged `WorkerFilesystemAuthority::None` を使う。
|
||||||
|
- workspace root / process cwd / runtime cwd / manifest override / default scope を filesystem authority fallback にしない。
|
||||||
|
- filesystem tools (`Read`, `Write`, `Edit`, `Glob`, `Grep`) と `Bash` は no-workdir embedded Worker では construct/register/model-visible にしない。
|
||||||
|
- Bash は filesystem sandbox ではないため、no-workdir embedded では登録しない。
|
||||||
|
- non-embedded または local/filesystem-authority-required launch path では、既存の workdir 必須制約または明示 authority requirement を維持する。
|
||||||
|
- Worker 直下の `cwd` property / `worker.cwd()` 前提に戻さない。
|
||||||
|
|
||||||
|
Requirements / acceptance criteria:
|
||||||
|
- UI/API 経由で embedded Runtime の workdir 未指定 Worker create ができる。
|
||||||
|
- embedded + workdir ありは materialized binding を `WorkerFilesystemAuthority::Local` と Worker summary/detail に反映する。
|
||||||
|
- embedded + workdir なしは `WorkerFilesystemAuthority::None` になり、model-visible tool surface に fs tools / Bash が出ないことをテストで確認できる。
|
||||||
|
- `Glob` / `Grep` path 省略や `Bash` initial directory が no-workdir embedded の権限漏れにならない。
|
||||||
|
- Browser/UI では workdir optional が embedded Runtime に限られ、non-embedded/local authority path の制約を緩めない。
|
||||||
|
|
||||||
|
Implementation latitude:
|
||||||
|
- API DTO / UI form model / runtime spawn request shape の具体的な整理は coder が既存 pattern に沿って選んでよい。
|
||||||
|
- workdir 未指定状態の UI 表示文言・validation placement は、binding invariant を満たす範囲で local tactic としてよい。
|
||||||
|
- tests の配置は worker-runtime / workspace-server / web の既存近接 test pattern に合わせてよい。
|
||||||
|
|
||||||
|
Escalate if:
|
||||||
|
- embedded no-workdir Worker に workspace API access を付けるために `WorkspaceBackend` / `WorkspaceClient` の先行導入が必須になる場合。
|
||||||
|
- no-workdir Worker へ fs/Bash tool の error-only stub や empty-scope fallback を登録したくなる場合。
|
||||||
|
- non-embedded Worker の workdir requirement を緩める必要が出る場合。
|
||||||
|
- `WorkerFilesystemAuthority` の既存 invariant を変更する必要が出る場合。
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- `git diff --check`
|
||||||
|
- focused grep/check that no no-workdir path reintroduces `worker.cwd()` or local cwd fallback。
|
||||||
|
- `cargo test -p worker --lib --tests` if worker crate is touched。
|
||||||
|
- `cargo test -p worker-runtime --features ws-server,fs-store`
|
||||||
|
- `cargo test -p yoi-workspace-server --lib`
|
||||||
|
- `cargo check -p yoi`
|
||||||
|
- `cd web/workspace && deno task check && deno task test` if web/API/types are touched。
|
||||||
|
- `yoi ticket doctor`
|
||||||
|
- `nix build .#yoi --no-link`
|
||||||
|
|
||||||
|
Current code map / likely touch points:
|
||||||
|
- `crates/worker/src/worker.rs`, `crates/worker/src/controller.rs` for existing authority boundary verification only if needed。
|
||||||
|
- `crates/worker-runtime/src/worker_backend.rs` / working-directory binding and embedded spawn paths。
|
||||||
|
- `crates/workspace-server/src/server.rs` / API DTOs for Worker create/runtime options。
|
||||||
|
- `web/workspace` worker creation UI/types if workdir optionality is exposed there。
|
||||||
|
|
||||||
|
Critical risks / reviewer focus:
|
||||||
|
- Hidden filesystem authority through workspace root / process cwd / runtime cwd fallback。
|
||||||
|
- no-workdir Worker showing fs tools/Bash in model-visible schema。
|
||||||
|
- empty-scope/error-only fs tool registration rather than no registration。
|
||||||
|
- UI/API accidentally allowing non-embedded no-workdir Worker where local filesystem authority is still required。
|
||||||
|
- regression of embedded workdir-present Worker status/tool cwd behavior。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-07-10T23:38:34Z from: queued to: inprogress reason: accepted_for_implementation field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Prerequisite `00001KX6Y2A9Q` is now closed and merged. This Ticket is accepted for implementation before creating a worktree or spawning role Pods.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-07-11T00:00:55Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Implementation progress report:
|
||||||
|
|
||||||
|
- Coder Pod `yoi-coder-00001KX6WVNPD-no-workdir-policy` completed an implementation pass in `/home/hare/Projects/yoi/.worktree/00001KX6WVNPD-embedded-no-workdir-policy` on branch `work/00001KX6WVNPD-embedded-no-workdir-policy`。
|
||||||
|
- Implementation commit: `10267869ae0dfe02e186dc69fec14151926f5009 feat: enforce embedded no-workdir worker policy`。
|
||||||
|
- Files touched:
|
||||||
|
- `crates/workspace-server/src/server.rs`
|
||||||
|
- `web/workspace/src/lib/workspace-sidebar/types.ts`
|
||||||
|
- `web/workspace/src/lib/workspace-sidebar/worker-launch.test.ts`
|
||||||
|
- `web/workspace/src/routes/w/[workspaceId]/workers/new/+page.svelte`
|
||||||
|
- Orchestrator inspected worktree status, branch log, commit stats, `git diff --check 83c9d884..HEAD`, and `rg 'worker\\.cwd\\(\\)' .`; worktree was clean, diff check passed, and no `worker.cwd()` matches were found。
|
||||||
|
|
||||||
|
Implementation summary:
|
||||||
|
- Added Browser/API launch metadata for whether a runtime requires working directory selection。
|
||||||
|
- Embedded Runtime can be marked as workdir-optional; non-embedded Runtime remains workdir-required。
|
||||||
|
- Server-side creation path rejects no-workdir Worker creation unless the target runtime is embedded, using a typed diagnostic。
|
||||||
|
- Browser Worker creation UI offers “no workdir / embedded conversation only” only when the selected runtime allows it。
|
||||||
|
- Relative cwd input is hidden for no-workdir launch and UI text explains no filesystem tools or Bash are available without workdir。
|
||||||
|
- Tests were added/updated for launch metadata, non-embedded no-workdir rejection, and web request construction for embedded no-workdir launch。
|
||||||
|
|
||||||
|
Next action:
|
||||||
|
- Route to external Reviewer Pod before merge/close decisions。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: review author: reviewer at: 2026-07-11T00:12:24Z status: approve -->
|
||||||
|
|
||||||
|
## Review: approve
|
||||||
|
|
||||||
|
External review result: approve
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- UI/API optionality is correctly bounded: launch options expose `working_directory_required` and set it false only for embedded runtime (`crates/workspace-server/src/server.rs:749`, `:3823`), while browser request construction omits `working_directory` for embedded no-workdir mode and tests cover that (`web/workspace/src/lib/workspace-sidebar/worker-launch.test.ts:80`)。
|
||||||
|
- Non-embedded no-workdir is rejected in workspace/API launch paths (`server.rs:2639`, `:2839`) with tests covering the 400 behavior (`server.rs:6383`, `:6554`)。
|
||||||
|
- Authority boundary is preserved: absent materialized workdir reaches runtime as no working directory (`server.rs:2652`) and worker-runtime maps that to `WorkerFilesystemAuthority::None`; workdir-present maps to `Local(root, cwd)` (`crates/worker-runtime/src/worker_backend.rs:301`)。
|
||||||
|
- Tool surface/no fallback is covered by existing runtime tests asserting no cwd observation and omission of `Read`/`Write`/`Edit`/`Glob`/`Grep`/`Bash` for no-workdir workers (`worker_backend.rs:1151`, `:1216`)。
|
||||||
|
- Workdir-present behavior remains covered by materialized binding assertions (`worker_backend.rs:1151`) and server worker creation still links/summarizes workdir-present workers。
|
||||||
|
- Browser labeling is explicit: embedded mode offers “No working directory — guidance-only Worker; filesystem tools are disabled” and non-embedded paths require selection (`+page.svelte:153`, `:289`)。
|
||||||
|
|
||||||
|
Validation performed by reviewer:
|
||||||
|
- `cargo test -p yoi-workspace-server no_workdir -- --nocapture`
|
||||||
|
- `cargo test -p worker-runtime --features ws-server --lib -- --nocapture`
|
||||||
|
- `cd web/workspace && deno task test`
|
||||||
|
- Final `git status --short` was clean。
|
||||||
|
|
||||||
|
Blockers: none。
|
||||||
|
Non-blocking notes: none。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-07-11T00:13:31Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Merge / final validation report:
|
||||||
|
|
||||||
|
- Reviewer approved the implementation with no blockers。
|
||||||
|
- Merged branch `work/00001KX6WVNPD-embedded-no-workdir-policy` into orchestration branch with merge commit `2f7b8094 merge: embedded no-workdir worker policy`。
|
||||||
|
- Final validation was rerun in `/home/hare/Projects/yoi/.worktree/orchestration` after merge。
|
||||||
|
|
||||||
|
Validation passed:
|
||||||
|
- `rg 'worker\\.cwd\\(' . || true`: no matches。
|
||||||
|
- `git diff --check`
|
||||||
|
- `cargo test -p worker-runtime --features ws-server,fs-store`
|
||||||
|
- `cargo test -p yoi-workspace-server --lib`
|
||||||
|
- `cargo check -p yoi`
|
||||||
|
- `cd web/workspace && deno task check`
|
||||||
|
- `cd web/workspace && deno task test`
|
||||||
|
- `yoi ticket doctor`
|
||||||
|
- `nix build .#yoi --no-link`
|
||||||
|
|
||||||
|
Validation log:
|
||||||
|
- `/run/user/1000/yoi/yoi-orchestrator/bash-output/embedded-no-workdir-final-validation-1783728763.txt`
|
||||||
|
|
||||||
|
Reviewed/merged implementation commit:
|
||||||
|
- `10267869 feat: enforce embedded no-workdir worker policy`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-07-11T00:13:37Z from: inprogress to: done reason: implementation_validated field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Implementation was reviewed, merged into orchestration branch, and final validation passed. Moving to done before closure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: hare at: 2026-07-11T00:13:51Z from: done to: closed reason: closed field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を closed にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: close author: hare at: 2026-07-11T00:13:51Z status: closed -->
|
||||||
|
|
||||||
|
## 完了
|
||||||
|
|
||||||
|
Embedded no-workdir Worker authority policy を実装・レビュー・merge・検証した。
|
||||||
|
|
||||||
|
実装内容:
|
||||||
|
- Runtime launch metadata に `working_directory_required` を追加し、embedded Runtime のみ workdir optional として扱うようにした。
|
||||||
|
- Workspace/API Worker create path で、embedded Runtime では no-workdir Worker creation を許可し、non-embedded Runtime では no-workdir creation を typed diagnostic で拒否するようにした。
|
||||||
|
- Embedded no-workdir launch は Runtime へ working directory なしで渡され、merged `WorkerFilesystemAuthority` 境界により `WorkerFilesystemAuthority::None` になる。
|
||||||
|
- Workdir-present embedded Worker は materialized binding を `WorkerFilesystemAuthority::Local` として保持する既存 behavior を維持。
|
||||||
|
- Browser Worker creation UI で embedded Runtime の場合だけ “No working directory — guidance-only Worker; filesystem tools are disabled” を選べるようにした。
|
||||||
|
- No-workdir launch では relative cwd input を隠し、filesystem tools / Bash が disabled であることを明示。
|
||||||
|
- Tests で launch metadata、non-embedded no-workdir rejection、embedded no-workdir request construction、および existing runtime tests による no-workdir tool omission/no cwd fallback を確認。
|
||||||
|
|
||||||
|
Review:
|
||||||
|
- Reviewer approved with no blockers。
|
||||||
|
- Evidence included API optionality bounds, non-embedded rejection, authority construction path, no filesystem/Bash tool surface coverage, workdir-present behavior, and Browser labeling。
|
||||||
|
|
||||||
|
Merge / validation:
|
||||||
|
- Merge commit: `2f7b8094 merge: embedded no-workdir worker policy`。
|
||||||
|
- Final validation passed:
|
||||||
|
- `rg 'worker\\.cwd\\(' . || true`
|
||||||
|
- `git diff --check`
|
||||||
|
- `cargo test -p worker-runtime --features ws-server,fs-store`
|
||||||
|
- `cargo test -p yoi-workspace-server --lib`
|
||||||
|
- `cargo check -p yoi`
|
||||||
|
- `cd web/workspace && deno task check`
|
||||||
|
- `cd web/workspace && deno task test`
|
||||||
|
- `yoi ticket doctor`
|
||||||
|
- `nix build .#yoi --no-link`
|
||||||
|
- Validation log: `/run/user/1000/yoi/yoi-orchestrator/bash-output/embedded-no-workdir-final-validation-1783728763.txt`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
---
|
---
|
||||||
title: 'Implement WorkerFilesystemAuthority for no-workdir Workers'
|
title: 'Implement WorkerFilesystemAuthority for no-workdir Workers'
|
||||||
state: 'inprogress'
|
state: 'closed'
|
||||||
priority: 'P1'
|
priority: 'P1'
|
||||||
created_at: '2026-07-10T21:13:49Z'
|
created_at: '2026-07-10T21:13:49Z'
|
||||||
updated_at: '2026-07-10T21:51:33Z'
|
updated_at: '2026-07-10T22:45:01Z'
|
||||||
assignee: null
|
assignee: null
|
||||||
queued_by: 'workspace-panel'
|
queued_by: 'workspace-panel'
|
||||||
queued_at: '2026-07-10T21:51:00Z'
|
queued_at: '2026-07-10T21:51:00Z'
|
||||||
|
|
|
||||||
32
.yoi/tickets/00001KX6Y2A9Q/resolution.md
Normal file
32
.yoi/tickets/00001KX6Y2A9Q/resolution.md
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
Worker filesystem authority の明示境界を実装・レビュー・merge・検証した。
|
||||||
|
|
||||||
|
実装内容:
|
||||||
|
- `WorkerFilesystemAuthority::{None, Local(LocalWorkingDirectory)}` を追加。
|
||||||
|
- `LocalWorkingDirectory` は filesystem authority `root` と default `cwd` を分離して保持する。
|
||||||
|
- Worker-level `cwd: PathBuf` field と `worker.cwd()` accessor を削除。
|
||||||
|
- core filesystem/Bash tool registration は local filesystem authority がある場合だけ行うようにした。
|
||||||
|
- No-workdir Worker では `Read` / `Write` / `Edit` / `Glob` / `Grep` / `Bash` を construct/register/model-visible にしない。
|
||||||
|
- Embedded no-workdir Worker spawn は Runtime/process cwd fallback ではなく `WorkerFilesystemAuthority::None` を明示的に渡す。
|
||||||
|
- Ticket / worker-spawn / peer-discovery など cwd 依存箇所は local filesystem authority 必須に整理し、workspace root fallback を authority として使わないようにした。
|
||||||
|
- tests で no-workdir tool omission、no cwd fallback、local/materialized Worker の filesystem/Bash behavior を確認。
|
||||||
|
|
||||||
|
Review:
|
||||||
|
- Reviewer approved with no blockers。
|
||||||
|
- Evidence included explicit authority type, no Worker cwd field/accessor, filesystem/Bash registration gate, embedded Runtime authority construction, and tests。
|
||||||
|
- Non-blocking note: workspace-context surfaces such as memory/MCP/resident prompt loading can still be backed by `workspace_root` when enabled; this is outside the focused core filesystem/Bash authority gate and may become follow-up if the invariant expands to “no local file reads of any kind”。
|
||||||
|
|
||||||
|
Merge / validation:
|
||||||
|
- Merge commit: `c29e9135 merge: worker filesystem authority`。
|
||||||
|
- Final validation passed:
|
||||||
|
- `rg "worker\\.cwd\\(" crates || true`
|
||||||
|
- `rg "pub fn cwd\\(" crates/worker/src/worker.rs || true`
|
||||||
|
- `git diff --check`
|
||||||
|
- `cargo test -p worker --lib --tests`
|
||||||
|
- `cargo test -p worker-runtime --features ws-server,fs-store`
|
||||||
|
- `cargo test -p yoi-workspace-server --lib`
|
||||||
|
- `cargo check -p yoi`
|
||||||
|
- `cd web/workspace && deno task check`
|
||||||
|
- `cd web/workspace && deno task test`
|
||||||
|
- `yoi ticket doctor`
|
||||||
|
- `nix build .#yoi --no-link`
|
||||||
|
- Validation log: `/run/user/1000/yoi/yoi-orchestrator/bash-output/worker-fs-authority-final-validation-1783723253.txt`
|
||||||
|
|
@ -101,3 +101,158 @@ Implementation focus:
|
||||||
Dashboard queue authorization was inspected, no blockers were found, and an accepted plan was recorded. Moving queued Ticket to inprogress before creating implementation worktree or spawning Pods.
|
Dashboard queue authorization was inspected, no blockers were found, and an accepted plan was recorded. Moving queued Ticket to inprogress before creating implementation worktree or spawning Pods.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-07-10T22:31:16Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Implementation progress report:
|
||||||
|
|
||||||
|
- Coder Pod `yoi-coder-00001KX6Y2A9Q-worker-fs-authority` completed implementation in `/home/hare/Projects/yoi/.worktree/00001KX6Y2A9Q-worker-filesystem-authority` on branch `work/00001KX6Y2A9Q-worker-filesystem-authority`。
|
||||||
|
- Implementation commit: `b50b94612a1185dc5bdc935d83d63f9d6ace826b feat: add explicit worker filesystem authority`。
|
||||||
|
- Orchestrator inspected the worktree status, branch log, commit stats, `git diff --check db3a7165..HEAD`, and grep checks for `worker.cwd()` / Worker `fn cwd`; worktree was clean, diff check passed, and no matches were found。
|
||||||
|
|
||||||
|
Coder-reported implementation summary:
|
||||||
|
- Added explicit `WorkerFilesystemAuthority::{None, Local(LocalWorkingDirectory)}` boundary。
|
||||||
|
- Removed Worker-level `cwd: PathBuf` and `worker.cwd()` accessor。
|
||||||
|
- Local Workers carry authority `root` and `cwd` separately。
|
||||||
|
- No-workdir embedded Worker spawn passes `WorkerFilesystemAuthority::None` explicitly instead of falling back to runtime/process cwd。
|
||||||
|
- Controller tool registration branches on local filesystem authority:
|
||||||
|
- Local Workers register `Read`, `Write`, `Edit`, `Glob`, `Grep`, `Bash` and get fs view/completions。
|
||||||
|
- No-workdir Workers do not construct/register filesystem or Bash tools。
|
||||||
|
- Ticket / worker-spawn / peer-discovery cwd-dependent paths now require local filesystem authority instead of using workspace root as fallback。
|
||||||
|
- Tests added/updated for no-workdir tool omission, no cwd fallback, and local/materialized Worker filesystem/Bash behavior。
|
||||||
|
|
||||||
|
Coder-reported validation passed:
|
||||||
|
- `cargo test -p worker --lib --tests`
|
||||||
|
- `cargo test -p worker-runtime --features ws-server,fs-store`
|
||||||
|
- `cargo test -p yoi-workspace-server --lib`
|
||||||
|
- `cargo check -p yoi`
|
||||||
|
- `git diff --check`
|
||||||
|
- `cargo run -p yoi -- ticket doctor`
|
||||||
|
- `nix build .#yoi --no-link`
|
||||||
|
- `rg "worker\\.cwd\\(" crates || true`: no matches。
|
||||||
|
- `rg "pub fn cwd\\(" crates/worker/src/worker.rs || true`: no Worker cwd accessor remains。
|
||||||
|
|
||||||
|
Not run by Coder:
|
||||||
|
- `cd web/workspace && deno task check && deno task test` because web/API/types were not touched。
|
||||||
|
|
||||||
|
Next action:
|
||||||
|
- Route to external Reviewer Pod before merge/close decisions。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: review author: reviewer at: 2026-07-10T22:40:35Z status: approve -->
|
||||||
|
|
||||||
|
## Review: approve
|
||||||
|
|
||||||
|
External review result: approve
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- `WorkerFilesystemAuthority::{None, Local(LocalWorkingDirectory)}` and separate `root`/`cwd` are present in `crates/worker/src/worker.rs`; `Worker` now stores `filesystem_authority` plus separate `workspace_root`, with no `cwd: PathBuf` Worker field。
|
||||||
|
- Core filesystem/Bash registration is gated on `worker.local_working_directory()` in `crates/worker/src/controller.rs:619-636`; no-local Workers return `(None, None)` and do not construct/register `Read`/`Write`/`Edit`/`Glob`/`Grep`/`Bash`。
|
||||||
|
- Ticket and worker-spawn surfaces require local authority rather than falling back to `workspace_root` (`controller.rs:647-667`, `719-733`)。
|
||||||
|
- Embedded runtime creates `WorkerFilesystemAuthority::Local(binding.root, binding.cwd)` only when a working directory binding exists, otherwise `WorkerFilesystemAuthority::None` (`crates/worker-runtime/src/worker_backend.rs:297-310`)。
|
||||||
|
- Tests cover no-workdir omission and no cwd fallback via empty observed materialized cwds plus forbidden core tool names (`worker_backend.rs:1133-1201`), and materialized local Workers exposing core tools with materialized cwd (`worker_backend.rs:1203-1260`)。
|
||||||
|
- Grep check found no `worker.cwd()` / Worker `fn cwd`; the only remaining `pub fn cwd` hit is `WorkingDirectory` binding API, not `Worker`。
|
||||||
|
|
||||||
|
Blockers: none。
|
||||||
|
|
||||||
|
Validation performed by reviewer:
|
||||||
|
- Read-only ticket/diff/source/test inspection and grep checks。
|
||||||
|
- Reviewer did not rerun cargo/nix tests; coder-reported validation covers the requested test/build set。
|
||||||
|
|
||||||
|
Non-blocking note:
|
||||||
|
- Workspace-context surfaces such as memory/MCP/resident prompt loading can still be backed by `workspace_root` when enabled; this was treated as outside this Ticket’s focused core filesystem/Bash authority gate, but may need follow-up if the intended invariant becomes “no local file reads of any kind” for no-workdir Workers。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-07-10T22:44:39Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Merge / final validation report:
|
||||||
|
|
||||||
|
- Reviewer approved the implementation with no blockers。
|
||||||
|
- Merged branch `work/00001KX6Y2A9Q-worker-filesystem-authority` into orchestration branch with merge commit `c29e9135 merge: worker filesystem authority`。
|
||||||
|
- Final validation was rerun in `/home/hare/Projects/yoi/.worktree/orchestration` after merge。
|
||||||
|
|
||||||
|
Validation passed:
|
||||||
|
- `rg "worker\\.cwd\\(" crates || true`: no matches。
|
||||||
|
- `rg "pub fn cwd\\(" crates/worker/src/worker.rs || true`: no Worker cwd accessor matches。
|
||||||
|
- `git diff --check`
|
||||||
|
- `cargo test -p worker --lib --tests`
|
||||||
|
- `cargo test -p worker-runtime --features ws-server,fs-store`
|
||||||
|
- `cargo test -p yoi-workspace-server --lib`
|
||||||
|
- `cargo check -p yoi`
|
||||||
|
- `cd web/workspace && deno task check`
|
||||||
|
- `cd web/workspace && deno task test`
|
||||||
|
- `yoi ticket doctor`
|
||||||
|
- `nix build .#yoi --no-link`
|
||||||
|
|
||||||
|
Validation log:
|
||||||
|
- `/run/user/1000/yoi/yoi-orchestrator/bash-output/worker-fs-authority-final-validation-1783723253.txt`
|
||||||
|
|
||||||
|
Reviewed/merged implementation commit:
|
||||||
|
- `b50b9461 feat: add explicit worker filesystem authority`
|
||||||
|
|
||||||
|
Non-blocking reviewer note:
|
||||||
|
- Workspace-context surfaces such as memory/MCP/resident prompt loading can still be backed by `workspace_root` when enabled; this was treated as outside this Ticket’s focused core filesystem/Bash authority gate, but may be follow-up material if the intended invariant becomes “no local file reads of any kind” for no-workdir Workers。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-07-10T22:44:45Z from: inprogress to: done reason: implementation_validated field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Implementation was reviewed, merged into orchestration branch, and final validation passed. Moving to done before closure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: hare at: 2026-07-10T22:45:01Z from: done to: closed reason: closed field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を closed にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: close author: hare at: 2026-07-10T22:45:01Z status: closed -->
|
||||||
|
|
||||||
|
## 完了
|
||||||
|
|
||||||
|
Worker filesystem authority の明示境界を実装・レビュー・merge・検証した。
|
||||||
|
|
||||||
|
実装内容:
|
||||||
|
- `WorkerFilesystemAuthority::{None, Local(LocalWorkingDirectory)}` を追加。
|
||||||
|
- `LocalWorkingDirectory` は filesystem authority `root` と default `cwd` を分離して保持する。
|
||||||
|
- Worker-level `cwd: PathBuf` field と `worker.cwd()` accessor を削除。
|
||||||
|
- core filesystem/Bash tool registration は local filesystem authority がある場合だけ行うようにした。
|
||||||
|
- No-workdir Worker では `Read` / `Write` / `Edit` / `Glob` / `Grep` / `Bash` を construct/register/model-visible にしない。
|
||||||
|
- Embedded no-workdir Worker spawn は Runtime/process cwd fallback ではなく `WorkerFilesystemAuthority::None` を明示的に渡す。
|
||||||
|
- Ticket / worker-spawn / peer-discovery など cwd 依存箇所は local filesystem authority 必須に整理し、workspace root fallback を authority として使わないようにした。
|
||||||
|
- tests で no-workdir tool omission、no cwd fallback、local/materialized Worker の filesystem/Bash behavior を確認。
|
||||||
|
|
||||||
|
Review:
|
||||||
|
- Reviewer approved with no blockers。
|
||||||
|
- Evidence included explicit authority type, no Worker cwd field/accessor, filesystem/Bash registration gate, embedded Runtime authority construction, and tests。
|
||||||
|
- Non-blocking note: workspace-context surfaces such as memory/MCP/resident prompt loading can still be backed by `workspace_root` when enabled; this is outside the focused core filesystem/Bash authority gate and may become follow-up if the invariant expands to “no local file reads of any kind”。
|
||||||
|
|
||||||
|
Merge / validation:
|
||||||
|
- Merge commit: `c29e9135 merge: worker filesystem authority`。
|
||||||
|
- Final validation passed:
|
||||||
|
- `rg "worker\\.cwd\\(" crates || true`
|
||||||
|
- `rg "pub fn cwd\\(" crates/worker/src/worker.rs || true`
|
||||||
|
- `git diff --check`
|
||||||
|
- `cargo test -p worker --lib --tests`
|
||||||
|
- `cargo test -p worker-runtime --features ws-server,fs-store`
|
||||||
|
- `cargo test -p yoi-workspace-server --lib`
|
||||||
|
- `cargo check -p yoi`
|
||||||
|
- `cd web/workspace && deno task check`
|
||||||
|
- `cd web/workspace && deno task test`
|
||||||
|
- `yoi ticket doctor`
|
||||||
|
- `nix build .#yoi --no-link`
|
||||||
|
- Validation log: `/run/user/1000/yoi/yoi-orchestrator/bash-output/worker-fs-authority-final-validation-1783723253.txt`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{"id":"orch-plan-20260710-220343-1","ticket_id":"00001KX6Y6ZEA","kind":"after","related_ticket":"00001KX6Y2A9Q","note":"Ticket has explicit typed `depends_on` relation to `00001KX6Y2A9Q`. The prerequisite WorkerFilesystemAuthority refactor is currently `inprogress` and establishes the filesystem-authority/workspace-boundary this Ticket builds on. Start only after the prerequisite is approved, merged, and closed or an explicit combined-work decision is made.","author":"orchestrator","at":"2026-07-10T22:03:43Z"}
|
||||||
|
{"id":"orch-plan-20260710-220349-2","ticket_id":"00001KX6Y6ZEA","kind":"waiting_capacity_note","note":"Dashboard queue routing inspected. This Ticket is concrete but blocked by typed `depends_on` relation to `00001KX6Y2A9Q`, which is currently inprogress. Starting now would conflict with the Worker struct/context/constructor refactor and filesystem-authority boundary. Leave queued and re-route after prerequisite merge/close.","author":"orchestrator","at":"2026-07-10T22:03:49Z"}
|
||||||
|
{"id":"orch-plan-20260710-233841-3","ticket_id":"00001KX6Y6ZEA","kind":"waiting_capacity_note","note":"Queue review resumed after prerequisite `00001KX6Y2A9Q` closed. `00001KX6WVNPD` was accepted first because it is the smaller embedded no-workdir policy MVP and explicitly does not require WorkspaceBackend migration. This Ticket remains queued due migration-boundary/conflict risk: it removes Worker `workspace_root` and introduces WorkspaceBackend/WorkspaceClient surfaces in the same Worker context/constructor/runtime code that `00001KX6WVNPD` is now changing. Re-route after `00001KX6WVNPD` is reviewed/merged/closed, unless a human explicitly requests a combined branch.","author":"orchestrator","at":"2026-07-10T23:38:41Z"}
|
||||||
|
{"id":"orch-plan-20260711-001433-4","ticket_id":"00001KX6Y6ZEA","kind":"accepted_plan","accepted_plan":{"summary":"Replace Worker `workspace_root: PathBuf` identity with path-free workspace identity and Runtime/host-owned WorkspaceBackend/WorkspaceClient boundary. Preserve filesystem authority exclusively under `WorkerFilesystemAuthority::Local`, allow workspace_id/client without local filesystem authority, and migrate workspace-aware Worker references away from local workspace paths where in scope.","branch":"work/00001KX6Y6ZEA-workspace-backend","worktree":"/home/hare/Projects/yoi/.worktree/00001KX6Y6ZEA-workspace-backend","role_plan":"Orchestrator accepts queued Ticket after prerequisites `00001KX6Y2A9Q` and related `00001KX6WVNPD` were approved, merged, validated, and closed. Use sibling Coder for implementation in a dedicated worktree, then sibling Reviewer for read-only review. Orchestrator retains merge/final-validation/close/worktree cleanup authority. Per user instruction, do not call StopPod during cleanup."},"author":"orchestrator","at":"2026-07-11T00:14:33Z"}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
---
|
---
|
||||||
title: 'Replace Worker workspace_root path with WorkspaceBackend'
|
title: 'Replace Worker workspace_root path with WorkspaceBackend'
|
||||||
state: 'queued'
|
state: 'closed'
|
||||||
priority: 'P1'
|
priority: 'P1'
|
||||||
created_at: '2026-07-10T21:16:22Z'
|
created_at: '2026-07-10T21:16:22Z'
|
||||||
updated_at: '2026-07-10T22:03:27Z'
|
updated_at: '2026-07-11T01:09:59Z'
|
||||||
assignee: null
|
assignee: null
|
||||||
queued_by: 'workspace-panel'
|
queued_by: 'workspace-panel'
|
||||||
queued_at: '2026-07-10T22:03:27Z'
|
queued_at: '2026-07-10T22:03:27Z'
|
||||||
|
|
|
||||||
32
.yoi/tickets/00001KX6Y6ZEA/resolution.md
Normal file
32
.yoi/tickets/00001KX6Y6ZEA/resolution.md
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
Worker workspace identity / WorkspaceBackend boundary を実装・レビュー・merge・検証した。
|
||||||
|
|
||||||
|
実装内容:
|
||||||
|
- Worker-held `workspace_root: PathBuf` identity を削除し、path-free `WorkerWorkspaceContext`, `WorkspaceId`, `WorkspaceClient` に置き換えた。
|
||||||
|
- Worker は `workspace_context` と `filesystem_authority` を分離して保持し、local filesystem authority は `WorkerFilesystemAuthority::{None, Local(LocalWorkingDirectory)}` に閉じ込めた。
|
||||||
|
- Worker constructors / restore / spawn paths を workspace context と filesystem authority の分離形に更新した。
|
||||||
|
- Runtime 側に `RuntimeWorkspaceBackendRef` を追加し、host-owned local backend binding から narrow Worker workspace context を注入するようにした。
|
||||||
|
- `workspace_id/client` が存在しても local filesystem authority を意味しないようにした。
|
||||||
|
- no-filesystem Workers では memory/workflow local layouts を workspace path fallback せず unavailable/fail-closed にした。
|
||||||
|
- legacy metadata `workspace_root` は compatibility/local filesystem hint として、`WorkerFilesystemAuthority::Local` 由来の場合だけ扱うようにした。
|
||||||
|
- tests で path-free workspace identity/client と local cwd/root の分離、filesystem authority なしの workspace client、local path-returning Worker workspace accessor 不在、constructor/restore/spawn behavior を確認した。
|
||||||
|
|
||||||
|
Review:
|
||||||
|
- Reviewer approved with no blockers。
|
||||||
|
- Evidence included path-free Worker context, no Worker `workspace_root()` accessor, Runtime/host backend materialization boundary, workspace-client-without-filesystem tests, fail-closed local workspace features, and legacy metadata confinement。
|
||||||
|
- Non-blocking note: `set_active_with_workspace_context` preserves existing metadata fields when passed `None`; future no-workspace reuse of an existing Pod name may want explicit clearing semantics to avoid stale UI hints。
|
||||||
|
|
||||||
|
Merge / validation:
|
||||||
|
- Merge commit: `391f11fc merge: workspace backend worker context`。
|
||||||
|
- Final validation passed:
|
||||||
|
- `rg -n 'workspace_root' crates/worker crates/worker-runtime crates/client crates/workspace-server || true`
|
||||||
|
- `rg -n 'worker\\.workspace_root|pub fn workspace_root|fn workspace_root' crates/worker crates/worker-runtime crates/client crates/workspace-server || true`
|
||||||
|
- `git diff --check`
|
||||||
|
- `cargo test -p worker --lib --tests`
|
||||||
|
- `cargo test -p worker-runtime --features ws-server,fs-store`
|
||||||
|
- `cargo test -p yoi-workspace-server --lib`
|
||||||
|
- `cargo check -p yoi`
|
||||||
|
- `cd web/workspace && deno task check`
|
||||||
|
- `cd web/workspace && deno task test`
|
||||||
|
- `yoi ticket doctor`
|
||||||
|
- `nix build .#yoi --no-link`
|
||||||
|
- Validation log: `/run/user/1000/yoi/yoi-orchestrator/bash-output/workspace-backend-final-validation-1783732052.txt`
|
||||||
|
|
@ -72,4 +72,324 @@ Marked ready by `yoi ticket state`.
|
||||||
Ticket を `workspace-panel` が queued にしました。
|
Ticket を `workspace-panel` が queued にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: orchestrator at: 2026-07-10T22:03:57Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: blocked_by_dependency
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- Dashboard queue authorization was inspected, but this Ticket has an explicit typed `depends_on` relation to `00001KX6Y2A9Q`。
|
||||||
|
- `00001KX6Y2A9Q` is currently `inprogress` and is refactoring Worker filesystem authority / cwd removal / no-workdir boundaries。
|
||||||
|
- This Ticket replaces Worker `workspace_root` path usage with `WorkspaceBackend`, which builds on the same Worker struct/context/constructor boundary。
|
||||||
|
- Starting now would likely conflict with the active filesystem-authority refactor and obscure review boundaries。
|
||||||
|
- Therefore this routing pass leaves the Ticket `queued` and does not record `queued -> inprogress`, create a worktree, or spawn role Pods。
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- Ticket body / recent thread / artifacts。
|
||||||
|
- `TicketRelationQuery(00001KX6Y6ZEA)`: includes dependency on `00001KX6Y2A9Q`。
|
||||||
|
- `TicketList(inprogress)`: `00001KX6Y2A9Q` is the active inprogress Ticket。
|
||||||
|
- Orchestrator worktree status: clean。
|
||||||
|
|
||||||
|
Next action:
|
||||||
|
- Re-route this Ticket after `00001KX6Y2A9Q` is approved, merged, validated, and closed。
|
||||||
|
- Escalate only if a human explicitly requests combined implementation in the same branch/worktree。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: orchestrator at: 2026-07-10T23:38:54Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: hold_queued_due_migration_boundary_conflict
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- 前提 `00001KX6Y2A9Q` は closed になったため dependency 自体は解消済み。
|
||||||
|
- ただし、この routing pass では `00001KX6WVNPD` を先に受理した。
|
||||||
|
- `00001KX6WVNPD` は embedded no-workdir policy MVP であり、WorkspaceBackend migration を必要としない小さい境界変更である。
|
||||||
|
- この Ticket は Worker `workspace_root` path を `WorkspaceBackend` に置き換える refactor で、Worker struct/context/constructor/runtime spawn/UI API 周辺に触れる。
|
||||||
|
- `00001KX6WVNPD` も同じ Worker create / embedded runtime / no-workdir authority surface に触るため、並行 branch で進めると conflict と review boundary 混濁の可能性が高い。
|
||||||
|
- よって、この Ticket は `queued` のまま保持し、`00001KX6WVNPD` が reviewed/merged/closed になってから再 routing する。
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- Ticket body / thread / relations / orchestration plan。
|
||||||
|
- `TicketRelationQuery(00001KX6Y6ZEA)`: prerequisite `00001KX6Y2A9Q` は closed。
|
||||||
|
- `TicketList(inprogress)`: routing pass 中に `00001KX6WVNPD` を accepted for implementation。
|
||||||
|
- Orchestrator worktree status: clean。
|
||||||
|
|
||||||
|
Next action:
|
||||||
|
- `00001KX6WVNPD` の implementation/review/merge/close を完了後、この Ticket を再 routing する。
|
||||||
|
- 人間が combined branch を明示希望する場合のみ、この待機判断を再検討する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: orchestrator at: 2026-07-11T00:15:05Z -->
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Routing decision: implementation_ready
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
- 前提 `00001KX6Y2A9Q` は closed 済みで、`WorkerFilesystemAuthority::{None, Local}` と cwd 除去境界が merge 済み。
|
||||||
|
- 関連 `00001KX6WVNPD` も closed 済みで、embedded no-workdir Worker policy / UI/API optionality が merge 済み。
|
||||||
|
- この Ticket の typed `depends_on 00001KX6Y2A9Q` は解消済み。`00001KX6WVNPD` は `related` であり blocker ではなく、直前の conflict/migration-boundary waiting note もその Ticket の merge/close により解消済み。
|
||||||
|
- `TicketList(inprogress)` は 0 件で、Orchestrator worktree は clean。
|
||||||
|
- Ticket body/thread に Worker-level local path identity をなくし、Runtime/host-owned backend/client 境界へ移す binding decision が記録されている。
|
||||||
|
|
||||||
|
Evidence checked:
|
||||||
|
- Ticket body / thread / relations / orchestration plan。
|
||||||
|
- Closed prerequisite `00001KX6Y2A9Q` and closed related policy Ticket `00001KX6WVNPD`。
|
||||||
|
- `TicketRelationQuery(00001KX6Y6ZEA)`: outgoing `depends_on 00001KX6Y2A9Q` is closed; other relations are `related` only。
|
||||||
|
- `TicketOrchestrationPlanQuery(00001KX6Y6ZEA)`: prior waiting notes were dependency/conflict notes now resolved by prerequisite and related Ticket closure。
|
||||||
|
- `TicketList(inprogress)`: 0 件。
|
||||||
|
- Orchestrator worktree status: clean。
|
||||||
|
|
||||||
|
IntentPacket:
|
||||||
|
|
||||||
|
Intent:
|
||||||
|
- Worker の workspace identity/context を local path (`workspace_root: PathBuf`) から path-free identity (`Option<WorkspaceId>`) と Runtime/host injected workspace client/handle 境界へ移行する。
|
||||||
|
- Filesystem authority は `WorkerFilesystemAuthority` に閉じ込め、workspace identity/client が local filesystem authority を意味しないようにする。
|
||||||
|
|
||||||
|
Binding decisions / invariants:
|
||||||
|
- Worker struct は durable/context identity として local workspace path を保持しない。
|
||||||
|
- Worker-level `workspace_root: PathBuf` field と local path-returning workspace identity accessor を削除する。
|
||||||
|
- Worker が `WorkspaceBackendRef` / endpoint / auth / SecretRef / adapter を直接解決しない。
|
||||||
|
- Runtime/host 側が backend binding source を保持・解決し、Worker には narrow `WorkspaceClient` / handle を注入する。
|
||||||
|
- `workspace_id = Some(...)` かつ `filesystem = None` を表現できること。
|
||||||
|
- `workspace_id = None` かつ `filesystem = None` の会話専用 Worker も表現できること。
|
||||||
|
- local path が必要な処理は Runtime/host backend adapter または `WorkerFilesystemAuthority::Local` の内側に閉じ込める。
|
||||||
|
- capability を導入する場合でも authority source にせず、tool registration / UX / discovery hint として扱い、enforcement は backend 側に置く。
|
||||||
|
- `WorkerFilesystemAuthority` / embedded no-workdir policy を崩さない。workspace root fallback や process cwd fallback を filesystem authority として復活させない。
|
||||||
|
|
||||||
|
Requirements / acceptance criteria:
|
||||||
|
- Worker context に `workspace_id: Option<WorkspaceId>` equivalent と injected workspace client/handle equivalent を持てる。
|
||||||
|
- Worker struct に `workspace_root: PathBuf` が存在しない。
|
||||||
|
- workspace-aware features(Ticket / memory / workflow / project records / workspace metadata)は local workspace path authority ではなく injected workspace client/handle 経由へ移行する、または移行が必要な箇所を fail-closed / unavailable diagnostic にする。
|
||||||
|
- Runtime/host が `WorkspaceBackendRef` equivalent を保持・解決し、Worker launch/restore/embedded/spawn path へ安全に materialize できる。
|
||||||
|
- No-workdir Worker は workspace API access と local filesystem authority の有無を独立に表現できる。
|
||||||
|
- Existing local/workdir Workers still work with `WorkerFilesystemAuthority::Local` and local-path-only operations confined there。
|
||||||
|
- Tests cover no local workspace path identity on Worker, workspace_id/client injection shape, no filesystem authority mixing, and representative workspace-aware API/tool behavior。
|
||||||
|
|
||||||
|
Implementation latitude:
|
||||||
|
- `WorkspaceId`, `WorkspaceBackendRef`, `WorkspaceClient` / handle の exact placement/name/API surface は existing crate boundaries に合わせてよい。
|
||||||
|
- Minimal client/handle can support only currently needed workspace-aware operations if broad API migration would exceed Ticket scope, but it must not leave Worker-local path authority as the active path。
|
||||||
|
- If a currently path-backed feature cannot be migrated safely in this Ticket, prefer explicit unavailable/fail-closed diagnostic with follow-up note rather than hidden path fallback。
|
||||||
|
- Capability discovery is optional unless needed for safe tool registration/UX。
|
||||||
|
|
||||||
|
Escalate if:
|
||||||
|
- Removing `workspace_root` from Worker requires changing product semantics for memory/Ticket/workflow availability beyond local refactor。
|
||||||
|
- A workspace-aware feature cannot be migrated without introducing a broad external backend/protocol design not captured by this Ticket。
|
||||||
|
- You need to reintroduce Worker-held local workspace path as identity/authority。
|
||||||
|
- You need to weaken no-workdir fs/Bash tool omission or `WorkerFilesystemAuthority` invariants。
|
||||||
|
- Public API/serialization migration requires incompatible durable-state handling beyond existing restore/test coverage。
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- `rg "workspace_root" crates/worker crates/worker-runtime crates/yoi-pod-client crates/yoi-workspace-server` with expected remaining hits only outside Worker identity/authority or documented adapter boundaries。
|
||||||
|
- `rg "worker\.workspace_root|workspace_root\(\)" crates` or equivalent showing Worker local path accessor removed。
|
||||||
|
- `git diff --check`
|
||||||
|
- `cargo test -p worker --lib --tests`
|
||||||
|
- `cargo test -p worker-runtime --features ws-server,fs-store`
|
||||||
|
- `cargo test -p yoi-workspace-server --lib`
|
||||||
|
- `cargo check -p yoi`
|
||||||
|
- `cd web/workspace && deno task check && deno task test` if API/types/UI are touched。
|
||||||
|
- `yoi ticket doctor`
|
||||||
|
- `nix build .#yoi --no-link`
|
||||||
|
|
||||||
|
Current code map / likely touch points:
|
||||||
|
- `crates/worker/src/worker.rs` for Worker context fields and accessors。
|
||||||
|
- `crates/worker/src/controller.rs` for workspace-aware tool/resource setup currently tied to workspace path。
|
||||||
|
- `crates/worker-runtime/src/worker_backend.rs` for Runtime/embedded/restore construction and backend binding materialization。
|
||||||
|
- workspace-server/client crates if backend/client DTOs or Worker launch summaries expose workspace identity。
|
||||||
|
- tests around no-workdir Workers, restore, child spawn, Ticket/memory/workflow availability。
|
||||||
|
|
||||||
|
Critical risks / reviewer focus:
|
||||||
|
- Hidden local path authority retained in Worker under a renamed field/accessor。
|
||||||
|
- Workspace identity/client being treated as filesystem authority。
|
||||||
|
- WorkspaceBackendRef/endpoint/secret leaking into Worker-owned state。
|
||||||
|
- Breaking local/workdir Workers or child spawn/restore while removing path identity。
|
||||||
|
- Regressing embedded no-workdir policy or filesystem/Bash tool omission。
|
||||||
|
- Over-broad migration that invents a large remote-workspace protocol instead of the minimal safe boundary needed here。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-07-11T00:15:11Z from: queued to: inprogress reason: accepted_for_implementation field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Prerequisite and prior conflict/migration-boundary Ticket are now closed. This Ticket is accepted for implementation before creating a worktree or spawning role Pods.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-07-11T00:59:55Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Implementation progress report:
|
||||||
|
|
||||||
|
- Coder Pod `yoi-coder-00001KX6Y6ZEA-workspace-backend` completed implementation in `/home/hare/Projects/yoi/.worktree/00001KX6Y6ZEA-workspace-backend` on branch `work/00001KX6Y6ZEA-workspace-backend`。
|
||||||
|
- Implementation commit: `142b60e1b3ca refactor: separate worker workspace identity`。
|
||||||
|
- Orchestrator inspected worktree status, branch log, commit stats, `git diff --check 74b07bd7..HEAD`, and workspace-root accessor grep. Worktree was clean and diff check passed。
|
||||||
|
|
||||||
|
Implementation summary:
|
||||||
|
- Replaced Worker-held `workspace_root: PathBuf` identity with path-free `WorkerWorkspaceContext`, `WorkspaceId`, and `WorkspaceClient`。
|
||||||
|
- Kept local filesystem authority under `WorkerFilesystemAuthority::{None, Local(LocalWorkingDirectory)}`。
|
||||||
|
- Updated Worker constructors / restore / spawn paths to receive workspace context separately from filesystem authority。
|
||||||
|
- Added runtime-side `RuntimeWorkspaceBackendRef` as host-owned local backend binding that injects narrow Worker workspace context。
|
||||||
|
- No-filesystem Workers keep memory/workflow local layouts unavailable/fail-closed rather than falling back to workspace paths。
|
||||||
|
- Preserved legacy metadata `workspace_root` only as a local filesystem hint derived from `WorkerFilesystemAuthority::Local`, not as Worker identity。
|
||||||
|
- Updated tests for path-free workspace identity/client separation from local tool cwd/root, workspace client without filesystem authority, no local path-returning Worker workspace accessor, and constructor usage。
|
||||||
|
|
||||||
|
Files touched:
|
||||||
|
- `crates/worker/src/worker.rs`
|
||||||
|
- `crates/worker/src/controller.rs`
|
||||||
|
- `crates/worker/src/entrypoint.rs`
|
||||||
|
- `crates/worker/src/lib.rs`
|
||||||
|
- `crates/worker/src/discovery.rs`
|
||||||
|
- `crates/worker/src/ticket_event_notify.rs`
|
||||||
|
- `crates/worker/tests/*`
|
||||||
|
- `crates/worker-runtime/src/worker_backend.rs`
|
||||||
|
- `crates/worker-runtime/Cargo.toml`
|
||||||
|
- `crates/session-store/src/worker_metadata.rs`
|
||||||
|
- `Cargo.lock`
|
||||||
|
- `package.nix`
|
||||||
|
|
||||||
|
Coder-reported validation passed:
|
||||||
|
- `rg -n "workspace_root" crates/worker crates/worker-runtime crates/client crates/workspace-server` with remaining hits as legacy metadata/local filesystem adapter or host/runtime local backend boundaries plus tests。
|
||||||
|
- `rg -n "worker\\.workspace_root|pub fn workspace_root|fn workspace_root" crates/worker crates/worker-runtime crates/client crates/workspace-server || true`; no Worker `workspace_root()` accessor hits. Orchestrator grep observed an unrelated `crates/workspace-server/src/records.rs` project-record accessor hit outside Worker identity。
|
||||||
|
- `git diff --check`
|
||||||
|
- `cargo test -p worker --lib --tests`
|
||||||
|
- `cargo test -p worker-runtime --features ws-server,fs-store`
|
||||||
|
- `cargo test -p yoi-workspace-server --lib`
|
||||||
|
- `cargo check -p yoi`
|
||||||
|
- `yoi ticket doctor`
|
||||||
|
- `nix build .#yoi --no-link`
|
||||||
|
|
||||||
|
Not run by Coder:
|
||||||
|
- `cd web/workspace && deno task check && deno task test` because web/API/types were not touched。
|
||||||
|
|
||||||
|
Next action:
|
||||||
|
- Route to external Reviewer Pod before merge/close decisions。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: review author: reviewer at: 2026-07-11T01:07:06Z status: approve -->
|
||||||
|
|
||||||
|
## Review: approve
|
||||||
|
|
||||||
|
External review result: approve
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- Worker identity is path-free: `WorkerWorkspaceContext` carries `Option<WorkspaceId>` plus narrow `WorkspaceClient`/unavailable state, while `Worker` stores `workspace_context` and `filesystem_authority` instead of a `workspace_root: PathBuf` (`crates/worker/src/worker.rs:61-109`, `384-405`)。
|
||||||
|
- No `worker.workspace_root()` / local path-returning workspace accessor remains; local paths are exposed only through `WorkerFilesystemAuthority::Local` helpers such as `local_working_directory()` (`worker.rs:741-840`)。
|
||||||
|
- Runtime/host boundary now resolves local workspace backend context before constructing Workers (`crates/worker-runtime/src/worker_backend.rs:321-390`), and Worker construction accepts injected workspace context + filesystem authority (`worker.rs:4219-4260`)。
|
||||||
|
- Workspace/client without filesystem and no-workspace/no-filesystem Workers are represented and covered by tests (`worker.rs:5441-5530`; worker-runtime tests around `901+`)。
|
||||||
|
- Workspace-aware local features fail closed when no local filesystem authority is available; Ticket/memory/workflow/spawn-dependent surfaces require `filesystem_authority.local()` and otherwise return unavailable errors rather than cwd/root fallback (`crates/worker/src/controller.rs:630-672`, `709-779`)。
|
||||||
|
- Legacy `workspace_root` metadata is written only as a compatibility/local hint from local filesystem authority, not as Worker identity (`worker.rs:4604-4621`; `session-store/src/worker_metadata.rs`)。
|
||||||
|
|
||||||
|
Validation performed:
|
||||||
|
- `git diff --check 142b60e1^ 142b60e1`
|
||||||
|
- Focused grep for forbidden `workspace_root()` accessor: no matches。
|
||||||
|
- Focused tests:
|
||||||
|
- `cargo test -p worker spawned_context_tests --lib`
|
||||||
|
- `cargo test -p worker-runtime worker_spawn_receives_materialized_workspace_cwd_instead_of_source_repo --features ws-server,fs-store`
|
||||||
|
- `cargo test -p worker-runtime adapter_dispatches_user_input_through_worker_run_lifecycle --features ws-server,fs-store`
|
||||||
|
|
||||||
|
Blockers: none。
|
||||||
|
|
||||||
|
Non-blocking notes:
|
||||||
|
- Remaining `workspace_root` hits are local runtime/entrypoint/spawn-tool adapter, legacy metadata, or test contexts; reviewer did not find Worker identity/accessor use。
|
||||||
|
- `set_active_with_workspace_context` preserves existing metadata fields when passed `None`; acceptable for this Ticket as compatibility metadata, but future no-workspace reuse of an existing Pod name may want explicit clearing semantics to avoid stale UI hints。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: orchestrator at: 2026-07-11T01:09:39Z -->
|
||||||
|
|
||||||
|
## Implementation report
|
||||||
|
|
||||||
|
Merge / final validation report:
|
||||||
|
|
||||||
|
- Reviewer approved the implementation with no blockers。
|
||||||
|
- Merged branch `work/00001KX6Y6ZEA-workspace-backend` into orchestration branch with merge commit `391f11fc merge: workspace backend worker context`。
|
||||||
|
- Final validation was rerun in `/home/hare/Projects/yoi/.worktree/orchestration` after merge。
|
||||||
|
|
||||||
|
Validation passed:
|
||||||
|
- `rg -n 'workspace_root' crates/worker crates/worker-runtime crates/client crates/workspace-server || true` with remaining hits limited to legacy metadata/local adapter/test contexts。
|
||||||
|
- `rg -n 'worker\\.workspace_root|pub fn workspace_root|fn workspace_root' crates/worker crates/worker-runtime crates/client crates/workspace-server || true`; no Worker identity/accessor hits were found. The expected unrelated workspace-server project-record accessor remains outside Worker identity。
|
||||||
|
- `git diff --check`
|
||||||
|
- `cargo test -p worker --lib --tests`
|
||||||
|
- `cargo test -p worker-runtime --features ws-server,fs-store`
|
||||||
|
- `cargo test -p yoi-workspace-server --lib`
|
||||||
|
- `cargo check -p yoi`
|
||||||
|
- `cd web/workspace && deno task check`
|
||||||
|
- `cd web/workspace && deno task test`
|
||||||
|
- `yoi ticket doctor`
|
||||||
|
- `nix build .#yoi --no-link`
|
||||||
|
|
||||||
|
Validation log:
|
||||||
|
- `/run/user/1000/yoi/yoi-orchestrator/bash-output/workspace-backend-final-validation-1783732052.txt`
|
||||||
|
|
||||||
|
Reviewed/merged implementation commit:
|
||||||
|
- `142b60e1 refactor: separate worker workspace identity`
|
||||||
|
|
||||||
|
Non-blocking reviewer note:
|
||||||
|
- `set_active_with_workspace_context` preserves existing metadata fields when passed `None`; acceptable for this Ticket as compatibility metadata, but future no-workspace reuse of an existing Pod name may want explicit clearing semantics to avoid stale UI hints。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: orchestrator at: 2026-07-11T01:09:43Z from: inprogress to: done reason: implementation_validated field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Implementation was reviewed, merged into orchestration branch, and final validation passed. Moving to done before closure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: hare at: 2026-07-11T01:09:59Z from: done to: closed reason: closed field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を closed にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: close author: hare at: 2026-07-11T01:09:59Z status: closed -->
|
||||||
|
|
||||||
|
## 完了
|
||||||
|
|
||||||
|
Worker workspace identity / WorkspaceBackend boundary を実装・レビュー・merge・検証した。
|
||||||
|
|
||||||
|
実装内容:
|
||||||
|
- Worker-held `workspace_root: PathBuf` identity を削除し、path-free `WorkerWorkspaceContext`, `WorkspaceId`, `WorkspaceClient` に置き換えた。
|
||||||
|
- Worker は `workspace_context` と `filesystem_authority` を分離して保持し、local filesystem authority は `WorkerFilesystemAuthority::{None, Local(LocalWorkingDirectory)}` に閉じ込めた。
|
||||||
|
- Worker constructors / restore / spawn paths を workspace context と filesystem authority の分離形に更新した。
|
||||||
|
- Runtime 側に `RuntimeWorkspaceBackendRef` を追加し、host-owned local backend binding から narrow Worker workspace context を注入するようにした。
|
||||||
|
- `workspace_id/client` が存在しても local filesystem authority を意味しないようにした。
|
||||||
|
- no-filesystem Workers では memory/workflow local layouts を workspace path fallback せず unavailable/fail-closed にした。
|
||||||
|
- legacy metadata `workspace_root` は compatibility/local filesystem hint として、`WorkerFilesystemAuthority::Local` 由来の場合だけ扱うようにした。
|
||||||
|
- tests で path-free workspace identity/client と local cwd/root の分離、filesystem authority なしの workspace client、local path-returning Worker workspace accessor 不在、constructor/restore/spawn behavior を確認した。
|
||||||
|
|
||||||
|
Review:
|
||||||
|
- Reviewer approved with no blockers。
|
||||||
|
- Evidence included path-free Worker context, no Worker `workspace_root()` accessor, Runtime/host backend materialization boundary, workspace-client-without-filesystem tests, fail-closed local workspace features, and legacy metadata confinement。
|
||||||
|
- Non-blocking note: `set_active_with_workspace_context` preserves existing metadata fields when passed `None`; future no-workspace reuse of an existing Pod name may want explicit clearing semantics to avoid stale UI hints。
|
||||||
|
|
||||||
|
Merge / validation:
|
||||||
|
- Merge commit: `391f11fc merge: workspace backend worker context`。
|
||||||
|
- Final validation passed:
|
||||||
|
- `rg -n 'workspace_root' crates/worker crates/worker-runtime crates/client crates/workspace-server || true`
|
||||||
|
- `rg -n 'worker\\.workspace_root|pub fn workspace_root|fn workspace_root' crates/worker crates/worker-runtime crates/client crates/workspace-server || true`
|
||||||
|
- `git diff --check`
|
||||||
|
- `cargo test -p worker --lib --tests`
|
||||||
|
- `cargo test -p worker-runtime --features ws-server,fs-store`
|
||||||
|
- `cargo test -p yoi-workspace-server --lib`
|
||||||
|
- `cargo check -p yoi`
|
||||||
|
- `cd web/workspace && deno task check`
|
||||||
|
- `cd web/workspace && deno task test`
|
||||||
|
- `yoi ticket doctor`
|
||||||
|
- `nix build .#yoi --no-link`
|
||||||
|
- Validation log: `/run/user/1000/yoi/yoi-orchestrator/bash-output/workspace-backend-final-validation-1783732052.txt`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -5848,6 +5848,7 @@ dependencies = [
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite 0.29.0",
|
"tokio-tungstenite 0.29.0",
|
||||||
|
"toml",
|
||||||
"tower",
|
"tower",
|
||||||
"worker",
|
"worker",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -100,8 +100,13 @@ pub struct WorkerMetadata {
|
||||||
pub worker_name: String,
|
pub worker_name: String,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub active: Option<WorkerActiveSegmentRef>,
|
pub active: Option<WorkerActiveSegmentRef>,
|
||||||
|
/// Legacy local path hint retained for host/runtime compatibility. It is not
|
||||||
|
/// Worker workspace identity or authority.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub workspace_root: Option<PathBuf>,
|
pub workspace_root: Option<PathBuf>,
|
||||||
|
/// Path-free workspace identity supplied by the host/runtime boundary.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub workspace_id: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
pub spawned_children: Vec<WorkerSpawnedChild>,
|
pub spawned_children: Vec<WorkerSpawnedChild>,
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
|
@ -119,6 +124,7 @@ impl WorkerMetadata {
|
||||||
worker_name: worker_name.into(),
|
worker_name: worker_name.into(),
|
||||||
active,
|
active,
|
||||||
workspace_root: None,
|
workspace_root: None,
|
||||||
|
workspace_id: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: Vec::new(),
|
peers: Vec::new(),
|
||||||
|
|
@ -130,6 +136,11 @@ impl WorkerMetadata {
|
||||||
self.workspace_root = Some(workspace_root);
|
self.workspace_root = Some(workspace_root);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_workspace_id(mut self, workspace_id: impl Into<String>) -> Self {
|
||||||
|
self.workspace_id = Some(workspace_id.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sync persistence backend for Worker metadata.
|
/// Sync persistence backend for Worker metadata.
|
||||||
|
|
@ -180,6 +191,44 @@ pub trait WorkerMetadataStore: Send + Sync {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the active pointer and workspace ownership while preserving unrelated fields.
|
/// Set the active pointer and workspace ownership while preserving unrelated fields.
|
||||||
|
fn set_active_with_workspace_context(
|
||||||
|
&self,
|
||||||
|
worker_name: &str,
|
||||||
|
active: Option<WorkerActiveSegmentRef>,
|
||||||
|
resolved_manifest_snapshot: Option<serde_json::Value>,
|
||||||
|
workspace_id: Option<String>,
|
||||||
|
workspace_root: Option<PathBuf>,
|
||||||
|
) -> Result<WorkerMetadata, WorkerStoreError> {
|
||||||
|
self.update_by_name(worker_name, |metadata| {
|
||||||
|
metadata.active = active;
|
||||||
|
metadata.resolved_manifest_snapshot = resolved_manifest_snapshot;
|
||||||
|
if let Some(workspace_id) = workspace_id {
|
||||||
|
metadata.workspace_id = Some(workspace_id);
|
||||||
|
}
|
||||||
|
if let Some(workspace_root) = workspace_root {
|
||||||
|
metadata.workspace_root = Some(workspace_root);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the active pointer and path-free workspace identity while preserving unrelated fields.
|
||||||
|
fn set_active_with_workspace_id(
|
||||||
|
&self,
|
||||||
|
worker_name: &str,
|
||||||
|
active: Option<WorkerActiveSegmentRef>,
|
||||||
|
resolved_manifest_snapshot: Option<serde_json::Value>,
|
||||||
|
workspace_id: Option<String>,
|
||||||
|
) -> Result<WorkerMetadata, WorkerStoreError> {
|
||||||
|
self.update_by_name(worker_name, |metadata| {
|
||||||
|
metadata.active = active;
|
||||||
|
metadata.resolved_manifest_snapshot = resolved_manifest_snapshot;
|
||||||
|
if let Some(workspace_id) = workspace_id {
|
||||||
|
metadata.workspace_id = Some(workspace_id);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the active pointer and legacy local workspace-root hint while preserving unrelated fields.
|
||||||
fn set_active_with_workspace_root(
|
fn set_active_with_workspace_root(
|
||||||
&self,
|
&self,
|
||||||
worker_name: &str,
|
worker_name: &str,
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ reqwest = { version = "0.13", optional = true, default-features = false, feature
|
||||||
tar.workspace = true
|
tar.workspace = true
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["net", "rt", "sync", "time"] }
|
tokio = { workspace = true, features = ["net", "rt", "sync", "time"] }
|
||||||
|
toml.workspace = true
|
||||||
tower = { workspace = true, features = ["util"], optional = true }
|
tower = { workspace = true, features = ["util"], optional = true }
|
||||||
worker.workspace = true
|
worker.workspace = true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, Mutex, mpsc};
|
use std::sync::{Arc, Mutex, mpsc};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
@ -35,7 +35,10 @@ use session_store::{CombinedStore, FsWorkerStore};
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
use worker::{Worker, WorkerController, WorkerHandle};
|
use worker::{
|
||||||
|
Worker, WorkerController, WorkerFilesystemAuthority, WorkerHandle, WorkerWorkspaceContext,
|
||||||
|
WorkspaceId,
|
||||||
|
};
|
||||||
|
|
||||||
const DEFAULT_BACKEND_ID: &str = "worker-crate";
|
const DEFAULT_BACKEND_ID: &str = "worker-crate";
|
||||||
const RUNTIME_TASK_TIMEOUT: Duration = Duration::from_secs(10);
|
const RUNTIME_TASK_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
|
|
@ -230,6 +233,41 @@ fn sanitize_worker_name_component(value: &str) -> String {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum RuntimeWorkspaceBackendRef {
|
||||||
|
None,
|
||||||
|
LocalFilesystem { root: PathBuf },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RuntimeWorkspaceBackendRef {
|
||||||
|
fn from_working_directory(binding: Option<&WorkingDirectoryBinding>) -> Self {
|
||||||
|
match binding {
|
||||||
|
Some(binding) => Self::LocalFilesystem {
|
||||||
|
root: binding.root().to_path_buf(),
|
||||||
|
},
|
||||||
|
None => Self::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn worker_context(&self) -> WorkerWorkspaceContext {
|
||||||
|
match self {
|
||||||
|
Self::None => WorkerWorkspaceContext::no_workspace(),
|
||||||
|
Self::LocalFilesystem { root } => local_workspace_context(root),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn local_workspace_context(root: &Path) -> WorkerWorkspaceContext {
|
||||||
|
WorkerWorkspaceContext::local_filesystem(read_workspace_id_hint(root))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_workspace_id_hint(root: &Path) -> Option<WorkspaceId> {
|
||||||
|
let contents = std::fs::read_to_string(root.join(".yoi/workspace.toml")).ok()?;
|
||||||
|
let value = toml::from_str::<toml::Value>(&contents).ok()?;
|
||||||
|
let id = value.get("id")?.as_str()?.to_string();
|
||||||
|
WorkspaceId::new(id).ok()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "http-server")]
|
#[cfg(feature = "http-server")]
|
||||||
async fn fetch_profile_source_archive_http(
|
async fn fetch_profile_source_archive_http(
|
||||||
location: &ProfileSourceArchiveHttpRef,
|
location: &ProfileSourceArchiveHttpRef,
|
||||||
|
|
@ -298,11 +336,19 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|binding| binding.root().to_path_buf())
|
.map(|binding| binding.root().to_path_buf())
|
||||||
.unwrap_or_else(|| self.profile_base_dir.clone());
|
.unwrap_or_else(|| self.profile_base_dir.clone());
|
||||||
let cwd = request
|
let filesystem_authority = request
|
||||||
.working_directory
|
.working_directory
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|binding| binding.cwd().to_path_buf())
|
.map(|binding| {
|
||||||
.unwrap_or_else(|| self.cwd.clone());
|
WorkerFilesystemAuthority::local(
|
||||||
|
binding.root().to_path_buf(),
|
||||||
|
binding.cwd().to_path_buf(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or(WorkerFilesystemAuthority::None);
|
||||||
|
let workspace_backend_ref =
|
||||||
|
RuntimeWorkspaceBackendRef::from_working_directory(request.working_directory.as_ref());
|
||||||
|
let workspace_context = workspace_backend_ref.worker_context();
|
||||||
let selector = profile.as_deref().unwrap_or("builtin:default");
|
let selector = profile.as_deref().unwrap_or("builtin:default");
|
||||||
let archive = self
|
let archive = self
|
||||||
.resolve_profile_source_archive(&request.request.profile_source)
|
.resolve_profile_source_archive(&request.request.profile_source)
|
||||||
|
|
@ -335,9 +381,15 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
|
||||||
})?;
|
})?;
|
||||||
let store = CombinedStore::new(session_store, worker_metadata_store);
|
let store = CombinedStore::new(session_store, worker_metadata_store);
|
||||||
|
|
||||||
let worker = Worker::from_manifest_with_context(manifest, store, loader, worker_root, cwd)
|
let worker = Worker::from_manifest_with_context(
|
||||||
.await
|
manifest,
|
||||||
.map_err(|err| format!("failed to create Worker from profile: {err}"))?;
|
store,
|
||||||
|
loader,
|
||||||
|
workspace_context,
|
||||||
|
filesystem_authority,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|err| format!("failed to create Worker from profile: {err}"))?;
|
||||||
|
|
||||||
let runtime_base = self.runtime_base_dir()?;
|
let runtime_base = self.runtime_base_dir()?;
|
||||||
let (handle, _shutdown_rx) = WorkerController::spawn(worker, &runtime_base)
|
let (handle, _shutdown_rx) = WorkerController::spawn(worker, &runtime_base)
|
||||||
|
|
@ -806,7 +858,7 @@ where
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
@ -902,18 +954,31 @@ mod tests {
|
||||||
FsStore::new(&self.store_dir).map_err(|err| err.to_string())?,
|
FsStore::new(&self.store_dir).map_err(|err| err.to_string())?,
|
||||||
FsWorkerStore::new(&self.worker_metadata_dir).map_err(|err| err.to_string())?,
|
FsWorkerStore::new(&self.worker_metadata_dir).map_err(|err| err.to_string())?,
|
||||||
);
|
);
|
||||||
let cwd = request
|
let filesystem_authority = request
|
||||||
.working_directory
|
.working_directory
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|binding| binding.cwd().to_path_buf())
|
.map(|binding| {
|
||||||
|
let cwd = binding.cwd().to_path_buf();
|
||||||
|
self.observed_cwds.lock().unwrap().push(cwd.clone());
|
||||||
|
WorkerFilesystemAuthority::local(binding.root().to_path_buf(), cwd)
|
||||||
|
})
|
||||||
|
.unwrap_or(WorkerFilesystemAuthority::None);
|
||||||
|
let scope_root = request
|
||||||
|
.working_directory
|
||||||
|
.as_ref()
|
||||||
|
.map(|binding| binding.root().to_path_buf())
|
||||||
.unwrap_or_else(|| self.cwd.clone());
|
.unwrap_or_else(|| self.cwd.clone());
|
||||||
self.observed_cwds.lock().unwrap().push(cwd.clone());
|
let workspace_backend_ref = RuntimeWorkspaceBackendRef::from_working_directory(
|
||||||
let scope = Scope::writable(&cwd).map_err(|err| err.to_string())?;
|
request.working_directory.as_ref(),
|
||||||
|
);
|
||||||
|
let workspace_context = workspace_backend_ref.worker_context();
|
||||||
|
let scope = Scope::writable(&scope_root).map_err(|err| err.to_string())?;
|
||||||
let worker = Worker::new(
|
let worker = Worker::new(
|
||||||
manifest,
|
manifest,
|
||||||
Engine::new(self.client.clone()),
|
Engine::new(self.client.clone()),
|
||||||
store,
|
store,
|
||||||
cwd,
|
workspace_context,
|
||||||
|
filesystem_authority,
|
||||||
scope,
|
scope,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|
@ -925,6 +990,20 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn core_filesystem_tool_names() -> BTreeSet<&'static str> {
|
||||||
|
["Read", "Write", "Edit", "Glob", "Grep", "Bash"]
|
||||||
|
.into_iter()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn captured_tool_names(client: &MockClient, index: usize) -> BTreeSet<String> {
|
||||||
|
client.captured.lock().unwrap()[index]
|
||||||
|
.tools
|
||||||
|
.iter()
|
||||||
|
.map(|tool| tool.name.clone())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn simple_text_events() -> Vec<LlmEvent> {
|
fn simple_text_events() -> Vec<LlmEvent> {
|
||||||
vec![
|
vec![
|
||||||
LlmEvent::text_block_start(0),
|
LlmEvent::text_block_start(0),
|
||||||
|
|
@ -1109,7 +1188,7 @@ mod tests {
|
||||||
cwd: cwd.path().to_path_buf(),
|
cwd: cwd.path().to_path_buf(),
|
||||||
store_dir: store.path().join("sessions"),
|
store_dir: store.path().join("sessions"),
|
||||||
worker_metadata_dir: store.path().join("workers"),
|
worker_metadata_dir: store.path().join("workers"),
|
||||||
observed_cwds,
|
observed_cwds: observed_cwds.clone(),
|
||||||
};
|
};
|
||||||
let backend = WorkerRuntimeExecutionBackend::new(factory).unwrap();
|
let backend = WorkerRuntimeExecutionBackend::new(factory).unwrap();
|
||||||
let runtime = EmbeddedRuntime::with_execution_backend(
|
let runtime = EmbeddedRuntime::with_execution_backend(
|
||||||
|
|
@ -1148,6 +1227,14 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(client.captured.lock().unwrap().len(), 1);
|
assert_eq!(client.captured.lock().unwrap().len(), 1);
|
||||||
|
assert!(observed_cwds.lock().unwrap().is_empty());
|
||||||
|
let names = captured_tool_names(&client, 0);
|
||||||
|
for forbidden in core_filesystem_tool_names() {
|
||||||
|
assert!(
|
||||||
|
!names.contains(forbidden),
|
||||||
|
"no-workdir Worker unexpectedly exposed {forbidden}; tools={names:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
let observations = runtime
|
let observations = runtime
|
||||||
.read_worker_observation_events(&detail.worker_ref, WorkerObservationCursor::zero())
|
.read_worker_observation_events(&detail.worker_ref, WorkerObservationCursor::zero())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -1166,7 +1253,7 @@ mod tests {
|
||||||
let store = tempfile::tempdir().unwrap();
|
let store = tempfile::tempdir().unwrap();
|
||||||
let observed_cwds = Arc::new(Mutex::new(Vec::new()));
|
let observed_cwds = Arc::new(Mutex::new(Vec::new()));
|
||||||
let factory = MockFactory {
|
let factory = MockFactory {
|
||||||
client,
|
client: client.clone(),
|
||||||
runtime_base: runtime_base.path().to_path_buf(),
|
runtime_base: runtime_base.path().to_path_buf(),
|
||||||
cwd: repo.path().to_path_buf(),
|
cwd: repo.path().to_path_buf(),
|
||||||
store_dir: store.path().join("sessions"),
|
store_dir: store.path().join("sessions"),
|
||||||
|
|
@ -1191,6 +1278,24 @@ mod tests {
|
||||||
request.working_directory_request = Some(working_directory_request(repo.path()));
|
request.working_directory_request = Some(working_directory_request(repo.path()));
|
||||||
|
|
||||||
let detail = runtime.create_worker(request).unwrap();
|
let detail = runtime.create_worker(request).unwrap();
|
||||||
|
runtime
|
||||||
|
.send_input(&detail.worker_ref, WorkerInput::user("inspect tools"))
|
||||||
|
.unwrap();
|
||||||
|
let deadline = std::time::Instant::now() + Duration::from_secs(5);
|
||||||
|
while client.captured.lock().unwrap().is_empty() {
|
||||||
|
assert!(
|
||||||
|
std::time::Instant::now() < deadline,
|
||||||
|
"timed out waiting for materialized-worker request"
|
||||||
|
);
|
||||||
|
std::thread::sleep(Duration::from_millis(20));
|
||||||
|
}
|
||||||
|
let names = captured_tool_names(&client, 0);
|
||||||
|
for expected in core_filesystem_tool_names() {
|
||||||
|
assert!(
|
||||||
|
names.contains(expected),
|
||||||
|
"local Worker did not expose {expected}; tools={names:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
assert!(detail.execution.working_directory.is_some());
|
assert!(detail.execution.working_directory.is_some());
|
||||||
let cwds = observed_cwds.lock().unwrap();
|
let cwds = observed_cwds.lock().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -274,7 +274,9 @@ impl WorkerController {
|
||||||
manifest_toml.clone(),
|
manifest_toml.clone(),
|
||||||
greeting,
|
greeting,
|
||||||
));
|
));
|
||||||
shared_state.set_fs_view(crate::fs_view::WorkerFsView::new(fs_for_view));
|
if let Some(fs_for_view) = fs_for_view {
|
||||||
|
shared_state.set_fs_view(crate::fs_view::WorkerFsView::new(fs_for_view));
|
||||||
|
}
|
||||||
shared_state.set_workflows(
|
shared_state.set_workflows(
|
||||||
worker
|
worker
|
||||||
.workflow_completions()
|
.workflow_completions()
|
||||||
|
|
@ -520,15 +522,17 @@ fn install_ticket_event_companion_notify_hook<C, St>(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(companion_worker_name) = companion_worker_name_for_workspace(worker.workspace_root())
|
let Some(local) = worker.local_working_directory() else {
|
||||||
else {
|
return;
|
||||||
|
};
|
||||||
|
let Some(companion_worker_name) = companion_worker_name_for_workspace(&local.root) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if companion_worker_name == worker.manifest().worker.name {
|
if companion_worker_name == worker.manifest().worker.name {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Ok(ticket_config) = TicketConfig::load_workspace(worker.cwd()) else {
|
let Ok(ticket_config) = TicketConfig::load_workspace(&local.cwd) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let backend_root = ticket_config.backend_root().to_path_buf();
|
let backend_root = ticket_config.backend_root().to_path_buf();
|
||||||
|
|
@ -540,7 +544,7 @@ fn install_ticket_event_companion_notify_hook<C, St>(
|
||||||
worker.worker_metadata_store(),
|
worker.worker_metadata_store(),
|
||||||
worker.manifest().worker.name.clone(),
|
worker.manifest().worker.name.clone(),
|
||||||
runtime_base,
|
runtime_base,
|
||||||
worker.cwd().to_path_buf(),
|
Some(local.cwd.clone()),
|
||||||
spawned_registry,
|
spawned_registry,
|
||||||
);
|
);
|
||||||
match discovery.ensure_existing_peer(&companion_worker_name) {
|
match discovery.ensure_existing_peer(&companion_worker_name) {
|
||||||
|
|
@ -589,7 +593,7 @@ async fn register_worker_tools<C, St>(
|
||||||
spawner_socket: PathBuf,
|
spawner_socket: PathBuf,
|
||||||
runtime_base: PathBuf,
|
runtime_base: PathBuf,
|
||||||
spawned_registry: Arc<SpawnedWorkerRegistry>,
|
spawned_registry: Arc<SpawnedWorkerRegistry>,
|
||||||
) -> std::io::Result<tools::ScopedFs>
|
) -> std::io::Result<Option<tools::ScopedFs>>
|
||||||
where
|
where
|
||||||
C: LlmClient + Clone + 'static,
|
C: LlmClient + Clone + 'static,
|
||||||
St: Store + WorkerMetadataStore + Clone + 'static,
|
St: Store + WorkerMetadataStore + Clone + 'static,
|
||||||
|
|
@ -597,8 +601,8 @@ where
|
||||||
// Worker-immutable snapshots taken before the mutable worker borrow
|
// Worker-immutable snapshots taken before the mutable worker borrow
|
||||||
// below so the worker borrow doesn't conflict with reads on `worker`.
|
// below so the worker borrow doesn't conflict with reads on `worker`.
|
||||||
let scope_handle = worker.scope().clone();
|
let scope_handle = worker.scope().clone();
|
||||||
let cwd = worker.cwd().to_path_buf();
|
let local_filesystem = worker.local_working_directory().cloned();
|
||||||
let workspace_root = worker.workspace_root().to_path_buf();
|
let local_workspace_root = local_filesystem.as_ref().map(|local| local.root.clone());
|
||||||
let task_feature = worker.task_feature();
|
let task_feature = worker.task_feature();
|
||||||
let session_id_for_usage = worker.segment_id().to_string();
|
let session_id_for_usage = worker.segment_id().to_string();
|
||||||
let memory_config = worker.manifest().memory.clone();
|
let memory_config = worker.manifest().memory.clone();
|
||||||
|
|
@ -611,24 +615,24 @@ where
|
||||||
let worker_metadata_store = worker.store().clone();
|
let worker_metadata_store = worker.store().clone();
|
||||||
let self_parent_socket = worker.callback_socket().cloned();
|
let self_parent_socket = worker.callback_socket().cloned();
|
||||||
|
|
||||||
// The Worker's SharedScope (already augmented with the bash-output
|
// The Worker's SharedScope is the single source of truth for every
|
||||||
// Read rule by the caller) is the single source of truth — every
|
// ScopedFs when local filesystem authority exists. No-workdir Workers
|
||||||
// ScopedFs (builtin tools, fs_view, compact worker) reads from it,
|
// deliberately skip constructing/registering filesystem and Bash tools.
|
||||||
// and any future scope mutation (SpawnWorker-style revoke, future
|
let (fs_for_view, tracker) = if let Some(local) = local_filesystem.as_ref() {
|
||||||
// GrantScope) propagates through it.
|
let fs = tools::ScopedFs::with_shared_scope(scope_handle.clone(), local.cwd.clone());
|
||||||
let fs = tools::ScopedFs::with_shared_scope(scope_handle.clone(), cwd.clone());
|
let tracker = tools::Tracker::new();
|
||||||
let tracker = tools::Tracker::new();
|
let fs_for_view = fs.clone();
|
||||||
// Same ScopedFs also powers the IPC `ListCompletions` query — keep
|
worker
|
||||||
// a clone for the FS view we attach below, since the tools consume
|
.engine_mut()
|
||||||
// `fs` itself.
|
.register_tools(tools::core_builtin_tools(
|
||||||
let fs_for_view = fs.clone();
|
fs,
|
||||||
worker
|
tracker.clone(),
|
||||||
.engine_mut()
|
bash_output_dir,
|
||||||
.register_tools(tools::core_builtin_tools(
|
));
|
||||||
fs,
|
(Some(fs_for_view), Some(tracker))
|
||||||
tracker.clone(),
|
} else {
|
||||||
bash_output_dir,
|
(None, None)
|
||||||
));
|
};
|
||||||
if feature_config.web.enabled {
|
if feature_config.web.enabled {
|
||||||
worker
|
worker
|
||||||
.engine_mut()
|
.engine_mut()
|
||||||
|
|
@ -649,11 +653,20 @@ where
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Ticket tools are typed operations over the currently checked-out work
|
// Ticket tools are typed operations over the currently checked-out work
|
||||||
// tree. Use the Worker cwd rather than the runtime workspace root so a
|
// tree. They require explicit local filesystem authority and must not
|
||||||
// dedicated Orchestrator worktree gets its own `.yoi/tickets` backend.
|
// use workspace identity as a cwd fallback.
|
||||||
|
let ticket_cwd = local_filesystem
|
||||||
|
.as_ref()
|
||||||
|
.map(|local| &local.cwd)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidInput,
|
||||||
|
"ticket tools require local Worker filesystem authority",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
feature_registry.add_module(
|
feature_registry.add_module(
|
||||||
crate::feature::builtin::ticket::ticket_tools_feature_with_options(
|
crate::feature::builtin::ticket::ticket_tools_feature_with_options(
|
||||||
&cwd,
|
ticket_cwd,
|
||||||
feature_config.ticket.enabled.then_some(ticket_access),
|
feature_config.ticket.enabled.then_some(ticket_access),
|
||||||
feature_config.ticket_orchestration.enabled,
|
feature_config.ticket_orchestration.enabled,
|
||||||
),
|
),
|
||||||
|
|
@ -665,10 +678,12 @@ where
|
||||||
) {
|
) {
|
||||||
feature_registry = feature_registry.with_module(module);
|
feature_registry = feature_registry.with_module(module);
|
||||||
}
|
}
|
||||||
if let Some(module) =
|
if let Some(workspace_root) = local_workspace_root.as_ref() {
|
||||||
crate::feature::mcp::discover_stdio_tool_feature(&mcp_config, &workspace_root).await
|
if let Some(module) =
|
||||||
{
|
crate::feature::mcp::discover_stdio_tool_feature(&mcp_config, workspace_root).await
|
||||||
feature_registry = feature_registry.with_module(module);
|
{
|
||||||
|
feature_registry = feature_registry.with_module(module);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -684,7 +699,13 @@ where
|
||||||
"[feature.memory].enabled = true requires a [memory] configuration section",
|
"[feature.memory].enabled = true requires a [memory] configuration section",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let layout = memory::WorkspaceLayout::resolve(mem, &workspace_root);
|
let workspace_root = local_workspace_root.as_ref().ok_or_else(|| {
|
||||||
|
std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidInput,
|
||||||
|
"memory tools require local Worker filesystem authority",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let layout = memory::WorkspaceLayout::resolve(mem, workspace_root);
|
||||||
let query_cfg = memory::tool::QueryConfig::from(mem);
|
let query_cfg = memory::tool::QueryConfig::from(mem);
|
||||||
worker.register_tool(memory::tool::read_tool_with_usage(
|
worker.register_tool(memory::tool::read_tool_with_usage(
|
||||||
layout.clone(),
|
layout.clone(),
|
||||||
|
|
@ -709,12 +730,27 @@ where
|
||||||
"[feature.workers].enabled = true requires non-empty [[delegation_scope.allow]]",
|
"[feature.workers].enabled = true requires non-empty [[delegation_scope.allow]]",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
let spawner_cwd = local_filesystem
|
||||||
|
.as_ref()
|
||||||
|
.map(|local| local.cwd.clone())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidInput,
|
||||||
|
"worker spawn tools require local Worker filesystem authority",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let spawner_workspace_root = local_workspace_root.clone().ok_or_else(|| {
|
||||||
|
std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidInput,
|
||||||
|
"worker spawn tools require local Worker filesystem authority",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
worker.register_tool(spawn_worker_tool(
|
worker.register_tool(spawn_worker_tool(
|
||||||
spawner_name.clone(),
|
spawner_name.clone(),
|
||||||
spawner_socket,
|
spawner_socket,
|
||||||
runtime_base.clone(),
|
runtime_base.clone(),
|
||||||
workspace_root.clone(),
|
spawner_workspace_root,
|
||||||
cwd.clone(),
|
spawner_cwd.clone(),
|
||||||
spawned_registry.clone(),
|
spawned_registry.clone(),
|
||||||
self_parent_socket,
|
self_parent_socket,
|
||||||
spawner_manifest,
|
spawner_manifest,
|
||||||
|
|
@ -728,7 +764,7 @@ where
|
||||||
worker_metadata_store,
|
worker_metadata_store,
|
||||||
spawner_name,
|
spawner_name,
|
||||||
runtime_base,
|
runtime_base,
|
||||||
cwd,
|
Some(spawner_cwd),
|
||||||
spawned_registry,
|
spawned_registry,
|
||||||
);
|
);
|
||||||
worker.register_tool(list_workers_tool(discovery.clone()));
|
worker.register_tool(list_workers_tool(discovery.clone()));
|
||||||
|
|
@ -737,7 +773,9 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _feature_install_report = worker.install_features(feature_registry);
|
let _feature_install_report = worker.install_features(feature_registry);
|
||||||
worker.attach_tracker(tracker);
|
if let Some(tracker) = tracker {
|
||||||
|
worker.attach_tracker(tracker);
|
||||||
|
}
|
||||||
Ok(fs_for_view)
|
Ok(fs_for_view)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -774,11 +812,14 @@ async fn controller_loop<C, St>(
|
||||||
.parent()
|
.parent()
|
||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
.unwrap_or_else(|| runtime_dir.path().to_path_buf());
|
.unwrap_or_else(|| runtime_dir.path().to_path_buf());
|
||||||
|
let discovery_cwd = worker
|
||||||
|
.local_working_directory()
|
||||||
|
.map(|local| local.cwd.clone());
|
||||||
let discovery = WorkerDiscovery::new(
|
let discovery = WorkerDiscovery::new(
|
||||||
worker.store().clone(),
|
worker.store().clone(),
|
||||||
spawner_name.clone(),
|
spawner_name.clone(),
|
||||||
discovery_runtime_base,
|
discovery_runtime_base,
|
||||||
worker.cwd().to_path_buf(),
|
discovery_cwd,
|
||||||
spawned_registry.clone(),
|
spawned_registry.clone(),
|
||||||
);
|
);
|
||||||
let mut pending: Option<PendingRun> = None;
|
let mut pending: Option<PendingRun> = None;
|
||||||
|
|
@ -1445,7 +1486,10 @@ where
|
||||||
.collect();
|
.collect();
|
||||||
protocol::Greeting {
|
protocol::Greeting {
|
||||||
worker_name: manifest.worker.name.clone(),
|
worker_name: manifest.worker.name.clone(),
|
||||||
cwd: worker.cwd().display().to_string(),
|
cwd: worker
|
||||||
|
.local_working_directory()
|
||||||
|
.map(|local| local.cwd.display().to_string())
|
||||||
|
.unwrap_or_default(),
|
||||||
provider: provider_name,
|
provider: provider_name,
|
||||||
model: model_id,
|
model: model_id,
|
||||||
scope_summary: worker.scope_snapshot().summary(),
|
scope_summary: worker.scope_snapshot().summary(),
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ pub struct WorkerDiscovery<St> {
|
||||||
store: St,
|
store: St,
|
||||||
self_worker_name: String,
|
self_worker_name: String,
|
||||||
runtime_base: PathBuf,
|
runtime_base: PathBuf,
|
||||||
cwd: PathBuf,
|
cwd: Option<PathBuf>,
|
||||||
store_dir: Option<PathBuf>,
|
store_dir: Option<PathBuf>,
|
||||||
spawned_registry: Arc<SpawnedWorkerRegistry>,
|
spawned_registry: Arc<SpawnedWorkerRegistry>,
|
||||||
}
|
}
|
||||||
|
|
@ -55,7 +55,7 @@ where
|
||||||
store: St,
|
store: St,
|
||||||
self_worker_name: String,
|
self_worker_name: String,
|
||||||
runtime_base: PathBuf,
|
runtime_base: PathBuf,
|
||||||
cwd: PathBuf,
|
cwd: Option<PathBuf>,
|
||||||
spawned_registry: Arc<SpawnedWorkerRegistry>,
|
spawned_registry: Arc<SpawnedWorkerRegistry>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let store_dir = store.root_dir();
|
let store_dir = store.root_dir();
|
||||||
|
|
@ -432,13 +432,19 @@ where
|
||||||
) -> Result<(), WorkerDiscoveryError> {
|
) -> Result<(), WorkerDiscoveryError> {
|
||||||
let runtime_command =
|
let runtime_command =
|
||||||
WorkerRuntimeCommand::resolve().map_err(WorkerDiscoveryError::RestoreSpawn)?;
|
WorkerRuntimeCommand::resolve().map_err(WorkerDiscoveryError::RestoreSpawn)?;
|
||||||
|
let Some(cwd) = &self.cwd else {
|
||||||
|
return Err(WorkerDiscoveryError::NotRestorable {
|
||||||
|
worker_name: worker_name.to_string(),
|
||||||
|
reason: "restore requires local Worker filesystem authority".into(),
|
||||||
|
});
|
||||||
|
};
|
||||||
let mut command = Command::new(runtime_command.program());
|
let mut command = Command::new(runtime_command.program());
|
||||||
command
|
command
|
||||||
.args(runtime_command.prefix_args())
|
.args(runtime_command.prefix_args())
|
||||||
.arg("--worker")
|
.arg("--worker")
|
||||||
.arg(worker_name)
|
.arg(worker_name)
|
||||||
.arg("--require-worker-state")
|
.arg("--require-worker-state")
|
||||||
.current_dir(&self.cwd)
|
.current_dir(cwd)
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::null())
|
||||||
|
|
@ -1139,6 +1145,7 @@ mod tests {
|
||||||
worker_name: "parent".into(),
|
worker_name: "parent".into(),
|
||||||
active: None,
|
active: None,
|
||||||
workspace_root: None,
|
workspace_root: None,
|
||||||
|
workspace_id: None,
|
||||||
spawned_children: vec![
|
spawned_children: vec![
|
||||||
child("child-live", &live_socket),
|
child("child-live", &live_socket),
|
||||||
child("child-stale", &stale_socket),
|
child("child-stale", &stale_socket),
|
||||||
|
|
@ -1159,6 +1166,7 @@ mod tests {
|
||||||
active_child_segment,
|
active_child_segment,
|
||||||
)),
|
)),
|
||||||
workspace_root: None,
|
workspace_root: None,
|
||||||
|
workspace_id: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: Vec::new(),
|
peers: Vec::new(),
|
||||||
|
|
@ -1173,6 +1181,7 @@ mod tests {
|
||||||
active_child_segment,
|
active_child_segment,
|
||||||
)),
|
)),
|
||||||
workspace_root: None,
|
workspace_root: None,
|
||||||
|
workspace_id: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: Vec::new(),
|
peers: Vec::new(),
|
||||||
|
|
@ -1184,6 +1193,7 @@ mod tests {
|
||||||
worker_name: "child-pending".into(),
|
worker_name: "child-pending".into(),
|
||||||
active: Some(WorkerActiveSegmentRef::pending_segment(pending_session_id)),
|
active: Some(WorkerActiveSegmentRef::pending_segment(pending_session_id)),
|
||||||
workspace_root: None,
|
workspace_root: None,
|
||||||
|
workspace_id: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: Vec::new(),
|
peers: Vec::new(),
|
||||||
|
|
@ -1198,6 +1208,7 @@ mod tests {
|
||||||
new_segment_id(),
|
new_segment_id(),
|
||||||
)),
|
)),
|
||||||
workspace_root: None,
|
workspace_root: None,
|
||||||
|
workspace_id: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: Vec::new(),
|
peers: Vec::new(),
|
||||||
|
|
@ -1209,6 +1220,7 @@ mod tests {
|
||||||
worker_name: "peer".into(),
|
worker_name: "peer".into(),
|
||||||
active: None,
|
active: None,
|
||||||
workspace_root: None,
|
workspace_root: None,
|
||||||
|
workspace_id: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: vec![session_store::WorkerPeer {
|
peers: vec![session_store::WorkerPeer {
|
||||||
|
|
@ -1228,7 +1240,7 @@ mod tests {
|
||||||
store.clone(),
|
store.clone(),
|
||||||
"parent".into(),
|
"parent".into(),
|
||||||
runtime_base.clone(),
|
runtime_base.clone(),
|
||||||
root.path().to_path_buf(),
|
Some(root.path().to_path_buf()),
|
||||||
registry,
|
registry,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1355,7 +1367,7 @@ mod tests {
|
||||||
store.clone(),
|
store.clone(),
|
||||||
"source".into(),
|
"source".into(),
|
||||||
runtime_base.clone(),
|
runtime_base.clone(),
|
||||||
root.path().to_path_buf(),
|
Some(root.path().to_path_buf()),
|
||||||
SpawnedWorkerRegistry::new(runtime_dir),
|
SpawnedWorkerRegistry::new(runtime_dir),
|
||||||
);
|
);
|
||||||
let result = discovery.register_peer("target").unwrap();
|
let result = discovery.register_peer("target").unwrap();
|
||||||
|
|
@ -1390,7 +1402,7 @@ mod tests {
|
||||||
store,
|
store,
|
||||||
"source".into(),
|
"source".into(),
|
||||||
runtime_base,
|
runtime_base,
|
||||||
root.path().to_path_buf(),
|
Some(root.path().to_path_buf()),
|
||||||
SpawnedWorkerRegistry::new(runtime_dir),
|
SpawnedWorkerRegistry::new(runtime_dir),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1415,6 +1427,7 @@ mod tests {
|
||||||
worker_name: "source".into(),
|
worker_name: "source".into(),
|
||||||
active: None,
|
active: None,
|
||||||
workspace_root: None,
|
workspace_root: None,
|
||||||
|
workspace_id: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: vec![session_store::WorkerPeer {
|
peers: vec![session_store::WorkerPeer {
|
||||||
|
|
@ -1430,7 +1443,7 @@ mod tests {
|
||||||
store.clone(),
|
store.clone(),
|
||||||
"source".into(),
|
"source".into(),
|
||||||
runtime_base,
|
runtime_base,
|
||||||
root.path().to_path_buf(),
|
Some(root.path().to_path_buf()),
|
||||||
SpawnedWorkerRegistry::new(runtime_dir),
|
SpawnedWorkerRegistry::new(runtime_dir),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1455,6 +1468,7 @@ mod tests {
|
||||||
worker_name: "source".into(),
|
worker_name: "source".into(),
|
||||||
active: None,
|
active: None,
|
||||||
workspace_root: None,
|
workspace_root: None,
|
||||||
|
workspace_id: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: vec![session_store::WorkerPeer {
|
peers: vec![session_store::WorkerPeer {
|
||||||
|
|
@ -1468,6 +1482,7 @@ mod tests {
|
||||||
worker_name: "target".into(),
|
worker_name: "target".into(),
|
||||||
active: None,
|
active: None,
|
||||||
workspace_root: None,
|
workspace_root: None,
|
||||||
|
workspace_id: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: vec![session_store::WorkerPeer {
|
peers: vec![session_store::WorkerPeer {
|
||||||
|
|
@ -1481,7 +1496,7 @@ mod tests {
|
||||||
store,
|
store,
|
||||||
"source".into(),
|
"source".into(),
|
||||||
runtime_base.clone(),
|
runtime_base.clone(),
|
||||||
root.path().to_path_buf(),
|
Some(root.path().to_path_buf()),
|
||||||
SpawnedWorkerRegistry::new(runtime_dir),
|
SpawnedWorkerRegistry::new(runtime_dir),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1573,6 +1588,7 @@ mod tests {
|
||||||
worker_name: "source".into(),
|
worker_name: "source".into(),
|
||||||
active: None,
|
active: None,
|
||||||
workspace_root: None,
|
workspace_root: None,
|
||||||
|
workspace_id: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: vec![session_store::WorkerPeer {
|
peers: vec![session_store::WorkerPeer {
|
||||||
|
|
@ -1586,6 +1602,7 @@ mod tests {
|
||||||
worker_name: "target".into(),
|
worker_name: "target".into(),
|
||||||
active: None,
|
active: None,
|
||||||
workspace_root: None,
|
workspace_root: None,
|
||||||
|
workspace_id: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: vec![session_store::WorkerPeer {
|
peers: vec![session_store::WorkerPeer {
|
||||||
|
|
@ -1599,7 +1616,7 @@ mod tests {
|
||||||
store,
|
store,
|
||||||
"source".into(),
|
"source".into(),
|
||||||
runtime_base.clone(),
|
runtime_base.clone(),
|
||||||
root.path().to_path_buf(),
|
Some(root.path().to_path_buf()),
|
||||||
SpawnedWorkerRegistry::new(runtime_dir),
|
SpawnedWorkerRegistry::new(runtime_dir),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1689,6 +1706,7 @@ mod tests {
|
||||||
worker_name: "source".into(),
|
worker_name: "source".into(),
|
||||||
active: None,
|
active: None,
|
||||||
workspace_root: None,
|
workspace_root: None,
|
||||||
|
workspace_id: None,
|
||||||
spawned_children: vec![child("target", &socket)],
|
spawned_children: vec![child("target", &socket)],
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: Vec::new(),
|
peers: Vec::new(),
|
||||||
|
|
@ -1701,7 +1719,7 @@ mod tests {
|
||||||
store,
|
store,
|
||||||
"source".into(),
|
"source".into(),
|
||||||
runtime_base,
|
runtime_base,
|
||||||
root.path().to_path_buf(),
|
Some(root.path().to_path_buf()),
|
||||||
SpawnedWorkerRegistry::new(runtime_dir),
|
SpawnedWorkerRegistry::new(runtime_dir),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@ use std::ffi::OsString;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
use crate::{PromptLoader, Worker, WorkerController};
|
use crate::{
|
||||||
|
PromptLoader, Worker, WorkerController, WorkerFilesystemAuthority, WorkerWorkspaceContext,
|
||||||
|
WorkspaceId,
|
||||||
|
};
|
||||||
use clap::{CommandFactory, FromArgMatches, Parser};
|
use clap::{CommandFactory, FromArgMatches, Parser};
|
||||||
use manifest::{
|
use manifest::{
|
||||||
Permission, ProfileResolveOptions, ProfileResolver, ProfileSelector, ScopeConfig, ScopeRule,
|
Permission, ProfileResolveOptions, ProfileResolver, ProfileSelector, ScopeConfig, ScopeRule,
|
||||||
|
|
@ -103,6 +106,24 @@ fn runtime_workspace_root(cli: &Cli) -> Result<PathBuf, String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn runtime_workspace_context(workspace_root: &Path) -> WorkerWorkspaceContext {
|
||||||
|
WorkerWorkspaceContext::local_filesystem(read_workspace_id_hint(workspace_root))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_workspace_id_hint(workspace_root: &Path) -> Option<WorkspaceId> {
|
||||||
|
let path = workspace_root.join(".yoi/workspace.toml");
|
||||||
|
let contents = std::fs::read_to_string(path).ok()?;
|
||||||
|
let value = toml::from_str::<toml::Value>(&contents).ok()?;
|
||||||
|
let id = value.get("id")?.as_str()?.to_string();
|
||||||
|
match WorkspaceId::new(id) {
|
||||||
|
Ok(id) => Some(id),
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!("ignoring invalid workspace id in .yoi/workspace.toml: {err}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn runtime_worker_name(cli: &Cli, workspace_root: &Path) -> String {
|
fn runtime_worker_name(cli: &Cli, workspace_root: &Path) -> String {
|
||||||
cli.worker
|
cli.worker
|
||||||
.as_deref()
|
.as_deref()
|
||||||
|
|
@ -511,6 +532,9 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let store = CombinedStore::new(session_store, worker_metadata_store);
|
let store = CombinedStore::new(session_store, worker_metadata_store);
|
||||||
|
let filesystem_authority =
|
||||||
|
WorkerFilesystemAuthority::local(workspace_root.clone(), cwd.clone());
|
||||||
|
let workspace_context = runtime_workspace_context(&workspace_root);
|
||||||
|
|
||||||
let mut worker = if cli.adopt {
|
let mut worker = if cli.adopt {
|
||||||
let callback = match cli.callback.clone() {
|
let callback = match cli.callback.clone() {
|
||||||
|
|
@ -525,8 +549,8 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
|
||||||
store,
|
store,
|
||||||
loader,
|
loader,
|
||||||
callback,
|
callback,
|
||||||
workspace_root.clone(),
|
workspace_context.clone(),
|
||||||
cwd.clone(),
|
filesystem_authority.clone(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|
@ -556,8 +580,8 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
|
||||||
manifest,
|
manifest,
|
||||||
store,
|
store,
|
||||||
loader,
|
loader,
|
||||||
workspace_root.clone(),
|
workspace_context.clone(),
|
||||||
cwd.clone(),
|
filesystem_authority.clone(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|
@ -576,8 +600,8 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
|
||||||
manifest,
|
manifest,
|
||||||
store,
|
store,
|
||||||
loader,
|
loader,
|
||||||
workspace_root.clone(),
|
workspace_context.clone(),
|
||||||
cwd.clone(),
|
filesystem_authority.clone(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|
@ -597,8 +621,8 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
|
||||||
manifest,
|
manifest,
|
||||||
store,
|
store,
|
||||||
loader,
|
loader,
|
||||||
workspace_root.clone(),
|
workspace_context.clone(),
|
||||||
cwd.clone(),
|
filesystem_authority.clone(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|
@ -619,8 +643,8 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
|
||||||
manifest,
|
manifest,
|
||||||
store,
|
store,
|
||||||
loader,
|
loader,
|
||||||
workspace_root.clone(),
|
workspace_context.clone(),
|
||||||
cwd.clone(),
|
filesystem_authority.clone(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -38,4 +38,7 @@ pub use provider::{ProviderError, build_client};
|
||||||
pub use runtime::dir::RuntimeDir;
|
pub use runtime::dir::RuntimeDir;
|
||||||
pub use segment_log_sink::SegmentLogSink;
|
pub use segment_log_sink::SegmentLogSink;
|
||||||
pub use shared_state::WorkerSharedState;
|
pub use shared_state::WorkerSharedState;
|
||||||
pub use worker::{Worker, WorkerError, WorkerRunResult, apply_worker_manifest};
|
pub use worker::{
|
||||||
|
LocalWorkingDirectory, Worker, WorkerError, WorkerFilesystemAuthority, WorkerRunResult,
|
||||||
|
WorkerWorkspaceContext, WorkspaceClient, WorkspaceId, WorkspaceIdError, apply_worker_manifest,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@
|
||||||
//! `set_system_prompt`. Subsequent turns and compactions reuse that
|
//! `set_system_prompt`. Subsequent turns and compactions reuse that
|
||||||
//! materialised string verbatim.
|
//! materialised string verbatim.
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
#[cfg(test)]
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
@ -146,7 +148,7 @@ impl std::fmt::Debug for SystemPromptTemplate {
|
||||||
/// templates cannot drop them on the floor.
|
/// templates cannot drop them on the floor.
|
||||||
pub struct SystemPromptContext<'a> {
|
pub struct SystemPromptContext<'a> {
|
||||||
pub now: DateTime<Utc>,
|
pub now: DateTime<Utc>,
|
||||||
pub cwd: &'a Path,
|
pub cwd: Cow<'a, str>,
|
||||||
/// Language policy exposed to instruction templates as `{{ language }}`.
|
/// Language policy exposed to instruction templates as `{{ language }}`.
|
||||||
pub language: &'a str,
|
pub language: &'a str,
|
||||||
pub scope: &'a Scope,
|
pub scope: &'a Scope,
|
||||||
|
|
@ -189,7 +191,7 @@ impl<'a> SystemPromptContext<'a> {
|
||||||
"datetime".into(),
|
"datetime".into(),
|
||||||
Value::from(self.now.to_rfc3339_opts(SecondsFormat::Secs, true)),
|
Value::from(self.now.to_rfc3339_opts(SecondsFormat::Secs, true)),
|
||||||
);
|
);
|
||||||
root.insert("cwd".into(), Value::from(self.cwd.display().to_string()));
|
root.insert("cwd".into(), Value::from(self.cwd.as_ref()));
|
||||||
root.insert("language".into(), Value::from(self.language));
|
root.insert("language".into(), Value::from(self.language));
|
||||||
root.insert(
|
root.insert(
|
||||||
"tools".into(),
|
"tools".into(),
|
||||||
|
|
@ -442,7 +444,7 @@ mod tests {
|
||||||
) -> SystemPromptContext<'a> {
|
) -> SystemPromptContext<'a> {
|
||||||
SystemPromptContext {
|
SystemPromptContext {
|
||||||
now: fixed_now(),
|
now: fixed_now(),
|
||||||
cwd,
|
cwd: cwd.display().to_string().into(),
|
||||||
language: manifest::defaults::WORKER_LANGUAGE,
|
language: manifest::defaults::WORKER_LANGUAGE,
|
||||||
scope,
|
scope,
|
||||||
tool_names: tools,
|
tool_names: tools,
|
||||||
|
|
@ -461,7 +463,7 @@ mod tests {
|
||||||
) -> SystemPromptContext<'a> {
|
) -> SystemPromptContext<'a> {
|
||||||
SystemPromptContext {
|
SystemPromptContext {
|
||||||
now: fixed_now(),
|
now: fixed_now(),
|
||||||
cwd,
|
cwd: cwd.display().to_string().into(),
|
||||||
language: manifest::defaults::WORKER_LANGUAGE,
|
language: manifest::defaults::WORKER_LANGUAGE,
|
||||||
scope,
|
scope,
|
||||||
tool_names: Vec::new(),
|
tool_names: Vec::new(),
|
||||||
|
|
@ -480,7 +482,7 @@ mod tests {
|
||||||
) -> SystemPromptContext<'a> {
|
) -> SystemPromptContext<'a> {
|
||||||
SystemPromptContext {
|
SystemPromptContext {
|
||||||
now: fixed_now(),
|
now: fixed_now(),
|
||||||
cwd,
|
cwd: cwd.display().to_string().into(),
|
||||||
language: manifest::defaults::WORKER_LANGUAGE,
|
language: manifest::defaults::WORKER_LANGUAGE,
|
||||||
scope,
|
scope,
|
||||||
tool_names: Vec::new(),
|
tool_names: Vec::new(),
|
||||||
|
|
@ -499,7 +501,7 @@ mod tests {
|
||||||
) -> SystemPromptContext<'a> {
|
) -> SystemPromptContext<'a> {
|
||||||
SystemPromptContext {
|
SystemPromptContext {
|
||||||
now: fixed_now(),
|
now: fixed_now(),
|
||||||
cwd,
|
cwd: cwd.display().to_string().into(),
|
||||||
language: manifest::defaults::WORKER_LANGUAGE,
|
language: manifest::defaults::WORKER_LANGUAGE,
|
||||||
scope,
|
scope,
|
||||||
tool_names: Vec::new(),
|
tool_names: Vec::new(),
|
||||||
|
|
|
||||||
|
|
@ -384,6 +384,7 @@ mod tests {
|
||||||
worker_name: "orchestrator".into(),
|
worker_name: "orchestrator".into(),
|
||||||
active: None,
|
active: None,
|
||||||
workspace_root: None,
|
workspace_root: None,
|
||||||
|
workspace_id: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: Vec::new(),
|
peers: Vec::new(),
|
||||||
|
|
@ -395,6 +396,7 @@ mod tests {
|
||||||
worker_name: "companion".into(),
|
worker_name: "companion".into(),
|
||||||
active: None,
|
active: None,
|
||||||
workspace_root: None,
|
workspace_root: None,
|
||||||
|
workspace_id: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: Vec::new(),
|
peers: Vec::new(),
|
||||||
|
|
@ -414,7 +416,7 @@ mod tests {
|
||||||
store,
|
store,
|
||||||
"orchestrator".into(),
|
"orchestrator".into(),
|
||||||
runtime_base.clone(),
|
runtime_base.clone(),
|
||||||
root.path().to_path_buf(),
|
Some(root.path().to_path_buf()),
|
||||||
SpawnedWorkerRegistry::new(runtime_dir),
|
SpawnedWorkerRegistry::new(runtime_dir),
|
||||||
),
|
),
|
||||||
"companion",
|
"companion",
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -164,9 +164,16 @@ async fn make_worker_with_manifest(
|
||||||
std::mem::forget(pwd_tmp);
|
std::mem::forget(pwd_tmp);
|
||||||
|
|
||||||
let worker = Engine::new(client);
|
let worker = Engine::new(client);
|
||||||
let mut worker = Worker::new(manifest, worker, store, pwd, scope)
|
let mut worker = Worker::new(
|
||||||
.await
|
manifest,
|
||||||
.unwrap();
|
worker,
|
||||||
|
store,
|
||||||
|
worker::WorkerWorkspaceContext::local_filesystem(None),
|
||||||
|
worker::WorkerFilesystemAuthority::local(pwd.clone(), pwd.clone()),
|
||||||
|
scope,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
worker.enable_worker_metadata_write_through().unwrap();
|
worker.enable_worker_metadata_write_through().unwrap();
|
||||||
worker
|
worker
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -170,9 +170,16 @@ async fn make_worker_with(
|
||||||
|
|
||||||
let scope = worker::Scope::writable(&pwd).unwrap();
|
let scope = worker::Scope::writable(&pwd).unwrap();
|
||||||
let worker = Engine::new(client);
|
let worker = Engine::new(client);
|
||||||
Worker::new(manifest, worker, store, pwd, scope)
|
Worker::new(
|
||||||
.await
|
manifest,
|
||||||
.unwrap()
|
worker,
|
||||||
|
store,
|
||||||
|
worker::WorkerWorkspaceContext::local_filesystem(None),
|
||||||
|
worker::WorkerFilesystemAuthority::local(pwd.clone(), pwd.clone()),
|
||||||
|
scope,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_n_staging(layout: &WorkspaceLayout, n: usize) -> Vec<uuid::Uuid> {
|
fn write_n_staging(layout: &WorkspaceLayout, n: usize) -> Vec<uuid::Uuid> {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,10 @@ use llm_engine::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
||||||
use session_store::{CombinedStore, FsWorkerStore};
|
use session_store::{CombinedStore, FsWorkerStore};
|
||||||
use session_store::{FsStore, LogEntry};
|
use session_store::{FsStore, LogEntry};
|
||||||
|
|
||||||
use worker::{Event, Method, Worker, WorkerController, WorkerHandle, WorkerManifest, WorkerStatus};
|
use worker::{
|
||||||
|
Event, Method, Worker, WorkerController, WorkerFilesystemAuthority, WorkerHandle,
|
||||||
|
WorkerManifest, WorkerStatus, WorkerWorkspaceContext,
|
||||||
|
};
|
||||||
|
|
||||||
type TestStore = CombinedStore<FsStore, FsWorkerStore>;
|
type TestStore = CombinedStore<FsStore, FsWorkerStore>;
|
||||||
|
|
||||||
|
|
@ -186,9 +189,17 @@ async fn make_worker_with_pwd_and_manifest(
|
||||||
std::mem::forget(pwd_tmp);
|
std::mem::forget(pwd_tmp);
|
||||||
|
|
||||||
let worker = Engine::new(client);
|
let worker = Engine::new(client);
|
||||||
let worker = Worker::new(manifest, worker, store, pwd.clone(), scope)
|
let authority = WorkerFilesystemAuthority::local(pwd.clone(), pwd.clone());
|
||||||
.await
|
let worker = Worker::new(
|
||||||
.unwrap();
|
manifest,
|
||||||
|
worker,
|
||||||
|
store,
|
||||||
|
WorkerWorkspaceContext::local_filesystem(None),
|
||||||
|
authority,
|
||||||
|
scope,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
(worker, pwd)
|
(worker, pwd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -190,9 +190,16 @@ async fn make_worker(
|
||||||
let mut worker = Engine::new(client);
|
let mut worker = Engine::new(client);
|
||||||
worker.register_tool(big_content_tool_definition(tool_name));
|
worker.register_tool(big_content_tool_definition(tool_name));
|
||||||
|
|
||||||
let worker = Worker::new(manifest, worker, store, pwd, scope)
|
let worker = Worker::new(
|
||||||
.await
|
manifest,
|
||||||
.unwrap();
|
worker,
|
||||||
|
store,
|
||||||
|
worker::WorkerWorkspaceContext::local_filesystem(None),
|
||||||
|
worker::WorkerFilesystemAuthority::local(pwd.clone(), pwd.clone()),
|
||||||
|
scope,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
(worker, store_tmp, pwd_tmp)
|
(worker, store_tmp, pwd_tmp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -453,9 +460,16 @@ async fn metric_write_failure_emits_warn_alert_and_does_not_abort_run() {
|
||||||
// the failure path: at least one metric attempts to write.
|
// the failure path: at least one metric attempts to write.
|
||||||
let client = MockClient::new(vec![text_response_with_cache("hi", 0, 0)]);
|
let client = MockClient::new(vec![text_response_with_cache("hi", 0, 0)]);
|
||||||
let worker = Engine::new(client);
|
let worker = Engine::new(client);
|
||||||
let mut worker = Worker::new(manifest, worker, store.clone(), pwd, scope)
|
let mut worker = Worker::new(
|
||||||
.await
|
manifest,
|
||||||
.unwrap();
|
worker,
|
||||||
|
store.clone(),
|
||||||
|
worker::WorkerWorkspaceContext::local_filesystem(None),
|
||||||
|
worker::WorkerFilesystemAuthority::local(pwd.clone(), pwd.clone()),
|
||||||
|
scope,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let (tx, mut rx) = broadcast::channel::<Event>(64);
|
let (tx, mut rx) = broadcast::channel::<Event>(64);
|
||||||
let alerter = worker::Alerter::new(tx);
|
let alerter = worker::Alerter::new(tx);
|
||||||
|
|
@ -522,9 +536,16 @@ permission = "write"
|
||||||
let pwd = pwd_tmp.path().to_path_buf();
|
let pwd = pwd_tmp.path().to_path_buf();
|
||||||
let scope = worker::Scope::writable(&pwd).unwrap();
|
let scope = worker::Scope::writable(&pwd).unwrap();
|
||||||
let worker = Engine::new(client);
|
let worker = Engine::new(client);
|
||||||
let mut worker = Worker::new(manifest, worker, store.clone(), pwd, scope)
|
let mut worker = Worker::new(
|
||||||
.await
|
manifest,
|
||||||
.unwrap();
|
worker,
|
||||||
|
store.clone(),
|
||||||
|
worker::WorkerWorkspaceContext::local_filesystem(None),
|
||||||
|
worker::WorkerFilesystemAuthority::local(pwd.clone(), pwd.clone()),
|
||||||
|
scope,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let session_id = worker.session_id();
|
let session_id = worker.session_id();
|
||||||
let segment_id = worker.segment_id();
|
let segment_id = worker.segment_id();
|
||||||
worker.run_text("hello").await.unwrap();
|
worker.run_text("hello").await.unwrap();
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,15 @@ async fn make_worker_with_body(
|
||||||
std::mem::forget(user_prompts_tmp);
|
std::mem::forget(user_prompts_tmp);
|
||||||
|
|
||||||
let worker = Engine::new(client);
|
let worker = Engine::new(client);
|
||||||
let mut worker = Worker::new(manifest, worker, store, pwd.clone(), scope).await?;
|
let mut worker = Worker::new(
|
||||||
|
manifest,
|
||||||
|
worker,
|
||||||
|
store,
|
||||||
|
worker::WorkerWorkspaceContext::local_filesystem(None),
|
||||||
|
worker::WorkerFilesystemAuthority::local(pwd.clone(), pwd.clone()),
|
||||||
|
scope,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let template = SystemPromptTemplate::parse("$user/test", loader)
|
let template = SystemPromptTemplate::parse("$user/test", loader)
|
||||||
.map_err(|source| WorkerError::InvalidSystemPromptTemplate { source })?;
|
.map_err(|source| WorkerError::InvalidSystemPromptTemplate { source })?;
|
||||||
|
|
|
||||||
|
|
@ -752,6 +752,7 @@ pub struct WorkerLaunchRuntimeOption {
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
pub built_in: bool,
|
pub built_in: bool,
|
||||||
pub can_spawn_worker: bool,
|
pub can_spawn_worker: bool,
|
||||||
|
pub working_directory_required: bool,
|
||||||
pub status: String,
|
pub status: String,
|
||||||
pub diagnostics: Vec<RuntimeDiagnostic>,
|
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||||
}
|
}
|
||||||
|
|
@ -2640,6 +2641,9 @@ async fn create_workspace_worker(
|
||||||
relative_cwd: selection.relative_cwd,
|
relative_cwd: selection.relative_cwd,
|
||||||
});
|
});
|
||||||
validate_working_directory_claim_for_browser(resolved_working_directory.as_ref())?;
|
validate_working_directory_claim_for_browser(resolved_working_directory.as_ref())?;
|
||||||
|
if resolved_working_directory.is_none() {
|
||||||
|
reject_no_workdir_for_non_embedded_runtime(&request.runtime_id)?;
|
||||||
|
}
|
||||||
let result = api
|
let result = api
|
||||||
.runtime
|
.runtime
|
||||||
.spawn_worker(
|
.spawn_worker(
|
||||||
|
|
@ -2809,11 +2813,36 @@ struct RuntimeConfigBundleAvailabilityQuery {
|
||||||
digest: String,
|
digest: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reject_no_workdir_for_non_embedded_runtime(
|
||||||
|
runtime_id: &str,
|
||||||
|
) -> std::result::Result<(), ApiError> {
|
||||||
|
if runtime_id == EMBEDDED_WORKER_RUNTIME_ID {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(ApiError::with_diagnostics(
|
||||||
|
Error::RuntimeOperationFailed {
|
||||||
|
runtime_id: runtime_id.to_string(),
|
||||||
|
code: "workspace_worker_workdir_required".to_string(),
|
||||||
|
message: "Only the embedded Runtime can launch a Worker without a working directory"
|
||||||
|
.to_string(),
|
||||||
|
},
|
||||||
|
vec![RuntimeDiagnostic {
|
||||||
|
code: "workspace_worker_workdir_required".to_string(),
|
||||||
|
severity: DiagnosticSeverity::Error,
|
||||||
|
message: "Select a working directory for this Runtime, or choose the embedded Runtime for a conversation-only Worker."
|
||||||
|
.to_string(),
|
||||||
|
}],
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
async fn create_runtime_worker(
|
async fn create_runtime_worker(
|
||||||
State(api): State<WorkspaceApi>,
|
State(api): State<WorkspaceApi>,
|
||||||
AxumPath(runtime_id): AxumPath<String>,
|
AxumPath(runtime_id): AxumPath<String>,
|
||||||
Json(mut request): Json<WorkerSpawnRequest>,
|
Json(mut request): Json<WorkerSpawnRequest>,
|
||||||
) -> ApiResult<Json<WorkerSpawnResult>> {
|
) -> ApiResult<Json<WorkerSpawnResult>> {
|
||||||
|
if request.working_directory_request.is_none() && request.resolved_working_directory.is_none() {
|
||||||
|
reject_no_workdir_for_non_embedded_runtime(&runtime_id)?;
|
||||||
|
}
|
||||||
request.resolved_working_directory_request = request
|
request.resolved_working_directory_request = request
|
||||||
.working_directory_request
|
.working_directory_request
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -3765,6 +3794,7 @@ fn worker_launch_options_response(api: &WorkspaceApi) -> WorkerLaunchOptionsResp
|
||||||
display_name: runtime.label,
|
display_name: runtime.label,
|
||||||
built_in,
|
built_in,
|
||||||
can_spawn_worker: runtime.capabilities.can_spawn_worker,
|
can_spawn_worker: runtime.capabilities.can_spawn_worker,
|
||||||
|
working_directory_required: !built_in,
|
||||||
status: runtime.status,
|
status: runtime.status,
|
||||||
diagnostics: runtime.diagnostics,
|
diagnostics: runtime.diagnostics,
|
||||||
}
|
}
|
||||||
|
|
@ -4694,6 +4724,7 @@ impl IntoResponse for ApiError {
|
||||||
|| code.starts_with("unsupported_worker_profile")
|
|| code.starts_with("unsupported_worker_profile")
|
||||||
|| code.starts_with("working_directory_")
|
|| code.starts_with("working_directory_")
|
||||||
|| code.starts_with("workspace_cleanup_")
|
|| code.starts_with("workspace_cleanup_")
|
||||||
|
|| code == "workspace_worker_workdir_required"
|
||||||
|| code.ends_with("_already_exists")
|
|| code.ends_with("_already_exists")
|
||||||
|| code.ends_with("_not_config_managed")
|
|| code.ends_with("_not_config_managed")
|
||||||
|| code.ends_with("_unsupported") =>
|
|| code.ends_with("_unsupported") =>
|
||||||
|
|
@ -6034,13 +6065,17 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
let launch_options = get_json(app.clone(), "/api/workers/launch-options").await;
|
let launch_options = get_json(app.clone(), "/api/workers/launch-options").await;
|
||||||
assert!(
|
let runtimes = launch_options["runtimes"].as_array().unwrap();
|
||||||
launch_options["runtimes"]
|
let embedded_runtime = runtimes
|
||||||
.as_array()
|
.iter()
|
||||||
.unwrap()
|
.find(|runtime| runtime["runtime_id"] == EMBEDDED_WORKER_RUNTIME_ID)
|
||||||
.iter()
|
.expect("embedded runtime launch option");
|
||||||
.any(|runtime| runtime["runtime_id"] == "team-runtime")
|
assert_eq!(embedded_runtime["working_directory_required"], false);
|
||||||
);
|
let team_runtime = runtimes
|
||||||
|
.iter()
|
||||||
|
.find(|runtime| runtime["runtime_id"] == "team-runtime")
|
||||||
|
.expect("team runtime launch option");
|
||||||
|
assert_eq!(team_runtime["working_directory_required"], true);
|
||||||
|
|
||||||
let deleted = request_json(
|
let deleted = request_json(
|
||||||
app.clone(),
|
app.clone(),
|
||||||
|
|
@ -6091,18 +6126,6 @@ mod tests {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert_eq!(added["restart_required"], false);
|
assert_eq!(added["restart_required"], false);
|
||||||
let created = post_json(
|
|
||||||
app.clone(),
|
|
||||||
"/api/workers",
|
|
||||||
serde_json::json!({
|
|
||||||
"runtime_id": "busy-runtime",
|
|
||||||
"display_name": "Remote Test Worker",
|
|
||||||
"profile": "runtime_default",
|
|
||||||
"initial_text": ""
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert_eq!(created["runtime_id"], "busy-runtime");
|
|
||||||
let workers = get_json(app.clone(), "/api/workers").await;
|
let workers = get_json(app.clone(), "/api/workers").await;
|
||||||
assert!(
|
assert!(
|
||||||
workers["items"]
|
workers["items"]
|
||||||
|
|
@ -6306,6 +6329,38 @@ mod tests {
|
||||||
assert!(!projected.contains("http://"));
|
assert!(!projected.contains("http://"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn browser_worker_create_rejects_non_embedded_no_workdir() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let app = test_app(dir.path()).await;
|
||||||
|
let response = request_json(
|
||||||
|
app,
|
||||||
|
"POST",
|
||||||
|
"/api/workers",
|
||||||
|
Some(serde_json::json!({
|
||||||
|
"runtime_id": "remote-runtime",
|
||||||
|
"display_name": "Remote Worker",
|
||||||
|
"profile": "runtime_default",
|
||||||
|
"initial_text": ""
|
||||||
|
})),
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
response["message"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("workspace_worker_workdir_required")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
response["diagnostics"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.any(|diagnostic| { diagnostic["code"] == "workspace_worker_workdir_required" })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn runtime_worker_spawn_rejects_raw_working_directory_fields() {
|
async fn runtime_worker_spawn_rejects_raw_working_directory_fields() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ rustPlatform.buildRustPackage rec {
|
||||||
filter = sourceFilter;
|
filter = sourceFilter;
|
||||||
};
|
};
|
||||||
|
|
||||||
cargoHash = "sha256-vM2ea6W3hqnmzqOXfzQo+ZYrQBoy2Kh69vz1fkU+DIs=";
|
cargoHash = "sha256-av+Iix50MMLcrzxw8UDTT0tUqBFIPkSN1i23WYegNcM=";
|
||||||
|
|
||||||
depsExtraArgs = {
|
depsExtraArgs = {
|
||||||
# Older fetchCargoVendor utilities used crates.io's API download endpoint,
|
# Older fetchCargoVendor utilities used crates.io's API download endpoint,
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ export type WorkerLaunchRuntimeOption = {
|
||||||
display_name: string;
|
display_name: string;
|
||||||
built_in: boolean;
|
built_in: boolean;
|
||||||
can_spawn_worker: boolean;
|
can_spawn_worker: boolean;
|
||||||
|
working_directory_required: boolean;
|
||||||
status: string;
|
status: string;
|
||||||
diagnostics: Diagnostic[];
|
diagnostics: Diagnostic[];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ const options: WorkerLaunchOptionsResponse = {
|
||||||
status: "active",
|
status: "active",
|
||||||
can_spawn_worker: true,
|
can_spawn_worker: true,
|
||||||
built_in: false,
|
built_in: false,
|
||||||
|
working_directory_required: true,
|
||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -32,7 +33,8 @@ const options: WorkerLaunchOptionsResponse = {
|
||||||
display_name: "Embedded",
|
display_name: "Embedded",
|
||||||
status: "active",
|
status: "active",
|
||||||
can_spawn_worker: true,
|
can_spawn_worker: true,
|
||||||
built_in: false,
|
built_in: true,
|
||||||
|
working_directory_required: false,
|
||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -107,3 +109,23 @@ Deno.test("buildBrowserCreateWorkerRequest sends working_directory id and relati
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test("buildBrowserCreateWorkerRequest omits working_directory for embedded no-workdir launches", () => {
|
||||||
|
const request = buildBrowserCreateWorkerRequest({
|
||||||
|
runtime_id: "embedded",
|
||||||
|
display_name: "Worker",
|
||||||
|
profile: "builtin:companion",
|
||||||
|
initial_text: "chat",
|
||||||
|
working_directory_id: "",
|
||||||
|
working_directory_repository_id: "",
|
||||||
|
working_directory_selector: "",
|
||||||
|
relative_cwd: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
assertEquals(request, {
|
||||||
|
runtime_id: "embedded",
|
||||||
|
display_name: "Worker",
|
||||||
|
profile: "builtin:companion",
|
||||||
|
initial_text: "chat",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,14 @@
|
||||||
let relativeCwd = $state('');
|
let relativeCwd = $state('');
|
||||||
let creatingWorkingDirectory = $state(false);
|
let creatingWorkingDirectory = $state(false);
|
||||||
let isNewWorkingDirectorySelected = $derived(workingDirectoryId === NEW_WORKING_DIRECTORY_VALUE);
|
let isNewWorkingDirectorySelected = $derived(workingDirectoryId === NEW_WORKING_DIRECTORY_VALUE);
|
||||||
let canStartWorker = $derived(Boolean(runtimeId && profile && workingDirectoryId && !isNewWorkingDirectorySelected));
|
let selectedRuntime = $derived(options?.runtimes.find((runtime) => runtime.runtime_id === runtimeId));
|
||||||
|
let selectedRuntimeAllowsNoWorkdir = $derived(selectedRuntime?.working_directory_required === false);
|
||||||
|
let hasSelectedExistingWorkdir = $derived(Boolean(workingDirectoryId && !isNewWorkingDirectorySelected));
|
||||||
|
let canStartWorker = $derived(Boolean(
|
||||||
|
runtimeId &&
|
||||||
|
profile &&
|
||||||
|
(hasSelectedExistingWorkdir || (selectedRuntimeAllowsNoWorkdir && !isNewWorkingDirectorySelected)),
|
||||||
|
));
|
||||||
|
|
||||||
function workerApiPath(path: string): string {
|
function workerApiPath(path: string): string {
|
||||||
return workspaceApiPath(workspaceId, path);
|
return workspaceApiPath(workspaceId, path);
|
||||||
|
|
@ -81,7 +88,7 @@
|
||||||
runtimeId = form.runtime_id;
|
runtimeId = form.runtime_id;
|
||||||
displayName = form.display_name;
|
displayName = form.display_name;
|
||||||
profile = form.profile;
|
profile = form.profile;
|
||||||
workingDirectoryId = form.working_directory_id || NEW_WORKING_DIRECTORY_VALUE;
|
workingDirectoryId = form.working_directory_id;
|
||||||
workingDirectoryRepositoryId = form.working_directory_repository_id;
|
workingDirectoryRepositoryId = form.working_directory_repository_id;
|
||||||
workingDirectorySelector = form.working_directory_selector;
|
workingDirectorySelector = form.working_directory_selector;
|
||||||
relativeCwd = form.relative_cwd;
|
relativeCwd = form.relative_cwd;
|
||||||
|
|
@ -149,8 +156,8 @@
|
||||||
submitError = { message: 'workspace id is unavailable', diagnostics: [] };
|
submitError = { message: 'workspace id is unavailable', diagnostics: [] };
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isNewWorkingDirectorySelected || !workingDirectoryId) {
|
if (isNewWorkingDirectorySelected || (!workingDirectoryId && !selectedRuntimeAllowsNoWorkdir)) {
|
||||||
submitError = { message: 'select or create a workdir before starting a Worker', diagnostics: [] };
|
submitError = { message: 'select or create a workdir before starting a Worker; only embedded Runtime can start without one', diagnostics: [] };
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -220,7 +227,7 @@
|
||||||
<header class="worker-new-page-header">
|
<header class="worker-new-page-header">
|
||||||
<div>
|
<div>
|
||||||
<h1 id="new-worker-heading">New Worker</h1>
|
<h1 id="new-worker-heading">New Worker</h1>
|
||||||
<p>Create a Worker on a selected Runtime and workdir.</p>
|
<p>Create a Worker on a selected Runtime. Workdir-less conversation Workers are only available on embedded Runtime.</p>
|
||||||
</div>
|
</div>
|
||||||
<a class="secondary-link" href={`/w/${workspaceId}`}>Back to workspace</a>
|
<a class="secondary-link" href={`/w/${workspaceId}`}>Back to workspace</a>
|
||||||
</header>
|
</header>
|
||||||
|
|
@ -237,7 +244,11 @@
|
||||||
<div class="worker-launch-sentence">
|
<div class="worker-launch-sentence">
|
||||||
<span>Run at</span>
|
<span>Run at</span>
|
||||||
<select class="worker-inline-select wd-select" bind:value={workingDirectoryId} aria-label="Workdir">
|
<select class="worker-inline-select wd-select" bind:value={workingDirectoryId} aria-label="Workdir">
|
||||||
<option value="">Select workdir</option>
|
{#if selectedRuntimeAllowsNoWorkdir}
|
||||||
|
<option value="">No workdir · embedded conversation only</option>
|
||||||
|
{:else}
|
||||||
|
<option value="" disabled>Select workdir</option>
|
||||||
|
{/if}
|
||||||
{#each options?.working_directories ?? [] as directory}
|
{#each options?.working_directories ?? [] as directory}
|
||||||
<option value={directory.working_directory_id} disabled={directory.status !== 'active'}>
|
<option value={directory.working_directory_id} disabled={directory.status !== 'active'}>
|
||||||
{directory.repository_id} · {directory.requested_selector ?? 'HEAD'}
|
{directory.repository_id} · {directory.requested_selector ?? 'HEAD'}
|
||||||
|
|
@ -259,6 +270,12 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if !selectedRuntimeAllowsNoWorkdir && !workingDirectoryId && !isNewWorkingDirectorySelected}
|
||||||
|
<p class="worker-workdir-note">This Runtime requires a selected workdir before starting a Worker.</p>
|
||||||
|
{:else if selectedRuntimeAllowsNoWorkdir && !workingDirectoryId}
|
||||||
|
<p class="worker-workdir-note">No filesystem tools or Bash will be available without a workdir.</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if isNewWorkingDirectorySelected}
|
{#if isNewWorkingDirectorySelected}
|
||||||
<div class="new-working-directory-panel">
|
<div class="new-working-directory-panel">
|
||||||
<h3>New workdir</h3>
|
<h3>New workdir</h3>
|
||||||
|
|
@ -286,10 +303,12 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<label class="relative-cwd-field">
|
{#if hasSelectedExistingWorkdir || isNewWorkingDirectorySelected}
|
||||||
<span>Relative cwd inside workdir</span>
|
<label class="relative-cwd-field">
|
||||||
<input bind:value={relativeCwd} autocomplete="off" placeholder="Optional path inside workdir" />
|
<span>Relative cwd inside workdir</span>
|
||||||
</label>
|
<input bind:value={relativeCwd} autocomplete="off" placeholder="Optional path inside workdir" />
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="worker-form-section" aria-labelledby="worker-details-heading">
|
<section class="worker-form-section" aria-labelledby="worker-details-heading">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user