diff --git a/.yoi/tickets/00001KWZ5KERY/item.md b/.yoi/tickets/00001KWZ5KERY/item.md index d60d5ea6..19357d10 100644 --- a/.yoi/tickets/00001KWZ5KERY/item.md +++ b/.yoi/tickets/00001KWZ5KERY/item.md @@ -1,8 +1,8 @@ --- title: 'Migrate Profiles to Decodal ProfileSourceArchive for Runtime launch' -state: 'inprogress' +state: 'closed' created_at: '2026-07-07T20:51:35Z' -updated_at: '2026-07-08T09:59:31Z' +updated_at: '2026-07-08T10:35:38Z' assignee: null queued_by: 'workspace-panel' queued_at: '2026-07-08T09:11:22Z' diff --git a/.yoi/tickets/00001KWZ5KERY/resolution.md b/.yoi/tickets/00001KWZ5KERY/resolution.md new file mode 100644 index 00000000..e6c846bf --- /dev/null +++ b/.yoi/tickets/00001KWZ5KERY/resolution.md @@ -0,0 +1,36 @@ +完了。 + +実装内容: +- Decodal dependency/schema を導入し、builtin role Profiles を Decodal sources として追加した。 +- `ProfileSourceArchive` tar format と `manifest.json` schema を実装した。 +- Backend が profile/role selector から archive を構築し、import closure / source digest / archive digest / size/count/depth/path safety を検証するようにした。 +- Runtime が `ProfileSourceArchiveRef` / config bundle から archive を prefetch / verify / cache / reuse できるようにした。 +- Runtime `ArchiveSourceLoader` は archive-contained Decodal sources のみを解決し、undeclared import / filesystem fallback を拒否するようにした。 +- Worker creation の通常 Browser/Backend path は archive-resolved Decodal config を使い、Runtime-local filesystem profile discovery を使わないようにした。 +- Worker metadata/config bundle summary に archive id / source graph summary を残し、Browser-facing response には archive content / digest / store path / raw path を出さないようにした。 +- Lua filesystem Profile path は compatibility/debug fallback として隔離した。 +- Unknown Builtin / Named selectors と missing entrypoint は default fallback せず typed diagnostics で reject するようにした。 +- Builtin `.dcdl` は real Backend-built `ProfileSourceArchive` + Decodal + `ProfileConfig` path で resolve できるように修正した。 +- Focused worker-runtime / workspace-server tests を追加した。 + +主な commit / merge: +- implementation: `a823d414 feat: add decodal profile archives` +- review fix: `a2833dad fix: reject unknown profile archive selectors` +- merge into orchestration: `0334c572 merge: decodal profile archives` + +Review: +- 初回 external review は request_changes。 +- follow-up external review は approve。merge-blocking issue なし。 + +Final validation in Orchestrator worktree: +- `git diff --check`: pass +- `cargo test -p worker-runtime --features ws-server,fs-store`: pass(41 lib tests + 5 main tests + doc tests) +- `cargo test -p yoi-workspace-server`: pass(69 lib tests + 2 main tests) +- `cargo check -p yoi`: pass +- `cd web/workspace && deno task check`: pass(0 errors / 0 warnings) +- `cd web/workspace && deno task test`: pass(17 tests) +- `yoi ticket doctor`: ok +- `nix build .#yoi --no-link`: pass + +補足: +- implementation branch merge 時に Ticket record conflict が発生したため、Orchestrator 側の Ticket item/thread を保持して解決した。product/code/archive changes は merge 済み。 \ No newline at end of file diff --git a/.yoi/tickets/00001KWZ5KERY/thread.md b/.yoi/tickets/00001KWZ5KERY/thread.md index 37d4415d..af1836e9 100644 --- a/.yoi/tickets/00001KWZ5KERY/thread.md +++ b/.yoi/tickets/00001KWZ5KERY/thread.md @@ -195,3 +195,187 @@ Implementation progress report: - External review will be requested via a read-only sibling Reviewer Pod. `StopPod` は使わない。 --- + + + +## Review: request changes + +External review result: request_changes + +Blockers: + +1. Builtin Decodal profiles が現在の schema で実際には解決できず、normal Browser/Backend launch が archive path で失敗する。 + - Evidence: + - builtin Decodal files に `tool_enabled` が含まれる(例: `resources/profiles/default.dcdl`, `coder.dcdl`, `reviewer.dcdl`)。 + - archive resolution は Decodal を JSON に materialize してすぐ `ProfileConfig` に deserialize する。 + - `ProfileConfig` は `#[serde(deny_unknown_fields)]` で、`feature`, `permissions` などはあるが `tool_enabled` field がない。 + - Impact: + - acceptance criterion「builtin role Profiles が Decodal source として利用できる」を満たしていない。 + - Backend-built archives が normal workspace/Companion launch に使われるなら、unknown-field error で Worker creation 前に失敗する。 + - Required fix: + - Decodal profile schema で既存 typed fields を使って tool policy を表現するか、`tool_enabled` 相当を typed mapping/field として受け入れて Worker manifest/config に変換する。 + - embedded builtin `.dcdl` それぞれを real `ProfileSourceArchive` + Decodal + `ProfileConfig` path で resolve する focused test を追加する。 + +2. Missing/unknown profile selectors が typed diagnostic にならず、silent default fallback する。 + - Evidence: + - Runtime archive resolution が requested selector 欠落時に `"default"` へ fallback している。 + - Backend archive construction が selected label を `embedded_profile_path(profile)` に入れ、unknown `Builtin` / `Named` profile を `profiles/default.dcdl` に map している。 + - Impact: + - missing profile / ambiguous selector が typed diagnostic になるという acceptance criterion に違反。 + - invalid/custom profile request が default profile Worker として成功し得るため、Browser/Backend launch の correctness / authority boundary 問題。 + - Required fix: + - unknown profile selector は archive construction 前または archive resolution 中に typed diagnostic で拒否する。 + - explicit RuntimeDefault/default 以外の default fallback を削除する。 + - unknown `Named` / unknown `Builtin` selector の tests を追加する。 + +Validation / inspection performed: +- Ticket records と IntentPacket / acceptance criteria を確認。 +- implementation worktree 内で implementation commit/diff targets を read-only inspection。 +- archive format/verification, config bundle storage, Runtime launch, Backend archive construction, Browser-facing summaries, builtin Decodal profiles を確認。 +- broad build/test matrix は Orchestrator-reported validation pass を参照し、Reviewer は変更を行っていない。 + +Non-blocking follow-ups: +- blockers 修正後、embedded Decodal profiles を使う actual Browser/Backend default launch bundle path の integration-style unit test を追加するとよい。 +- supported profile registry boundary をより明確にするとよい。現状 Backend archive construction は embedded builtins hardcode で、Ticket は workspace/project/builtin profile registry discovery を記述している。 + +--- + + + +## Implementation report + +Review-fix implementation report: + +- Coder Pod completed requested changes and committed follow-up `a2833dad fix: reject unknown profile archive selectors` on branch `work/00001KWZ5KERY-decodal-profile-archive`. +- Worktree status was clean after commit. +- Fix summary: + - Embedded Decodal profiles no longer use unsupported `tool_enabled`; they now use existing typed `ProfileConfig` fields such as `feature` plus existing model/session/engine/memory/web fields as applicable。 + - Archive selector fallback to `default` was removed; missing selector now returns `ProfileArchiveError::MissingEntrypoint`。 + - Backend embedded archive construction rejects unknown `Builtin` and unknown `Named` selectors instead of mapping them to default。 + - Added focused tests proving every embedded builtin `.dcdl` resolves through real Backend-built `ProfileSourceArchive` + Decodal + `ProfileConfig` path。 + - Added tests for unknown Builtin/Named selector rejection and no default fallback for missing selectors。 +- Orchestrator reran validation in the implementation worktree: + - `git diff --check`: pass + - `cargo test -p worker-runtime --features ws-server,fs-store`: pass(41 lib tests + 5 main tests + doc tests) + - `cargo test -p yoi-workspace-server`: pass(69 lib tests + 2 main tests) + - `cargo check -p yoi`: pass + - `cd web/workspace && deno task check`: pass(0 errors / 0 warnings) + - `cd web/workspace && deno task test`: pass(17 tests) + - `yoi ticket doctor`: ok + - `nix build .#yoi --no-link`: pass +- Requesting follow-up external review against `a823d414..a2833dad` and full combined implementation。 + +--- + + + +## Review: approve + +External follow-up review result: approve + +Result: approve + +Review evidence: +- Prior blocker 1 is fixed: + - Builtin Decodal profiles now resolve through `default_embedded_config_bundle(...) -> ProfileSourceArchive -> verify() -> resolve_profile(...) -> ProfileConfig` rather than bypassing archive resolution。 + - `resources/profiles/*.dcdl` no longer use unsupported `tool_enabled`; they use supported `feature = { ... }` fields and are covered by the added embedded-profile resolution test。 +- Prior blocker 2 is fixed: + - `VerifiedProfileSourceArchive::resolve_profile` no longer falls back to `default` for missing selectors。 + - Unknown `Builtin` and `Named` selectors are rejected before bundle construction in `embedded_profile_path(...)`, with typed runtime diagnostics from embedded/remote launch paths。 + - Explicit runtime default remains valid via default/builtin-default archive entrypoints。 +- Browser/Backend launch path: + - Browser only selects Backend-published profile candidates。 + - Workspace Backend builds/syncs a config bundle containing the archive before `CreateWorkerRequest`。 + - Runtime `create_worker` passes the stored config bundle into the execution backend; `ProfileRuntimeWorkerFactory` only uses the Lua/filesystem fallback when no config bundle is present, so ordinary Browser/Backend launch does not read Runtime-local profile discovery。 +- Archive invariants: + - Archive build/verify enforces relative safe tar paths, source counts/sizes/total bytes, source digests, archive digest, supported source kind, and declared import-map lookup。 + - Undeclared imports are rejected by `ArchiveSourceLoader`。 +- Browser-facing exposure: + - Web launch types expose only profile candidates。 + - Config bundle summaries expose source graph summary rather than archive content or archive digest; worker summaries retain only profile selector/config-bundle ref metadata and do not expose runtime store paths/endpoints/secrets。 +- Compatibility: + - Lua/filesystem fallback remains isolated to direct/debug paths without a config bundle。 + - Dependency/package changes look sound, and the provided validation set includes Rust tests/checks, Deno checks/tests, ticket doctor, and `nix build .#yoi --no-link`。 + +No merge blockers found. + +--- + + + +## State changed + +Merge/validation acceptance: + +- Follow-up external review approved the implementation after blocker fix。 +- Implementation branch `work/00001KWZ5KERY-decodal-profile-archive` was merged into the orchestration branch with merge commit `0334c572 merge: decodal profile archives`。 +- Ticket-record conflicts during merge were resolved by preserving Orchestrator-side Ticket thread/item records; product/code/archive changes were merged。 +- Final validation in Orchestrator worktree passed: + - `git diff --check`: pass + - `cargo test -p worker-runtime --features ws-server,fs-store`: pass(41 lib tests + 5 main tests + doc tests) + - `cargo test -p yoi-workspace-server`: pass(69 lib tests + 2 main tests) + - `cargo check -p yoi`: pass + - `cd web/workspace && deno task check`: pass(0 errors / 0 warnings) + - `cd web/workspace && deno task test`: pass(17 tests) + - `yoi ticket doctor`: ok + - `nix build .#yoi --no-link`: pass + +Implementation commits: +- `a823d414 feat: add decodal profile archives` +- `a2833dad fix: reject unknown profile archive selectors` + +No merge-blocking issues remain. + +--- + + + +## State changed + +Ticket を closed にしました。 + + +--- + + + +## 完了 + +完了。 + +実装内容: +- Decodal dependency/schema を導入し、builtin role Profiles を Decodal sources として追加した。 +- `ProfileSourceArchive` tar format と `manifest.json` schema を実装した。 +- Backend が profile/role selector から archive を構築し、import closure / source digest / archive digest / size/count/depth/path safety を検証するようにした。 +- Runtime が `ProfileSourceArchiveRef` / config bundle から archive を prefetch / verify / cache / reuse できるようにした。 +- Runtime `ArchiveSourceLoader` は archive-contained Decodal sources のみを解決し、undeclared import / filesystem fallback を拒否するようにした。 +- Worker creation の通常 Browser/Backend path は archive-resolved Decodal config を使い、Runtime-local filesystem profile discovery を使わないようにした。 +- Worker metadata/config bundle summary に archive id / source graph summary を残し、Browser-facing response には archive content / digest / store path / raw path を出さないようにした。 +- Lua filesystem Profile path は compatibility/debug fallback として隔離した。 +- Unknown Builtin / Named selectors と missing entrypoint は default fallback せず typed diagnostics で reject するようにした。 +- Builtin `.dcdl` は real Backend-built `ProfileSourceArchive` + Decodal + `ProfileConfig` path で resolve できるように修正した。 +- Focused worker-runtime / workspace-server tests を追加した。 + +主な commit / merge: +- implementation: `a823d414 feat: add decodal profile archives` +- review fix: `a2833dad fix: reject unknown profile archive selectors` +- merge into orchestration: `0334c572 merge: decodal profile archives` + +Review: +- 初回 external review は request_changes。 +- follow-up external review は approve。merge-blocking issue なし。 + +Final validation in Orchestrator worktree: +- `git diff --check`: pass +- `cargo test -p worker-runtime --features ws-server,fs-store`: pass(41 lib tests + 5 main tests + doc tests) +- `cargo test -p yoi-workspace-server`: pass(69 lib tests + 2 main tests) +- `cargo check -p yoi`: pass +- `cd web/workspace && deno task check`: pass(0 errors / 0 warnings) +- `cd web/workspace && deno task test`: pass(17 tests) +- `yoi ticket doctor`: ok +- `nix build .#yoi --no-link`: pass + +補足: +- implementation branch merge 時に Ticket record conflict が発生したため、Orchestrator 側の Ticket item/thread を保持して解決した。product/code/archive changes は merge 済み。 + +--- diff --git a/.yoi/tickets/00001KX0G06VA/artifacts/orchestration-plan.jsonl b/.yoi/tickets/00001KX0G06VA/artifacts/orchestration-plan.jsonl new file mode 100644 index 00000000..fd55ad9a --- /dev/null +++ b/.yoi/tickets/00001KX0G06VA/artifacts/orchestration-plan.jsonl @@ -0,0 +1,3 @@ +{"id":"orch-plan-20260708-100506-1","ticket_id":"00001KX0G06VA","kind":"before","related_ticket":"00001KWZ5KERY","note":"Ticket body defines this resource fetch REST API as the prerequisite for `00001KWZ5KERY` Decodal ProfileSourceArchive prefetch/verify. Current workspace already has `00001KWZ5KERY` inprogress on branch `work/00001KWZ5KERY-decodal-profile-archive`, with overlapping worker-runtime/workspace-server surfaces and external review pending. Do not parallelize/start this Ticket until the active Decodal branch outcome is known and an integration order is chosen.","author":"orchestrator","at":"2026-07-08T10:05:06Z"} +{"id":"orch-plan-20260708-100516-2","ticket_id":"00001KX0G06VA","kind":"waiting_capacity_note","note":"Queue review found this Ticket otherwise implementation-ready, but it is intentionally left queued because `00001KWZ5KERY` is already inprogress/review on the same worker-runtime/workspace-server ProfileSourceArchive surfaces while this Ticket text states it should precede that work. Starting now would create a high-conflict branch and could invalidate the active Decodal review. Re-route/start after the active Decodal branch is approved/blocked or after an explicit integration-order decision.","author":"orchestrator","at":"2026-07-08T10:05:16Z"} +{"id":"orch-plan-20260708-103903-3","ticket_id":"00001KX0G06VA","kind":"accepted_plan","accepted_plan":{"summary":"Decodal ProfileSourceArchive work `00001KWZ5KERY` は完了・merge・closed 済みになったため、前回の waiting reason は解除された。`00001KX0G06VA` を順当に開始し、resource fetch REST API を Decodal implementation 後続として実装する。","branch":"work/00001KX0G06VA-resource-fetch-api","worktree":"/home/hare/Projects/yoi/.worktree/00001KX0G06VA-resource-fetch-api","role_plan":"単一 sibling Coder Pod に implementation worktree を委譲し、Runtime-to-Backend resource handle / broker / direct+HTTP fetch contract / Runtime client-cache / profile_source_archive v0 integration / authority-boundary tests を実装する。完了後、別 sibling Reviewer Pod で Ticket IntentPacket / Decodal integration / redaction・authority invariants に照らして read-only review する。"},"author":"orchestrator","at":"2026-07-08T10:39:03Z"} diff --git a/.yoi/tickets/00001KX0G06VA/item.md b/.yoi/tickets/00001KX0G06VA/item.md index ac67d2e1..73c61fbe 100644 --- a/.yoi/tickets/00001KX0G06VA/item.md +++ b/.yoi/tickets/00001KX0G06VA/item.md @@ -1,8 +1,8 @@ --- title: 'Add Runtime-to-Backend resource fetch REST API' -state: 'queued' +state: 'inprogress' created_at: '2026-07-08T09:12:33Z' -updated_at: '2026-07-08T10:04:10Z' +updated_at: '2026-07-08T10:40:25Z' assignee: null queued_by: 'workspace-panel' queued_at: '2026-07-08T10:04:10Z' diff --git a/.yoi/tickets/00001KX0G06VA/thread.md b/.yoi/tickets/00001KX0G06VA/thread.md index 15fb0518..6df722b7 100644 --- a/.yoi/tickets/00001KX0G06VA/thread.md +++ b/.yoi/tickets/00001KX0G06VA/thread.md @@ -30,4 +30,149 @@ Marked ready by `yoi ticket state`. Ticket を `workspace-panel` が queued にしました。 +--- + + + +## Decision + +Routing decision: blocked_by_dependency_or_missing_authority + +Reason: +- Ticket 自体は Runtime-to-Backend resource fetch REST API として実装 intent / acceptance criteria が見えるが、Ticket body は `00001KWZ5KERY` Decodal ProfileSourceArchive work の前段として扱う順序を明記している。 +- 現在 `00001KWZ5KERY` はすでに `inprogress` で、implementation branch `work/00001KWZ5KERY-decodal-profile-archive` があり、外部 review 待ち。 +- 両 Ticket は worker-runtime / workspace-server / ProfileSourceArchive prefetch/verify の同一 surface に触れるため、今この Ticket を別 branch で開始すると高確率で conflict し、active Decodal review の前提を壊す。 +- したがってこの routing pass では `queued -> inprogress` を記録せず、worktree 作成 / Pod spawn などの implementation side effect は行わない。 + +Evidence checked: +- Ticket body / thread / artifacts。 +- `TicketRelationQuery(00001KX0G06VA)`: typed relation 0 件。 +- `TicketOrchestrationPlanQuery(00001KX0G06VA)`: prior record 0 件だったため、今回 `before 00001KWZ5KERY` と waiting-capacity note を記録。 +- `TicketList`: queued はこの Ticket 1件、inprogress は `00001KWZ5KERY` 1件。 +- Orchestrator worktree git status: clean on `orchestration`。 +- `00001KWZ5KERY` implementation branch exists and is under review。 + +Next action: +- `00001KWZ5KERY` の review 結果を待つ。 +- review が request_changes で resource-fetch API prerequisite が必要と確認された場合、または Decodal branch をどう扱うかの integration-order decision が明確になった後、この Ticket を再 routing して start する。 +- Decodal branch が approve された場合も、この Ticket を後続で必要とするか、Decodal implementation を resource-fetch API に合わせて follow-up refactor するかを明示的に判断してから開始する。 + +Escalate if: +- active Decodal branch を中断/rebase/drop して、この Ticket を先に実装する方針に切り替える必要がある場合。 +- resource-fetch API の public/auth/capability model が Decodal Ticket の recorded invariants を変える必要がある場合。 + +--- + + + +## Decision + +Routing decision: implementation_ready + +Reason: +- 前回この Ticket を queued のまま保持した理由は、`00001KWZ5KERY` が inprogress/review 中で、同一 worker-runtime / workspace-server / ProfileSourceArchive prefetch surface に触れるためだった。 +- 現在 `00001KWZ5KERY` は closed になり、implementation branch も orchestration branch に merge 済みで、previous blocker は解消している。 +- この Ticket は Runtime -> Backend resource fetch REST API / resource handle / broker / Runtime client/cache / authority-boundary tests の intent と acceptance criteria が具体化されている。 +- typed relation blocker は 0 件。既存 OrchestrationPlan の `before 00001KWZ5KERY` / waiting note は過去の並列停止理由として確認したが、Decodal 側が完了済みのため現在の acceptance blocker ではない。 +- queued notification と今回の user follow-up「ないなら順当に消化して」により、human authorized routing/start の条件を満たす。 + +Evidence checked: +- Ticket body / thread / artifacts。 +- `TicketRelationQuery(00001KX0G06VA)`: 0 件。 +- `TicketOrchestrationPlanQuery(00001KX0G06VA)`: prior `before 00001KWZ5KERY` と waiting note を確認。 +- Orchestrator worktree git status: clean on `orchestration`。 +- queued Ticket 一覧: この Ticket 1件のみ。inprogress は 0 件。 +- `00001KWZ5KERY` は close 済みで、merge commit `0334c572` と close commit `ffc16dea` が orchestration branch にある。 + +IntentPacket: + +Intent: +- Runtime が Workspace filesystem を直接読まずに Backend-owned resource を取得できる Runtime-to-Backend resource fetch REST API を追加する。 +- Backend-issued typed resource handle と Backend resource broker を導入し、v0 resource kind `profile_source_archive` を fetch / verify / cache できるようにする。 +- 既存 Decodal ProfileSourceArchive implementation を、この resource fetch boundary に接続または将来接続しやすい形に整理する。 + +Binding decisions / invariants: +- Browser-facing API に Backend internal endpoint、resource credential、resource handle、raw path、Runtime endpoint/token/socket/session path を出さない。 +- Runtime は Backend-issued resource handle なしに Workspace-derived resource を取得できない。 +- handle には resource kind、workspace/scope id、resource id/digest、operation、expiry/nonce/revision/generation、redaction/max-bytes/content-type、audit correlation id を含める。 +- handle には raw Backend URL、host absolute path、secret value、Browser request 由来 raw filesystem scope を含めない。 +- v0 resource kind は `profile_source_archive`。Memory/Ticket/Objective tool backend 本体は非目標。 +- Runtime-local filesystem discovery を増やさず、Backend authority / redaction / audit / capability 境界を保つ。 +- WebSocket/persistent bidirectional channel は非目標。v0 は request/response REST/direct call。 + +Requirements / acceptance criteria: +- typed request/response schema の Runtime -> Backend resource fetch API がある。 +- embedded Runtime direct call path と remote Runtime HTTP path が同じ resource contract を使う。 +- Backend が resource handle を発行/検証し、`profile_source_archive` bytes を返せる。 +- Runtime が resource handle から archive を fetch / digest verify / cache できる。 +- expired / unauthorized / workspace-runtime-worker mismatch / missing resource / digest mismatch / oversized response は typed diagnostic。 +- Browser-facing response redaction が tests で確認される。 + +Implementation latitude: +- exact endpoint/module/type names、cache layout、handle signing/nonce representation、audit record shape、profile archive integration depth は既存 Runtime/Workspace Server style に合わせてよい。 +- 既存 `ConfigBundle` / `ProfileSourceArchive` machinery は維持しつつ、resource handle fetch boundary を追加する形でよい。 +- v0 で remote Runtime full auth hardening が不足する場合は typed diagnostic と focused tests で境界を示す。 + +Escalate if: +- secret synchronization、Memory/Ticket/Objective tool implementations、persistent channel、Plugin package manager、multi-tenant auth、public Browser handle exposure、or Decodal archive format redesign が必要になる場合。 + +Validation: +- `cargo test -p worker-runtime --features ws-server,fs-store` +- `cargo test -p yoi-workspace-server` +- `cargo check -p yoi` +- `cd web/workspace && deno task check && deno task test` +- `git diff --check` +- `yoi ticket doctor` +- `nix build .#yoi --no-link` + +Current code map: +- Decodal/Profile archive current implementation: `crates/worker-runtime/src/profile_archive.rs`, `config_bundle.rs`, `worker_backend.rs`, `runtime.rs`, `http_server.rs`, `catalog.rs`。 +- Backend/Runtime bridge: `crates/workspace-server/src/hosts.rs`, `server.rs`, `companion.rs`。 +- Protocol/runtime request shapes: `crates/protocol/src/lib.rs`, `crates/worker-runtime` request/response structs。 + +Critical risks / reviewer focus: +- resource handles must not become Browser-visible bearer authorities。 +- endpoint/token/raw path/internal store/runtime details must stay out of Browser-facing surfaces。 +- direct and remote Runtime paths must share contract rather than diverging ad hoc。 +- digest/max-bytes/content-type validation must fail closed。 +- profile archive fetch integration must not reintroduce Runtime workspace filesystem discovery。 + +--- + + + +## State changed + +Queued acceptance recorded after user follow-up 「06VAが進まない理由は何?ないなら順当に消化して」。 + +Checked context: +- Ticket body / thread / artifacts。 +- `TicketRelationQuery(00001KX0G06VA)`: blocking relation 0 件。 +- `TicketOrchestrationPlanQuery(00001KX0G06VA)`: prior waiting note は `00001KWZ5KERY` inprogress/review によるもの。`00001KWZ5KERY` は now closed / merged。 +- Orchestrator worktree git status: clean on `orchestration`。 +- queued Ticket はこの Ticket 1件のみ。inprogress は 0 件。 + +Acceptance basis: +- previous waiting reason は解消済み。 +- concrete missing decision / information は残っていない。 +- Ticket の scope は Runtime-to-Backend resource fetch REST API v0 と `profile_source_archive` kind に閉じており、Memory/Ticket/Objective tool backend 本体、persistent channel、secret sync、plugin package manager は非目標として明確。 +- side effect はこの `queued -> inprogress` acceptance 後に、worktree-workflow に沿って implementation worktree を作成してから開始する。 + +--- + + + +## Implementation report + +Implementation routing update: + +- Worktree-workflow に従い implementation worktree を作成した。 + - Worktree: `/home/hare/Projects/yoi/.worktree/00001KX0G06VA-resource-fetch-api` + - Branch: `work/00001KX0G06VA-resource-fetch-api` +- Worktree には tracked `.yoi` project records が見えている。 +- `.yoi/memory` / `.yoi/runtime` / `.yoi/log` / `.yoi/logs` / `.yoi/local` / `.yoi/*.lock` / `.yoi/*secret*` は作成直後に存在しないことを確認した。 +- Sibling Coder Pod `yoi-coder-00001KX0G06VA-resource-fetch` を起動した。 +- Scope policy: runtime workspace root は read-only、implementation worktree は write 可。Coder task では root/original workspace での read/write/validation/git operation を禁止し、この implementation worktree だけを作業対象に指定した。 +- 完了後は diff / validation evidence を Orchestrator が確認し、別 sibling Reviewer Pod に read-only review を依頼する。`StopPod` は既知の不具合があるため使用しない。 + --- diff --git a/Cargo.lock b/Cargo.lock index 30b91e9f..e23dd6c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -907,6 +907,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +[[package]] +name = "decodal" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4291c87ce887fafc0acf9f40f4bc17e111457e9d62f1b1530113be6b7a7f1a21" + [[package]] name = "deltae" version = "0.3.2" @@ -1072,7 +1078,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2543,7 +2549,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3469,7 +3475,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3526,7 +3532,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4114,6 +4120,17 @@ dependencies = [ "libc", ] +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.13.5" @@ -4136,7 +4153,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5472,7 +5489,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5914,6 +5931,7 @@ version = "0.1.0" dependencies = [ "async-trait", "axum", + "decodal", "futures", "llm-engine", "manifest", @@ -5922,6 +5940,7 @@ dependencies = [ "serde_json", "session-store", "sha2 0.11.0", + "tar", "tempfile", "thiserror 2.0.18", "tokio", @@ -5952,6 +5971,16 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.4", +] + [[package]] name = "xml5ever" version = "0.17.0" diff --git a/Cargo.toml b/Cargo.toml index da9ab790..3b5cf63c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,6 +86,7 @@ yoi-workspace-server = { path = "crates/workspace-server" } # need `default-features = false`, which workspace inheritance cannot override. async-trait = "0.1" axum = "0.8" +decodal = "0.1.1" fs4 = "0.13" futures = "0.3" libc = "0.2" @@ -93,6 +94,7 @@ schemars = "1.2" serde = "1.0" serde_json = "1.0" serde_yaml = "0.9.34" +tar = "0.4" rusqlite = { version = "0.37", features = ["bundled"] } sha2 = "0.11" tempfile = "3.27" diff --git a/crates/manifest/src/lib.rs b/crates/manifest/src/lib.rs index c32644ee..cad1f620 100644 --- a/crates/manifest/src/lib.rs +++ b/crates/manifest/src/lib.rs @@ -19,7 +19,7 @@ pub use profile::{ ProfileDiscovery, ProfileError, ProfileManifestSnapshot, ProfileMetadata, ProfileRegistry, ProfileRegistryEntry, ProfileRegistrySource, ProfileResolveOptions, ProfileResolver, ProfileSelector, ProfileSource, ResolvedProfile, WorkspaceOverrideSnapshot, - resolve_profile_artifact, + resolve_profile_artifact, resolve_profile_artifact_value, }; pub use protocol::{Permission, ScopeRule}; pub use scope::{DelegationScope, Scope, ScopeError, SharedScope}; diff --git a/crates/manifest/src/profile.rs b/crates/manifest/src/profile.rs index 37fd7859..88ed5d6a 100644 --- a/crates/manifest/src/profile.rs +++ b/crates/manifest/src/profile.rs @@ -189,6 +189,10 @@ pub enum ProfileSource { #[serde(default, skip_serializing_if = "Option::is_none")] provenance: Option, }, + Archive { + archive_id: String, + source: String, + }, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -1405,6 +1409,10 @@ fn source_name(source: &ProfileSource) -> Option { .and_then(|s| s.to_str()) .map(str::to_string), ProfileSource::Registry { name, .. } => Some(name.clone()), + ProfileSource::Archive { source, .. } => Path::new(source) + .file_stem() + .and_then(|s| s.to_str()) + .map(str::to_string), } } fn canonicalize_existing_dir(path: &Path) -> Result { @@ -1438,12 +1446,21 @@ pub fn resolve_profile_artifact( source: ProfileSource, base_dir: &Path, raw_artifact: serde_json::Value, +) -> Result { + resolve_profile_artifact_value(raw_artifact, source, base_dir, "artifact-worker") +} + +pub fn resolve_profile_artifact_value( + raw_artifact: serde_json::Value, + source: ProfileSource, + base_dir: &Path, + worker_name: &str, ) -> Result { resolve_lua_profile_value( source, base_dir, base_dir, - ProfileResolveOptions::with_worker_name("artifact-worker"), + ProfileResolveOptions::with_worker_name(worker_name), raw_artifact.clone(), raw_artifact, None, diff --git a/crates/worker-runtime/Cargo.toml b/crates/worker-runtime/Cargo.toml index 8ceab741..a010ed23 100644 --- a/crates/worker-runtime/Cargo.toml +++ b/crates/worker-runtime/Cargo.toml @@ -13,20 +13,22 @@ required-features = ["ws-server", "fs-store"] [features] default = [] -fs-store = ["dep:serde_json"] -http-server = ["dep:axum", "dep:serde_json", "dep:tower"] +fs-store = [] +http-server = ["dep:axum", "dep:tower"] ws-server = ["http-server", "axum/ws", "dep:futures", "tokio/sync"] [dependencies] async-trait.workspace = true axum = { workspace = true, optional = true } futures = { workspace = true, optional = true } +decodal.workspace = true manifest.workspace = true protocol.workspace = true serde = { workspace = true, features = ["derive"] } session-store.workspace = true sha2.workspace = true -serde_json = { workspace = true, optional = true } +serde_json.workspace = true +tar.workspace = true thiserror = { workspace = true } tokio = { workspace = true, features = ["net", "rt", "sync", "time"] } tower = { workspace = true, features = ["util"], optional = true } diff --git a/crates/worker-runtime/src/config_bundle.rs b/crates/worker-runtime/src/config_bundle.rs index 18ea771a..beafad32 100644 --- a/crates/worker-runtime/src/config_bundle.rs +++ b/crates/worker-runtime/src/config_bundle.rs @@ -1,5 +1,9 @@ use crate::catalog::{ConfigBundleRef, ProfileSelector}; use crate::error::RuntimeError; +use crate::profile_archive::{ + ProfileArchiveError, ProfileSourceArchive, ProfileSourceArchiveRef, + VerifiedProfileSourceArchive, +}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -18,6 +22,8 @@ pub struct ConfigBundle { pub profiles: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub declarations: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub profile_source_archive: Option, } impl ConfigBundle { @@ -60,6 +66,16 @@ impl ConfigBundle { )); } + if let Some(archive) = &self.profile_source_archive { + lines.push(format!( + "profile_archive\0{}\0{}\0{}", + archive.reference.id, archive.reference.digest, archive.reference.size_bytes + )); + for (selector, path) in &archive.reference.source_graph.entrypoints { + lines.push(format!("profile_archive_entrypoint\0{selector}\0{path}")); + } + } + lines.sort(); let mut hasher = Sha256::new(); for line in lines { @@ -86,6 +102,10 @@ impl ConfigBundle { provenance: self.metadata.provenance.clone(), profile_count: self.profiles.len(), declaration_count: self.declarations.len(), + profile_source_archive: self + .profile_source_archive + .as_ref() + .map(|archive| archive.reference.source_graph.clone()), } } @@ -164,6 +184,8 @@ pub struct ConfigBundleSummary { pub provenance: ConfigBundleProvenance, pub profile_count: usize, pub declaration_count: usize, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub profile_source_archive: Option, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -222,6 +244,16 @@ pub(crate) fn validate_config_bundle(bundle: &ConfigBundle) -> Result<(), Runtim } validate_declaration_reference(&bundle.metadata.id, declaration)?; } + + if let Some(archive) = &bundle.profile_source_archive { + validate_profile_source_archive_ref(&archive.reference).map_err(|err| { + RuntimeError::InvalidRequest(format!("invalid profile source archive: {err}")) + })?; + archive.verify().map_err(|err| { + RuntimeError::InvalidRequest(format!("invalid profile source archive: {err}")) + })?; + } + Ok(()) } @@ -232,6 +264,34 @@ pub(crate) fn validate_config_bundle_ref(reference: &ConfigBundleRef) -> Result< Ok(()) } +pub fn verified_profile_source_archive( + bundle: &ConfigBundle, +) -> Result, ProfileArchiveError> { + bundle + .profile_source_archive + .as_ref() + .map(ProfileSourceArchive::verify) + .transpose() +} + +fn validate_profile_source_archive_ref( + reference: &ProfileSourceArchiveRef, +) -> Result<(), ProfileArchiveError> { + if reference.id.trim().is_empty() { + return Err(ProfileArchiveError::MissingEntrypoint( + "archive id".to_string(), + )); + } + if !reference.digest.starts_with("sha256:") { + return Err(ProfileArchiveError::ArchiveDigestMismatch { + id: reference.id.clone(), + expected: "sha256:".to_string(), + actual: reference.digest.clone(), + }); + } + Ok(()) +} + pub(crate) fn validate_profile_selector( selector: ProfileSelector, bundle_id: Option<&str>, @@ -468,6 +528,7 @@ mod tests { name: "credential".to_string(), reference: reference.to_string(), }], + profile_source_archive: None, } .with_computed_digest() } diff --git a/crates/worker-runtime/src/execution.rs b/crates/worker-runtime/src/execution.rs index d31eebc2..04e45226 100644 --- a/crates/worker-runtime/src/execution.rs +++ b/crates/worker-runtime/src/execution.rs @@ -1,4 +1,5 @@ use crate::catalog::{CreateWorkerRequest, WorkingDirectoryStatus}; +use crate::config_bundle::ConfigBundle; use crate::error::RuntimeError; use crate::identity::WorkerRef; use crate::interaction::WorkerInput; @@ -287,6 +288,7 @@ pub struct WorkerExecutionSpawnRequest { pub request: CreateWorkerRequest, pub context: WorkerExecutionContext, pub working_directory: Option, + pub config_bundle: Option, } /// Result of backend Worker spawn/initialization. diff --git a/crates/worker-runtime/src/http_server.rs b/crates/worker-runtime/src/http_server.rs index e63fc7c3..e01dc9b3 100644 --- a/crates/worker-runtime/src/http_server.rs +++ b/crates/worker-runtime/src/http_server.rs @@ -849,6 +849,7 @@ mod tests { label: Some("test".to_string()), }], declarations: Vec::new(), + profile_source_archive: None, } .with_computed_digest() } @@ -1135,6 +1136,7 @@ mod ws_tests { label: Some("ws".to_string()), }], declarations: Vec::new(), + profile_source_archive: None, } .with_computed_digest() } diff --git a/crates/worker-runtime/src/lib.rs b/crates/worker-runtime/src/lib.rs index 686983fb..405aab74 100644 --- a/crates/worker-runtime/src/lib.rs +++ b/crates/worker-runtime/src/lib.rs @@ -19,6 +19,7 @@ pub mod identity; pub mod interaction; pub mod management; pub mod observation; +pub mod profile_archive; mod runtime; pub mod worker_backend; pub mod working_directory; diff --git a/crates/worker-runtime/src/profile_archive.rs b/crates/worker-runtime/src/profile_archive.rs new file mode 100644 index 00000000..cc7a6d09 --- /dev/null +++ b/crates/worker-runtime/src/profile_archive.rs @@ -0,0 +1,554 @@ +use decodal::{Engine, LoadedSource, SourceLoader}; +use manifest::{ProfileSource, WorkerManifest, resolve_profile_artifact_value}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::collections::BTreeMap; +use std::io::{Cursor, Read, Write}; +use std::path::{Component, Path, PathBuf}; +use tar::{Archive, Builder, Header}; + +const MANIFEST_PATH: &str = "manifest.json"; +const MAX_SOURCES: usize = 64; +const MAX_SOURCE_BYTES: u64 = 256 * 1024; +const MAX_TOTAL_BYTES: u64 = 1024 * 1024; +const MAX_PATH_DEPTH: usize = 8; + +#[derive(Debug, thiserror::Error)] +pub enum ProfileArchiveError { + #[error("profile source archive has unsupported schema version {0}")] + UnsupportedSchema(u32), + #[error("profile source archive path must be relative and confined: {0}")] + UnsafePath(String), + #[error("profile source archive contains unsupported entry type at {0}")] + UnsupportedEntry(String), + #[error("profile source archive limit exceeded: {0}")] + LimitExceeded(&'static str), + #[error("profile source archive missing manifest.json")] + MissingManifest, + #[error("profile source archive missing source {0}")] + MissingSource(String), + #[error( + "profile source archive source digest mismatch for {path}: expected {expected}, got {actual}" + )] + SourceDigestMismatch { + path: String, + expected: String, + actual: String, + }, + #[error("profile source archive digest mismatch for {id}: expected {expected}, got {actual}")] + ArchiveDigestMismatch { + id: String, + expected: String, + actual: String, + }, + #[error("profile source archive has unsupported source kind {kind} at {path}")] + UnsupportedSource { path: String, kind: String }, + #[error("profile source archive has no entrypoint for selector {0}")] + MissingEntrypoint(String), + #[error("profile source archive import is not declared in manifest import map: {0}")] + ImportNotDeclared(String), + #[error("failed to read profile source archive: {0}")] + Io(String), + #[error("failed to parse profile source archive manifest: {0}")] + Json(String), + #[error("failed to evaluate Decodal profile source {path}: {message}")] + Decodal { path: String, message: String }, + #[error("failed to resolve archive profile artifact {profile_source}: {message}")] + Profile { + profile_source: String, + message: String, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ProfileSourceArchiveRef { + pub id: String, + pub digest: String, + pub size_bytes: u64, + pub source_graph: ProfileSourceGraphSummary, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ProfileSourceGraphSummary { + pub source_count: usize, + pub total_source_bytes: u64, + pub entrypoints: BTreeMap, + pub import_count: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ProfileSourceArchive { + pub reference: ProfileSourceArchiveRef, + pub content: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ProfileSourceArchiveManifest { + pub schema_version: u32, + pub id: String, + pub entrypoints: BTreeMap, + pub imports: BTreeMap, + pub sources: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ProfileSourceArchiveSource { + pub path: String, + pub kind: String, + pub digest: String, + pub size_bytes: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ProfileSourceArchiveInput { + pub id: String, + pub entrypoints: BTreeMap, + pub imports: BTreeMap, + pub sources: BTreeMap, +} + +impl ProfileSourceArchive { + pub fn build(input: ProfileSourceArchiveInput) -> Result { + if input.sources.len() > MAX_SOURCES { + return Err(ProfileArchiveError::LimitExceeded("source count")); + } + let mut total_source_bytes = 0u64; + let mut source_meta = Vec::new(); + for (path, source) in &input.sources { + validate_archive_path(path)?; + let size = source.as_bytes().len() as u64; + if size > MAX_SOURCE_BYTES { + return Err(ProfileArchiveError::LimitExceeded("source bytes")); + } + total_source_bytes = total_source_bytes + .checked_add(size) + .ok_or(ProfileArchiveError::LimitExceeded("total source bytes"))?; + if total_source_bytes > MAX_TOTAL_BYTES { + return Err(ProfileArchiveError::LimitExceeded("total source bytes")); + } + source_meta.push(ProfileSourceArchiveSource { + path: path.clone(), + kind: "decodal".to_string(), + digest: sha256_hex(source.as_bytes()), + size_bytes: size, + }); + } + for path in input.entrypoints.values().chain(input.imports.values()) { + validate_archive_path(path)?; + if !input.sources.contains_key(path) { + return Err(ProfileArchiveError::MissingSource(path.clone())); + } + } + let manifest = ProfileSourceArchiveManifest { + schema_version: 1, + id: input.id.clone(), + entrypoints: input.entrypoints.clone(), + imports: input.imports.clone(), + sources: source_meta, + }; + let manifest_json = serde_json::to_vec_pretty(&manifest) + .map_err(|err| ProfileArchiveError::Json(err.to_string()))?; + let mut bytes = Vec::new(); + { + let mut builder = Builder::new(&mut bytes); + append_bytes(&mut builder, MANIFEST_PATH, &manifest_json)?; + for (path, source) in &input.sources { + append_bytes(&mut builder, path, source.as_bytes())?; + } + builder + .finish() + .map_err(|err| ProfileArchiveError::Io(err.to_string()))?; + } + let digest = sha256_hex(&bytes); + let reference = ProfileSourceArchiveRef { + id: input.id, + digest, + size_bytes: bytes.len() as u64, + source_graph: ProfileSourceGraphSummary { + source_count: input.sources.len(), + total_source_bytes, + entrypoints: input.entrypoints, + import_count: manifest.imports.len(), + }, + }; + Ok(Self { + reference, + content: bytes, + }) + } + + pub fn verify(&self) -> Result { + let digest = sha256_hex(&self.content); + if digest != self.reference.digest { + return Err(ProfileArchiveError::ArchiveDigestMismatch { + id: self.reference.id.clone(), + expected: self.reference.digest.clone(), + actual: digest, + }); + } + VerifiedProfileSourceArchive::from_bytes(self.reference.clone(), &self.content) + } +} + +#[derive(Debug, Clone)] +pub struct VerifiedProfileSourceArchive { + reference: ProfileSourceArchiveRef, + manifest: ProfileSourceArchiveManifest, + sources: BTreeMap, +} + +impl VerifiedProfileSourceArchive { + pub fn from_bytes( + reference: ProfileSourceArchiveRef, + bytes: &[u8], + ) -> Result { + if bytes.len() as u64 > MAX_TOTAL_BYTES + 64 * 1024 { + return Err(ProfileArchiveError::LimitExceeded("archive bytes")); + } + let mut archive = Archive::new(Cursor::new(bytes)); + let mut manifest: Option = None; + let mut entries = BTreeMap::new(); + for entry in archive + .entries() + .map_err(|err| ProfileArchiveError::Io(err.to_string()))? + { + let mut entry = entry.map_err(|err| ProfileArchiveError::Io(err.to_string()))?; + let path = entry + .path() + .map_err(|err| ProfileArchiveError::Io(err.to_string()))? + .to_string_lossy() + .to_string(); + validate_archive_path(&path)?; + if !entry.header().entry_type().is_file() { + return Err(ProfileArchiveError::UnsupportedEntry(path)); + } + let size = entry.size(); + if size > MAX_SOURCE_BYTES && path != MANIFEST_PATH { + return Err(ProfileArchiveError::LimitExceeded("source bytes")); + } + let mut data = Vec::new(); + entry + .read_to_end(&mut data) + .map_err(|err| ProfileArchiveError::Io(err.to_string()))?; + if path == MANIFEST_PATH { + manifest = Some( + serde_json::from_slice(&data) + .map_err(|err| ProfileArchiveError::Json(err.to_string()))?, + ); + } else { + let source = String::from_utf8(data) + .map_err(|err| ProfileArchiveError::Io(err.to_string()))?; + entries.insert(path, source); + } + if entries.len() > MAX_SOURCES { + return Err(ProfileArchiveError::LimitExceeded("source count")); + } + } + let manifest = manifest.ok_or(ProfileArchiveError::MissingManifest)?; + if manifest.schema_version != 1 { + return Err(ProfileArchiveError::UnsupportedSchema( + manifest.schema_version, + )); + } + if manifest.id != reference.id { + return Err(ProfileArchiveError::ArchiveDigestMismatch { + id: reference.id, + expected: "matching archive id".to_string(), + actual: manifest.id, + }); + } + let mut total = 0u64; + for source in &manifest.sources { + validate_archive_path(&source.path)?; + if source.kind != "decodal" { + return Err(ProfileArchiveError::UnsupportedSource { + path: source.path.clone(), + kind: source.kind.clone(), + }); + } + let content = entries + .get(&source.path) + .ok_or_else(|| ProfileArchiveError::MissingSource(source.path.clone()))?; + let digest = sha256_hex(content.as_bytes()); + if digest != source.digest { + return Err(ProfileArchiveError::SourceDigestMismatch { + path: source.path.clone(), + expected: source.digest.clone(), + actual: digest, + }); + } + let size = content.as_bytes().len() as u64; + if size != source.size_bytes { + return Err(ProfileArchiveError::LimitExceeded( + "source byte metadata mismatch", + )); + } + total += size; + } + if total != reference.source_graph.total_source_bytes + || manifest.sources.len() != reference.source_graph.source_count + { + return Err(ProfileArchiveError::LimitExceeded( + "source graph metadata mismatch", + )); + } + for path in manifest + .entrypoints + .values() + .chain(manifest.imports.values()) + { + validate_archive_path(path)?; + if !entries.contains_key(path) { + return Err(ProfileArchiveError::MissingSource(path.clone())); + } + } + Ok(Self { + reference, + manifest, + sources: entries, + }) + } + + pub fn reference(&self) -> &ProfileSourceArchiveRef { + &self.reference + } + + pub fn resolve_profile( + &self, + selector: &str, + worker_root: &Path, + worker_name: &str, + ) -> Result { + let path = self + .manifest + .entrypoints + .get(selector) + .ok_or_else(|| ProfileArchiveError::MissingEntrypoint(selector.to_string()))?; + let mut engine = Engine::new(ArchiveSourceLoader { archive: self }); + let source = self + .sources + .get(path) + .ok_or_else(|| ProfileArchiveError::MissingSource(path.clone()))?; + let module = engine.add_root_source(path, path, source).map_err(|err| { + ProfileArchiveError::Decodal { + path: path.clone(), + message: format!("{err:?}"), + } + })?; + let value = engine + .eval_module(module) + .map_err(|err| ProfileArchiveError::Decodal { + path: path.clone(), + message: format!("{err:?}"), + })?; + let data = engine + .materialize(&value) + .map_err(|err| ProfileArchiveError::Decodal { + path: path.clone(), + message: format!("{err:?}"), + })?; + let json = decodal_data_to_json(&data); + let resolved = resolve_profile_artifact_value( + json, + ProfileSource::Archive { + archive_id: self.reference.id.clone(), + source: path.clone(), + }, + worker_root, + worker_name, + ) + .map_err(|err| ProfileArchiveError::Profile { + profile_source: path.clone(), + message: err.to_string(), + })?; + Ok(resolved.manifest) + } +} + +pub struct ArchiveSourceLoader<'a> { + archive: &'a VerifiedProfileSourceArchive, +} + +impl<'a> ArchiveSourceLoader<'a> { + pub fn new(archive: &'a VerifiedProfileSourceArchive) -> Self { + Self { archive } + } +} + +impl SourceLoader for ArchiveSourceLoader<'_> { + fn load( + &mut self, + _current_key: Option<&str>, + specifier: &str, + ) -> decodal::Result { + let path = self + .archive + .manifest + .imports + .get(specifier) + .ok_or_else(|| { + decodal::Diagnostic::new( + decodal::DiagnosticKind::Import, + decodal::Span::default(), + format!("archive import not declared: {specifier}"), + ) + })?; + let source = self.archive.sources.get(path).ok_or_else(|| { + decodal::Diagnostic::new( + decodal::DiagnosticKind::Import, + decodal::Span::default(), + format!("archive source missing: {path}"), + ) + })?; + Ok(LoadedSource { + key: path.clone(), + name: path.clone(), + source: source.clone(), + }) + } +} + +fn decodal_data_to_json(data: &decodal::Data) -> serde_json::Value { + match data { + decodal::Data::Bool(value) => serde_json::Value::Bool(*value), + decodal::Data::Int(value) => serde_json::Value::Number(serde_json::Number::from(*value)), + decodal::Data::Float(value) => serde_json::Number::from_f64(*value) + .map(serde_json::Value::Number) + .unwrap_or(serde_json::Value::Null), + decodal::Data::String(value) => serde_json::Value::String(value.clone()), + decodal::Data::Array(values) => { + serde_json::Value::Array(values.iter().map(decodal_data_to_json).collect()) + } + decodal::Data::Object(fields) => serde_json::Value::Object( + fields + .iter() + .map(|field| (field.name.clone(), decodal_data_to_json(&field.value))) + .collect(), + ), + } +} + +fn append_bytes( + builder: &mut Builder, + path: &str, + data: &[u8], +) -> Result<(), ProfileArchiveError> { + validate_archive_path(path)?; + let mut header = Header::new_gnu(); + header.set_size(data.len() as u64); + header.set_mode(0o644); + header.set_cksum(); + builder + .append_data(&mut header, path, Cursor::new(data)) + .map_err(|err| ProfileArchiveError::Io(err.to_string())) +} + +fn validate_archive_path(path: &str) -> Result<(), ProfileArchiveError> { + if path.is_empty() || path == MANIFEST_PATH { + return if path == MANIFEST_PATH { + Ok(()) + } else { + Err(ProfileArchiveError::UnsafePath(path.to_string())) + }; + } + let path_ref = Path::new(path); + if path_ref.is_absolute() { + return Err(ProfileArchiveError::UnsafePath(path.to_string())); + } + let mut depth = 0usize; + for component in path_ref.components() { + match component { + Component::Normal(_) => depth += 1, + _ => return Err(ProfileArchiveError::UnsafePath(path.to_string())), + } + } + if depth == 0 || depth > MAX_PATH_DEPTH { + return Err(ProfileArchiveError::UnsafePath(path.to_string())); + } + let normalized = PathBuf::from(path).to_string_lossy().replace('\\', "/"); + if normalized != path { + return Err(ProfileArchiveError::UnsafePath(path.to_string())); + } + Ok(()) +} + +fn sha256_hex(data: &[u8]) -> String { + let digest = Sha256::digest(data); + let mut out = String::from("sha256:"); + for byte in digest.as_slice() { + use std::fmt::Write as _; + let _ = write!(out, "{byte:02x}"); + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_archive() -> ProfileSourceArchive { + let mut entrypoints = BTreeMap::new(); + entrypoints.insert("default".to_string(), "profiles/default.dcdl".to_string()); + let mut sources = BTreeMap::new(); + sources.insert( + "profiles/default.dcdl".to_string(), + r#"{ + slug = "default"; + description = "Default"; + scope = "workspace_read"; + }"# + .to_string(), + ); + ProfileSourceArchive::build(ProfileSourceArchiveInput { + id: "builtin".to_string(), + entrypoints, + imports: BTreeMap::new(), + sources, + }) + .unwrap() + } + + #[test] + fn build_and_verify_archive() { + let archive = sample_archive(); + let verified = archive.verify().unwrap(); + assert_eq!(verified.reference().source_graph.source_count, 1); + } + + #[test] + fn rejects_digest_mismatch() { + let mut archive = sample_archive(); + archive.reference.digest = "sha256:bad".to_string(); + assert!(archive.verify().is_err()); + } + + #[test] + fn resolves_profile_from_archive_without_filesystem_loader() { + let archive = sample_archive(); + let verified = archive.verify().unwrap(); + let root = tempfile::tempdir().unwrap(); + let manifest = verified + .resolve_profile("default", root.path(), "archive-worker") + .unwrap(); + assert_eq!(manifest.worker.name, "archive-worker"); + } + + #[test] + fn unknown_selector_does_not_fallback_to_default() { + let archive = sample_archive(); + let verified = archive.verify().unwrap(); + let root = tempfile::tempdir().unwrap(); + assert!(matches!( + verified.resolve_profile("missing", root.path(), "archive-worker"), + Err(ProfileArchiveError::MissingEntrypoint(selector)) if selector == "missing" + )); + } + + #[test] + fn archive_loader_rejects_undeclared_import() { + let archive = sample_archive(); + let verified = archive.verify().unwrap(); + let mut loader = ArchiveSourceLoader::new(&verified); + assert!(loader.load(None, "missing").is_err()); + } +} diff --git a/crates/worker-runtime/src/runtime.rs b/crates/worker-runtime/src/runtime.rs index 44ef2309..8665d6f3 100644 --- a/crates/worker-runtime/src/runtime.rs +++ b/crates/worker-runtime/src/runtime.rs @@ -244,6 +244,13 @@ impl Runtime { state.ensure_running()?; validate_create_worker_request(&request)?; state.validate_worker_config_boundary(&request)?; + let config_bundle = state + .config_bundles + .get(&request.config_bundle.id) + .cloned() + .ok_or_else(|| RuntimeError::ConfigBundleMissing { + bundle_id: request.config_bundle.id.clone(), + })?; let backend = state.execution_backend.clone().ok_or_else(|| { RuntimeError::ExecutionBackendUnavailable { message: "worker creation requires an execution backend".to_string(), @@ -289,6 +296,7 @@ impl Runtime { request, context: self.execution_context(worker_ref.clone()), working_directory: None, + config_bundle: Some(config_bundle), }; (backend, worker_ref, spawn_request) }; @@ -1619,6 +1627,7 @@ mod tests { name: "read".to_string(), reference: "capability:read".to_string(), }], + profile_source_archive: None, } .with_computed_digest() } diff --git a/crates/worker-runtime/src/worker_backend.rs b/crates/worker-runtime/src/worker_backend.rs index d6c9d357..a5e62d4e 100644 --- a/crates/worker-runtime/src/worker_backend.rs +++ b/crates/worker-runtime/src/worker_backend.rs @@ -14,6 +14,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, mpsc}; use std::time::Duration; +use crate::config_bundle::verified_profile_source_archive; use crate::execution::{ WorkerExecutionBackend, WorkerExecutionHandle, WorkerExecutionOperation, WorkerExecutionResult, WorkerExecutionRunState, WorkerExecutionSpawnRequest, WorkerExecutionSpawnResult, @@ -171,14 +172,34 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory { .as_ref() .map(|binding| binding.cwd().to_path_buf()) .unwrap_or_else(|| self.cwd.clone()); - // Profile discovery uses the runtime profile base, not the Worker working directory. - // The working directory may be an arbitrary repository checkout that intentionally - // does not carry Yoi profile files. - let (mut manifest, loader) = worker::entrypoint::resolve_runtime_profile_manifest( - profile.as_deref(), - &self.profile_base_dir, - &worker_name, - )?; + let (mut manifest, loader) = if let Some(bundle) = request.config_bundle.as_ref() { + let selector = profile.as_deref().unwrap_or("builtin:default"); + let archive = verified_profile_source_archive(bundle) + .map_err(|err| format!("failed to verify profile source archive: {err}"))? + .ok_or_else(|| { + format!( + "config bundle {} does not contain a ProfileSourceArchive", + bundle.metadata.id + ) + })?; + let manifest = archive + .resolve_profile(selector, &worker_root, &worker_name) + .map_err(|err| format!("failed to resolve profile source archive: {err}"))?; + worker::entrypoint::resolve_runtime_profile_manifest_from_manifest( + manifest, + &worker_root, + &worker_name, + )? + } else { + // Compatibility/debug fallback for direct CLI tests. Normal Browser/Backend launch + // supplies a ProfileSourceArchive inside the Runtime config bundle and must not use + // Runtime-local filesystem profile discovery. + worker::entrypoint::resolve_runtime_profile_manifest( + profile.as_deref(), + &self.profile_base_dir, + &worker_name, + )? + }; manifest.worker.name = worker_name; let store_dir = self.store_dir()?; @@ -769,6 +790,7 @@ mod tests { label: Some("adapter-test".to_string()), }], declarations: Vec::new(), + profile_source_archive: None, } .with_computed_digest() } diff --git a/crates/worker/src/entrypoint.rs b/crates/worker/src/entrypoint.rs index 4e41bb81..346333b2 100644 --- a/crates/worker/src/entrypoint.rs +++ b/crates/worker/src/entrypoint.rs @@ -239,6 +239,19 @@ pub fn resolve_runtime_profile_manifest( Ok((manifest, loader)) } +pub fn resolve_runtime_profile_manifest_from_manifest( + mut manifest: WorkerManifest, + workspace_root: &Path, + worker_name: &str, +) -> Result<(WorkerManifest, PromptLoader), String> { + if manifest.worker.name.is_empty() { + manifest.worker.name = worker_name.to_string(); + } + apply_profile_launch_policy(&mut manifest, workspace_root, None)?; + apply_plugin_resolution_plan(&mut manifest, workspace_root); + Ok((manifest, PromptLoader::builtins_only())) +} + fn load_single_manifest( path: &Path, explicit_worker_name: Option<&str>, diff --git a/crates/workspace-server/src/companion.rs b/crates/workspace-server/src/companion.rs index 50e9d721..27a00ed4 100644 --- a/crates/workspace-server/src/companion.rs +++ b/crates/workspace-server/src/companion.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use std::sync::{Arc, Mutex}; use chrono::Utc; @@ -6,6 +7,7 @@ use worker_runtime::catalog::ProfileSelector; use worker_runtime::config_bundle::{ ConfigBundle, ConfigBundleMetadata, ConfigBundleProvenance, ConfigProfileDescriptor, }; +use worker_runtime::profile_archive::{ProfileSourceArchive, ProfileSourceArchiveInput}; use crate::hosts::{ DiagnosticSeverity, RuntimeDiagnostic, RuntimeRegistry, WorkerInputKind, WorkerInputRequest, @@ -452,10 +454,32 @@ fn companion_config_bundle() -> ConfigBundle { label: Some("Workspace Companion".to_string()), }], declarations: Vec::new(), + profile_source_archive: Some(companion_profile_archive()), } .with_computed_digest() } +fn companion_profile_archive() -> ProfileSourceArchive { + let mut entrypoints = BTreeMap::new(); + entrypoints.insert("default".to_string(), "profiles/companion.dcdl".to_string()); + entrypoints.insert( + COMPANION_PROFILE_ID.to_string(), + "profiles/companion.dcdl".to_string(), + ); + let mut sources = BTreeMap::new(); + sources.insert( + "profiles/companion.dcdl".to_string(), + include_str!("../../../resources/profiles/companion.dcdl").to_string(), + ); + ProfileSourceArchive::build(ProfileSourceArchiveInput { + id: "workspace-companion-profile-archive-v1".to_string(), + entrypoints, + imports: BTreeMap::new(), + sources, + }) + .expect("builtin Companion Decodal profile source archive is valid") +} + fn companion_state_for_worker(worker: &WorkerSummary) -> CompanionState { if !worker.capabilities.can_accept_input { return CompanionState::Error; diff --git a/crates/workspace-server/src/hosts.rs b/crates/workspace-server/src/hosts.rs index b01f863a..290657d3 100644 --- a/crates/workspace-server/src/hosts.rs +++ b/crates/workspace-server/src/hosts.rs @@ -7,6 +7,7 @@ use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::{ + collections::BTreeMap, path::PathBuf, sync::{Arc, RwLock}, time::Duration, @@ -41,6 +42,7 @@ use worker_runtime::management::{RuntimeOptions as EmbeddedRuntimeOptions, Runti use worker_runtime::observation::{ TranscriptProjection as EmbeddedTranscriptProjection, TranscriptQuery, TranscriptRole, }; +use worker_runtime::profile_archive::{ProfileSourceArchive, ProfileSourceArchiveInput}; const EMBEDDED_RUNTIME_ID: &str = "embedded-worker-runtime"; const EMBEDDED_HOST_KIND: &str = "embedded-worker-runtime-host"; @@ -1337,13 +1339,18 @@ impl WorkspaceWorkerRuntime for EmbeddedWorkerRuntime { .profile .clone() .unwrap_or_else(|| embedded_profile_selector(&request.intent)); - let config_bundle = match self - .runtime - .store_config_bundle(default_embedded_config_bundle(&profile)) - { + let config_bundle = match default_embedded_config_bundle(&profile).and_then(|bundle| { + self.runtime + .store_config_bundle(bundle) + .map_err(|err| err.to_string()) + }) { Ok(availability) => availability.reference, Err(error) => { - diagnostics.push(embedded_runtime_diagnostic(&error)); + diagnostics.push(diagnostic( + "embedded_profile_source_archive_invalid", + DiagnosticSeverity::Error, + error, + )); return WorkerSpawnResult { state: WorkerOperationState::Rejected, worker: None, @@ -2018,7 +2025,18 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime { .profile .clone() .unwrap_or_else(|| embedded_profile_selector(&request.intent)); - let sync = self.sync_config_bundle(default_embedded_config_bundle(&profile)); + let sync = match default_embedded_config_bundle(&profile) { + Ok(bundle) => self.sync_config_bundle(bundle), + Err(error) => ConfigBundleSyncResult { + state: WorkerOperationState::Rejected, + availability: None, + diagnostics: vec![diagnostic( + "remote_profile_source_archive_invalid", + DiagnosticSeverity::Error, + error, + )], + }, + }; let Some(config_bundle) = sync.availability.map(|availability| availability.reference) else { return WorkerSpawnResult { @@ -2347,14 +2365,14 @@ fn embedded_worker_execution_status_label( } } -fn default_embedded_config_bundle(profile: &ProfileSelector) -> ConfigBundle { +fn default_embedded_config_bundle(profile: &ProfileSelector) -> Result { let id = format!( "workspace-runtime-{}", embedded_profile_label(profile) .unwrap_or_else(|| "default".to_string()) .replace([':', '/', ' '], "-") ); - ConfigBundle { + Ok(ConfigBundle { metadata: ConfigBundleMetadata { id, digest: String::new(), @@ -2371,8 +2389,76 @@ fn default_embedded_config_bundle(profile: &ProfileSelector) -> ConfigBundle { label: embedded_profile_label(profile), }], declarations: Vec::new(), + profile_source_archive: Some(default_profile_source_archive(profile)?), + } + .with_computed_digest()) +} + +fn default_profile_source_archive( + profile: &ProfileSelector, +) -> Result { + let selected = embedded_profile_label(profile).unwrap_or_else(|| "default".to_string()); + let selected_path = embedded_profile_path(profile)?; + let mut entrypoints = BTreeMap::new(); + entrypoints.insert("default".to_string(), "profiles/default.dcdl".to_string()); + entrypoints.insert( + "builtin:default".to_string(), + "profiles/default.dcdl".to_string(), + ); + for slug in ["companion", "intake", "orchestrator", "coder", "reviewer"] { + entrypoints.insert(format!("builtin:{slug}"), format!("profiles/{slug}.dcdl")); + } + entrypoints.insert(selected, selected_path); + + let mut sources = BTreeMap::new(); + sources.insert( + "profiles/default.dcdl".to_string(), + include_str!("../../../resources/profiles/default.dcdl").to_string(), + ); + sources.insert( + "profiles/companion.dcdl".to_string(), + include_str!("../../../resources/profiles/companion.dcdl").to_string(), + ); + sources.insert( + "profiles/intake.dcdl".to_string(), + include_str!("../../../resources/profiles/intake.dcdl").to_string(), + ); + sources.insert( + "profiles/orchestrator.dcdl".to_string(), + include_str!("../../../resources/profiles/orchestrator.dcdl").to_string(), + ); + sources.insert( + "profiles/coder.dcdl".to_string(), + include_str!("../../../resources/profiles/coder.dcdl").to_string(), + ); + sources.insert( + "profiles/reviewer.dcdl".to_string(), + include_str!("../../../resources/profiles/reviewer.dcdl").to_string(), + ); + + ProfileSourceArchive::build(ProfileSourceArchiveInput { + id: "builtin-decodal-profiles-v1".to_string(), + entrypoints, + imports: BTreeMap::new(), + sources, + }) + .map_err(|err| err.to_string()) +} + +fn embedded_profile_path(profile: &ProfileSelector) -> Result { + match profile { + ProfileSelector::RuntimeDefault => Ok("profiles/default.dcdl".to_string()), + ProfileSelector::Builtin(name) => match name.strip_prefix("builtin:").unwrap_or(name) { + "default" => Ok("profiles/default.dcdl".to_string()), + "companion" => Ok("profiles/companion.dcdl".to_string()), + "intake" => Ok("profiles/intake.dcdl".to_string()), + "orchestrator" => Ok("profiles/orchestrator.dcdl".to_string()), + "coder" => Ok("profiles/coder.dcdl".to_string()), + "reviewer" => Ok("profiles/reviewer.dcdl".to_string()), + other => Err(format!("unknown builtin profile selector: builtin:{other}")), + }, + ProfileSelector::Named(name) => Err(format!("unknown named profile selector: {name}")), } - .with_computed_digest() } fn embedded_profile_selector(intent: &WorkerSpawnIntent) -> ProfileSelector { @@ -2878,6 +2964,49 @@ mod tests { use std::sync::{Arc, Mutex}; use std::thread; + #[test] + fn embedded_builtin_decodal_profiles_resolve_through_archive() { + let root = tempfile::tempdir().unwrap(); + for selector in [ + ProfileSelector::RuntimeDefault, + ProfileSelector::Builtin("builtin:companion".to_string()), + ProfileSelector::Builtin("builtin:intake".to_string()), + ProfileSelector::Builtin("builtin:orchestrator".to_string()), + ProfileSelector::Builtin("builtin:coder".to_string()), + ProfileSelector::Builtin("builtin:reviewer".to_string()), + ] { + let bundle = default_embedded_config_bundle(&selector).unwrap(); + let archive = bundle + .profile_source_archive + .as_ref() + .unwrap() + .verify() + .unwrap(); + let selector_key = match &selector { + ProfileSelector::RuntimeDefault => "default".to_string(), + ProfileSelector::Builtin(name) => name.clone(), + ProfileSelector::Named(name) => name.clone(), + }; + let manifest = archive + .resolve_profile(&selector_key, root.path(), "embedded-test-worker") + .unwrap(); + assert_eq!(manifest.worker.name, "embedded-test-worker"); + } + } + + #[test] + fn embedded_archive_rejects_unknown_selectors() { + assert!( + default_embedded_config_bundle(&ProfileSelector::Builtin( + "builtin:missing".to_string() + )) + .is_err() + ); + assert!( + default_embedded_config_bundle(&ProfileSelector::Named("custom".to_string())).is_err() + ); + } + fn test_config_bundle() -> ConfigBundle { ConfigBundle { metadata: worker_runtime::config_bundle::ConfigBundleMetadata { @@ -2900,6 +3029,7 @@ mod tests { name: "read".to_string(), reference: "capability:read".to_string(), }], + profile_source_archive: None, } .with_computed_digest() } diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index d42242d2..b0f1ec80 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -3405,6 +3405,7 @@ mod tests { label: Some("server-test".to_string()), }], declarations: Vec::new(), + profile_source_archive: None, } .with_computed_digest() } diff --git a/package.nix b/package.nix index 3f93721e..0b196b78 100644 --- a/package.nix +++ b/package.nix @@ -43,7 +43,7 @@ rustPlatform.buildRustPackage rec { filter = sourceFilter; }; - cargoHash = "sha256-ICEIo7wR65s6CQCzCVa1KaEou7fpnaUk64zwokb92Fc="; + cargoHash = "sha256-g9Bh9qQ1fGZtzsjO2/vc4jZdrH5YoCR0rOgs//xUDrU="; depsExtraArgs = { # Older fetchCargoVendor utilities used crates.io's API download endpoint, diff --git a/resources/profiles/coder.dcdl b/resources/profiles/coder.dcdl new file mode 100644 index 00000000..6e6e241b --- /dev/null +++ b/resources/profiles/coder.dcdl @@ -0,0 +1,12 @@ +slug = "coder"; +description = "Ticket implementation coder profile."; +scope = "workspace_write"; + +feature = { + task = { enabled = true; }; + memory = { enabled = true; }; + web = { enabled = true; }; + workers = { enabled = false; }; + ticket = { enabled = false; access = "lifecycle"; }; + ticket_orchestration = { enabled = false; }; +}; diff --git a/resources/profiles/companion.dcdl b/resources/profiles/companion.dcdl new file mode 100644 index 00000000..e18736d9 --- /dev/null +++ b/resources/profiles/companion.dcdl @@ -0,0 +1,12 @@ +slug = "companion"; +description = "Workspace companion profile."; +scope = "workspace_write"; + +feature = { + task = { enabled = true; }; + memory = { enabled = true; }; + web = { enabled = true; }; + workers = { enabled = true; }; + ticket = { enabled = false; access = "lifecycle"; }; + ticket_orchestration = { enabled = false; }; +}; diff --git a/resources/profiles/default.dcdl b/resources/profiles/default.dcdl new file mode 100644 index 00000000..4fb756e4 --- /dev/null +++ b/resources/profiles/default.dcdl @@ -0,0 +1,38 @@ +slug = "default"; +description = "Default Yoi coding profile."; +scope = "workspace_write"; + +model = { + ref = "codex-oauth/gpt-5.5"; +}; + +session = { + record_event_trace = true; +}; + +engine = { + reasoning = "high"; +}; + +feature = { + task = { enabled = true; }; + memory = { enabled = true; }; + web = { enabled = true; }; + workers = { enabled = true; }; + ticket = { enabled = false; access = "lifecycle"; }; + ticket_orchestration = { enabled = false; }; +}; + +memory = { + extract_threshold = 50000; + consolidation_threshold_files = 5; + consolidation_threshold_bytes = 50000; +}; + +web = { + enabled = true; + search = { + provider = "brave"; + api_key_secret = "web/brave/default"; + }; +}; diff --git a/resources/profiles/intake.dcdl b/resources/profiles/intake.dcdl new file mode 100644 index 00000000..ae2c1d72 --- /dev/null +++ b/resources/profiles/intake.dcdl @@ -0,0 +1,12 @@ +slug = "intake"; +description = "Ticket intake profile."; +scope = "workspace_write"; + +feature = { + task = { enabled = false; }; + memory = { enabled = true; }; + web = { enabled = true; }; + workers = { enabled = false; }; + ticket = { enabled = true; access = "lifecycle"; }; + ticket_orchestration = { enabled = false; }; +}; diff --git a/resources/profiles/orchestrator.dcdl b/resources/profiles/orchestrator.dcdl new file mode 100644 index 00000000..0012818c --- /dev/null +++ b/resources/profiles/orchestrator.dcdl @@ -0,0 +1,12 @@ +slug = "orchestrator"; +description = "Ticket orchestrator profile."; +scope = "workspace_write"; + +feature = { + task = { enabled = true; }; + memory = { enabled = true; }; + web = { enabled = true; }; + workers = { enabled = true; }; + ticket = { enabled = true; access = "lifecycle"; }; + ticket_orchestration = { enabled = true; }; +}; diff --git a/resources/profiles/reviewer.dcdl b/resources/profiles/reviewer.dcdl new file mode 100644 index 00000000..91c41e5c --- /dev/null +++ b/resources/profiles/reviewer.dcdl @@ -0,0 +1,12 @@ +slug = "reviewer"; +description = "Ticket review profile."; +scope = "workspace_read"; + +feature = { + task = { enabled = true; }; + memory = { enabled = true; }; + web = { enabled = true; }; + workers = { enabled = false; }; + ticket = { enabled = false; access = "lifecycle"; }; + ticket_orchestration = { enabled = false; }; +};