diff --git a/README.md b/README.md index bcb8fb6e..00f8df00 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ ZAR-Zig-Agent-Runtime is the Zig runtime port of OpenClaw, with parity-first del - Original OpenClaw baseline (`v2026.3.13-1`): `100/100` covered - Original OpenClaw beta baseline (`v2026.3.13-beta.1`): `100/100` covered - Union baseline: `141/141` covered (`MISSING_IN_ZIG=0`) -- Latest local validation: `zig build test --summary all` -> `381/381` passed +- Latest local validation: `zig build test --summary all` -> `398/398` passed - Current edge release target tag: `v0.2.0-zig-edge.31` - License posture: repo-wide `GPL-2.0-only` with Linux-style SPDX headers on repo-owned source and script files - Toolchain policy: Codeberg `master` is canonical; `adybag14-cyber/zig` publishes rolling `latest-master` and immutable `upstream-` Windows releases for refresh and reproducibility. @@ -97,7 +97,7 @@ ZAR-Zig-Agent-Runtime is the Zig runtime port of OpenClaw, with parity-first del - `src/baremetal/filesystem.zig` implements directory creation plus file read/write/stat on the shared storage backend - `src/pal/fs.zig` routes the freestanding PAL filesystem surface through that layer - hosted and host validation now prove persistence over both RAM-disk and ATA PIO backends, including the partition-mounted ATA view - - the filesystem entry budget is now `80`, which is the current bounded baseline that keeps the deeper FS5.5 package/trust/app/autorun/workspace runtime state fitting on the persisted filesystem surface without live-service `NoSpace` failures + - the filesystem entry budget is now `128`, which is the current bounded baseline that keeps the deeper FS5.5 package/trust/app/workspace/workspace-suite release surface fitting on the persisted filesystem surface without live-service `NoSpace` failures - bare-metal tool execution is now also on a real freestanding path: - `src/baremetal/tool_exec.zig` provides the builtin command substrate instead of falling back to hosted process execution, including canonical `run-package` support plus `package-verify`, `package-release-list`, `package-release-info`, `package-release-save`, `package-release-activate`, `package-release-delete`, `package-release-prune`, `package-release-channel-list`, `package-release-channel-info`, `package-release-channel-set`, `package-release-channel-activate`, `package-app`, `package-display`, `package-ls`, `package-cat`, `package-delete`, `app-delete`, `app-autorun-list`, `app-autorun-add`, `app-autorun-remove`, `app-autorun-run`, `app-suite-list`, `app-suite-info`, `app-suite-save`, `app-suite-apply`, `app-suite-run`, `app-suite-delete`, `app-suite-release-list`, `app-suite-release-info`, `app-suite-release-save`, `app-suite-release-activate`, `app-suite-release-delete`, `app-suite-release-prune`, `app-suite-release-channel-list`, `app-suite-release-channel-info`, `app-suite-release-channel-set`, `app-suite-release-channel-activate`, `display-info`, `display-modes`, and `display-set` - `src/pal/proc.zig` exposes the explicit freestanding capture path used by the bare-metal PAL @@ -113,9 +113,13 @@ ZAR-Zig-Agent-Runtime is the Zig runtime port of OpenClaw, with parity-first del - the current FS5.5 app-suite-release slice now adds persisted `/runtime/app-suite-releases///suite.txt` plus `release.txt` through `src/baremetal/app_runtime.zig`, new `tool_exec` builtins (`app-suite-release-list`, `app-suite-release-info`, `app-suite-release-save`, `app-suite-release-activate`, `app-suite-release-delete`, `app-suite-release-prune`), new typed TCP verbs (`APPSUITERELEASELIST`, `APPSUITERELEASEINFO`, `APPSUITERELEASESAVE`, `APPSUITERELEASEACTIVATE`, `APPSUITERELEASEDELETE`, `APPSUITERELEASEPRUNE`), ATA/RAM-backed app-suite-release tests with deterministic `saved_seq` / `saved_tick` metadata, and live RTL8139 TCP proof for save -> mutate -> list -> info -> activate -> delete -> prune plus restored suite readback - the current FS5.5 app-suite-release-channel slice now adds persisted `/runtime/app-suite-release-channels//.txt` mappings through `src/baremetal/app_runtime.zig`, new `tool_exec` builtins (`app-suite-release-channel-list`, `app-suite-release-channel-info`, `app-suite-release-channel-set`, `app-suite-release-channel-activate`), new typed TCP verbs (`APPSUITECHANNELLIST`, `APPSUITECHANNELINFO`, `APPSUITECHANNELSET`, `APPSUITECHANNELACTIVATE`), ATA/RAM-backed app-suite-release-channel persistence tests, and live RTL8139 TCP proof for set -> persisted channel-target readback -> list -> info -> activate plus restored suite readback through the selected suite release channel - the current FS5.5 workspace slice now adds persisted `/runtime/workspaces/.txt` orchestration state plus `/runtime/workspace-runs//last_run.txt`, `history.log`, `stdout.log`, `stderr.log`, `/runtime/workspace-runs/autorun.txt`, and versioned release snapshots under `/runtime/workspace-releases///workspace.txt` plus `release.txt` through `src/baremetal/workspace_runtime.zig`, new `tool_exec` builtins (`workspace-list`, `workspace-info`, `workspace-save`, `workspace-apply`, `workspace-run`, `workspace-state`, `workspace-history`, `workspace-stdout`, `workspace-stderr`, `workspace-delete`, `workspace-release-list`, `workspace-release-info`, `workspace-release-save`, `workspace-release-activate`, `workspace-release-delete`, `workspace-release-prune`, `workspace-autorun-list`, `workspace-autorun-add`, `workspace-autorun-remove`, `workspace-autorun-run`), new typed TCP verbs (`WORKSPACELIST`, `WORKSPACEINFO`, `WORKSPACESAVE`, `WORKSPACEAPPLY`, `WORKSPACERUN`, `WORKSPACESTATE`, `WORKSPACEHISTORY`, `WORKSPACESTDOUT`, `WORKSPACESTDERR`, `WORKSPACEDELETE`, `WORKSPACERELEASELIST`, `WORKSPACERELEASEINFO`, `WORKSPACERELEASESAVE`, `WORKSPACERELEASEACTIVATE`, `WORKSPACERELEASEDELETE`, `WORKSPACERELEASEPRUNE`, `WORKSPACEAUTORUNLIST`, `WORKSPACEAUTORUNADD`, `WORKSPACEAUTORUNREMOVE`, `WORKSPACEAUTORUNRUN`), RAM-disk and ATA-backed workspace/release/autorun tests, and live RTL8139 TCP proof for save/list/info/apply/run/state/history/stdout/stderr/delete, workspace release save/mutate/list/info/activate/delete/prune with persisted release readback and restored workspace info, workspace autorun add/list/run/remove, persisted `/runtime/workspace-runs/autorun.txt` readback, persisted workspace-run receipt readback, restored canonical package script content, trust-bundle selection, display mode, app-suite active-plan markers, and delete cleanup on the workspace surface + - the current FS5.5 workspace-plan slice now adds persisted `/runtime/workspace-plans//.txt` plus `/runtime/workspace-plans//active.txt` through `src/baremetal/workspace_runtime.zig`, new `tool_exec` builtins (`workspace-plan-list`, `workspace-plan-info`, `workspace-plan-active`, `workspace-plan-save`, `workspace-plan-apply`, `workspace-plan-delete`), new typed TCP verbs (`WORKSPACEPLANLIST`, `WORKSPACEPLANINFO`, `WORKSPACEPLANACTIVE`, `WORKSPACEPLANSAVE`, `WORKSPACEPLANAPPLY`, `WORKSPACEPLANDELETE`), RAM-disk and ATA-backed workspace-plan persistence tests, and a live RTL8139 TCP proof for save -> list -> info -> apply -> active -> restore -> delete with restored suite/trust/display/channel readback on the workspace orchestration surface - the current FS5.5 workspace-release-channel slice now adds persisted `/runtime/workspace-release-channels//.txt` mappings through `src/baremetal/workspace_runtime.zig`, new `tool_exec` builtins (`workspace-release-channel-list`, `workspace-release-channel-info`, `workspace-release-channel-set`, `workspace-release-channel-activate`), new typed TCP verbs (`WORKSPACECHANNELLIST`, `WORKSPACECHANNELINFO`, `WORKSPACECHANNELSET`, `WORKSPACECHANNELACTIVATE`), RAM-disk and ATA-backed workspace-channel persistence tests, and live RTL8139 TCP proof for set -> info -> list -> activate plus persisted channel-target readback and restored workspace info through the selected workspace release channel - - `src/baremetal/filesystem.zig` now carries a `96`-entry filesystem budget so the deeper FS5.5 package/trust/app/workspace release surface fits on the persisted path without live-service `NoSpace` failures - - the current local Windows master-Zig refresh now uses the GitHub mirror install target `47d2e5de90faec1221f61255c36e2be81c9e3db3` under `toolchains/zig-master/current`, restores the real Windows `zig build test` runner path in `build.zig` instead of the stale fake-green system-command workaround, widens the PVH boot/runtime stack in `scripts/baremetal/pvh_boot.S` to keep the deeper FS5.5 probe depth stable, and now passes both the stable `ReleaseSafe` and local `Debug` broad live RTL8139 TCP QEMU probes on this toolchain + - the current FS5.5 workspace-suite slice now adds persisted `/runtime/workspace-suites/.txt` orchestration groups through `src/baremetal/workspace_runtime.zig`, new `tool_exec` builtins (`workspace-suite-list`, `workspace-suite-info`, `workspace-suite-save`, `workspace-suite-apply`, `workspace-suite-run`, `workspace-suite-delete`), new typed TCP verbs (`WORKSPACESUITELIST`, `WORKSPACESUITEINFO`, `WORKSPACESUITESAVE`, `WORKSPACESUITEAPPLY`, `WORKSPACESUITERUN`, `WORKSPACESUITEDELETE`), RAM-disk and ATA-backed workspace-suite persistence tests, and live RTL8139 TCP proof for save -> persisted suite-file readback -> list -> info -> apply -> run -> delete plus post-delete suite absence on the higher-level workspace orchestration surface + - the current FS5.5 workspace-suite-release slice now adds persisted `/runtime/workspace-suite-releases///suite.txt` plus `release.txt` through `src/baremetal/workspace_runtime.zig`, new `tool_exec` builtins (`workspace-suite-release-list`, `workspace-suite-release-info`, `workspace-suite-release-save`, `workspace-suite-release-activate`, `workspace-suite-release-delete`, `workspace-suite-release-prune`), new typed TCP verbs (`WORKSPACESUITERELEASELIST`, `WORKSPACESUITERELEASEINFO`, `WORKSPACESUITERELEASESAVE`, `WORKSPACESUITERELEASEACTIVATE`, `WORKSPACESUITERELEASEDELETE`, `WORKSPACESUITERELEASEPRUNE`), RAM-disk and ATA-backed workspace-suite-release persistence tests with deterministic `saved_seq` / `saved_tick` metadata, and live RTL8139 TCP proof for save -> mutate -> list -> info -> activate -> delete -> prune plus restored suite readback and post-delete suite-release absence + - the current FS5.5 workspace-suite-release-channel slice now adds persisted `/runtime/workspace-suite-release-channels//.txt` mappings through `src/baremetal/workspace_runtime.zig`, new `tool_exec` builtins (`workspace-suite-release-channel-list`, `workspace-suite-release-channel-info`, `workspace-suite-release-channel-set`, `workspace-suite-release-channel-activate`), new typed TCP verbs (`WORKSPACESUITECHANNELLIST`, `WORKSPACESUITECHANNELINFO`, `WORKSPACESUITECHANNELSET`, `WORKSPACESUITECHANNELACTIVATE`), RAM-disk and ATA-backed workspace-suite-release-channel persistence tests, and live RTL8139 TCP proof for set -> persisted channel-target readback -> list -> info -> activate plus restored suite readback through the selected workspace-suite release channel + - `src/baremetal/filesystem.zig` now carries a `128`-entry filesystem budget so the deeper FS5.5 package/trust/app/workspace/workspace-suite/workspace-plan surface fits on the persisted path without live-service `NoSpace` failures + - the current local Windows master-Zig install remains on `820308b3d44ed7ca2cabd07a11b2099992ed4e6b` under `toolchains/zig-master/current`, while Codeberg/GitHub `master` is newer at `08416b44f92a0917ad8b71829704674477752788`; the repo keeps that mismatch explicit instead of hiding it - Recent FS6 progress (2026-03-06): - `update.*` now has a real `canary` rollout lane instead of collapsing `canary` into `edge` - appliance rollout boundary is now enforced by live smoke validation (`canary` selection, secure-boot block, canary apply, stable promotion) diff --git a/docs/zig-port/FS5_5_HARDWARE_DRIVERS_SYSTEMS.md b/docs/zig-port/FS5_5_HARDWARE_DRIVERS_SYSTEMS.md index d743e951..95b07cba 100644 --- a/docs/zig-port/FS5_5_HARDWARE_DRIVERS_SYSTEMS.md +++ b/docs/zig-port/FS5_5_HARDWARE_DRIVERS_SYSTEMS.md @@ -465,10 +465,14 @@ Current local source-of-truth evidence: - the current FS5.5 app-suite-release slice now adds persisted `/runtime/app-suite-releases///suite.txt` plus `release.txt`, new `tool_exec` builtins (`app-suite-release-list`, `app-suite-release-info`, `app-suite-release-save`, `app-suite-release-activate`, `app-suite-release-delete`, `app-suite-release-prune`), new typed TCP verbs (`APPSUITERELEASELIST`, `APPSUITERELEASEINFO`, `APPSUITERELEASESAVE`, `APPSUITERELEASEACTIVATE`, `APPSUITERELEASEDELETE`, `APPSUITERELEASEPRUNE`), ATA/RAM-backed app-suite-release tests with deterministic `saved_seq` / `saved_tick` metadata, and a live RTL8139 TCP proof for save -> mutate -> list -> info -> activate -> delete -> prune plus restored suite readback - the current FS5.5 app-suite-release-channel slice now adds persisted `/runtime/app-suite-release-channels//.txt` mappings, `app-suite-release-channel-list` / `app-suite-release-channel-info` / `app-suite-release-channel-set` / `app-suite-release-channel-activate`, typed `APPSUITECHANNELLIST` / `APPSUITECHANNELINFO` / `APPSUITECHANNELSET` / `APPSUITECHANNELACTIVATE`, ATA/RAM-backed app-suite-release-channel persistence tests, and live RTL8139 TCP proof for set -> persisted channel-target readback -> list -> info -> activate plus restored suite readback through the selected suite release channel - the current FS5.5 workspace slice now adds persisted `/runtime/workspaces/.txt` plus `/runtime/workspace-runs//last_run.txt`, `history.log`, `stdout.log`, `stderr.log`, `/runtime/workspace-runs/autorun.txt`, and versioned release snapshots under `/runtime/workspace-releases///workspace.txt` plus `release.txt`, new `tool_exec` builtins (`workspace-list`, `workspace-info`, `workspace-save`, `workspace-apply`, `workspace-run`, `workspace-state`, `workspace-history`, `workspace-stdout`, `workspace-stderr`, `workspace-delete`, `workspace-release-list`, `workspace-release-info`, `workspace-release-save`, `workspace-release-activate`, `workspace-release-delete`, `workspace-release-prune`, `workspace-autorun-list`, `workspace-autorun-add`, `workspace-autorun-remove`, `workspace-autorun-run`), new typed TCP verbs (`WORKSPACELIST`, `WORKSPACEINFO`, `WORKSPACESAVE`, `WORKSPACEAPPLY`, `WORKSPACERUN`, `WORKSPACESTATE`, `WORKSPACEHISTORY`, `WORKSPACESTDOUT`, `WORKSPACESTDERR`, `WORKSPACEDELETE`, `WORKSPACERELEASELIST`, `WORKSPACERELEASEINFO`, `WORKSPACERELEASESAVE`, `WORKSPACERELEASEACTIVATE`, `WORKSPACERELEASEDELETE`, `WORKSPACERELEASEPRUNE`, `WORKSPACEAUTORUNLIST`, `WORKSPACEAUTORUNADD`, `WORKSPACEAUTORUNREMOVE`, `WORKSPACEAUTORUNRUN`), RAM-disk and ATA-backed workspace/release/autorun tests, and a live RTL8139 TCP proof for save/list/info/apply/run/state/history/stdout/stderr/delete, workspace release save/mutate/list/info/activate/delete/prune with persisted release readback and restored workspace info, workspace autorun add/list/run/remove, persisted `/runtime/workspace-runs/autorun.txt` readback, persisted workspace-run receipt readback, restored canonical package script content, trust-bundle selection, display mode, app-suite active-plan markers, and delete cleanup + - the current FS5.5 workspace-plan slice now adds persisted `/runtime/workspace-plans//.txt` plus `/runtime/workspace-plans//active.txt`, new `tool_exec` builtins (`workspace-plan-list`, `workspace-plan-info`, `workspace-plan-active`, `workspace-plan-save`, `workspace-plan-apply`, `workspace-plan-delete`), new typed TCP verbs (`WORKSPACEPLANLIST`, `WORKSPACEPLANINFO`, `WORKSPACEPLANACTIVE`, `WORKSPACEPLANSAVE`, `WORKSPACEPLANAPPLY`, `WORKSPACEPLANDELETE`), RAM-disk and ATA-backed workspace-plan persistence tests, and a live RTL8139 TCP proof for save -> list -> info -> apply -> active -> restore -> delete with restored suite/trust/display/channel readback - the current FS5.5 workspace-release-channel slice now adds persisted `/runtime/workspace-release-channels//.txt` mappings, `workspace-release-channel-list` / `workspace-release-channel-info` / `workspace-release-channel-set` / `workspace-release-channel-activate`, typed `WORKSPACECHANNELLIST` / `WORKSPACECHANNELINFO` / `WORKSPACECHANNELSET` / `WORKSPACECHANNELACTIVATE`, RAM-disk and ATA-backed workspace-channel persistence tests, and live RTL8139 TCP proof for set -> info -> list -> activate plus persisted channel-target readback and restored workspace info through the selected workspace release channel - - `src/baremetal/filesystem.zig` now carries a `96`-entry filesystem budget so the deeper FS5.5 package/trust/app/workspace release surface fits on the persisted path without live-service `NoSpace` failures + - the current FS5.5 workspace-suite slice now adds persisted `/runtime/workspace-suites/.txt` orchestration groups, `workspace-suite-list` / `workspace-suite-info` / `workspace-suite-save` / `workspace-suite-apply` / `workspace-suite-run` / `workspace-suite-delete`, typed `WORKSPACESUITELIST` / `WORKSPACESUITEINFO` / `WORKSPACESUITESAVE` / `WORKSPACESUITEAPPLY` / `WORKSPACESUITERUN` / `WORKSPACESUITEDELETE`, RAM-disk and ATA-backed workspace-suite persistence tests, and live RTL8139 TCP proof for save -> persisted suite-file readback -> list -> info -> apply -> run -> delete plus post-delete suite absence on the higher-level workspace orchestration surface + - the current FS5.5 workspace-suite-release slice now adds persisted `/runtime/workspace-suite-releases///suite.txt` plus `release.txt`, `workspace-suite-release-list` / `workspace-suite-release-info` / `workspace-suite-release-save` / `workspace-suite-release-activate` / `workspace-suite-release-delete` / `workspace-suite-release-prune`, typed `WORKSPACESUITERELEASELIST` / `WORKSPACESUITERELEASEINFO` / `WORKSPACESUITERELEASESAVE` / `WORKSPACESUITERELEASEACTIVATE` / `WORKSPACESUITERELEASEDELETE` / `WORKSPACESUITERELEASEPRUNE`, RAM-disk and ATA-backed workspace-suite-release persistence tests with deterministic `saved_seq` / `saved_tick` metadata, and live RTL8139 TCP proof for save -> mutate -> list -> info -> activate -> delete -> prune plus restored suite readback and post-delete suite-release absence + - the current FS5.5 workspace-suite-release-channel slice now adds persisted `/runtime/workspace-suite-release-channels//.txt` mappings, `workspace-suite-release-channel-list` / `workspace-suite-release-channel-info` / `workspace-suite-release-channel-set` / `workspace-suite-release-channel-activate`, typed `WORKSPACESUITECHANNELLIST` / `WORKSPACESUITECHANNELINFO` / `WORKSPACESUITECHANNELSET` / `WORKSPACESUITECHANNELACTIVATE`, RAM-disk and ATA-backed workspace-suite-release-channel persistence tests, and live RTL8139 TCP proof for set -> persisted channel-target readback -> list -> info -> activate plus restored suite readback through the selected workspace-suite release channel + - `src/baremetal/filesystem.zig` now carries a `128`-entry filesystem budget so the deeper FS5.5 package/trust/app/workspace/workspace-plan/workspace-suite release surface fits on the persisted path without live-service `NoSpace` failures - the current FS5.5 runtime-service slice now adds `src/baremetal/runtime_bridge.zig` as the shared bare-metal runtime seam, new `tool_exec` builtins (`runtime-snapshot`, `runtime-sessions`, `runtime-session`), new typed TCP verbs (`RUNTIMECALL`, `RUNTIMESNAPSHOT`, `RUNTIMESESSIONS`, `RUNTIMESESSION`), lifetime-safe `runtime.session.get` handling in `src/runtime/tool_runtime.zig`, logical `/runtime/...` PAL filesystem routing through `src/pal/fs.zig` during hosted bare-metal tests so runtime persistence lands on the same RAM-disk/ATA-backed surface as the rest of FS5.5, hosted/module validation for runtime snapshot/session query and RPC call bridging, and a dedicated live RTL8139 runtime-service proof (`scripts/baremetal-qemu-rtl8139-runtime-service-probe-check.ps1`) for runtime file-write/exec/read plus persisted `/runtime/state/runtime-state.json` and `/runtime/tmp/service-runtime.txt` readback - - the current local Windows master-Zig stabilization slice now uses the wider `262144`-byte PVH boot/runtime stack in `scripts/baremetal/pvh_boot.S`; hosted master-Zig validation is green (hosted `378/378`, bare-metal host `346 passed / 1 skipped`), and the broad live RTL8139 TCP QEMU proof now passes on both the local `Debug` and stable `ReleaseSafe` probe lanes alongside the live HTTPS proof + - the current local Windows master-Zig stabilization slice now uses the wider `262144`-byte PVH boot/runtime stack in `scripts/baremetal/pvh_boot.S`; hosted master-Zig validation is green (`zig build test --summary all` -> `398/398` passed), and the broad live RTL8139 TCP QEMU proof now passes on both the local `Debug` and stable `ReleaseSafe` probe lanes alongside the live HTTPS proof ## Non-Goals For This Track diff --git a/docs/zig-port/PHASE_CHECKLIST.md b/docs/zig-port/PHASE_CHECKLIST.md index 34e54a27..238972c5 100644 --- a/docs/zig-port/PHASE_CHECKLIST.md +++ b/docs/zig-port/PHASE_CHECKLIST.md @@ -4,8 +4,8 @@ Release lock: no release tag is allowed until all phases are complete and parity Historical note: milestone validation counts below are preserved as captured at the time of each slice; current project-wide test gate is refreshed after each strict hosted-phase signoff. Current edge release target: `v0.2.0-zig-edge.31` is validated locally from this branch for binaries, parity evidence, SBOM/provenance, npm tarball, wheel, and sdist output. Registry status: -- npm public publish still requires npm-side scope/package permission or `NPM_TOKEN`; GitHub release asset + GitHub Packages fallback are available now. -- PyPI public publish still requires a matching trusted publisher or `PYPI_API_TOKEN`; workflow claim shape is now confirmed as `repo:adybag14-cyber/ZAR-Zig-Agent-Runtime:environment:pypi`. +- npm public publish still requires npm-side scope/package permission or a publish-capable `NPM_TOKEN`; GitHub release asset + GitHub Packages fallback are available now. +- PyPI public publish is live for `openclaw-zig-rpc-client==0.2.0.dev31`, and the trusted publisher now matches `repo:adybag14-cyber/ZAR-Zig-Agent-Runtime:environment:pypi`. - `scripts/package-registry-status.ps1` now checks public npmjs/PyPI visibility correctly even when called with only `-ReleaseTag`, so local release diagnostics no longer silently skip the unresolved registry state. - release evidence now also includes `release-status.json` + `release-status.md`, which snapshot package visibility plus the latest `zig-ci` / `docs-pages` / `release-preview` / `npm-release` / `python-release` workflow state for the target tag. - `FS5.6` repo-wide license refresh is now strict-closed locally: root/package license files, release evidence, package metadata, and Linux-style SPDX headers now use `GPL-2.0-only` to match the Linux-derived RTL8139 slice. @@ -14,14 +14,18 @@ Registry status: - `FS5.5` storage/disk strict closure is now reached locally: `src/baremetal/storage_backend.zig` provides the shared backend selector, `src/baremetal/ata_pio_disk.zig` provides a real ATA PIO `IDENTIFY` / `READ` / `WRITE` / `FLUSH` path plus bounded multi-partition MBR/GPT discovery/export, first-usable-MBR and protective-MBR GPT partition mounting with logical LBA translation, `src/pal/storage.zig` plus `src/baremetal/tool_layout.zig` route through that shared backend, `src/pal/storage.zig` now also exports logical base-LBA plus bounded partition count/info/select on the mounted storage view, `src/baremetal_main.zig` exposes that same partition-aware storage seam through the `oc_storage_*` ABI exports, partition selection now invalidates stale tool-layout/filesystem state and is paired with explicit `oc_tool_layout_format` plus `oc_filesystem_format` control on the selected partition, `src/baremetal/disk_installer.zig` seeds the canonical persisted install layout (`/boot`, `/system`, `/runtime/install`, bootstrap package) on the active backend, hosted/host regressions prove backend preference, identify-backed capacity, ATA mock-device read/write/flush behavior, multi-partition MBR/GPT discovery plus explicit selection, logical base-LBA translation, installer-layout persistence, ATA-backed export reporting, direct `oc_storage_*` partition export/selection behavior, rebind-safe tool-layout/filesystem invalidation, and per-partition persistence after switching between primary and secondary MBR partitions, and the live QEMU proofs `scripts/baremetal-qemu-ata-storage-probe-check.ps1` and `scripts/baremetal-qemu-ata-gpt-installer-probe-check.ps1` now boot real MBR and protective-MBR GPT raw images and prove raw ATA block mutation + readback, secondary-partition export/selection through the exported seam, secondary-partition tool-layout formatting + payload persistence, secondary-partition filesystem formatting + superblock persistence, ATA-backed tool-layout persistence, ATA-backed filesystem persistence, GPT-backed installer layout seeding, and persisted bootstrap package execution over the freestanding PVH artifact. - `FS5.5` Ethernet-driver strict closure is now reached locally: `src/baremetal/rtl8139.zig` provides the real RTL8139 PCI-discovered bring-up, MAC readout, RX ring programming, TX slot programming, and loopback-friendly datapath validation, `src/baremetal/pci.zig` discovers the I/O BAR + IRQ line and enables I/O plus bus mastering on the selected PCI function, `src/pal/net.zig` and `src/baremetal_main.zig` expose the same raw-frame PAL/export seam, host regressions prove mock-device init/send/receive behavior, and the live QEMU proof `scripts/baremetal-qemu-rtl8139-probe-check.ps1` now proves MAC readout, TX, RX loopback, payload validation, and TX/RX counter advance over the freestanding PVH artifact. - `FS5.5` TCP/IP strict closure is now reached locally: `src/protocol/ethernet.zig` and `src/protocol/arp.zig` provide Ethernet/ARP framing plus ARP reply encode/decode, `src/protocol/ipv4.zig` and `src/protocol/udp.zig` provide IPv4/UDP framing plus checksum handling, `src/protocol/tcp.zig` now provides strict TCP framing plus a minimal client/server session state machine for `SYN -> SYN-ACK -> ACK`, established payload exchange, bounded four-way teardown, bounded SYN/payload/FIN retransmission recovery, bounded multi-flow session-table management, bounded cumulative-ACK advancement across multiple in-flight payload chunks, strict remote-window enforcement for bounded sequential payload chunking, zero-window blocking until a pure ACK reopens the remote window, and bounded sender congestion-window growth after ACK plus payload-timeout collapse on the chunked send path, `src/protocol/dhcp.zig` now provides strict DHCP discover encode/decode, `src/protocol/dns.zig` now provides strict DNS query and A-response encode/decode plus caller-owned decode storage on the freestanding path, `src/pal/tls_client_light.zig` now provides the bounded freestanding TLS client used by the PAL network path plus precise last-certificate-error reporting, `src/pal/net.zig` exposes `sendArpRequest` / `pollArpPacket`, `sendIpv4Frame` / `pollIpv4PacketStrict`, `sendUdpPacket` / `pollUdpPacketStrictInto`, `sendTcpPacket` / `pollTcpPacketStrictInto`, `sendDnsQuery` / `pollDnsPacketStrictInto`, DHCP send/poll helpers, routed networking helpers (`configureIpv4Route`, `configureIpv4RouteFromDhcp`, `resolveNextHop`, `learnArpPacket`, `sendUdpPacketRouted`), explicit DNS server configuration (`configureDnsServers`, `configureDnsServersFromDhcp`), a real freestanding bounded `http://` POST path, and a real freestanding bounded `https://` POST transport path with deterministic filesystem-backed CA-bundle verification, `src/baremetal/tool_service.zig` now exposes the bounded typed framed request/response shim used by the bare-metal TCP proof with `CMD` / `EXEC` / `GET` / `PUT` / `STAT` / `LIST` / `INSTALL` / `MANIFEST` / `PKG` / `PKGLIST` / `PKGINFO` / `PKGRUN` / `PKGAPP` / `PKGDISPLAY` / `PKGPUT` / `PKGLS` / `PKGGET` / `PKGDELETE` / `APPLIST` / `APPINFO` / `APPSTATE` / `APPHISTORY` / `APPSTDOUT` / `APPSTDERR` / `APPTRUST` / `APPCONNECTOR` / `APPRUN` / `APPDELETE` / `DISPLAYINFO` / `DISPLAYMODES` / `DISPLAYSET` / `TRUSTPUT` / `TRUSTLIST` / `TRUSTINFO` / `TRUSTACTIVE` / `TRUSTSELECT` / `TRUSTDELETE` plus bounded batched request parsing/execution on one flow, host regressions prove ARP, IPv4, UDP, TCP handshake/payload, bounded four-way close, dropped-first-SYN retransmission recovery, dropped-first-payload retransmission recovery, dropped-first-FIN retransmission recovery on both close sides, bounded multi-flow session isolation, bounded cumulative-ACK advancement through multiple in-flight chunks, bounded sender congestion-window growth/collapse on the chunked send path, DHCP, DNS, DHCP-driven route configuration, gateway ARP learning, routed off-subnet UDP delivery, direct-subnet UDP bypass, zero-window block/reopen, bounded sequential payload chunking, framed multi-request command-service exchange, structured `EXEC` service behavior, bounded typed batch request multiplexing, typed `PUT`/`GET`/`STAT`/`LIST` service behavior, typed `INSTALL` / `MANIFEST` runtime-layout service behavior, typed `PKG` / `PKGLIST` / `PKGINFO` / `PKGRUN` / `PKGAPP` / `PKGDISPLAY` / `PKGPUT` / `PKGLS` / `PKGGET` / `PKGDELETE` package-service behavior, typed `APPLIST` / `APPINFO` / `APPSTATE` / `APPHISTORY` / `APPSTDOUT` / `APPSTDERR` / `APPTRUST` / `APPCONNECTOR` / `APPRUN` / `APPDELETE` app-lifecycle behavior, typed `DISPLAYINFO` / `DISPLAYMODES` / `DISPLAYSET` display behavior, typed `TRUSTPUT` / `TRUSTLIST` / `TRUSTINFO` / `TRUSTACTIVE` / `TRUSTSELECT` / `TRUSTDELETE` trust-store behavior, persisted `run-script` execution over the mock RTL8139 device, hostname-resolved plain-HTTP POST/response exchange over the freestanding PAL network path, bundle-trust configuration from an embedded root, and TLS `ClientHello` emission through the same mock RTL8139 seam, the PVH boot stack was expanded to `128 KiB` to cover the real DNS + TCP + HTTP + HTTPS + service path, and the live QEMU proofs `scripts/baremetal-qemu-rtl8139-arp-probe-check.ps1`, `scripts/baremetal-qemu-rtl8139-ipv4-probe-check.ps1`, `scripts/baremetal-qemu-rtl8139-udp-probe-check.ps1`, `scripts/baremetal-qemu-rtl8139-tcp-probe-check.ps1`, `scripts/baremetal-qemu-rtl8139-dhcp-probe-check.ps1`, `scripts/baremetal-qemu-rtl8139-dns-probe-check.ps1`, `scripts/baremetal-qemu-rtl8139-gateway-probe-check.ps1`, `scripts/baremetal-qemu-rtl8139-http-post-probe-check.ps1`, and `scripts/baremetal-qemu-rtl8139-https-post-probe-check.ps1` now prove ARP, IPv4, UDP, TCP handshake/payload exchange, bounded four-way close, bounded SYN/payload/FIN retransmission recovery, bounded two-flow session isolation, zero-window block/reopen, bounded sequential payload chunking, bounded sender congestion-window growth after ACK plus payload-timeout collapse, framed multi-request command-service exchange, structured `EXEC` request/response exchange, bounded typed batch request multiplexing on one flow with concatenated framed responses, typed TCP `PUT` upload with direct filesystem readback over attached disk media, typed `INSTALL` / `MANIFEST` runtime-layout service exchange with `/boot/loader.cfg` readback, typed TCP `PKG` / `PKGLIST` / `PKGINFO` / `PKGRUN` / `PKGAPP` / `PKGDISPLAY` package-service exchange, typed `PKGPUT` / `PKGLS` / `PKGGET` / `PKGDELETE` package-asset and uninstall exchange, typed `APPLIST` / `APPINFO` / `APPSTATE` / `APPHISTORY` / `APPSTDOUT` / `APPSTDERR` / `APPTRUST` / `APPCONNECTOR` / `APPRUN` / `APPDELETE` app-lifecycle exchange with persisted runtime-state readback, persisted history-log readback, persisted stdout/stderr readback, and uninstall cleanup, typed `DISPLAYINFO` / `DISPLAYMODES` / `DISPLAYSET` display exchange, typed `TRUSTPUT` / `TRUSTLIST` / `TRUSTINFO` / `TRUSTACTIVE` / `TRUSTSELECT` / `TRUSTDELETE` trust-store exchange, selected trust-bundle query/path readback, trust-bundle deletion, post-delete remaining-list readback, package manifest readback, package app-manifest readback, package display-profile persistence, package-directory listing, package output readback, live `run-package` display-mode application, explicit `DISPLAYSET` mode change/readback, DHCP discover framing/decode, DNS query/A-response transport, ARP-cache learning, gateway-routed UDP delivery, direct-subnet gateway bypass, hostname-resolved plain-HTTP POST/response exchange, and live TLS-backed HTTPS request/response exchange against a deterministic self-hosted harness over direct-IP transport with fixed probe time and filesystem-backed CA-bundle trust, and TX/RX counter advance over the freestanding PVH artifact. +- `FS5.5` broader runtime/service depth continues to advance above the strict closure bar: the latest slice adds persisted workspace plans under `/runtime/workspace-plans//.txt` plus `/runtime/workspace-plans//active.txt`, new CLI verbs (`workspace-plan-list`, `workspace-plan-info`, `workspace-plan-active`, `workspace-plan-save`, `workspace-plan-apply`, `workspace-plan-delete`), typed TCP verbs (`WORKSPACEPLANLIST`, `WORKSPACEPLANINFO`, `WORKSPACEPLANACTIVE`, `WORKSPACEPLANSAVE`, `WORKSPACEPLANAPPLY`, `WORKSPACEPLANDELETE`), RAM-disk and ATA-backed persistence tests, and a live RTL8139 TCP proof for save -> list -> info -> apply -> active -> restore -> delete with restored suite/trust/display/channel state; current local validation is green at `zig build test --summary all` -> `398/398` passed. - `FS5.5` filesystem usage strict closure is now reached locally: `src/baremetal/filesystem.zig` now provides a real path-based filesystem layer above the shared storage backend, `src/pal/fs.zig` routes the freestanding PAL through that layer, `src/baremetal_main.zig` exports filesystem state/entries, and both hosted + bare-metal host regressions prove path-based persistence over RAM-disk and ATA PIO (`/runtime/state/agent.json`, `/tools/cache/tool.txt`, `/tools/scripts/bootstrap.oc`, `/tools/script/output.txt`). - `FS5.5` bare-metal tool execution strict closure is now reached locally: `src/baremetal/tool_exec.zig` now provides the real freestanding builtin command substrate including persisted `run-script` execution, canonical `run-package`, `package-verify`, `package-app`, `package-display`, `package-ls`, `package-cat`, `app-list`, `app-info`, `app-state`, `app-history`, `app-stdout`, `app-stderr`, `app-trust`, `app-connector`, `app-run`, `display-info`, `display-modes`, and `display-set`, `src/baremetal/package_store.zig` now provides the canonical persisted package layout under `/packages//bin/main.oc`, `/packages//meta/package.txt`, `/packages//meta/app.txt`, and `/packages//assets/...` plus manifest `script_checksum`, `app_manifest_checksum`, and `asset_tree_checksum` fields, `src/pal/proc.zig` exposes explicit freestanding capture through `runCaptureFreestanding(...)`, `src/baremetal/tool_service.zig` now exposes the bounded typed request/response shim used by the bare-metal TCP proof including typed `PKGVERIFY`, the storage/filesystem dependency chain closes through `src/baremetal/filesystem.zig`, `src/pal/fs.zig`, and the shared storage backend, host/module validation proves the builtin dispatch path plus typed TCP file-service behavior, typed package-service behavior, typed package-app/package-display service behavior, typed app-lifecycle service behavior, typed app stdout/stderr service behavior, typed package-asset and display-query/control behavior, ATA-backed package persistence, persisted package display profiles, deterministic package-integrity mismatch reporting on script tamper via `field=script_checksum`, persisted app runtime-state receipts, persisted app stdout/stderr receipts, and canonical `run-package` / `app-run` execution on top of the same filesystem layer, and the live QEMU proof `scripts/baremetal-qemu-tool-exec-probe-check.ps1` now proves `help`, `mkdir`, `write-file`, `cat`, `stat`, `run-script`, direct filesystem readback, persisted script readback after filesystem reset/re-init, and `echo` over the freestanding PVH artifact with attached disk media while the live RTL8139 TCP proof now covers persisted app-state and stdout/stderr readback plus the typed `PKGVERIFY` success receipt on the live package tree. -- Latest FS5.5 autorun slice: `src/baremetal/app_runtime.zig` now persists `/runtime/apps/autorun.txt`, `src/baremetal/tool_exec.zig` now exposes `app-autorun-list`, `app-autorun-add`, `app-autorun-remove`, and `app-autorun-run`, `src/baremetal/tool_service.zig` now exposes `APPAUTORUNLIST`, `APPAUTORUNADD`, `APPAUTORUNREMOVE`, and `APPAUTORUNRUN`, host/module validation proves RAM-disk and ATA-backed autorun registry persistence plus autorun execution receipts, `src/baremetal/filesystem.zig` now carries an `80`-entry filesystem budget so the deeper FS5.5 package/trust/app/autorun/workspace runtime state fits on the persisted surface without live-service `NoSpace` failures, and the live RTL8139 TCP proof now covers autorun add/list/run/remove plus `/runtime/apps/autorun.txt`, `/runtime/apps/aux/last_run.txt`, and `/runtime/apps/aux/stdout.log` readback. +- Latest FS5.5 autorun slice: `src/baremetal/app_runtime.zig` now persists `/runtime/apps/autorun.txt`, `src/baremetal/tool_exec.zig` now exposes `app-autorun-list`, `app-autorun-add`, `app-autorun-remove`, and `app-autorun-run`, `src/baremetal/tool_service.zig` now exposes `APPAUTORUNLIST`, `APPAUTORUNADD`, `APPAUTORUNREMOVE`, and `APPAUTORUNRUN`, host/module validation proves RAM-disk and ATA-backed autorun registry persistence plus autorun execution receipts, `src/baremetal/filesystem.zig` now carries a `128`-entry filesystem budget so the deeper FS5.5 package/trust/app/autorun/workspace runtime state fits on the persisted surface without live-service `NoSpace` failures, and the live RTL8139 TCP proof now covers autorun add/list/run/remove plus `/runtime/apps/autorun.txt`, `/runtime/apps/aux/last_run.txt`, and `/runtime/apps/aux/stdout.log` readback. - Latest FS5.5 app-suite slice: `src/baremetal/app_runtime.zig` now persists `/runtime/app-suites/.txt`, `src/baremetal/tool_exec.zig` now exposes `app-suite-list`, `app-suite-info`, `app-suite-save`, `app-suite-apply`, `app-suite-run`, and `app-suite-delete`, `src/baremetal/tool_service.zig` now exposes `APPSUITELIST`, `APPSUITEINFO`, `APPSUITESAVE`, `APPSUITEAPPLY`, `APPSUITERUN`, and `APPSUITEDELETE`, host/module validation proves RAM-disk and ATA-backed app-suite persistence plus suite-driven active-plan restoration across multiple apps, and the live RTL8139 TCP proof now covers save/list/info/apply/run/delete plus restored active-plan, autorun, and stdout readback. - Latest FS5.5 app-suite-release slice: `src/baremetal/app_runtime.zig` now persists `/runtime/app-suite-releases///suite.txt` plus `release.txt`, `src/baremetal/tool_exec.zig` now exposes `app-suite-release-list`, `app-suite-release-info`, `app-suite-release-save`, `app-suite-release-activate`, `app-suite-release-delete`, and `app-suite-release-prune`, `src/baremetal/tool_service.zig` now exposes `APPSUITERELEASELIST`, `APPSUITERELEASEINFO`, `APPSUITERELEASESAVE`, `APPSUITERELEASEACTIVATE`, `APPSUITERELEASEDELETE`, and `APPSUITERELEASEPRUNE`, host/module validation proves RAM-disk and ATA-backed app-suite-release persistence with deterministic `saved_seq` / `saved_tick` metadata, and the live RTL8139 TCP proof now covers save -> mutate -> list -> info -> activate -> delete -> prune plus restored suite readback. - Latest FS5.5 app-suite-release-channel slice: `src/baremetal/app_runtime.zig` now persists `/runtime/app-suite-release-channels//.txt`, `src/baremetal/tool_exec.zig` now exposes `app-suite-release-channel-list`, `app-suite-release-channel-info`, `app-suite-release-channel-set`, and `app-suite-release-channel-activate`, `src/baremetal/tool_service.zig` now exposes `APPSUITECHANNELLIST`, `APPSUITECHANNELINFO`, `APPSUITECHANNELSET`, and `APPSUITECHANNELACTIVATE`, host/module validation proves RAM-disk and ATA-backed app-suite-release-channel persistence, and the live RTL8139 TCP proof now covers set -> persisted channel-target readback -> list -> info -> activate plus restored suite readback through the selected suite release channel. - Latest FS5.5 workspace slice: `src/baremetal/workspace_runtime.zig` now persists `/runtime/workspaces/.txt` orchestration state plus `/runtime/workspace-runs//last_run.txt`, `history.log`, `stdout.log`, `stderr.log`, `/runtime/workspace-runs/autorun.txt`, and versioned release snapshots under `/runtime/workspace-releases///workspace.txt` plus `release.txt`, `src/baremetal/tool_exec.zig` now exposes `workspace-list`, `workspace-info`, `workspace-save`, `workspace-apply`, `workspace-run`, `workspace-state`, `workspace-history`, `workspace-stdout`, `workspace-stderr`, `workspace-delete`, `workspace-release-list`, `workspace-release-info`, `workspace-release-save`, `workspace-release-activate`, `workspace-release-delete`, `workspace-release-prune`, `workspace-autorun-list`, `workspace-autorun-add`, `workspace-autorun-remove`, and `workspace-autorun-run`, `src/baremetal/tool_service.zig` now exposes `WORKSPACELIST`, `WORKSPACEINFO`, `WORKSPACESAVE`, `WORKSPACEAPPLY`, `WORKSPACERUN`, `WORKSPACESTATE`, `WORKSPACEHISTORY`, `WORKSPACESTDOUT`, `WORKSPACESTDERR`, `WORKSPACEDELETE`, `WORKSPACERELEASELIST`, `WORKSPACERELEASEINFO`, `WORKSPACERELEASESAVE`, `WORKSPACERELEASEACTIVATE`, `WORKSPACERELEASEDELETE`, `WORKSPACERELEASEPRUNE`, `WORKSPACEAUTORUNLIST`, `WORKSPACEAUTORUNADD`, `WORKSPACEAUTORUNREMOVE`, and `WORKSPACEAUTORUNRUN`, host/module validation proves RAM-disk and ATA-backed workspace persistence plus workspace release lifecycle, workspace autorun registry persistence, persisted runtime-receipt readback, and delete cleanup, the live RTL8139 TCP proof now covers save/list/info/apply/run/state/history/stdout/stderr/delete plus workspace release save/mutate/list/info/activate/delete/prune and workspace autorun add/list/run/remove on the persisted workspace surface together with persisted `/runtime/workspace-runs/autorun.txt` readback, persisted release readback, restored workspace info, restored canonical package script content, trust-bundle selection, display mode, and app-suite active-plan markers, and `src/baremetal/filesystem.zig` now carries a `96`-entry filesystem budget so that deeper persisted surface no longer fails with live-service `NoSpace`. - Latest FS5.5 workspace-release-channel slice: `src/baremetal/workspace_runtime.zig` now persists `/runtime/workspace-release-channels//.txt` mappings, `src/baremetal/tool_exec.zig` now exposes `workspace-release-channel-list`, `workspace-release-channel-info`, `workspace-release-channel-set`, and `workspace-release-channel-activate`, `src/baremetal/tool_service.zig` now exposes `WORKSPACECHANNELLIST`, `WORKSPACECHANNELINFO`, `WORKSPACECHANNELSET`, and `WORKSPACECHANNELACTIVATE`, host/module validation proves RAM-disk and ATA-backed workspace-channel persistence, and the live RTL8139 TCP proof now covers set -> info -> list -> activate plus persisted channel-target readback and restored workspace info through the selected workspace release channel. +- Latest FS5.5 workspace-suite slice: `src/baremetal/workspace_runtime.zig` now persists `/runtime/workspace-suites/.txt` orchestration groups, `src/baremetal/tool_exec.zig` now exposes `workspace-suite-list`, `workspace-suite-info`, `workspace-suite-save`, `workspace-suite-apply`, `workspace-suite-run`, and `workspace-suite-delete`, `src/baremetal/tool_service.zig` now exposes `WORKSPACESUITELIST`, `WORKSPACESUITEINFO`, `WORKSPACESUITESAVE`, `WORKSPACESUITEAPPLY`, `WORKSPACESUITERUN`, and `WORKSPACESUITEDELETE`, host/module validation proves RAM-disk and ATA-backed workspace-suite persistence, and the live RTL8139 TCP proof now covers save -> persisted suite-file readback -> list -> info -> apply -> run -> delete plus post-delete suite absence on the higher-level workspace orchestration surface. +- Latest FS5.5 workspace-suite-release slice: `src/baremetal/workspace_runtime.zig` now persists `/runtime/workspace-suite-releases///suite.txt` plus `release.txt`, `src/baremetal/tool_exec.zig` now exposes `workspace-suite-release-list`, `workspace-suite-release-info`, `workspace-suite-release-save`, `workspace-suite-release-activate`, `workspace-suite-release-delete`, and `workspace-suite-release-prune`, `src/baremetal/tool_service.zig` now exposes `WORKSPACESUITERELEASELIST`, `WORKSPACESUITERELEASEINFO`, `WORKSPACESUITERELEASESAVE`, `WORKSPACESUITERELEASEACTIVATE`, `WORKSPACESUITERELEASEDELETE`, and `WORKSPACESUITERELEASEPRUNE`, host/module validation proves RAM-disk and ATA-backed workspace-suite-release persistence with deterministic `saved_seq` / `saved_tick` metadata, and the live RTL8139 TCP proof now covers save -> mutate -> list -> info -> activate -> delete -> prune plus restored suite readback and post-delete suite-release absence. +- Latest FS5.5 workspace-suite-release-channel slice: `src/baremetal/workspace_runtime.zig` now persists `/runtime/workspace-suite-release-channels//.txt`, `src/baremetal/tool_exec.zig` now exposes `workspace-suite-release-channel-list`, `workspace-suite-release-channel-info`, `workspace-suite-release-channel-set`, and `workspace-suite-release-channel-activate`, `src/baremetal/tool_service.zig` now exposes `WORKSPACESUITECHANNELLIST`, `WORKSPACESUITECHANNELINFO`, `WORKSPACESUITECHANNELSET`, and `WORKSPACESUITECHANNELACTIVATE`, host/module validation proves RAM-disk and ATA-backed workspace-suite-release-channel persistence, and the live RTL8139 TCP proof now covers set -> persisted channel-target readback -> list -> info -> activate plus restored suite readback through the selected workspace-suite release channel. - Latest FS5.5 package-release slice: `src/baremetal/package_store.zig` now snapshots, reports, deletes, prunes, and reactivates persisted package releases under `/packages//releases//...` with deterministic `saved_seq` / `saved_tick` metadata, `src/baremetal/tool_exec.zig` now exposes `package-release-list`, `package-release-info`, `package-release-save`, `package-release-activate`, `package-release-delete`, and `package-release-prune`, `src/baremetal/tool_service.zig` now exposes `PKGRELEASELIST`, `PKGRELEASEINFO`, `PKGRELEASESAVE`, `PKGRELEASEACTIVATE`, `PKGRELEASEDELETE`, and `PKGRELEASEPRUNE`, host/module validation proves RAM-disk and ATA-backed release info/delete/prune behavior with deterministic newest-release retention plus restored canonical script and asset readback, and the live RTL8139 TCP proof now covers save -> mutate -> info -> list -> activate -> delete -> prune plus restored `PKGRUN`, `/packages//bin/main.oc`, `config/app.json`, and asset readback. - Latest FS5.5 package-release-channel slice: `src/baremetal/package_store.zig` now persists `/packages//channels/.txt` release-target mappings, `src/baremetal/tool_exec.zig` now exposes `package-release-channel-list`, `package-release-channel-info`, `package-release-channel-set`, and `package-release-channel-activate`, `src/baremetal/tool_service.zig` now exposes `PKGCHANNELLIST`, `PKGCHANNELINFO`, `PKGCHANNELSET`, and `PKGCHANNELACTIVATE`, host/module validation proves RAM-disk and ATA-backed release-channel persistence plus restored canonical script and asset readback after channel activation, and the live RTL8139 TCP proof now covers set -> info -> list -> mutate -> activate plus restored `PKGRUN`, `/packages//bin/main.oc`, and `config/app.json` readback through the selected channel target. - Latest FS5.5 runtime-service slice: `src/baremetal/runtime_bridge.zig` now provides the shared bare-metal runtime seam, `src/baremetal/tool_exec.zig` now exposes `runtime-snapshot`, `runtime-sessions`, and `runtime-session`, `src/baremetal/tool_service.zig` now exposes `RUNTIMECALL`, `RUNTIMESNAPSHOT`, `RUNTIMESESSIONS`, and `RUNTIMESESSION`, `src/runtime/tool_runtime.zig` now keeps `runtime.session.get` lifetime-safe, `src/pal/fs.zig` now routes logical `/runtime/...` paths through the bare-metal filesystem during hosted FS5.5 validation, host/module validation proves runtime snapshot/session query and RPC call bridging on the persisted bare-metal surface, and the dedicated live RTL8139 runtime-service proof `scripts/baremetal-qemu-rtl8139-runtime-service-probe-check.ps1` now covers runtime file-write/exec/read plus `/runtime/state/runtime-state.json` and `/runtime/tmp/service-runtime.txt` readback. diff --git a/docs/zig-port/PORT_PLAN.md b/docs/zig-port/PORT_PLAN.md index 9c51b66a..2dab43a8 100644 --- a/docs/zig-port/PORT_PLAN.md +++ b/docs/zig-port/PORT_PLAN.md @@ -1960,7 +1960,7 @@ Full-stack replacement execution reference: - `src/baremetal_main.zig` now also drives the live RTL8139 TCP proof through `PKGCHANNELSET`, `PKGCHANNELINFO`, `PKGCHANNELLIST`, canonical package mutation, and `PKGCHANNELACTIVATE`, with restored `PKGRUN`, restored `/packages//bin/main.oc` readback, and restored `config/app.json` readback through the selected release-channel target. - `src/baremetal/runtime_bridge.zig` now provides the shared bare-metal runtime seam, `src/baremetal/tool_exec.zig` now exposes `runtime-snapshot`, `runtime-sessions`, and `runtime-session`, `src/baremetal/tool_service.zig` now exposes `RUNTIMECALL`, `RUNTIMESNAPSHOT`, `RUNTIMESESSIONS`, and `RUNTIMESESSION`, `src/runtime/tool_runtime.zig` now keeps `runtime.session.get` lifetime-safe, and `src/pal/fs.zig` now routes logical `/runtime/...` paths through the bare-metal filesystem during hosted FS5.5 validation so runtime persistence lands on the same RAM-disk/ATA-backed surface as the rest of the bare-metal stack. - `src/baremetal_main.zig` now keeps the broad live RTL8139 TCP proof on the stable package/trust/app/display seam and adds a dedicated `runRtl8139RuntimeServiceProbe()` live path for runtime file-write/exec/read plus `RUNTIMESNAPSHOT`, `RUNTIMESESSIONS`, and `RUNTIMESESSION`, with persisted `/runtime/state/runtime-state.json` and `/runtime/tmp/service-runtime.txt` readback over the same live service seam. - - `src/baremetal/filesystem.zig` now carries an `80`-entry filesystem budget so the deeper FS5.5 package/trust/app/autorun/workspace runtime state fits on the persisted surface without live-service `NoSpace` failures. + - `src/baremetal/filesystem.zig` now carries a `128`-entry filesystem budget so the deeper FS5.5 package/trust/app/autorun/workspace runtime state fits on the persisted surface without live-service `NoSpace` failures. - `scripts/baremetal-qemu-rtl8139-https-post-probe-check.ps1` plus `scripts/qemu-rtl8139-https-post-server.ps1` now prove the live freestanding HTTPS transport path end to end against a deterministic self-hosted TLS harness, including direct-IP transport (`https://10.0.2.2:8443/...`), TCP connect, TLS handshake, HTTPS POST write, HTTPS response readback, persistent filesystem-backed trust-store selection plus bounded CA-bundle verification with fixed probe time, and allocator-owned body buffering. - the raw `debugcon` byte trail used to isolate the original `ClientHello` stall was removed after closure; the durable stage/counter diagnostics remain. - full validation after the package-release retention slice is green: @@ -2019,18 +2019,38 @@ Full-stack replacement execution reference: - `src/baremetal/tool_service.zig` now exposes `WORKSPACELIST`, `WORKSPACEINFO`, `WORKSPACESAVE`, `WORKSPACEAPPLY`, `WORKSPACERUN`, `WORKSPACESTATE`, `WORKSPACEHISTORY`, `WORKSPACESTDOUT`, `WORKSPACESTDERR`, `WORKSPACEDELETE`, `WORKSPACERELEASELIST`, `WORKSPACERELEASEINFO`, `WORKSPACERELEASESAVE`, `WORKSPACERELEASEACTIVATE`, `WORKSPACERELEASEDELETE`, `WORKSPACERELEASEPRUNE`, `WORKSPACEAUTORUNLIST`, `WORKSPACEAUTORUNADD`, `WORKSPACEAUTORUNREMOVE`, and `WORKSPACEAUTORUNRUN` - host validation now proves RAM-disk and ATA-backed workspace persistence plus workspace release save/list/info/activate/delete/prune, workspace autorun registry persistence, persisted runtime-receipt readback, restored canonical package script content, trust-bundle selection, display mode, and app-suite active-plan markers - the broad live RTL8139 TCP proof now covers save -> list -> info -> apply -> run -> state -> history -> stdout -> stderr -> delete plus workspace release save -> mutate -> list -> info -> activate -> delete -> prune, workspace autorun add -> list -> run -> remove, persisted `/runtime/workspace-runs/autorun.txt` readback, persisted release file readback, restored workspace info, restored canonical package content, persisted runtime-receipt readback, and delete cleanup on the workspace surface + - `src/baremetal/workspace_runtime.zig` now also persists workspace plans under `/runtime/workspace-plans//.txt` plus `/runtime/workspace-plans//active.txt`, with `planListAlloc`, `planInfoAlloc`, `activePlanInfoAlloc`, `savePlan`, `applyPlan`, and `deletePlan` + - `src/baremetal/tool_exec.zig` now also exposes `workspace-plan-list`, `workspace-plan-info`, `workspace-plan-active`, `workspace-plan-save`, `workspace-plan-apply`, and `workspace-plan-delete` + - `src/baremetal/tool_service.zig` now also exposes `WORKSPACEPLANLIST`, `WORKSPACEPLANINFO`, `WORKSPACEPLANACTIVE`, `WORKSPACEPLANSAVE`, `WORKSPACEPLANAPPLY`, and `WORKSPACEPLANDELETE` + - host validation now also proves RAM-disk and ATA-backed workspace-plan persistence plus restored suite/trust/display/channel state after plan apply + - the broad live RTL8139 TCP proof now also covers workspace plan save -> list -> info -> apply -> active -> restore -> delete with restored suite/trust/display/channel readback on the persisted workspace surface - `src/baremetal/workspace_runtime.zig` now also persists workspace release-channel targets under `/runtime/workspace-release-channels//.txt`, with `channelListAlloc`, `channelInfoAlloc`, `setWorkspaceReleaseChannel`, and `activateWorkspaceReleaseChannel` - `src/baremetal/tool_exec.zig` now also exposes `workspace-release-channel-list`, `workspace-release-channel-info`, `workspace-release-channel-set`, and `workspace-release-channel-activate` - `src/baremetal/tool_service.zig` now also exposes `WORKSPACECHANNELLIST`, `WORKSPACECHANNELINFO`, `WORKSPACECHANNELSET`, and `WORKSPACECHANNELACTIVATE` - host validation now also proves RAM-disk and ATA-backed workspace release-channel persistence plus restored workspace info through channel activation - the broad live RTL8139 TCP proof now also covers workspace release channel set -> info -> list -> activate, persisted channel-target readback, and restored workspace info through the selected workspace release channel - - `src/baremetal/filesystem.zig` now carries a `96`-entry filesystem budget so the deeper FS5.5 package/trust/app/workspace release surface no longer fails with live-service `NoSpace` - - current local validation after the app-suite-release-channel slice is green: - - `zig build test --summary all` -> `387/387` passed + - `src/baremetal/workspace_runtime.zig` now also persists workspace suites under `/runtime/workspace-suites/.txt`, with `suiteListAlloc`, `suiteEntriesAlloc`, `suiteInfoAlloc`, `saveSuite`, `applySuite`, and `deleteSuite` + - `src/baremetal/tool_exec.zig` now also exposes `workspace-suite-list`, `workspace-suite-info`, `workspace-suite-save`, `workspace-suite-apply`, `workspace-suite-run`, and `workspace-suite-delete` + - `src/baremetal/tool_service.zig` now also exposes `WORKSPACESUITELIST`, `WORKSPACESUITEINFO`, `WORKSPACESUITESAVE`, `WORKSPACESUITEAPPLY`, `WORKSPACESUITERUN`, and `WORKSPACESUITEDELETE` + - host validation now also proves RAM-disk and ATA-backed workspace-suite persistence on top of the existing workspace orchestration surface + - the broad live RTL8139 TCP proof now also covers workspace suite save -> persisted suite-file readback -> list -> info -> apply -> run -> delete plus post-delete suite absence + - `src/baremetal/workspace_runtime.zig` now also persists workspace suite releases under `/runtime/workspace-suite-releases///suite.txt` plus `release.txt`, with `suiteReleaseListAlloc`, `suiteReleaseInfoAlloc`, `snapshotSuiteRelease`, `activateSuiteRelease`, `deleteSuiteRelease`, and `pruneSuiteReleases` + - `src/baremetal/tool_exec.zig` now also exposes `workspace-suite-release-list`, `workspace-suite-release-info`, `workspace-suite-release-save`, `workspace-suite-release-activate`, `workspace-suite-release-delete`, and `workspace-suite-release-prune` + - `src/baremetal/tool_service.zig` now also exposes `WORKSPACESUITERELEASELIST`, `WORKSPACESUITERELEASEINFO`, `WORKSPACESUITERELEASESAVE`, `WORKSPACESUITERELEASEACTIVATE`, `WORKSPACESUITERELEASEDELETE`, and `WORKSPACESUITERELEASEPRUNE` + - host validation now also proves RAM-disk and ATA-backed workspace-suite-release persistence with deterministic `saved_seq` / `saved_tick` metadata + - the broad live RTL8139 TCP proof now also covers workspace suite release save -> mutate -> list -> info -> activate -> delete -> prune plus restored suite readback and post-delete suite-release absence + - `src/baremetal/workspace_runtime.zig` now also persists workspace suite release-channel targets under `/runtime/workspace-suite-release-channels//.txt`, with `suiteChannelListAlloc`, `suiteChannelInfoAlloc`, `setSuiteReleaseChannel`, and `activateSuiteReleaseChannel` + - `src/baremetal/tool_exec.zig` now also exposes `workspace-suite-release-channel-list`, `workspace-suite-release-channel-info`, `workspace-suite-release-channel-set`, and `workspace-suite-release-channel-activate` + - `src/baremetal/tool_service.zig` now also exposes `WORKSPACESUITECHANNELLIST`, `WORKSPACESUITECHANNELINFO`, `WORKSPACESUITECHANNELSET`, and `WORKSPACESUITECHANNELACTIVATE` + - host validation now also proves RAM-disk and ATA-backed workspace-suite-release-channel persistence plus restored suite readback through channel activation + - the broad live RTL8139 TCP proof now also covers workspace suite release-channel set -> persisted channel-target readback -> list -> info -> activate plus restored suite readback through the selected workspace-suite release channel + - `src/baremetal/filesystem.zig` now carries a `128`-entry filesystem budget so the deeper FS5.5 package/trust/app/workspace/workspace-plan/workspace-suite release surface no longer fails with live-service `NoSpace` + - current local validation after the workspace-plan slice is green: + - `zig build test --summary all` -> `398/398` passed - `scripts/baremetal-qemu-rtl8139-tcp-probe-check.ps1 -TimeoutSeconds 120` -> pass - parity gate -> pass (`union 141/141`, `events 19/19`) - docs status gate -> pass - - the real regressions fixed during the slice were a stale live fallback snapshot that still targeted `demo:golden` instead of `demo:canary`, plus downstream workspace and app-plan-delete expectations that still assumed the pre-channel state; the runtime, CLI, typed service, and live proof now match the current app-suite-release-channel contract + - the real regressions fixed during the slice were proof-order drift that referenced pruned package release `r2` after the earlier release lifecycle moved on to `r3`, a wrong staged delete payload length constant for `WORKSPACEPLANDELETE`, and the new plan surface itself (`WORKSPACEPLAN*` parsing, handlers, runtime persistence, and live proof expectations); the runtime, CLI, typed service, and live proof now match the current workspace-plan contract - `build.zig` now runs the compiled native test executables directly on Windows, which avoids the current Zig master `--listen=-` test-runner hang while keeping the real hosted and baremetal-host test matrix intact diff --git a/src/baremetal/filesystem.zig b/src/baremetal/filesystem.zig index 6953f23d..c81c2431 100644 --- a/src/baremetal/filesystem.zig +++ b/src/baremetal/filesystem.zig @@ -4,7 +4,7 @@ const abi = @import("abi.zig"); const storage_backend = @import("storage_backend.zig"); const tool_layout = @import("tool_layout.zig"); -pub const max_entries: usize = 96; +pub const max_entries: usize = 128; // Keep filesystem paths large enough for hosted absolute temp/workspace roots while // still leaving each persisted entry at a tidy 256-byte ABI footprint. pub const max_path_len: usize = 224; diff --git a/src/baremetal/tool_exec.zig b/src/baremetal/tool_exec.zig index 519451f2..741cebf6 100644 --- a/src/baremetal/tool_exec.zig +++ b/src/baremetal/tool_exec.zig @@ -154,7 +154,7 @@ fn execute( if (depth > max_script_depth) return error.ScriptDepthExceeded; if (std.ascii.eqlIgnoreCase(parsed.name, "help")) { - try stdout_buffer.appendLine("OpenClaw bare-metal builtins: help, echo, cat, write-file, mkdir, stat, ls, package-info, package-verify, package-app, package-display, package-ls, package-cat, package-delete, package-release-list, package-release-info, package-release-save, package-release-activate, package-release-delete, package-release-prune, package-release-channel-list, package-release-channel-info, package-release-channel-set, package-release-channel-activate, app-list, app-info, app-state, app-history, app-stdout, app-stderr, app-trust, app-connector, app-plan-list, app-plan-info, app-plan-active, app-plan-save, app-plan-apply, app-plan-delete, app-suite-list, app-suite-info, app-suite-save, app-suite-apply, app-suite-run, app-suite-delete, app-suite-release-list, app-suite-release-info, app-suite-release-save, app-suite-release-activate, app-suite-release-delete, app-suite-release-prune, app-suite-release-channel-list, app-suite-release-channel-info, app-suite-release-channel-set, app-suite-release-channel-activate, app-delete, app-autorun-list, app-autorun-add, app-autorun-remove, app-autorun-run, workspace-list, workspace-info, workspace-save, workspace-apply, workspace-run, workspace-state, workspace-history, workspace-stdout, workspace-stderr, workspace-delete, workspace-release-list, workspace-release-info, workspace-release-save, workspace-release-activate, workspace-release-delete, workspace-release-prune, workspace-release-channel-list, workspace-release-channel-info, workspace-release-channel-set, workspace-release-channel-activate, workspace-autorun-list, workspace-autorun-add, workspace-autorun-remove, workspace-autorun-run, trust-list, trust-info, trust-active, trust-select, trust-delete, runtime-snapshot, runtime-sessions, runtime-session, display-info, display-modes, display-set, run-script, run-package, app-run"); + try stdout_buffer.appendLine("OpenClaw bare-metal builtins: help, echo, cat, write-file, mkdir, stat, ls, package-info, package-verify, package-app, package-display, package-ls, package-cat, package-delete, package-release-list, package-release-info, package-release-save, package-release-activate, package-release-delete, package-release-prune, package-release-channel-list, package-release-channel-info, package-release-channel-set, package-release-channel-activate, app-list, app-info, app-state, app-history, app-stdout, app-stderr, app-trust, app-connector, app-plan-list, app-plan-info, app-plan-active, app-plan-save, app-plan-apply, app-plan-delete, app-suite-list, app-suite-info, app-suite-save, app-suite-apply, app-suite-run, app-suite-delete, app-suite-release-list, app-suite-release-info, app-suite-release-save, app-suite-release-activate, app-suite-release-delete, app-suite-release-prune, app-suite-release-channel-list, app-suite-release-channel-info, app-suite-release-channel-set, app-suite-release-channel-activate, app-delete, app-autorun-list, app-autorun-add, app-autorun-remove, app-autorun-run, workspace-plan-list, workspace-plan-info, workspace-plan-active, workspace-plan-save, workspace-plan-apply, workspace-plan-delete, workspace-suite-list, workspace-suite-info, workspace-suite-save, workspace-suite-apply, workspace-suite-run, workspace-suite-delete, workspace-suite-release-list, workspace-suite-release-info, workspace-suite-release-save, workspace-suite-release-activate, workspace-suite-release-delete, workspace-suite-release-prune, workspace-suite-release-channel-list, workspace-suite-release-channel-info, workspace-suite-release-channel-set, workspace-suite-release-channel-activate, workspace-list, workspace-info, workspace-save, workspace-apply, workspace-run, workspace-state, workspace-history, workspace-stdout, workspace-stderr, workspace-delete, workspace-release-list, workspace-release-info, workspace-release-save, workspace-release-activate, workspace-release-delete, workspace-release-prune, workspace-release-channel-list, workspace-release-channel-info, workspace-release-channel-set, workspace-release-channel-activate, workspace-autorun-list, workspace-autorun-add, workspace-autorun-remove, workspace-autorun-run, trust-list, trust-info, trust-active, trust-select, trust-delete, runtime-snapshot, runtime-sessions, runtime-session, display-info, display-modes, display-set, run-script, run-package, app-run"); return; } @@ -1528,6 +1528,545 @@ fn execute( return; } + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-plan-list")) { + const workspace_name = parseFirstArg(parsed.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-plan-list "); + return; + }; + if (workspace_name.rest.len != 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-plan-list "); + return; + } + const listing = workspace_runtime.planListAlloc(allocator, workspace_name.arg, stdout_buffer.limit) catch |err| { + exit_code.* = 1; + try stderr_buffer.appendFmt("workspace-plan-list failed: {s}\n", .{@errorName(err)}); + return; + }; + defer allocator.free(listing); + try stdout_buffer.appendSlice(listing); + return; + } + + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-plan-info")) { + const workspace_name = parseFirstArg(parsed.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-plan-info "); + return; + }; + const plan_name = parseFirstArg(workspace_name.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-plan-info "); + return; + }; + if (plan_name.rest.len != 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-plan-info "); + return; + } + const info = workspace_runtime.planInfoAlloc(allocator, workspace_name.arg, plan_name.arg, stdout_buffer.limit) catch |err| { + exit_code.* = 1; + try stderr_buffer.appendFmt("workspace-plan-info failed: {s}\n", .{@errorName(err)}); + return; + }; + defer allocator.free(info); + try stdout_buffer.appendSlice(info); + return; + } + + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-plan-active")) { + const workspace_name = parseFirstArg(parsed.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-plan-active "); + return; + }; + if (workspace_name.rest.len != 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-plan-active "); + return; + } + const info = workspace_runtime.activePlanInfoAlloc(allocator, workspace_name.arg, stdout_buffer.limit) catch |err| { + exit_code.* = 1; + try stderr_buffer.appendFmt("workspace-plan-active failed: {s}\n", .{@errorName(err)}); + return; + }; + defer allocator.free(info); + try stdout_buffer.appendSlice(info); + return; + } + + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-plan-save")) { + const workspace_name = parseFirstArg(parsed.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-plan-save [package:channel:release...]"); + return; + }; + const plan_name = parseFirstArg(workspace_name.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-plan-save [package:channel:release...]"); + return; + }; + const suite_name = parseFirstArg(plan_name.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-plan-save [package:channel:release...]"); + return; + }; + const trust_name = parseFirstArg(suite_name.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-plan-save [package:channel:release...]"); + return; + }; + const width_arg = parseFirstArg(trust_name.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-plan-save [package:channel:release...]"); + return; + }; + const height_arg = parseFirstArg(width_arg.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-plan-save [package:channel:release...]"); + return; + }; + const width = std.fmt.parseInt(u16, width_arg.arg, 10) catch { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-plan-save [package:channel:release...]"); + return; + }; + const height = std.fmt.parseInt(u16, height_arg.arg, 10) catch { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-plan-save [package:channel:release...]"); + return; + }; + const selected_suite = if (std.ascii.eqlIgnoreCase(suite_name.arg, "none")) "" else suite_name.arg; + const selected_trust = if (std.ascii.eqlIgnoreCase(trust_name.arg, "none")) "" else trust_name.arg; + workspace_runtime.savePlan(workspace_name.arg, plan_name.arg, selected_suite, selected_trust, width, height, height_arg.rest, 0) catch |err| { + exit_code.* = 1; + try stderr_buffer.appendFmt("workspace-plan-save failed: {s}\n", .{@errorName(err)}); + return; + }; + try stdout_buffer.appendFmt("workspace plan saved {s} {s}\n", .{ workspace_name.arg, plan_name.arg }); + return; + } + + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-plan-apply")) { + const workspace_name = parseFirstArg(parsed.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-plan-apply "); + return; + }; + const plan_name = parseFirstArg(workspace_name.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-plan-apply "); + return; + }; + if (plan_name.rest.len != 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-plan-apply "); + return; + } + workspace_runtime.applyPlan(workspace_name.arg, plan_name.arg, 0) catch |err| { + exit_code.* = 1; + try stderr_buffer.appendFmt("workspace-plan-apply failed: {s}\n", .{@errorName(err)}); + return; + }; + try stdout_buffer.appendFmt("workspace plan applied {s} {s}\n", .{ workspace_name.arg, plan_name.arg }); + return; + } + + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-plan-delete")) { + const workspace_name = parseFirstArg(parsed.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-plan-delete "); + return; + }; + const plan_name = parseFirstArg(workspace_name.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-plan-delete "); + return; + }; + if (plan_name.rest.len != 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-plan-delete "); + return; + } + workspace_runtime.deletePlan(workspace_name.arg, plan_name.arg, 0) catch |err| { + exit_code.* = 1; + try stderr_buffer.appendFmt("workspace-plan-delete failed: {s}\n", .{@errorName(err)}); + return; + }; + try stdout_buffer.appendFmt("workspace plan deleted {s} {s}\n", .{ workspace_name.arg, plan_name.arg }); + return; + } + + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-suite-list")) { + if (parsed.rest.len != 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-suite-list"); + return; + } + const listing = workspace_runtime.suiteListAlloc(allocator, stdout_buffer.limit) catch |err| { + exit_code.* = 1; + try stderr_buffer.appendFmt("workspace-suite-list failed: {s}\n", .{@errorName(err)}); + return; + }; + defer allocator.free(listing); + try stdout_buffer.appendSlice(listing); + return; + } + + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-suite-info")) { + const arg = parseFirstArg(parsed.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-info "); + return; + }; + if (arg.rest.len != 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-suite-info "); + return; + } + const info = workspace_runtime.suiteInfoAlloc(allocator, arg.arg, stdout_buffer.limit) catch |err| { + exit_code.* = 1; + try stderr_buffer.appendFmt("workspace-suite-info failed: {s}\n", .{@errorName(err)}); + return; + }; + defer allocator.free(info); + try stdout_buffer.appendSlice(info); + return; + } + + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-suite-save")) { + const suite_name = parseFirstArg(parsed.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-save "); + return; + }; + if (suite_name.rest.len == 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-suite-save "); + return; + } + workspace_runtime.saveSuite(suite_name.arg, suite_name.rest, 0) catch |err| { + exit_code.* = 1; + try stderr_buffer.appendFmt("workspace-suite-save failed: {s}\n", .{@errorName(err)}); + return; + }; + try stdout_buffer.appendFmt("workspace suite saved {s}\n", .{suite_name.arg}); + return; + } + + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-suite-apply")) { + const arg = parseFirstArg(parsed.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-apply "); + return; + }; + if (arg.rest.len != 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-suite-apply "); + return; + } + workspace_runtime.applySuite(arg.arg, 0) catch |err| { + exit_code.* = 1; + try stderr_buffer.appendFmt("workspace-suite-apply failed: {s}\n", .{@errorName(err)}); + return; + }; + try stdout_buffer.appendFmt("workspace suite applied {s}\n", .{arg.arg}); + return; + } + + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-suite-run")) { + const arg = parseFirstArg(parsed.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-run "); + return; + }; + if (arg.rest.len != 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-suite-run "); + return; + } + try runWorkspaceSuite(arg.arg, "workspace-suite-run", stdout_buffer, stderr_buffer, exit_code, allocator, depth); + return; + } + + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-suite-delete")) { + const arg = parseFirstArg(parsed.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-delete "); + return; + }; + if (arg.rest.len != 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-suite-delete "); + return; + } + workspace_runtime.deleteSuite(arg.arg, 0) catch |err| { + exit_code.* = 1; + try stderr_buffer.appendFmt("workspace-suite-delete failed: {s}\n", .{@errorName(err)}); + return; + }; + try stdout_buffer.appendFmt("workspace suite deleted {s}\n", .{arg.arg}); + return; + } + + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-suite-release-list")) { + const arg = parseFirstArg(parsed.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-release-list "); + return; + }; + if (arg.rest.len != 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-suite-release-list "); + return; + } + const listing = workspace_runtime.suiteReleaseListAlloc(allocator, arg.arg, stdout_buffer.limit) catch |err| { + exit_code.* = 1; + try stderr_buffer.appendFmt("workspace-suite-release-list failed: {s}\n", .{@errorName(err)}); + return; + }; + defer allocator.free(listing); + try stdout_buffer.appendSlice(listing); + return; + } + + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-suite-release-info")) { + const suite_name = parseFirstArg(parsed.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-release-info "); + return; + }; + const release_name = parseFirstArg(suite_name.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-release-info "); + return; + }; + if (release_name.rest.len != 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-suite-release-info "); + return; + } + const info = workspace_runtime.suiteReleaseInfoAlloc(allocator, suite_name.arg, release_name.arg, stdout_buffer.limit) catch |err| { + exit_code.* = 1; + try stderr_buffer.appendFmt("workspace-suite-release-info failed: {s}\n", .{@errorName(err)}); + return; + }; + defer allocator.free(info); + try stdout_buffer.appendSlice(info); + return; + } + + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-suite-release-save")) { + const suite_name = parseFirstArg(parsed.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-release-save "); + return; + }; + const release_name = parseFirstArg(suite_name.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-release-save "); + return; + }; + if (release_name.rest.len != 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-suite-release-save "); + return; + } + workspace_runtime.snapshotSuiteRelease(suite_name.arg, release_name.arg, 0) catch |err| { + exit_code.* = 1; + try stderr_buffer.appendFmt("workspace-suite-release-save failed: {s}\n", .{@errorName(err)}); + return; + }; + try stdout_buffer.appendFmt("workspace suite release saved {s} {s}\n", .{ suite_name.arg, release_name.arg }); + return; + } + + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-suite-release-activate")) { + const suite_name = parseFirstArg(parsed.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-release-activate "); + return; + }; + const release_name = parseFirstArg(suite_name.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-release-activate "); + return; + }; + if (release_name.rest.len != 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-suite-release-activate "); + return; + } + workspace_runtime.activateSuiteRelease(suite_name.arg, release_name.arg, 0) catch |err| { + exit_code.* = 1; + try stderr_buffer.appendFmt("workspace-suite-release-activate failed: {s}\n", .{@errorName(err)}); + return; + }; + try stdout_buffer.appendFmt("workspace suite release activated {s} {s}\n", .{ suite_name.arg, release_name.arg }); + return; + } + + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-suite-release-delete")) { + const suite_name = parseFirstArg(parsed.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-release-delete "); + return; + }; + const release_name = parseFirstArg(suite_name.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-release-delete "); + return; + }; + if (release_name.rest.len != 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-suite-release-delete "); + return; + } + workspace_runtime.deleteSuiteRelease(suite_name.arg, release_name.arg, 0) catch |err| { + exit_code.* = 1; + try stderr_buffer.appendFmt("workspace-suite-release-delete failed: {s}\n", .{@errorName(err)}); + return; + }; + try stdout_buffer.appendFmt("workspace suite release deleted {s} {s}\n", .{ suite_name.arg, release_name.arg }); + return; + } + + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-suite-release-prune")) { + const suite_name = parseFirstArg(parsed.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-release-prune "); + return; + }; + const keep_arg = parseFirstArg(suite_name.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-release-prune "); + return; + }; + if (keep_arg.rest.len != 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-suite-release-prune "); + return; + } + const keep = std.fmt.parseInt(usize, keep_arg.arg, 10) catch { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-suite-release-prune "); + return; + }; + const result = workspace_runtime.pruneSuiteReleases(suite_name.arg, keep, 0) catch |err| { + exit_code.* = 1; + try stderr_buffer.appendFmt("workspace-suite-release-prune failed: {s}\n", .{@errorName(err)}); + return; + }; + try stdout_buffer.appendFmt( + "workspace suite release pruned {s} keep={d} deleted={d} kept={d}\n", + .{ suite_name.arg, keep, result.deleted_count, result.kept_count }, + ); + return; + } + + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-suite-release-channel-list")) { + const arg = parseFirstArg(parsed.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-release-channel-list "); + return; + }; + if (arg.rest.len != 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-suite-release-channel-list "); + return; + } + const listing = workspace_runtime.suiteChannelListAlloc(allocator, arg.arg, stdout_buffer.limit) catch |err| { + exit_code.* = 1; + try stderr_buffer.appendFmt("workspace-suite-release-channel-list failed: {s}\n", .{@errorName(err)}); + return; + }; + defer allocator.free(listing); + try stdout_buffer.appendSlice(listing); + return; + } + + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-suite-release-channel-info")) { + const suite_name = parseFirstArg(parsed.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-release-channel-info "); + return; + }; + const channel_name = parseFirstArg(suite_name.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-release-channel-info "); + return; + }; + if (channel_name.rest.len != 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-suite-release-channel-info "); + return; + } + const info = workspace_runtime.suiteChannelInfoAlloc(allocator, suite_name.arg, channel_name.arg, stdout_buffer.limit) catch |err| { + exit_code.* = 1; + try stderr_buffer.appendFmt("workspace-suite-release-channel-info failed: {s}\n", .{@errorName(err)}); + return; + }; + defer allocator.free(info); + try stdout_buffer.appendSlice(info); + return; + } + + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-suite-release-channel-set")) { + const suite_name = parseFirstArg(parsed.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-release-channel-set "); + return; + }; + const channel_name = parseFirstArg(suite_name.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-release-channel-set "); + return; + }; + const release_name = parseFirstArg(channel_name.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-release-channel-set "); + return; + }; + if (release_name.rest.len != 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-suite-release-channel-set "); + return; + } + workspace_runtime.setSuiteReleaseChannel(suite_name.arg, channel_name.arg, release_name.arg, 0) catch |err| { + exit_code.* = 1; + try stderr_buffer.appendFmt("workspace-suite-release-channel-set failed: {s}\n", .{@errorName(err)}); + return; + }; + try stdout_buffer.appendFmt("workspace suite release channel set {s} {s} {s}\n", .{ suite_name.arg, channel_name.arg, release_name.arg }); + return; + } + + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-suite-release-channel-activate")) { + const suite_name = parseFirstArg(parsed.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-release-channel-activate "); + return; + }; + const channel_name = parseFirstArg(suite_name.rest) catch |err| { + exit_code.* = 2; + try writeCommandError(stderr_buffer, err, "workspace-suite-release-channel-activate "); + return; + }; + if (channel_name.rest.len != 0) { + exit_code.* = 2; + try stderr_buffer.appendLine("usage: workspace-suite-release-channel-activate "); + return; + } + workspace_runtime.activateSuiteReleaseChannel(suite_name.arg, channel_name.arg, 0) catch |err| { + exit_code.* = 1; + try stderr_buffer.appendFmt("workspace-suite-release-channel-activate failed: {s}\n", .{@errorName(err)}); + return; + }; + try stdout_buffer.appendFmt("workspace suite release channel activated {s} {s}\n", .{ suite_name.arg, channel_name.arg }); + return; + } + if (std.ascii.eqlIgnoreCase(parsed.name, "workspace-list")) { if (parsed.rest.len != 0) { exit_code.* = 2; @@ -2575,6 +3114,31 @@ fn runWorkspace( }; } +fn runWorkspaceSuite( + suite_name: []const u8, + operation: []const u8, + stdout_buffer: *OutputBuffer, + stderr_buffer: *OutputBuffer, + exit_code: *u8, + allocator: std.mem.Allocator, + depth: usize, +) Error!void { + const suite_entries = workspace_runtime.suiteEntriesAlloc(allocator, suite_name, 1024) catch |err| { + exit_code.* = 1; + try stderr_buffer.appendFmt("{s} failed: {s}\n", .{ operation, @errorName(err) }); + return; + }; + defer allocator.free(suite_entries); + + var lines = std.mem.splitScalar(u8, suite_entries, '\n'); + while (lines.next()) |raw_line| { + const workspace_name = std.mem.trim(u8, raw_line, " \t\r"); + if (workspace_name.len == 0) continue; + try runWorkspace(workspace_name, operation, stdout_buffer, stderr_buffer, exit_code, allocator, depth); + if (exit_code.* != 0) return; + } +} + fn runWorkspaceAutorun( operation: []const u8, stdout_buffer: *OutputBuffer, @@ -3516,6 +4080,72 @@ test "baremetal tool exec saves applies and deletes workspaces" { try std.testing.expect(std.mem.indexOf(u8, workspace_info.stdout, "display=1024x768") != null); try std.testing.expect(std.mem.indexOf(u8, workspace_info.stdout, "channel=demo:stable:r1") != null); + var save_workspace_plan_golden = try runCapture(std.testing.allocator, "workspace-plan-save ops golden duo root-a 1024 768 demo:stable:r1", 256, 256); + defer save_workspace_plan_golden.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_workspace_plan_golden.exit_code); + try std.testing.expectEqualStrings("workspace plan saved ops golden\n", save_workspace_plan_golden.stdout); + + var save_workspace_plan_staging = try runCapture(std.testing.allocator, "workspace-plan-save ops staging none none 640 400 demo:stable:r2", 256, 256); + defer save_workspace_plan_staging.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_workspace_plan_staging.exit_code); + try std.testing.expectEqualStrings("workspace plan saved ops staging\n", save_workspace_plan_staging.stdout); + + var workspace_plan_list = try runCapture(std.testing.allocator, "workspace-plan-list ops", 256, 256); + defer workspace_plan_list.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), workspace_plan_list.exit_code); + try std.testing.expectEqualStrings("golden\nstaging\n", workspace_plan_list.stdout); + + var workspace_plan_info = try runCapture(std.testing.allocator, "workspace-plan-info ops staging", 512, 256); + defer workspace_plan_info.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), workspace_plan_info.exit_code); + try std.testing.expect(std.mem.indexOf(u8, workspace_plan_info.stdout, "workspace=ops") != null); + try std.testing.expect(std.mem.indexOf(u8, workspace_plan_info.stdout, "plan=staging") != null); + try std.testing.expect(std.mem.indexOf(u8, workspace_plan_info.stdout, "suite=none") != null); + try std.testing.expect(std.mem.indexOf(u8, workspace_plan_info.stdout, "trust_bundle=none") != null); + try std.testing.expect(std.mem.indexOf(u8, workspace_plan_info.stdout, "display=640x400") != null); + try std.testing.expect(std.mem.indexOf(u8, workspace_plan_info.stdout, "channel=demo:stable:r2") != null); + + var apply_workspace_plan_staging = try runCapture(std.testing.allocator, "workspace-plan-apply ops staging", 256, 256); + defer apply_workspace_plan_staging.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), apply_workspace_plan_staging.exit_code); + try std.testing.expectEqualStrings("workspace plan applied ops staging\n", apply_workspace_plan_staging.stdout); + + var workspace_plan_active = try runCapture(std.testing.allocator, "workspace-plan-active ops", 512, 256); + defer workspace_plan_active.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), workspace_plan_active.exit_code); + try std.testing.expect(std.mem.indexOf(u8, workspace_plan_active.stdout, "active_plan=staging") != null); + + var staging_plan_workspace_info = try runCapture(std.testing.allocator, "workspace-info ops", 512, 256); + defer staging_plan_workspace_info.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), staging_plan_workspace_info.exit_code); + try std.testing.expect(std.mem.indexOf(u8, staging_plan_workspace_info.stdout, "suite=none") != null); + try std.testing.expect(std.mem.indexOf(u8, staging_plan_workspace_info.stdout, "trust_bundle=none") != null); + try std.testing.expect(std.mem.indexOf(u8, staging_plan_workspace_info.stdout, "display=640x400") != null); + try std.testing.expect(std.mem.indexOf(u8, staging_plan_workspace_info.stdout, "channel=demo:stable:r2") != null); + + var apply_workspace_plan_golden = try runCapture(std.testing.allocator, "workspace-plan-apply ops golden", 256, 256); + defer apply_workspace_plan_golden.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), apply_workspace_plan_golden.exit_code); + try std.testing.expectEqualStrings("workspace plan applied ops golden\n", apply_workspace_plan_golden.stdout); + + var restored_plan_workspace_info = try runCapture(std.testing.allocator, "workspace-info ops", 512, 256); + defer restored_plan_workspace_info.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), restored_plan_workspace_info.exit_code); + try std.testing.expect(std.mem.indexOf(u8, restored_plan_workspace_info.stdout, "suite=duo") != null); + try std.testing.expect(std.mem.indexOf(u8, restored_plan_workspace_info.stdout, "trust_bundle=root-a") != null); + try std.testing.expect(std.mem.indexOf(u8, restored_plan_workspace_info.stdout, "display=1024x768") != null); + try std.testing.expect(std.mem.indexOf(u8, restored_plan_workspace_info.stdout, "channel=demo:stable:r1") != null); + + var delete_workspace_plan_staging = try runCapture(std.testing.allocator, "workspace-plan-delete ops staging", 256, 256); + defer delete_workspace_plan_staging.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), delete_workspace_plan_staging.exit_code); + try std.testing.expectEqualStrings("workspace plan deleted ops staging\n", delete_workspace_plan_staging.stdout); + + var workspace_plan_list_after_delete = try runCapture(std.testing.allocator, "workspace-plan-list ops", 256, 256); + defer workspace_plan_list_after_delete.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), workspace_plan_list_after_delete.exit_code); + try std.testing.expectEqualStrings("golden\n", workspace_plan_list_after_delete.stdout); + var save_workspace_release = try runCapture(std.testing.allocator, "workspace-release-save ops golden", 256, 256); defer save_workspace_release.deinit(std.testing.allocator); try std.testing.expectEqual(@as(u8, 0), save_workspace_release.exit_code); @@ -3693,6 +4323,269 @@ test "baremetal tool exec saves applies and deletes workspaces" { try std.testing.expectEqualStrings("", workspace_list_after_delete.stdout); } +test "baremetal tool exec persists and runs workspace suites" { + storage_backend.resetForTest(); + filesystem.resetForTest(); + display_output.resetForTest(); + framebuffer_console.resetForTest(); + vga_text_console.resetForTest(); + + try trust_store.installBundle("root-a", "root-a-cert", 1); + try trust_store.installBundle("root-b", "root-b-cert", 2); + try package_store.installScriptPackage("demo", "echo demo-workspace", 3); + try package_store.installScriptPackage("aux", "echo aux-workspace", 4); + + var save_demo_plan = try runCapture(std.testing.allocator, "app-plan-save demo boot none none virtual 1024 768 0", 256, 256); + defer save_demo_plan.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_demo_plan.exit_code); + + var save_aux_plan = try runCapture(std.testing.allocator, "app-plan-save aux sidecar none none virtual 800 600 0", 256, 256); + defer save_aux_plan.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_aux_plan.exit_code); + + var save_demo_suite = try runCapture(std.testing.allocator, "app-suite-save demo-suite demo:boot", 256, 256); + defer save_demo_suite.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_demo_suite.exit_code); + + var save_aux_suite = try runCapture(std.testing.allocator, "app-suite-save aux-suite aux:sidecar", 256, 256); + defer save_aux_suite.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_aux_suite.exit_code); + + var save_ops = try runCapture(std.testing.allocator, "workspace-save ops demo-suite root-a 1024 768", 256, 256); + defer save_ops.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_ops.exit_code); + + var save_sidecar = try runCapture(std.testing.allocator, "workspace-save sidecar aux-suite root-b 800 600", 256, 256); + defer save_sidecar.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_sidecar.exit_code); + + var save_workspace_suite = try runCapture(std.testing.allocator, "workspace-suite-save crew ops sidecar", 256, 256); + defer save_workspace_suite.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_workspace_suite.exit_code); + try std.testing.expectEqualStrings("workspace suite saved crew\n", save_workspace_suite.stdout); + + var workspace_suite_list = try runCapture(std.testing.allocator, "workspace-suite-list", 256, 256); + defer workspace_suite_list.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), workspace_suite_list.exit_code); + try std.testing.expectEqualStrings("crew\n", workspace_suite_list.stdout); + + var workspace_suite_info = try runCapture(std.testing.allocator, "workspace-suite-info crew", 256, 256); + defer workspace_suite_info.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), workspace_suite_info.exit_code); + try std.testing.expect(std.mem.indexOf(u8, workspace_suite_info.stdout, "suite=crew") != null); + try std.testing.expect(std.mem.indexOf(u8, workspace_suite_info.stdout, "workspace=ops") != null); + try std.testing.expect(std.mem.indexOf(u8, workspace_suite_info.stdout, "workspace=sidecar") != null); + + var apply_workspace_suite = try runCapture(std.testing.allocator, "workspace-suite-apply crew", 256, 256); + defer apply_workspace_suite.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), apply_workspace_suite.exit_code); + try std.testing.expectEqualStrings("workspace suite applied crew\n", apply_workspace_suite.stdout); + + var trust_active = try runCapture(std.testing.allocator, "trust-active", 256, 256); + defer trust_active.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), trust_active.exit_code); + try std.testing.expect(std.mem.indexOf(u8, trust_active.stdout, "name=root-b") != null); + + var display_info = try runCapture(std.testing.allocator, "display-info", 256, 256); + defer display_info.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), display_info.exit_code); + try std.testing.expect(std.mem.indexOf(u8, display_info.stdout, "current=800x600") != null); + + var workspace_suite_run = try runCapture(std.testing.allocator, "workspace-suite-run crew", 256, 256); + defer workspace_suite_run.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), workspace_suite_run.exit_code); + try std.testing.expectEqualStrings("demo-workspace\naux-workspace\n", workspace_suite_run.stdout); + + var delete_workspace_suite = try runCapture(std.testing.allocator, "workspace-suite-delete crew", 256, 256); + defer delete_workspace_suite.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), delete_workspace_suite.exit_code); + try std.testing.expectEqualStrings("workspace suite deleted crew\n", delete_workspace_suite.stdout); + + var workspace_suite_list_after_delete = try runCapture(std.testing.allocator, "workspace-suite-list", 256, 256); + defer workspace_suite_list_after_delete.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), workspace_suite_list_after_delete.exit_code); + try std.testing.expectEqualStrings("", workspace_suite_list_after_delete.stdout); +} + +test "baremetal tool exec manages workspace suite releases" { + storage_backend.resetForTest(); + filesystem.resetForTest(); + display_output.resetForTest(); + framebuffer_console.resetForTest(); + vga_text_console.resetForTest(); + + try trust_store.installBundle("root-a", "root-a-cert", 1); + try trust_store.installBundle("root-b", "root-b-cert", 2); + try package_store.installScriptPackage("demo", "echo demo-workspace", 3); + try package_store.installScriptPackage("aux", "echo aux-workspace", 4); + try app_runtime.savePlan("demo", "boot", "", "", abi.display_connector_virtual, 1024, 768, false, 5); + try app_runtime.savePlan("demo", "canary", "", "", abi.display_connector_virtual, 640, 400, false, 6); + try app_runtime.savePlan("aux", "sidecar", "", "", abi.display_connector_virtual, 800, 600, false, 7); + try app_runtime.saveSuite("demo-suite", "demo:boot", 8); + try app_runtime.saveSuite("aux-suite", "aux:sidecar", 9); + + var save_ops = try runCapture(std.testing.allocator, "workspace-save ops demo-suite root-a 1024 768", 256, 256); + defer save_ops.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_ops.exit_code); + + var save_sidecar = try runCapture(std.testing.allocator, "workspace-save sidecar aux-suite root-b 800 600", 256, 256); + defer save_sidecar.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_sidecar.exit_code); + + var save_workspace_suite = try runCapture(std.testing.allocator, "workspace-suite-save crew ops sidecar", 256, 256); + defer save_workspace_suite.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_workspace_suite.exit_code); + + var save_release_golden = try runCapture(std.testing.allocator, "workspace-suite-release-save crew golden", 256, 256); + defer save_release_golden.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_release_golden.exit_code); + try std.testing.expectEqualStrings("workspace suite release saved crew golden\n", save_release_golden.stdout); + + var save_ops_canary = try runCapture(std.testing.allocator, "workspace-save ops demo-suite root-b 640 400", 256, 256); + defer save_ops_canary.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_ops_canary.exit_code); + + var save_workspace_suite_staging = try runCapture(std.testing.allocator, "workspace-suite-save crew ops", 256, 256); + defer save_workspace_suite_staging.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_workspace_suite_staging.exit_code); + + var save_release_staging = try runCapture(std.testing.allocator, "workspace-suite-release-save crew staging", 256, 256); + defer save_release_staging.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_release_staging.exit_code); + try std.testing.expectEqualStrings("workspace suite release saved crew staging\n", save_release_staging.stdout); + + var release_list = try runCapture(std.testing.allocator, "workspace-suite-release-list crew", 256, 256); + defer release_list.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), release_list.exit_code); + try std.testing.expectEqualStrings("golden\nstaging\n", release_list.stdout); + + var release_info = try runCapture(std.testing.allocator, "workspace-suite-release-info crew staging", 512, 256); + defer release_info.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), release_info.exit_code); + try std.testing.expect(std.mem.indexOf(u8, release_info.stdout, "suite=crew") != null); + try std.testing.expect(std.mem.indexOf(u8, release_info.stdout, "release=staging") != null); + try std.testing.expect(std.mem.indexOf(u8, release_info.stdout, "saved_seq=2") != null); + try std.testing.expect(std.mem.indexOf(u8, release_info.stdout, "workspace=ops") != null); + + var activate_release = try runCapture(std.testing.allocator, "workspace-suite-release-activate crew golden", 256, 256); + defer activate_release.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), activate_release.exit_code); + try std.testing.expectEqualStrings("workspace suite release activated crew golden\n", activate_release.stdout); + + var suite_info = try runCapture(std.testing.allocator, "workspace-suite-info crew", 256, 256); + defer suite_info.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), suite_info.exit_code); + try std.testing.expect(std.mem.indexOf(u8, suite_info.stdout, "workspace=ops") != null); + try std.testing.expect(std.mem.indexOf(u8, suite_info.stdout, "workspace=sidecar") != null); + + var delete_release = try runCapture(std.testing.allocator, "workspace-suite-release-delete crew staging", 256, 256); + defer delete_release.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), delete_release.exit_code); + try std.testing.expectEqualStrings("workspace suite release deleted crew staging\n", delete_release.stdout); + + var release_list_after_delete = try runCapture(std.testing.allocator, "workspace-suite-release-list crew", 256, 256); + defer release_list_after_delete.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), release_list_after_delete.exit_code); + try std.testing.expectEqualStrings("golden\n", release_list_after_delete.stdout); + + var save_ops_fallback = try runCapture(std.testing.allocator, "workspace-save ops demo-suite root-b 640 400", 256, 256); + defer save_ops_fallback.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_ops_fallback.exit_code); + + var save_workspace_suite_fallback = try runCapture(std.testing.allocator, "workspace-suite-save crew ops", 256, 256); + defer save_workspace_suite_fallback.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_workspace_suite_fallback.exit_code); + + var save_release_fallback = try runCapture(std.testing.allocator, "workspace-suite-release-save crew fallback", 256, 256); + defer save_release_fallback.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_release_fallback.exit_code); + try std.testing.expectEqualStrings("workspace suite release saved crew fallback\n", save_release_fallback.stdout); + + var prune_release = try runCapture(std.testing.allocator, "workspace-suite-release-prune crew 1", 256, 256); + defer prune_release.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), prune_release.exit_code); + try std.testing.expectEqualStrings("workspace suite release pruned crew keep=1 deleted=1 kept=1\n", prune_release.stdout); + + var release_list_after_prune = try runCapture(std.testing.allocator, "workspace-suite-release-list crew", 256, 256); + defer release_list_after_prune.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), release_list_after_prune.exit_code); + try std.testing.expectEqualStrings("fallback\n", release_list_after_prune.stdout); +} + +test "baremetal tool exec manages workspace suite release channels" { + storage_backend.resetForTest(); + filesystem.resetForTest(); + display_output.resetForTest(); + framebuffer_console.resetForTest(); + vga_text_console.resetForTest(); + + try trust_store.installBundle("root-a", "root-a-cert", 1); + try trust_store.installBundle("root-b", "root-b-cert", 2); + try package_store.installScriptPackage("demo", "echo demo-workspace", 3); + try package_store.installScriptPackage("aux", "echo aux-workspace", 4); + try app_runtime.savePlan("demo", "boot", "", "", abi.display_connector_virtual, 1024, 768, false, 5); + try app_runtime.savePlan("demo", "canary", "", "", abi.display_connector_virtual, 640, 400, false, 6); + try app_runtime.savePlan("aux", "sidecar", "", "", abi.display_connector_virtual, 800, 600, false, 7); + try app_runtime.saveSuite("demo-suite", "demo:boot", 8); + try app_runtime.saveSuite("aux-suite", "aux:sidecar", 9); + + var save_ops = try runCapture(std.testing.allocator, "workspace-save ops demo-suite root-a 1024 768", 256, 256); + defer save_ops.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_ops.exit_code); + + var save_sidecar = try runCapture(std.testing.allocator, "workspace-save sidecar aux-suite root-b 800 600", 256, 256); + defer save_sidecar.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_sidecar.exit_code); + + var save_workspace_suite = try runCapture(std.testing.allocator, "workspace-suite-save crew ops sidecar", 256, 256); + defer save_workspace_suite.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_workspace_suite.exit_code); + + var save_release_golden = try runCapture(std.testing.allocator, "workspace-suite-release-save crew golden", 256, 256); + defer save_release_golden.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_release_golden.exit_code); + + var save_ops_canary = try runCapture(std.testing.allocator, "workspace-save ops demo-suite root-b 640 400", 256, 256); + defer save_ops_canary.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_ops_canary.exit_code); + + var save_workspace_suite_staging = try runCapture(std.testing.allocator, "workspace-suite-save crew ops", 256, 256); + defer save_workspace_suite_staging.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_workspace_suite_staging.exit_code); + + var save_release_staging = try runCapture(std.testing.allocator, "workspace-suite-release-save crew staging", 256, 256); + defer save_release_staging.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), save_release_staging.exit_code); + + var set_channel_fallback = try runCapture(std.testing.allocator, "workspace-suite-release-channel-set crew stable staging", 256, 256); + defer set_channel_fallback.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), set_channel_fallback.exit_code); + try std.testing.expectEqualStrings("workspace suite release channel set crew stable staging\n", set_channel_fallback.stdout); + + var channel_list = try runCapture(std.testing.allocator, "workspace-suite-release-channel-list crew", 256, 256); + defer channel_list.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), channel_list.exit_code); + try std.testing.expectEqualStrings("stable\n", channel_list.stdout); + + var channel_info = try runCapture(std.testing.allocator, "workspace-suite-release-channel-info crew stable", 256, 256); + defer channel_info.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), channel_info.exit_code); + try std.testing.expect(std.mem.indexOf(u8, channel_info.stdout, "suite=crew") != null); + try std.testing.expect(std.mem.indexOf(u8, channel_info.stdout, "channel=stable") != null); + try std.testing.expect(std.mem.indexOf(u8, channel_info.stdout, "release=staging") != null); + + var activate_channel = try runCapture(std.testing.allocator, "workspace-suite-release-channel-activate crew stable", 256, 256); + defer activate_channel.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), activate_channel.exit_code); + try std.testing.expectEqualStrings("workspace suite release channel activated crew stable\n", activate_channel.stdout); + + var suite_info_after_channel = try runCapture(std.testing.allocator, "workspace-suite-info crew", 256, 256); + defer suite_info_after_channel.deinit(std.testing.allocator); + try std.testing.expectEqual(@as(u8, 0), suite_info_after_channel.exit_code); + try std.testing.expect(std.mem.indexOf(u8, suite_info_after_channel.stdout, "workspace=ops") != null); + try std.testing.expect(std.mem.indexOf(u8, suite_info_after_channel.stdout, "workspace=sidecar") == null); +} + test "baremetal tool exec persists and runs workspace autorun" { storage_backend.resetForTest(); filesystem.resetForTest(); diff --git a/src/baremetal/tool_service.zig b/src/baremetal/tool_service.zig index 5c44de23..e6210ab4 100644 --- a/src/baremetal/tool_service.zig +++ b/src/baremetal/tool_service.zig @@ -35,7 +35,14 @@ pub const AppPlanSaveRequest = codec.AppPlanSaveRequest; pub const AppSuiteSaveRequest = codec.AppSuiteSaveRequest; pub const AppSuiteChannelRequest = codec.AppSuiteChannelRequest; pub const AppSuiteChannelSetRequest = codec.AppSuiteChannelSetRequest; +pub const WorkspacePlanRequest = codec.WorkspacePlanRequest; +pub const WorkspacePlanSaveRequest = codec.WorkspacePlanSaveRequest; pub const WorkspaceSaveRequest = codec.WorkspaceSaveRequest; +pub const WorkspaceSuiteSaveRequest = codec.WorkspaceSuiteSaveRequest; +pub const WorkspaceSuiteReleaseRequest = codec.WorkspaceSuiteReleaseRequest; +pub const WorkspaceSuiteReleasePruneRequest = codec.WorkspaceSuiteReleasePruneRequest; +pub const WorkspaceSuiteChannelRequest = codec.WorkspaceSuiteChannelRequest; +pub const WorkspaceSuiteChannelSetRequest = codec.WorkspaceSuiteChannelSetRequest; pub const WorkspaceReleaseRequest = codec.WorkspaceReleaseRequest; pub const WorkspaceReleasePruneRequest = codec.WorkspaceReleasePruneRequest; pub const WorkspaceChannelRequest = codec.WorkspaceChannelRequest; @@ -236,6 +243,28 @@ fn handleFramedPayload( .app_suite_channel_info => |request| try handleAppSuiteChannelInfoRequest(allocator, request.suite_name, request.value, payload_limit), .app_suite_channel_set => |request| try handleAppSuiteChannelSetRequest(allocator, request.suite_name, request.channel, request.release, payload_limit), .app_suite_channel_activate => |request| try handleAppSuiteChannelActivateRequest(allocator, request.suite_name, request.value, payload_limit), + .workspace_plan_list => |workspace_name| try handleWorkspacePlanListRequest(allocator, workspace_name, payload_limit), + .workspace_plan_info => |request| try handleWorkspacePlanInfoRequest(allocator, request.workspace_name, request.plan_name, payload_limit), + .workspace_plan_active => |workspace_name| try handleWorkspacePlanActiveRequest(allocator, workspace_name, payload_limit), + .workspace_plan_save => |request| try handleWorkspacePlanSaveRequest(allocator, request, payload_limit), + .workspace_plan_apply => |request| try handleWorkspacePlanApplyRequest(allocator, request.workspace_name, request.plan_name, payload_limit), + .workspace_plan_delete => |request| try handleWorkspacePlanDeleteRequest(allocator, request.workspace_name, request.plan_name, payload_limit), + .workspace_suite_list => try handleWorkspaceSuiteListRequest(allocator, payload_limit), + .workspace_suite_info => |suite_name| try handleWorkspaceSuiteInfoRequest(allocator, suite_name, payload_limit), + .workspace_suite_save => |suite_request| try handleWorkspaceSuiteSaveRequest(allocator, suite_request, payload_limit), + .workspace_suite_apply => |suite_name| try handleWorkspaceSuiteApplyRequest(allocator, suite_name, payload_limit), + .workspace_suite_run => |suite_name| try handleWorkspaceSuiteRunRequest(allocator, suite_name, stdout_limit, stderr_limit, payload_limit), + .workspace_suite_delete => |suite_name| try handleWorkspaceSuiteDeleteRequest(allocator, suite_name, payload_limit), + .workspace_suite_release_list => |suite_name| try handleWorkspaceSuiteReleaseListRequest(allocator, suite_name, payload_limit), + .workspace_suite_release_info => |request| try handleWorkspaceSuiteReleaseInfoRequest(allocator, request.suite_name, request.release_name, payload_limit), + .workspace_suite_release_save => |request| try handleWorkspaceSuiteReleaseSaveRequest(allocator, request.suite_name, request.release_name, payload_limit), + .workspace_suite_release_activate => |request| try handleWorkspaceSuiteReleaseActivateRequest(allocator, request.suite_name, request.release_name, payload_limit), + .workspace_suite_release_delete => |request| try handleWorkspaceSuiteReleaseDeleteRequest(allocator, request.suite_name, request.release_name, payload_limit), + .workspace_suite_release_prune => |request| try handleWorkspaceSuiteReleasePruneRequest(allocator, request.suite_name, request.keep, payload_limit), + .workspace_suite_channel_list => |suite_name| try handleWorkspaceSuiteChannelListRequest(allocator, suite_name, payload_limit), + .workspace_suite_channel_info => |request| try handleWorkspaceSuiteChannelInfoRequest(allocator, request.suite_name, request.value, payload_limit), + .workspace_suite_channel_set => |request| try handleWorkspaceSuiteChannelSetRequest(allocator, request.suite_name, request.channel, request.release, payload_limit), + .workspace_suite_channel_activate => |request| try handleWorkspaceSuiteChannelActivateRequest(allocator, request.suite_name, request.value, payload_limit), .workspace_list => try handleWorkspaceListRequest(allocator, payload_limit), .workspace_info => |workspace_name| try handleWorkspaceInfoRequest(allocator, workspace_name, payload_limit), .workspace_save => |workspace_request| try handleWorkspaceSaveRequest(allocator, workspace_request, payload_limit), @@ -1030,6 +1059,290 @@ fn handleAppSuiteChannelActivateRequest( return response; } +fn handleWorkspacePlanListRequest( + allocator: std.mem.Allocator, + workspace_name: []const u8, + payload_limit: usize, +) Error![]u8 { + return workspace_runtime.planListAlloc(allocator, workspace_name, payload_limit) catch |err| { + return formatOperationError(allocator, "WORKSPACEPLANLIST", err, payload_limit); + }; +} + +fn handleWorkspacePlanInfoRequest( + allocator: std.mem.Allocator, + workspace_name: []const u8, + plan_name: []const u8, + payload_limit: usize, +) Error![]u8 { + return workspace_runtime.planInfoAlloc(allocator, workspace_name, plan_name, payload_limit) catch |err| { + return formatOperationError(allocator, "WORKSPACEPLANINFO", err, payload_limit); + }; +} + +fn handleWorkspacePlanActiveRequest( + allocator: std.mem.Allocator, + workspace_name: []const u8, + payload_limit: usize, +) Error![]u8 { + return workspace_runtime.activePlanInfoAlloc(allocator, workspace_name, payload_limit) catch |err| { + return formatOperationError(allocator, "WORKSPACEPLANACTIVE", err, payload_limit); + }; +} + +fn handleWorkspacePlanSaveRequest( + allocator: std.mem.Allocator, + request: WorkspacePlanSaveRequest, + payload_limit: usize, +) Error![]u8 { + const suite_name = if (std.ascii.eqlIgnoreCase(request.suite_name, "none")) "" else request.suite_name; + const trust_bundle = if (std.ascii.eqlIgnoreCase(request.trust_bundle, "none")) "" else request.trust_bundle; + workspace_runtime.savePlan(request.workspace_name, request.plan_name, suite_name, trust_bundle, request.width, request.height, request.entries_spec, 0) catch |err| { + return formatOperationError(allocator, "WORKSPACEPLANSAVE", err, payload_limit); + }; + const response = try std.fmt.allocPrint(allocator, "WORKSPACEPLANSAVE {s} {s}\n", .{ request.workspace_name, request.plan_name }); + errdefer allocator.free(response); + if (response.len > payload_limit) return error.ResponseTooLarge; + return response; +} + +fn handleWorkspacePlanApplyRequest( + allocator: std.mem.Allocator, + workspace_name: []const u8, + plan_name: []const u8, + payload_limit: usize, +) Error![]u8 { + workspace_runtime.applyPlan(workspace_name, plan_name, 0) catch |err| { + return formatOperationError(allocator, "WORKSPACEPLANAPPLY", err, payload_limit); + }; + const response = try std.fmt.allocPrint(allocator, "WORKSPACEPLANAPPLY {s} {s}\n", .{ workspace_name, plan_name }); + errdefer allocator.free(response); + if (response.len > payload_limit) return error.ResponseTooLarge; + return response; +} + +fn handleWorkspacePlanDeleteRequest( + allocator: std.mem.Allocator, + workspace_name: []const u8, + plan_name: []const u8, + payload_limit: usize, +) Error![]u8 { + workspace_runtime.deletePlan(workspace_name, plan_name, 0) catch |err| { + return formatOperationError(allocator, "WORKSPACEPLANDELETE", err, payload_limit); + }; + const response = try std.fmt.allocPrint(allocator, "WORKSPACEPLANDELETE {s} {s}\n", .{ workspace_name, plan_name }); + errdefer allocator.free(response); + if (response.len > payload_limit) return error.ResponseTooLarge; + return response; +} + +fn handleWorkspaceSuiteListRequest(allocator: std.mem.Allocator, payload_limit: usize) Error![]u8 { + return workspace_runtime.suiteListAlloc(allocator, payload_limit) catch |err| { + return formatOperationError(allocator, "WORKSPACESUITELIST", err, payload_limit); + }; +} + +fn handleWorkspaceSuiteInfoRequest( + allocator: std.mem.Allocator, + suite_name: []const u8, + payload_limit: usize, +) Error![]u8 { + return workspace_runtime.suiteInfoAlloc(allocator, suite_name, payload_limit) catch |err| { + return formatOperationError(allocator, "WORKSPACESUITEINFO", err, payload_limit); + }; +} + +fn handleWorkspaceSuiteSaveRequest( + allocator: std.mem.Allocator, + request: WorkspaceSuiteSaveRequest, + payload_limit: usize, +) Error![]u8 { + workspace_runtime.saveSuite(request.suite_name, request.entries_spec, 0) catch |err| { + return formatOperationError(allocator, "WORKSPACESUITESAVE", err, payload_limit); + }; + const response = try std.fmt.allocPrint(allocator, "WORKSPACESUITESAVE {s}\n", .{request.suite_name}); + errdefer allocator.free(response); + if (response.len > payload_limit) return error.ResponseTooLarge; + return response; +} + +fn handleWorkspaceSuiteApplyRequest( + allocator: std.mem.Allocator, + suite_name: []const u8, + payload_limit: usize, +) Error![]u8 { + workspace_runtime.applySuite(suite_name, 0) catch |err| { + return formatOperationError(allocator, "WORKSPACESUITEAPPLY", err, payload_limit); + }; + const response = try std.fmt.allocPrint(allocator, "WORKSPACESUITEAPPLY {s}\n", .{suite_name}); + errdefer allocator.free(response); + if (response.len > payload_limit) return error.ResponseTooLarge; + return response; +} + +fn handleWorkspaceSuiteRunRequest( + allocator: std.mem.Allocator, + suite_name: []const u8, + stdout_limit: usize, + stderr_limit: usize, + payload_limit: usize, +) Error![]u8 { + var command_buf: [96]u8 = undefined; + const command = std.fmt.bufPrint(&command_buf, "workspace-suite-run {s}", .{suite_name}) catch return error.InvalidFrame; + return handleCommandRequest(allocator, command, stdout_limit, stderr_limit, payload_limit); +} + +fn handleWorkspaceSuiteDeleteRequest( + allocator: std.mem.Allocator, + suite_name: []const u8, + payload_limit: usize, +) Error![]u8 { + workspace_runtime.deleteSuite(suite_name, 0) catch |err| { + return formatOperationError(allocator, "WORKSPACESUITEDELETE", err, payload_limit); + }; + const response = try std.fmt.allocPrint(allocator, "WORKSPACESUITEDELETE {s}\n", .{suite_name}); + errdefer allocator.free(response); + if (response.len > payload_limit) return error.ResponseTooLarge; + return response; +} + +fn handleWorkspaceSuiteReleaseListRequest( + allocator: std.mem.Allocator, + suite_name: []const u8, + payload_limit: usize, +) Error![]u8 { + return workspace_runtime.suiteReleaseListAlloc(allocator, suite_name, payload_limit) catch |err| { + return formatOperationError(allocator, "WORKSPACESUITERELEASELIST", err, payload_limit); + }; +} + +fn handleWorkspaceSuiteReleaseInfoRequest( + allocator: std.mem.Allocator, + suite_name: []const u8, + release_name: []const u8, + payload_limit: usize, +) Error![]u8 { + return workspace_runtime.suiteReleaseInfoAlloc(allocator, suite_name, release_name, payload_limit) catch |err| { + return formatOperationError(allocator, "WORKSPACESUITERELEASEINFO", err, payload_limit); + }; +} + +fn handleWorkspaceSuiteReleaseSaveRequest( + allocator: std.mem.Allocator, + suite_name: []const u8, + release_name: []const u8, + payload_limit: usize, +) Error![]u8 { + workspace_runtime.snapshotSuiteRelease(suite_name, release_name, 0) catch |err| { + return formatOperationError(allocator, "WORKSPACESUITERELEASESAVE", err, payload_limit); + }; + const response = try std.fmt.allocPrint(allocator, "WORKSPACESUITERELEASESAVE {s} {s}\n", .{ suite_name, release_name }); + errdefer allocator.free(response); + if (response.len > payload_limit) return error.ResponseTooLarge; + return response; +} + +fn handleWorkspaceSuiteReleaseActivateRequest( + allocator: std.mem.Allocator, + suite_name: []const u8, + release_name: []const u8, + payload_limit: usize, +) Error![]u8 { + workspace_runtime.activateSuiteRelease(suite_name, release_name, 0) catch |err| { + return formatOperationError(allocator, "WORKSPACESUITERELEASEACTIVATE", err, payload_limit); + }; + const response = try std.fmt.allocPrint(allocator, "WORKSPACESUITERELEASEACTIVATE {s} {s}\n", .{ suite_name, release_name }); + errdefer allocator.free(response); + if (response.len > payload_limit) return error.ResponseTooLarge; + return response; +} + +fn handleWorkspaceSuiteReleaseDeleteRequest( + allocator: std.mem.Allocator, + suite_name: []const u8, + release_name: []const u8, + payload_limit: usize, +) Error![]u8 { + workspace_runtime.deleteSuiteRelease(suite_name, release_name, 0) catch |err| { + return formatOperationError(allocator, "WORKSPACESUITERELEASEDELETE", err, payload_limit); + }; + const response = try std.fmt.allocPrint(allocator, "WORKSPACESUITERELEASEDELETE {s} {s}\n", .{ suite_name, release_name }); + errdefer allocator.free(response); + if (response.len > payload_limit) return error.ResponseTooLarge; + return response; +} + +fn handleWorkspaceSuiteReleasePruneRequest( + allocator: std.mem.Allocator, + suite_name: []const u8, + keep: u32, + payload_limit: usize, +) Error![]u8 { + const result = workspace_runtime.pruneSuiteReleases(suite_name, keep, 0) catch |err| { + return formatOperationError(allocator, "WORKSPACESUITERELEASEPRUNE", err, payload_limit); + }; + const response = try std.fmt.allocPrint( + allocator, + "WORKSPACESUITERELEASEPRUNE {s} keep={d} deleted={d} kept={d}\n", + .{ suite_name, keep, result.deleted_count, result.kept_count }, + ); + errdefer allocator.free(response); + if (response.len > payload_limit) return error.ResponseTooLarge; + return response; +} + +fn handleWorkspaceSuiteChannelListRequest( + allocator: std.mem.Allocator, + suite_name: []const u8, + payload_limit: usize, +) Error![]u8 { + return workspace_runtime.suiteChannelListAlloc(allocator, suite_name, payload_limit) catch |err| { + return formatOperationError(allocator, "WORKSPACESUITECHANNELLIST", err, payload_limit); + }; +} + +fn handleWorkspaceSuiteChannelInfoRequest( + allocator: std.mem.Allocator, + suite_name: []const u8, + channel_name: []const u8, + payload_limit: usize, +) Error![]u8 { + return workspace_runtime.suiteChannelInfoAlloc(allocator, suite_name, channel_name, payload_limit) catch |err| { + return formatOperationError(allocator, "WORKSPACESUITECHANNELINFO", err, payload_limit); + }; +} + +fn handleWorkspaceSuiteChannelSetRequest( + allocator: std.mem.Allocator, + suite_name: []const u8, + channel_name: []const u8, + release_name: []const u8, + payload_limit: usize, +) Error![]u8 { + workspace_runtime.setSuiteReleaseChannel(suite_name, channel_name, release_name, 0) catch |err| { + return formatOperationError(allocator, "WORKSPACESUITECHANNELSET", err, payload_limit); + }; + const response = try std.fmt.allocPrint(allocator, "WORKSPACESUITECHANNELSET {s} {s} {s}\n", .{ suite_name, channel_name, release_name }); + errdefer allocator.free(response); + if (response.len > payload_limit) return error.ResponseTooLarge; + return response; +} + +fn handleWorkspaceSuiteChannelActivateRequest( + allocator: std.mem.Allocator, + suite_name: []const u8, + channel_name: []const u8, + payload_limit: usize, +) Error![]u8 { + workspace_runtime.activateSuiteReleaseChannel(suite_name, channel_name, 0) catch |err| { + return formatOperationError(allocator, "WORKSPACESUITECHANNELACTIVATE", err, payload_limit); + }; + const response = try std.fmt.allocPrint(allocator, "WORKSPACESUITECHANNELACTIVATE {s} {s}\n", .{ suite_name, channel_name }); + errdefer allocator.free(response); + if (response.len > payload_limit) return error.ResponseTooLarge; + return response; +} + fn handleWorkspaceListRequest(allocator: std.mem.Allocator, payload_limit: usize) Error![]u8 { return workspace_runtime.listAlloc(allocator, payload_limit) catch |err| { return formatOperationError(allocator, "WORKSPACELIST", err, payload_limit); @@ -2111,6 +2424,130 @@ test "baremetal tool service parses typed framed requests" { else => return error.InvalidFrame, } + const workspace_suite_list = try parseFramedRequest("REQ 186 WORKSPACESUITELIST"); + switch (workspace_suite_list.operation) { + .workspace_suite_list => {}, + else => return error.InvalidFrame, + } + + const workspace_suite_info = try parseFramedRequest("REQ 187 WORKSPACESUITEINFO crew"); + switch (workspace_suite_info.operation) { + .workspace_suite_info => |suite_name| try std.testing.expectEqualStrings("crew", suite_name), + else => return error.InvalidFrame, + } + + const workspace_suite_save = try parseFramedRequest("REQ 188 WORKSPACESUITESAVE crew ops sidecar"); + switch (workspace_suite_save.operation) { + .workspace_suite_save => |payload| { + try std.testing.expectEqualStrings("crew", payload.suite_name); + try std.testing.expectEqualStrings("ops sidecar", payload.entries_spec); + }, + else => return error.InvalidFrame, + } + + const workspace_suite_apply = try parseFramedRequest("REQ 189 WORKSPACESUITEAPPLY crew"); + switch (workspace_suite_apply.operation) { + .workspace_suite_apply => |suite_name| try std.testing.expectEqualStrings("crew", suite_name), + else => return error.InvalidFrame, + } + + const workspace_suite_run = try parseFramedRequest("REQ 190 WORKSPACESUITERUN crew"); + switch (workspace_suite_run.operation) { + .workspace_suite_run => |suite_name| try std.testing.expectEqualStrings("crew", suite_name), + else => return error.InvalidFrame, + } + + const workspace_suite_delete = try parseFramedRequest("REQ 191 WORKSPACESUITEDELETE crew"); + switch (workspace_suite_delete.operation) { + .workspace_suite_delete => |suite_name| try std.testing.expectEqualStrings("crew", suite_name), + else => return error.InvalidFrame, + } + + const workspace_suite_release_list = try parseFramedRequest("REQ 192 WORKSPACESUITERELEASELIST crew"); + switch (workspace_suite_release_list.operation) { + .workspace_suite_release_list => |suite_name| try std.testing.expectEqualStrings("crew", suite_name), + else => return error.InvalidFrame, + } + + const workspace_suite_release_info = try parseFramedRequest("REQ 193 WORKSPACESUITERELEASEINFO crew golden"); + switch (workspace_suite_release_info.operation) { + .workspace_suite_release_info => |payload| { + try std.testing.expectEqualStrings("crew", payload.suite_name); + try std.testing.expectEqualStrings("golden", payload.release_name); + }, + else => return error.InvalidFrame, + } + + const workspace_suite_release_save = try parseFramedRequest("REQ 194 WORKSPACESUITERELEASESAVE crew golden"); + switch (workspace_suite_release_save.operation) { + .workspace_suite_release_save => |payload| { + try std.testing.expectEqualStrings("crew", payload.suite_name); + try std.testing.expectEqualStrings("golden", payload.release_name); + }, + else => return error.InvalidFrame, + } + + const workspace_suite_release_activate = try parseFramedRequest("REQ 195 WORKSPACESUITERELEASEACTIVATE crew golden"); + switch (workspace_suite_release_activate.operation) { + .workspace_suite_release_activate => |payload| { + try std.testing.expectEqualStrings("crew", payload.suite_name); + try std.testing.expectEqualStrings("golden", payload.release_name); + }, + else => return error.InvalidFrame, + } + + const workspace_suite_release_delete = try parseFramedRequest("REQ 196 WORKSPACESUITERELEASEDELETE crew golden"); + switch (workspace_suite_release_delete.operation) { + .workspace_suite_release_delete => |payload| { + try std.testing.expectEqualStrings("crew", payload.suite_name); + try std.testing.expectEqualStrings("golden", payload.release_name); + }, + else => return error.InvalidFrame, + } + + const workspace_suite_release_prune = try parseFramedRequest("REQ 197 WORKSPACESUITERELEASEPRUNE crew 1"); + switch (workspace_suite_release_prune.operation) { + .workspace_suite_release_prune => |payload| { + try std.testing.expectEqualStrings("crew", payload.suite_name); + try std.testing.expectEqual(@as(u32, 1), payload.keep); + }, + else => return error.InvalidFrame, + } + + const workspace_suite_channel_list = try parseFramedRequest("REQ 198 WORKSPACESUITECHANNELLIST crew"); + switch (workspace_suite_channel_list.operation) { + .workspace_suite_channel_list => |suite_name| try std.testing.expectEqualStrings("crew", suite_name), + else => return error.InvalidFrame, + } + + const workspace_suite_channel_info = try parseFramedRequest("REQ 199 WORKSPACESUITECHANNELINFO crew stable"); + switch (workspace_suite_channel_info.operation) { + .workspace_suite_channel_info => |payload| { + try std.testing.expectEqualStrings("crew", payload.suite_name); + try std.testing.expectEqualStrings("stable", payload.value); + }, + else => return error.InvalidFrame, + } + + const workspace_suite_channel_set = try parseFramedRequest("REQ 200 WORKSPACESUITECHANNELSET crew stable golden"); + switch (workspace_suite_channel_set.operation) { + .workspace_suite_channel_set => |payload| { + try std.testing.expectEqualStrings("crew", payload.suite_name); + try std.testing.expectEqualStrings("stable", payload.channel); + try std.testing.expectEqualStrings("golden", payload.release); + }, + else => return error.InvalidFrame, + } + + const workspace_suite_channel_activate = try parseFramedRequest("REQ 201 WORKSPACESUITECHANNELACTIVATE crew stable"); + switch (workspace_suite_channel_activate.operation) { + .workspace_suite_channel_activate => |payload| { + try std.testing.expectEqualStrings("crew", payload.suite_name); + try std.testing.expectEqualStrings("stable", payload.value); + }, + else => return error.InvalidFrame, + } + const workspace_list = try parseFramedRequest("REQ 152 WORKSPACELIST"); switch (workspace_list.operation) { .workspace_list => {}, @@ -3078,6 +3515,65 @@ test "baremetal tool service manages persisted workspaces" { try std.testing.expect(std.mem.indexOf(u8, info_response, "state_path=/runtime/workspace-runs/ops/last_run.txt") != null); try std.testing.expect(std.mem.indexOf(u8, info_response, "channel=demo:stable:r1") != null); + const plan_save_golden_response = try handleFramedRequest(std.testing.allocator, "REQ 1831 WORKSPACEPLANSAVE ops golden duo root-a 1024 768 demo:stable:r1", 512, 256, 512); + defer std.testing.allocator.free(plan_save_golden_response); + try std.testing.expectEqualStrings("RESP 1831 29\nWORKSPACEPLANSAVE ops golden\n", plan_save_golden_response); + + const plan_save_staging_response = try handleFramedRequest(std.testing.allocator, "REQ 1832 WORKSPACEPLANSAVE ops staging none none 640 400 demo:stable:r2", 512, 256, 512); + defer std.testing.allocator.free(plan_save_staging_response); + try std.testing.expectEqualStrings("RESP 1832 30\nWORKSPACEPLANSAVE ops staging\n", plan_save_staging_response); + + const plan_list_response = try handleFramedRequest(std.testing.allocator, "REQ 1833 WORKSPACEPLANLIST ops", 512, 256, 512); + defer std.testing.allocator.free(plan_list_response); + try std.testing.expectEqualStrings("RESP 1833 15\ngolden\nstaging\n", plan_list_response); + + const plan_info_response = try handleFramedRequest(std.testing.allocator, "REQ 1834 WORKSPACEPLANINFO ops staging", 512, 256, 512); + defer std.testing.allocator.free(plan_info_response); + try std.testing.expect(std.mem.startsWith(u8, plan_info_response, "RESP 1834 ")); + try std.testing.expect(std.mem.indexOf(u8, plan_info_response, "workspace=ops") != null); + try std.testing.expect(std.mem.indexOf(u8, plan_info_response, "plan=staging") != null); + try std.testing.expect(std.mem.indexOf(u8, plan_info_response, "suite=none") != null); + try std.testing.expect(std.mem.indexOf(u8, plan_info_response, "trust_bundle=none") != null); + try std.testing.expect(std.mem.indexOf(u8, plan_info_response, "display=640x400") != null); + try std.testing.expect(std.mem.indexOf(u8, plan_info_response, "channel=demo:stable:r2") != null); + + const plan_apply_staging_response = try handleFramedRequest(std.testing.allocator, "REQ 1835 WORKSPACEPLANAPPLY ops staging", 512, 256, 512); + defer std.testing.allocator.free(plan_apply_staging_response); + try std.testing.expectEqualStrings("RESP 1835 31\nWORKSPACEPLANAPPLY ops staging\n", plan_apply_staging_response); + + const plan_active_response = try handleFramedRequest(std.testing.allocator, "REQ 1836 WORKSPACEPLANACTIVE ops", 512, 256, 512); + defer std.testing.allocator.free(plan_active_response); + try std.testing.expect(std.mem.startsWith(u8, plan_active_response, "RESP 1836 ")); + try std.testing.expect(std.mem.indexOf(u8, plan_active_response, "active_plan=staging") != null); + + const plan_staging_info_response = try handleFramedRequest(std.testing.allocator, "REQ 1837 WORKSPACEINFO ops", 512, 256, 512); + defer std.testing.allocator.free(plan_staging_info_response); + try std.testing.expect(std.mem.startsWith(u8, plan_staging_info_response, "RESP 1837 ")); + try std.testing.expect(std.mem.indexOf(u8, plan_staging_info_response, "suite=none") != null); + try std.testing.expect(std.mem.indexOf(u8, plan_staging_info_response, "trust_bundle=none") != null); + try std.testing.expect(std.mem.indexOf(u8, plan_staging_info_response, "display=640x400") != null); + try std.testing.expect(std.mem.indexOf(u8, plan_staging_info_response, "channel=demo:stable:r2") != null); + + const plan_apply_golden_response = try handleFramedRequest(std.testing.allocator, "REQ 1838 WORKSPACEPLANAPPLY ops golden", 512, 256, 512); + defer std.testing.allocator.free(plan_apply_golden_response); + try std.testing.expectEqualStrings("RESP 1838 30\nWORKSPACEPLANAPPLY ops golden\n", plan_apply_golden_response); + + const plan_restored_info_response = try handleFramedRequest(std.testing.allocator, "REQ 1839 WORKSPACEINFO ops", 512, 256, 512); + defer std.testing.allocator.free(plan_restored_info_response); + try std.testing.expect(std.mem.startsWith(u8, plan_restored_info_response, "RESP 1839 ")); + try std.testing.expect(std.mem.indexOf(u8, plan_restored_info_response, "suite=duo") != null); + try std.testing.expect(std.mem.indexOf(u8, plan_restored_info_response, "trust_bundle=root-a") != null); + try std.testing.expect(std.mem.indexOf(u8, plan_restored_info_response, "display=1024x768") != null); + try std.testing.expect(std.mem.indexOf(u8, plan_restored_info_response, "channel=demo:stable:r1") != null); + + const plan_delete_staging_response = try handleFramedRequest(std.testing.allocator, "REQ 18391 WORKSPACEPLANDELETE ops staging", 512, 256, 512); + defer std.testing.allocator.free(plan_delete_staging_response); + try std.testing.expectEqualStrings("RESP 18391 32\nWORKSPACEPLANDELETE ops staging\n", plan_delete_staging_response); + + const plan_list_after_delete_response = try handleFramedRequest(std.testing.allocator, "REQ 18392 WORKSPACEPLANLIST ops", 512, 256, 512); + defer std.testing.allocator.free(plan_list_after_delete_response); + try std.testing.expectEqualStrings("RESP 18392 7\ngolden\n", plan_list_after_delete_response); + const release_save_response = try handleFramedRequest(std.testing.allocator, "REQ 1841 WORKSPACERELEASESAVE ops golden", 512, 256, 512); defer std.testing.allocator.free(release_save_response); try std.testing.expect(std.mem.startsWith(u8, release_save_response, "RESP 1841 ")); @@ -3242,6 +3738,204 @@ test "baremetal tool service manages persisted workspaces" { try std.testing.expectError(error.FileNotFound, filesystem.statSummary("/runtime/workspace-runs/ops")); } +test "baremetal tool service persists and runs workspace suites" { + resetPersistentStateForTest(); + + try trust_store.installBundle("root-a", "root-a-cert", 1); + try trust_store.installBundle("root-b", "root-b-cert", 2); + try package_store.installScriptPackage("demo", "echo demo-workspace", 3); + try package_store.installScriptPackage("aux", "echo aux-workspace", 4); + try app_runtime.savePlan("demo", "boot", "", "", abi.display_connector_virtual, 1024, 768, false, 5); + try app_runtime.savePlan("aux", "sidecar", "", "", abi.display_connector_virtual, 800, 600, false, 6); + try app_runtime.saveSuite("demo-suite", "demo:boot", 7); + try app_runtime.saveSuite("aux-suite", "aux:sidecar", 8); + + const save_ops_response = try handleFramedRequest(std.testing.allocator, "REQ 2031 WORKSPACESAVE ops demo-suite root-a 1024 768", 512, 256, 512); + defer std.testing.allocator.free(save_ops_response); + try std.testing.expectEqualStrings("RESP 2031 18\nWORKSPACESAVE ops\n", save_ops_response); + + const save_sidecar_response = try handleFramedRequest(std.testing.allocator, "REQ 2032 WORKSPACESAVE sidecar aux-suite root-b 800 600", 512, 256, 512); + defer std.testing.allocator.free(save_sidecar_response); + try std.testing.expectEqualStrings("RESP 2032 22\nWORKSPACESAVE sidecar\n", save_sidecar_response); + + const suite_save_response = try handleFramedRequest(std.testing.allocator, "REQ 2033 WORKSPACESUITESAVE crew ops sidecar", 512, 256, 512); + defer std.testing.allocator.free(suite_save_response); + const suite_save_payload = "WORKSPACESUITESAVE crew\n"; + const suite_save_expected = try std.fmt.allocPrint(std.testing.allocator, "RESP 2033 {d}\n{s}", .{ suite_save_payload.len, suite_save_payload }); + defer std.testing.allocator.free(suite_save_expected); + try std.testing.expectEqualStrings(suite_save_expected, suite_save_response); + + const suite_list_response = try handleFramedRequest(std.testing.allocator, "REQ 2034 WORKSPACESUITELIST", 512, 256, 512); + defer std.testing.allocator.free(suite_list_response); + try std.testing.expectEqualStrings("RESP 2034 5\ncrew\n", suite_list_response); + + const suite_info_response = try handleFramedRequest(std.testing.allocator, "REQ 2035 WORKSPACESUITEINFO crew", 512, 256, 512); + defer std.testing.allocator.free(suite_info_response); + try std.testing.expect(std.mem.startsWith(u8, suite_info_response, "RESP 2035 ")); + try std.testing.expect(std.mem.indexOf(u8, suite_info_response, "suite=crew") != null); + try std.testing.expect(std.mem.indexOf(u8, suite_info_response, "workspace=ops") != null); + try std.testing.expect(std.mem.indexOf(u8, suite_info_response, "workspace=sidecar") != null); + + const suite_apply_response = try handleFramedRequest(std.testing.allocator, "REQ 2036 WORKSPACESUITEAPPLY crew", 512, 256, 512); + defer std.testing.allocator.free(suite_apply_response); + const suite_apply_payload = "WORKSPACESUITEAPPLY crew\n"; + const suite_apply_expected = try std.fmt.allocPrint(std.testing.allocator, "RESP 2036 {d}\n{s}", .{ suite_apply_payload.len, suite_apply_payload }); + defer std.testing.allocator.free(suite_apply_expected); + try std.testing.expectEqualStrings(suite_apply_expected, suite_apply_response); + + const active_bundle = try trust_store.activeBundleNameAlloc(std.testing.allocator, trust_store.max_name_len); + defer std.testing.allocator.free(active_bundle); + try std.testing.expectEqualStrings("root-b", active_bundle); + + const display_state = framebuffer_console.statePtr(); + try std.testing.expectEqual(@as(u16, 800), display_state.width); + try std.testing.expectEqual(@as(u16, 600), display_state.height); + + const suite_run_response = try handleFramedRequest(std.testing.allocator, "REQ 2037 WORKSPACESUITERUN crew", 512, 256, 512); + defer std.testing.allocator.free(suite_run_response); + const suite_run_payload = "demo-workspace\naux-workspace\n"; + const suite_run_expected = try std.fmt.allocPrint(std.testing.allocator, "RESP 2037 {d}\n{s}", .{ suite_run_payload.len, suite_run_payload }); + defer std.testing.allocator.free(suite_run_expected); + try std.testing.expectEqualStrings(suite_run_expected, suite_run_response); + + const suite_delete_response = try handleFramedRequest(std.testing.allocator, "REQ 2038 WORKSPACESUITEDELETE crew", 512, 256, 512); + defer std.testing.allocator.free(suite_delete_response); + const suite_delete_payload = "WORKSPACESUITEDELETE crew\n"; + const suite_delete_expected = try std.fmt.allocPrint(std.testing.allocator, "RESP 2038 {d}\n{s}", .{ suite_delete_payload.len, suite_delete_payload }); + defer std.testing.allocator.free(suite_delete_expected); + try std.testing.expectEqualStrings(suite_delete_expected, suite_delete_response); + + const suite_list_after_delete = try handleFramedRequest(std.testing.allocator, "REQ 2039 WORKSPACESUITELIST", 512, 256, 512); + defer std.testing.allocator.free(suite_list_after_delete); + try std.testing.expectEqualStrings("RESP 2039 0\n", suite_list_after_delete); + + try std.testing.expectError(error.FileNotFound, filesystem.statSummary("/runtime/workspace-suites/crew.txt")); +} + +test "baremetal tool service manages workspace suite releases" { + resetPersistentStateForTest(); + + try trust_store.installBundle("root-a", "root-a-cert", 1); + try trust_store.installBundle("root-b", "root-b-cert", 2); + try package_store.installScriptPackage("demo", "echo demo-workspace", 3); + try package_store.installScriptPackage("aux", "echo aux-workspace", 4); + try app_runtime.savePlan("demo", "boot", "", "", abi.display_connector_virtual, 1024, 768, false, 5); + try app_runtime.savePlan("aux", "sidecar", "", "", abi.display_connector_virtual, 800, 600, false, 6); + try app_runtime.saveSuite("demo-suite", "demo:boot", 7); + try app_runtime.saveSuite("aux-suite", "aux:sidecar", 8); + + const save_ops_response = try handleFramedRequest(std.testing.allocator, "REQ 2041 WORKSPACESAVE ops demo-suite root-a 1024 768", 512, 256, 768); + defer std.testing.allocator.free(save_ops_response); + try std.testing.expectEqualStrings("RESP 2041 18\nWORKSPACESAVE ops\n", save_ops_response); + + const save_sidecar_response = try handleFramedRequest(std.testing.allocator, "REQ 2042 WORKSPACESAVE sidecar aux-suite root-b 800 600", 512, 256, 768); + defer std.testing.allocator.free(save_sidecar_response); + try std.testing.expectEqualStrings("RESP 2042 22\nWORKSPACESAVE sidecar\n", save_sidecar_response); + + const suite_save_response = try handleFramedRequest(std.testing.allocator, "REQ 2043 WORKSPACESUITESAVE crew ops sidecar", 512, 256, 768); + defer std.testing.allocator.free(suite_save_response); + try std.testing.expectEqualStrings("RESP 2043 24\nWORKSPACESUITESAVE crew\n", suite_save_response); + + const release_save_golden_response = try handleFramedRequest(std.testing.allocator, "REQ 2044 WORKSPACESUITERELEASESAVE crew golden", 512, 256, 768); + defer std.testing.allocator.free(release_save_golden_response); + try std.testing.expect(std.mem.startsWith(u8, release_save_golden_response, "RESP 2044 ")); + try std.testing.expect(std.mem.indexOf(u8, release_save_golden_response, "WORKSPACESUITERELEASESAVE crew golden\n") != null); + + const mutate_ops_response = try handleFramedRequest(std.testing.allocator, "REQ 2045 WORKSPACESAVE ops demo-suite root-b 640 400", 512, 256, 768); + defer std.testing.allocator.free(mutate_ops_response); + try std.testing.expectEqualStrings("RESP 2045 18\nWORKSPACESAVE ops\n", mutate_ops_response); + + const suite_overwrite_response = try handleFramedRequest(std.testing.allocator, "REQ 2046 WORKSPACESUITESAVE crew ops", 512, 256, 768); + defer std.testing.allocator.free(suite_overwrite_response); + try std.testing.expectEqualStrings("RESP 2046 24\nWORKSPACESUITESAVE crew\n", suite_overwrite_response); + + const release_save_staging_response = try handleFramedRequest(std.testing.allocator, "REQ 2047 WORKSPACESUITERELEASESAVE crew staging", 512, 256, 768); + defer std.testing.allocator.free(release_save_staging_response); + try std.testing.expect(std.mem.startsWith(u8, release_save_staging_response, "RESP 2047 ")); + try std.testing.expect(std.mem.indexOf(u8, release_save_staging_response, "WORKSPACESUITERELEASESAVE crew staging\n") != null); + + const release_list_response = try handleFramedRequest(std.testing.allocator, "REQ 2048 WORKSPACESUITERELEASELIST crew", 512, 256, 768); + defer std.testing.allocator.free(release_list_response); + try std.testing.expectEqualStrings("RESP 2048 15\ngolden\nstaging\n", release_list_response); + + const release_info_response = try handleFramedRequest(std.testing.allocator, "REQ 2049 WORKSPACESUITERELEASEINFO crew staging", 512, 256, 768); + defer std.testing.allocator.free(release_info_response); + try std.testing.expect(std.mem.startsWith(u8, release_info_response, "RESP 2049 ")); + try std.testing.expect(std.mem.indexOf(u8, release_info_response, "suite=crew") != null); + try std.testing.expect(std.mem.indexOf(u8, release_info_response, "release=staging") != null); + try std.testing.expect(std.mem.indexOf(u8, release_info_response, "saved_seq=2") != null); + try std.testing.expect(std.mem.indexOf(u8, release_info_response, "workspace=ops") != null); + + const release_activate_response = try handleFramedRequest(std.testing.allocator, "REQ 2050 WORKSPACESUITERELEASEACTIVATE crew golden", 512, 256, 768); + defer std.testing.allocator.free(release_activate_response); + try std.testing.expect(std.mem.startsWith(u8, release_activate_response, "RESP 2050 ")); + try std.testing.expect(std.mem.indexOf(u8, release_activate_response, "WORKSPACESUITERELEASEACTIVATE crew golden\n") != null); + + const suite_info_response = try handleFramedRequest(std.testing.allocator, "REQ 2051 WORKSPACESUITEINFO crew", 512, 256, 768); + defer std.testing.allocator.free(suite_info_response); + try std.testing.expect(std.mem.startsWith(u8, suite_info_response, "RESP 2051 ")); + try std.testing.expect(std.mem.indexOf(u8, suite_info_response, "workspace=ops") != null); + try std.testing.expect(std.mem.indexOf(u8, suite_info_response, "workspace=sidecar") != null); + + const release_delete_response = try handleFramedRequest(std.testing.allocator, "REQ 2052 WORKSPACESUITERELEASEDELETE crew staging", 512, 256, 768); + defer std.testing.allocator.free(release_delete_response); + try std.testing.expect(std.mem.startsWith(u8, release_delete_response, "RESP 2052 ")); + try std.testing.expect(std.mem.indexOf(u8, release_delete_response, "WORKSPACESUITERELEASEDELETE crew staging\n") != null); + + const release_list_after_delete_response = try handleFramedRequest(std.testing.allocator, "REQ 2053 WORKSPACESUITERELEASELIST crew", 512, 256, 768); + defer std.testing.allocator.free(release_list_after_delete_response); + try std.testing.expectEqualStrings("RESP 2053 7\ngolden\n", release_list_after_delete_response); + + const mutate_ops_fallback_response = try handleFramedRequest(std.testing.allocator, "REQ 2054 WORKSPACESAVE ops demo-suite root-b 640 400", 512, 256, 768); + defer std.testing.allocator.free(mutate_ops_fallback_response); + try std.testing.expectEqualStrings("RESP 2054 18\nWORKSPACESAVE ops\n", mutate_ops_fallback_response); + + const suite_fallback_response = try handleFramedRequest(std.testing.allocator, "REQ 2055 WORKSPACESUITESAVE crew ops", 512, 256, 768); + defer std.testing.allocator.free(suite_fallback_response); + try std.testing.expectEqualStrings("RESP 2055 24\nWORKSPACESUITESAVE crew\n", suite_fallback_response); + + const release_save_fallback_response = try handleFramedRequest(std.testing.allocator, "REQ 2056 WORKSPACESUITERELEASESAVE crew fallback", 512, 256, 768); + defer std.testing.allocator.free(release_save_fallback_response); + try std.testing.expect(std.mem.startsWith(u8, release_save_fallback_response, "RESP 2056 ")); + try std.testing.expect(std.mem.indexOf(u8, release_save_fallback_response, "WORKSPACESUITERELEASESAVE crew fallback\n") != null); + + const release_prune_response = try handleFramedRequest(std.testing.allocator, "REQ 2057 WORKSPACESUITERELEASEPRUNE crew 1", 512, 256, 768); + defer std.testing.allocator.free(release_prune_response); + try std.testing.expect(std.mem.startsWith(u8, release_prune_response, "RESP 2057 ")); + try std.testing.expect(std.mem.indexOf(u8, release_prune_response, "WORKSPACESUITERELEASEPRUNE crew keep=1 deleted=1 kept=1\n") != null); + + const release_list_final_response = try handleFramedRequest(std.testing.allocator, "REQ 2058 WORKSPACESUITERELEASELIST crew", 512, 256, 768); + defer std.testing.allocator.free(release_list_final_response); + try std.testing.expectEqualStrings("RESP 2058 9\nfallback\n", release_list_final_response); + + const channel_set_response = try handleFramedRequest(std.testing.allocator, "REQ 2059 WORKSPACESUITECHANNELSET crew stable fallback", 512, 256, 768); + defer std.testing.allocator.free(channel_set_response); + try std.testing.expect(std.mem.startsWith(u8, channel_set_response, "RESP 2059 ")); + try std.testing.expect(std.mem.indexOf(u8, channel_set_response, "WORKSPACESUITECHANNELSET crew stable fallback\n") != null); + + const channel_list_response = try handleFramedRequest(std.testing.allocator, "REQ 2060 WORKSPACESUITECHANNELLIST crew", 512, 256, 768); + defer std.testing.allocator.free(channel_list_response); + try std.testing.expectEqualStrings("RESP 2060 7\nstable\n", channel_list_response); + + const channel_info_response = try handleFramedRequest(std.testing.allocator, "REQ 2061 WORKSPACESUITECHANNELINFO crew stable", 512, 256, 768); + defer std.testing.allocator.free(channel_info_response); + try std.testing.expect(std.mem.startsWith(u8, channel_info_response, "RESP 2061 ")); + try std.testing.expect(std.mem.indexOf(u8, channel_info_response, "suite=crew") != null); + try std.testing.expect(std.mem.indexOf(u8, channel_info_response, "channel=stable") != null); + try std.testing.expect(std.mem.indexOf(u8, channel_info_response, "release=fallback") != null); + + const channel_activate_response = try handleFramedRequest(std.testing.allocator, "REQ 2062 WORKSPACESUITECHANNELACTIVATE crew stable", 512, 256, 768); + defer std.testing.allocator.free(channel_activate_response); + try std.testing.expect(std.mem.startsWith(u8, channel_activate_response, "RESP 2062 ")); + try std.testing.expect(std.mem.indexOf(u8, channel_activate_response, "WORKSPACESUITECHANNELACTIVATE crew stable\n") != null); + + const suite_info_after_channel_response = try handleFramedRequest(std.testing.allocator, "REQ 2063 WORKSPACESUITEINFO crew", 512, 256, 768); + defer std.testing.allocator.free(suite_info_after_channel_response); + try std.testing.expect(std.mem.startsWith(u8, suite_info_after_channel_response, "RESP 2063 ")); + try std.testing.expect(std.mem.indexOf(u8, suite_info_after_channel_response, "workspace=ops") != null); + try std.testing.expect(std.mem.indexOf(u8, suite_info_after_channel_response, "workspace=sidecar") == null); +} + test "baremetal tool service persists and runs workspace autorun requests" { resetPersistentStateForTest(); diff --git a/src/baremetal/tool_service/codec.zig b/src/baremetal/tool_service/codec.zig index d40de563..f822c013 100644 --- a/src/baremetal/tool_service/codec.zig +++ b/src/baremetal/tool_service/codec.zig @@ -73,6 +73,28 @@ pub const RequestOp = enum { app_suite_channel_info, app_suite_channel_set, app_suite_channel_activate, + workspace_plan_list, + workspace_plan_info, + workspace_plan_active, + workspace_plan_save, + workspace_plan_apply, + workspace_plan_delete, + workspace_suite_list, + workspace_suite_info, + workspace_suite_save, + workspace_suite_apply, + workspace_suite_run, + workspace_suite_delete, + workspace_suite_release_list, + workspace_suite_release_info, + workspace_suite_release_save, + workspace_suite_release_activate, + workspace_suite_release_delete, + workspace_suite_release_prune, + workspace_suite_channel_list, + workspace_suite_channel_info, + workspace_suite_channel_set, + workspace_suite_channel_activate, workspace_list, workspace_info, workspace_save, @@ -170,6 +192,21 @@ pub const WorkspaceChannelRequest = struct { value: []const u8, }; +pub const WorkspacePlanRequest = struct { + workspace_name: []const u8, + plan_name: []const u8, +}; + +pub const WorkspacePlanSaveRequest = struct { + workspace_name: []const u8, + plan_name: []const u8, + suite_name: []const u8, + trust_bundle: []const u8, + width: u16, + height: u16, + entries_spec: []const u8, +}; + pub const WorkspaceChannelSetRequest = struct { workspace_name: []const u8, channel: []const u8, @@ -228,6 +265,32 @@ pub const WorkspaceSaveRequest = struct { entries_spec: []const u8, }; +pub const WorkspaceSuiteSaveRequest = struct { + suite_name: []const u8, + entries_spec: []const u8, +}; + +pub const WorkspaceSuiteReleaseRequest = struct { + suite_name: []const u8, + release_name: []const u8, +}; + +pub const WorkspaceSuiteReleasePruneRequest = struct { + suite_name: []const u8, + keep: u32, +}; + +pub const WorkspaceSuiteChannelRequest = struct { + suite_name: []const u8, + value: []const u8, +}; + +pub const WorkspaceSuiteChannelSetRequest = struct { + suite_name: []const u8, + channel: []const u8, + release: []const u8, +}; + pub const FramedRequest = struct { request_id: u32, operation: union(RequestOp) { @@ -290,6 +353,28 @@ pub const FramedRequest = struct { app_suite_channel_info: AppSuiteChannelRequest, app_suite_channel_set: AppSuiteChannelSetRequest, app_suite_channel_activate: AppSuiteChannelRequest, + workspace_plan_list: []const u8, + workspace_plan_info: WorkspacePlanRequest, + workspace_plan_active: []const u8, + workspace_plan_save: WorkspacePlanSaveRequest, + workspace_plan_apply: WorkspacePlanRequest, + workspace_plan_delete: WorkspacePlanRequest, + workspace_suite_list: void, + workspace_suite_info: []const u8, + workspace_suite_save: WorkspaceSuiteSaveRequest, + workspace_suite_apply: []const u8, + workspace_suite_run: []const u8, + workspace_suite_delete: []const u8, + workspace_suite_release_list: []const u8, + workspace_suite_release_info: WorkspaceSuiteReleaseRequest, + workspace_suite_release_save: WorkspaceSuiteReleaseRequest, + workspace_suite_release_activate: WorkspaceSuiteReleaseRequest, + workspace_suite_release_delete: WorkspaceSuiteReleaseRequest, + workspace_suite_release_prune: WorkspaceSuiteReleasePruneRequest, + workspace_suite_channel_list: []const u8, + workspace_suite_channel_info: WorkspaceSuiteChannelRequest, + workspace_suite_channel_set: WorkspaceSuiteChannelSetRequest, + workspace_suite_channel_activate: WorkspaceSuiteChannelRequest, workspace_list: void, workspace_info: []const u8, workspace_save: WorkspaceSaveRequest, @@ -1430,6 +1515,438 @@ pub fn parseFramedRequestPrefix(request: []const u8) Error!ConsumedRequest { }; } + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACESUITELIST")) { + if (op_part.rest.len != 0) return error.InvalidFrame; + if (newline_index != null) { + return .{ + .framed = .{ .request_id = request_id, .operation = .{ .workspace_suite_list = {} } }, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = .{ .request_id = request_id, .operation = .{ .workspace_suite_list = {} } }, + .consumed_len = request.len, + }; + } + + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACESUITEINFO")) { + if (op_part.rest.len == 0) return error.InvalidFrame; + if (newline_index != null) { + return .{ + .framed = .{ .request_id = request_id, .operation = .{ .workspace_suite_info = op_part.rest } }, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = .{ .request_id = request_id, .operation = .{ .workspace_suite_info = op_part.rest } }, + .consumed_len = request.len, + }; + } + + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACESUITESAVE")) { + const suite_name = try splitFirstToken(op_part.rest); + const request_value = FramedRequest{ + .request_id = request_id, + .operation = .{ .workspace_suite_save = .{ + .suite_name = suite_name.token, + .entries_spec = suite_name.rest, + } }, + }; + if (newline_index != null) { + return .{ + .framed = request_value, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = request_value, + .consumed_len = request.len, + }; + } + + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACESUITEAPPLY")) { + if (op_part.rest.len == 0) return error.InvalidFrame; + if (newline_index != null) { + return .{ + .framed = .{ .request_id = request_id, .operation = .{ .workspace_suite_apply = op_part.rest } }, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = .{ .request_id = request_id, .operation = .{ .workspace_suite_apply = op_part.rest } }, + .consumed_len = request.len, + }; + } + + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACESUITERUN")) { + if (op_part.rest.len == 0) return error.InvalidFrame; + if (newline_index != null) { + return .{ + .framed = .{ .request_id = request_id, .operation = .{ .workspace_suite_run = op_part.rest } }, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = .{ .request_id = request_id, .operation = .{ .workspace_suite_run = op_part.rest } }, + .consumed_len = request.len, + }; + } + + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACESUITEDELETE")) { + if (op_part.rest.len == 0) return error.InvalidFrame; + if (newline_index != null) { + return .{ + .framed = .{ .request_id = request_id, .operation = .{ .workspace_suite_delete = op_part.rest } }, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = .{ .request_id = request_id, .operation = .{ .workspace_suite_delete = op_part.rest } }, + .consumed_len = request.len, + }; + } + + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACESUITERELEASELIST")) { + if (op_part.rest.len == 0) return error.InvalidFrame; + if (newline_index != null) { + return .{ + .framed = .{ .request_id = request_id, .operation = .{ .workspace_suite_release_list = op_part.rest } }, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = .{ .request_id = request_id, .operation = .{ .workspace_suite_release_list = op_part.rest } }, + .consumed_len = request.len, + }; + } + + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACESUITERELEASEINFO")) { + const suite_name = try splitFirstToken(op_part.rest); + const request_value = FramedRequest{ + .request_id = request_id, + .operation = .{ .workspace_suite_release_info = .{ + .suite_name = suite_name.token, + .release_name = suite_name.rest, + } }, + }; + if (newline_index != null) { + return .{ + .framed = request_value, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = request_value, + .consumed_len = request.len, + }; + } + + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACESUITERELEASESAVE")) { + const suite_name = try splitFirstToken(op_part.rest); + const request_value = FramedRequest{ + .request_id = request_id, + .operation = .{ .workspace_suite_release_save = .{ + .suite_name = suite_name.token, + .release_name = suite_name.rest, + } }, + }; + if (newline_index != null) { + return .{ + .framed = request_value, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = request_value, + .consumed_len = request.len, + }; + } + + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACESUITERELEASEACTIVATE")) { + const suite_name = try splitFirstToken(op_part.rest); + const request_value = FramedRequest{ + .request_id = request_id, + .operation = .{ .workspace_suite_release_activate = .{ + .suite_name = suite_name.token, + .release_name = suite_name.rest, + } }, + }; + if (newline_index != null) { + return .{ + .framed = request_value, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = request_value, + .consumed_len = request.len, + }; + } + + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACESUITERELEASEDELETE")) { + const suite_name = try splitFirstToken(op_part.rest); + const request_value = FramedRequest{ + .request_id = request_id, + .operation = .{ .workspace_suite_release_delete = .{ + .suite_name = suite_name.token, + .release_name = suite_name.rest, + } }, + }; + if (newline_index != null) { + return .{ + .framed = request_value, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = request_value, + .consumed_len = request.len, + }; + } + + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACESUITERELEASEPRUNE")) { + const suite_name = try splitFirstToken(op_part.rest); + if (suite_name.rest.len == 0) return error.InvalidFrame; + const keep = std.fmt.parseInt(u32, suite_name.rest, 10) catch return error.InvalidFrame; + const request_value = FramedRequest{ + .request_id = request_id, + .operation = .{ .workspace_suite_release_prune = .{ + .suite_name = suite_name.token, + .keep = keep, + } }, + }; + if (newline_index != null) { + return .{ + .framed = request_value, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = request_value, + .consumed_len = request.len, + }; + } + + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACESUITECHANNELLIST")) { + if (op_part.rest.len == 0) return error.InvalidFrame; + if (newline_index != null) { + return .{ + .framed = .{ .request_id = request_id, .operation = .{ .workspace_suite_channel_list = op_part.rest } }, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = .{ .request_id = request_id, .operation = .{ .workspace_suite_channel_list = op_part.rest } }, + .consumed_len = request.len, + }; + } + + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACESUITECHANNELINFO")) { + const suite_name = try splitFirstToken(op_part.rest); + if (suite_name.rest.len == 0) return error.InvalidFrame; + const channel_name = try splitFirstToken(suite_name.rest); + if (channel_name.rest.len != 0) return error.InvalidFrame; + const request_value = FramedRequest{ + .request_id = request_id, + .operation = .{ .workspace_suite_channel_info = .{ + .suite_name = suite_name.token, + .value = channel_name.token, + } }, + }; + if (newline_index != null) { + return .{ + .framed = request_value, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = request_value, + .consumed_len = request.len, + }; + } + + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACESUITECHANNELSET")) { + const suite_name = try splitFirstToken(op_part.rest); + if (suite_name.rest.len == 0) return error.InvalidFrame; + const channel_name = try splitFirstToken(suite_name.rest); + if (channel_name.rest.len == 0) return error.InvalidFrame; + const release_name = try splitFirstToken(channel_name.rest); + if (release_name.rest.len != 0) return error.InvalidFrame; + const request_value = FramedRequest{ + .request_id = request_id, + .operation = .{ .workspace_suite_channel_set = .{ + .suite_name = suite_name.token, + .channel = channel_name.token, + .release = release_name.token, + } }, + }; + if (newline_index != null) { + return .{ + .framed = request_value, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = request_value, + .consumed_len = request.len, + }; + } + + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACESUITECHANNELACTIVATE")) { + const suite_name = try splitFirstToken(op_part.rest); + if (suite_name.rest.len == 0) return error.InvalidFrame; + const channel_name = try splitFirstToken(suite_name.rest); + if (channel_name.rest.len != 0) return error.InvalidFrame; + const request_value = FramedRequest{ + .request_id = request_id, + .operation = .{ .workspace_suite_channel_activate = .{ + .suite_name = suite_name.token, + .value = channel_name.token, + } }, + }; + if (newline_index != null) { + return .{ + .framed = request_value, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = request_value, + .consumed_len = request.len, + }; + } + + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACEPLANLIST")) { + if (op_part.rest.len == 0) return error.InvalidFrame; + if (newline_index != null) { + return .{ + .framed = .{ .request_id = request_id, .operation = .{ .workspace_plan_list = op_part.rest } }, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = .{ .request_id = request_id, .operation = .{ .workspace_plan_list = op_part.rest } }, + .consumed_len = request.len, + }; + } + + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACEPLANINFO")) { + const workspace_name = try splitFirstToken(op_part.rest); + if (workspace_name.rest.len == 0) return error.InvalidFrame; + const plan_name = try splitFirstToken(workspace_name.rest); + if (plan_name.rest.len != 0) return error.InvalidFrame; + const request_value = FramedRequest{ + .request_id = request_id, + .operation = .{ .workspace_plan_info = .{ + .workspace_name = workspace_name.token, + .plan_name = plan_name.token, + } }, + }; + if (newline_index != null) { + return .{ + .framed = request_value, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = request_value, + .consumed_len = request.len, + }; + } + + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACEPLANACTIVE")) { + if (op_part.rest.len == 0) return error.InvalidFrame; + if (newline_index != null) { + return .{ + .framed = .{ .request_id = request_id, .operation = .{ .workspace_plan_active = op_part.rest } }, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = .{ .request_id = request_id, .operation = .{ .workspace_plan_active = op_part.rest } }, + .consumed_len = request.len, + }; + } + + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACEPLANSAVE")) { + const workspace_name = try splitFirstToken(op_part.rest); + const plan_name = try splitFirstToken(workspace_name.rest); + const suite_name = try splitFirstToken(plan_name.rest); + const trust_bundle = try splitFirstToken(suite_name.rest); + const width_part = try splitFirstToken(trust_bundle.rest); + const height_part = try splitFirstToken(width_part.rest); + const request_value = FramedRequest{ + .request_id = request_id, + .operation = .{ .workspace_plan_save = .{ + .workspace_name = workspace_name.token, + .plan_name = plan_name.token, + .suite_name = suite_name.token, + .trust_bundle = trust_bundle.token, + .width = std.fmt.parseInt(u16, width_part.token, 10) catch return error.InvalidFrame, + .height = std.fmt.parseInt(u16, height_part.token, 10) catch return error.InvalidFrame, + .entries_spec = height_part.rest, + } }, + }; + if (newline_index != null) { + return .{ + .framed = request_value, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = request_value, + .consumed_len = request.len, + }; + } + + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACEPLANAPPLY")) { + const workspace_name = try splitFirstToken(op_part.rest); + if (workspace_name.rest.len == 0) return error.InvalidFrame; + const plan_name = try splitFirstToken(workspace_name.rest); + if (plan_name.rest.len != 0) return error.InvalidFrame; + const request_value = FramedRequest{ + .request_id = request_id, + .operation = .{ .workspace_plan_apply = .{ + .workspace_name = workspace_name.token, + .plan_name = plan_name.token, + } }, + }; + if (newline_index != null) { + return .{ + .framed = request_value, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = request_value, + .consumed_len = request.len, + }; + } + + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACEPLANDELETE")) { + const workspace_name = try splitFirstToken(op_part.rest); + if (workspace_name.rest.len == 0) return error.InvalidFrame; + const plan_name = try splitFirstToken(workspace_name.rest); + if (plan_name.rest.len != 0) return error.InvalidFrame; + const request_value = FramedRequest{ + .request_id = request_id, + .operation = .{ .workspace_plan_delete = .{ + .workspace_name = workspace_name.token, + .plan_name = plan_name.token, + } }, + }; + if (newline_index != null) { + return .{ + .framed = request_value, + .consumed_len = prefix_len + newline_index.? + 1, + }; + } + return .{ + .framed = request_value, + .consumed_len = request.len, + }; + } + if (std.ascii.eqlIgnoreCase(op_part.token, "WORKSPACELIST")) { if (op_part.rest.len != 0) return error.InvalidFrame; if (newline_index != null) { diff --git a/src/baremetal/workspace_runtime.zig b/src/baremetal/workspace_runtime.zig index a93aa453..228e891c 100644 --- a/src/baremetal/workspace_runtime.zig +++ b/src/baremetal/workspace_runtime.zig @@ -13,10 +13,15 @@ pub const max_name_len: usize = 32; pub const max_workspace_entries: usize = 8; const root_dir = "/runtime/workspaces"; +const suite_root_dir = "/runtime/workspace-suites"; +const suite_release_root_dir = "/runtime/workspace-suite-releases"; +const suite_channel_root_dir = "/runtime/workspace-suite-release-channels"; const runtime_root_dir = "/runtime/workspace-runs"; const release_root_dir = "/runtime/workspace-releases"; const channel_root_dir = "/runtime/workspace-release-channels"; +const max_plan_bytes: usize = 1024; const max_workspace_bytes: usize = 1024; +const max_workspace_suite_bytes: usize = 1024; const max_history_bytes: usize = 1024; const max_autorun_bytes: usize = 1024; const release_list_scan_max_bytes: usize = storage_backend.block_size * 4; @@ -35,9 +40,19 @@ pub const Error = filesystem.Error || app_runtime.Error || package_store.Error | WorkspaceStdoutNotFound, WorkspaceStderrNotFound, WorkspaceAutorunEntryNotFound, + WorkspacePlanNotFound, + WorkspaceActivePlanNotSet, WorkspaceReleaseNotFound, WorkspaceReleaseAlreadyExists, WorkspaceReleaseChannelNotFound, + WorkspaceSuiteReleaseChannelNotFound, + InvalidWorkspacePlanName, + InvalidWorkspaceSuiteName, + WorkspaceSuiteNotFound, + WorkspaceSuiteReleaseNotFound, + WorkspaceSuiteReleaseAlreadyExists, + InvalidWorkspaceSuite, + WorkspaceSuiteEmpty, }; pub const ReleasePruneResult = struct { @@ -108,6 +123,57 @@ const Workspace = struct { } }; +const WorkspacePlan = struct { + workspace_name_len: u8 = 0, + workspace_name_storage: [max_name_len]u8 = [_]u8{0} ** max_name_len, + plan_name_len: u8 = 0, + plan_name_storage: [package_store.max_release_len]u8 = [_]u8{0} ** package_store.max_release_len, + suite_name_len: u8 = 0, + suite_name_storage: [package_store.max_release_len]u8 = [_]u8{0} ** package_store.max_release_len, + trust_bundle_len: u8 = 0, + trust_bundle_storage: [trust_store.max_name_len]u8 = [_]u8{0} ** trust_store.max_name_len, + display_width: u16 = 0, + display_height: u16 = 0, + entry_count: u8 = 0, + entries: [max_workspace_entries]ChannelEntry = [_]ChannelEntry{.{}} ** max_workspace_entries, + + fn workspaceName(self: *const @This()) []const u8 { + return self.workspace_name_storage[0..self.workspace_name_len]; + } + + fn planName(self: *const @This()) []const u8 { + return self.plan_name_storage[0..self.plan_name_len]; + } + + fn suiteName(self: *const @This()) []const u8 { + return self.suite_name_storage[0..self.suite_name_len]; + } + + fn trustBundle(self: *const @This()) []const u8 { + return self.trust_bundle_storage[0..self.trust_bundle_len]; + } +}; + +const WorkspaceSuiteEntry = struct { + workspace_name_len: u8 = 0, + workspace_name_storage: [max_name_len]u8 = [_]u8{0} ** max_name_len, + + fn workspaceName(self: *const @This()) []const u8 { + return self.workspace_name_storage[0..self.workspace_name_len]; + } +}; + +const WorkspaceSuite = struct { + suite_name_len: u8 = 0, + suite_name_storage: [max_name_len]u8 = [_]u8{0} ** max_name_len, + entry_count: u8 = 0, + entries: [max_workspace_entries]WorkspaceSuiteEntry = [_]WorkspaceSuiteEntry{.{}} ** max_workspace_entries, + + fn suiteName(self: *const @This()) []const u8 { + return self.suite_name_storage[0..self.suite_name_len]; + } +}; + pub fn listAlloc(allocator: std.mem.Allocator, max_bytes: usize) Error![]u8 { const raw_listing = filesystem.listDirectoryAlloc(allocator, root_dir, max_workspace_bytes) catch |err| switch (err) { error.FileNotFound => return allocator.alloc(u8, 0), @@ -135,10 +201,309 @@ pub fn listAlloc(allocator: std.mem.Allocator, max_bytes: usize) Error![]u8 { return out.toOwnedSlice(allocator); } +pub fn suiteListAlloc(allocator: std.mem.Allocator, max_bytes: usize) Error![]u8 { + const raw_listing = filesystem.listDirectoryAlloc(allocator, suite_root_dir, max_workspace_suite_bytes) catch |err| switch (err) { + error.FileNotFound => return allocator.alloc(u8, 0), + else => return err, + }; + defer allocator.free(raw_listing); + + var out = std.ArrayList(u8).empty; + defer out.deinit(allocator); + + var lines = std.mem.splitScalar(u8, raw_listing, '\n'); + while (lines.next()) |line| { + if (line.len == 0) continue; + if (!std.mem.startsWith(u8, line, "file ")) continue; + var parts = std.mem.splitScalar(u8, line["file ".len..], ' '); + const file_name = parts.next() orelse continue; + if (!std.mem.endsWith(u8, file_name, ".txt")) continue; + const suite_name = file_name[0 .. file_name.len - ".txt".len]; + if (suite_name.len == 0) continue; + if (out.items.len + suite_name.len + 1 > max_bytes) return error.ResponseTooLarge; + try out.appendSlice(allocator, suite_name); + try out.append(allocator, '\n'); + } + + return out.toOwnedSlice(allocator); +} + +pub fn suiteEntriesAlloc(allocator: std.mem.Allocator, suite_name: []const u8, max_bytes: usize) Error![]u8 { + const suite = try loadWorkspaceSuite(suite_name); + return renderWorkspaceSuiteEntriesAlloc(allocator, suite, max_bytes); +} + +pub fn suiteInfoAlloc(allocator: std.mem.Allocator, suite_name: []const u8, max_bytes: usize) Error![]u8 { + const suite = try loadWorkspaceSuite(suite_name); + var path_buffer: [filesystem.max_path_len]u8 = undefined; + const path = try workspaceSuitePath(suite_name, &path_buffer); + return renderWorkspaceSuiteInfoAlloc(allocator, suite, path, max_bytes); +} + +pub fn suiteReleaseListAlloc(allocator: std.mem.Allocator, suite_name: []const u8, max_bytes: usize) Error![]u8 { + try validateWorkspaceSuiteName(suite_name); + if (!try workspaceSuiteExists(suite_name)) return error.WorkspaceSuiteNotFound; + + var records: [filesystem.max_entries]ReleaseRecord = undefined; + const record_count = try collectSuiteReleaseRecords(suite_name, &records); + sortReleaseRecordsOldestFirst(records[0..record_count]); + + var out = std.ArrayList(u8).empty; + defer out.deinit(allocator); + + for (records[0..record_count]) |record| { + const release_name = record.name(); + if (out.items.len + release_name.len + 1 > max_bytes) return error.ResponseTooLarge; + try out.appendSlice(allocator, release_name); + try out.append(allocator, '\n'); + } + + return out.toOwnedSlice(allocator); +} + +pub fn suiteReleaseInfoAlloc( + allocator: std.mem.Allocator, + suite_name: []const u8, + release: []const u8, + max_bytes: usize, +) Error![]u8 { + try validateWorkspaceSuiteName(suite_name); + try package_store.validateReleaseName(release); + if (!try suiteReleaseExists(suite_name, release)) return error.WorkspaceSuiteReleaseNotFound; + + var metadata_path_buffer: [filesystem.max_path_len]u8 = undefined; + var release_path_buffer: [filesystem.max_path_len]u8 = undefined; + var release_root_buffer: [filesystem.max_path_len]u8 = undefined; + const metadata_path = try suiteReleaseMetadataPath(suite_name, release, &metadata_path_buffer); + const release_path = try suiteReleasePath(suite_name, release, &release_path_buffer); + const release_root = try suiteReleaseDirPath(suite_name, release, &release_root_buffer); + + var metadata_scratch: [256]u8 = undefined; + var metadata_fba = std.heap.FixedBufferAllocator.init(&metadata_scratch); + const metadata_raw = filesystem.readFileAlloc(metadata_fba.allocator(), metadata_path, metadata_scratch.len) catch |err| switch (err) { + error.FileNotFound => return error.WorkspaceSuiteReleaseNotFound, + else => return err, + }; + const metadata = try parseSuiteReleaseMetadata(metadata_raw); + + var payload_scratch: [max_workspace_suite_bytes]u8 = undefined; + var payload_fba = std.heap.FixedBufferAllocator.init(&payload_scratch); + const payload = filesystem.readFileAlloc(payload_fba.allocator(), release_path, payload_scratch.len) catch |err| switch (err) { + error.FileNotFound => return error.WorkspaceSuiteReleaseNotFound, + else => return err, + }; + const suite = try parseWorkspaceSuitePayload(suite_name, payload); + + var out = std.ArrayList(u8).empty; + defer out.deinit(allocator); + try appendLine(&out, allocator, max_bytes, try std.fmt.allocPrint(allocator, "suite={s}\n", .{suite_name})); + try appendLine(&out, allocator, max_bytes, try std.fmt.allocPrint(allocator, "release={s}\n", .{if (metadata.release.len == 0) release else metadata.release})); + try appendLine(&out, allocator, max_bytes, try std.fmt.allocPrint(allocator, "saved_seq={d}\n", .{metadata.saved_seq})); + try appendLine(&out, allocator, max_bytes, try std.fmt.allocPrint(allocator, "saved_tick={d}\n", .{metadata.saved_tick})); + try appendLine(&out, allocator, max_bytes, try std.fmt.allocPrint(allocator, "root={s}\n", .{release_root})); + try appendLine(&out, allocator, max_bytes, try std.fmt.allocPrint(allocator, "suite_path={s}\n", .{release_path})); + try appendLine(&out, allocator, max_bytes, try std.fmt.allocPrint(allocator, "metadata_path={s}\n", .{metadata_path})); + for (suite.entries[0..suite.entry_count]) |entry| { + try appendLine(&out, allocator, max_bytes, try std.fmt.allocPrint(allocator, "workspace={s}\n", .{entry.workspaceName()})); + } + return out.toOwnedSlice(allocator); +} + +pub fn suiteChannelListAlloc(allocator: std.mem.Allocator, suite_name: []const u8, max_bytes: usize) Error![]u8 { + try validateWorkspaceSuiteName(suite_name); + if (!try workspaceSuiteExists(suite_name)) return error.WorkspaceSuiteNotFound; + + var records: [filesystem.max_entries]ReleaseRecord = undefined; + const record_count = try collectSuiteChannelRecords(suite_name, &records); + sortReleaseRecordsOldestFirst(records[0..record_count]); + + var out = std.ArrayList(u8).empty; + defer out.deinit(allocator); + + for (records[0..record_count]) |record| { + const channel_name = record.name(); + if (channel_name.len == 0) continue; + if (out.items.len + channel_name.len + 1 > max_bytes) return error.ResponseTooLarge; + try out.appendSlice(allocator, channel_name); + try out.append(allocator, '\n'); + } + + return out.toOwnedSlice(allocator); +} + +pub fn setSuiteReleaseChannel(suite_name: []const u8, channel: []const u8, release: []const u8, tick: u64) Error!void { + try validateWorkspaceSuiteName(suite_name); + try package_store.validateChannelName(channel); + try package_store.validateReleaseName(release); + if (!try workspaceSuiteExists(suite_name)) return error.WorkspaceSuiteNotFound; + if (!try suiteReleaseExists(suite_name, release)) return error.WorkspaceSuiteReleaseNotFound; + + var channels_root_buffer: [filesystem.max_path_len]u8 = undefined; + var channel_path_buffer: [filesystem.max_path_len]u8 = undefined; + try filesystem.createDirPath(suiteChannelsRootPath(suite_name, &channels_root_buffer)); + try filesystem.writeFile(try suiteChannelPath(suite_name, channel, &channel_path_buffer), release, tick); +} + +pub fn activateSuiteReleaseChannel(suite_name: []const u8, channel: []const u8, tick: u64) Error!void { + var scratch: [package_store.max_release_len]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&scratch); + const release = try readSuiteChannelTargetAlloc(fba.allocator(), suite_name, channel, package_store.max_release_len); + try activateSuiteRelease(suite_name, release, tick); +} + +pub fn suiteChannelInfoAlloc( + allocator: std.mem.Allocator, + suite_name: []const u8, + channel: []const u8, + max_bytes: usize, +) Error![]u8 { + try validateWorkspaceSuiteName(suite_name); + try package_store.validateChannelName(channel); + if (!try workspaceSuiteExists(suite_name)) return error.WorkspaceSuiteNotFound; + + const release = try readSuiteChannelTargetAlloc(allocator, suite_name, channel, package_store.max_release_len); + defer allocator.free(release); + + const payload = try std.fmt.allocPrint( + allocator, + "suite={s}\nchannel={s}\nrelease={s}\n", + .{ suite_name, channel, release }, + ); + errdefer allocator.free(payload); + if (payload.len > max_bytes) return error.ResponseTooLarge; + return payload; +} + pub fn autorunListAlloc(allocator: std.mem.Allocator, max_bytes: usize) Error![]u8 { return readAutorunListAlloc(allocator, max_bytes); } +pub fn planListAlloc(allocator: std.mem.Allocator, name: []const u8, max_bytes: usize) Error![]u8 { + try validateWorkspaceName(name); + if (!try workspaceExists(name)) return error.WorkspaceNotFound; + + var plans_dir_buf: [filesystem.max_path_len]u8 = undefined; + const plans_dir = try plansDirPath(name, &plans_dir_buf); + const raw_listing = filesystem.listDirectoryAlloc(allocator, plans_dir, max_plan_bytes) catch |err| switch (err) { + error.FileNotFound => return allocator.alloc(u8, 0), + else => return err, + }; + defer allocator.free(raw_listing); + + var out = std.ArrayList(u8).empty; + defer out.deinit(allocator); + + var lines = std.mem.splitScalar(u8, raw_listing, '\n'); + while (lines.next()) |line| { + if (!std.mem.startsWith(u8, line, "file ")) continue; + var parts = std.mem.splitScalar(u8, line["file ".len..], ' '); + const file_name = parts.next() orelse continue; + if (!std.mem.endsWith(u8, file_name, ".txt")) continue; + const plan_name = file_name[0 .. file_name.len - ".txt".len]; + if (plan_name.len == 0) continue; + if (out.items.len + plan_name.len + 1 > max_bytes) return error.ResponseTooLarge; + try out.appendSlice(allocator, plan_name); + try out.append(allocator, '\n'); + } + + return out.toOwnedSlice(allocator); +} + +pub fn planInfoAlloc(allocator: std.mem.Allocator, name: []const u8, plan_name: []const u8, max_bytes: usize) Error![]u8 { + const plan = try loadPlan(name, plan_name); + var path_buf: [filesystem.max_path_len]u8 = undefined; + const path = try planPath(name, plan_name, &path_buf); + return renderWorkspacePlanAlloc(allocator, plan, path, null, max_bytes); +} + +pub fn activePlanInfoAlloc(allocator: std.mem.Allocator, name: []const u8, max_bytes: usize) Error![]u8 { + const active_plan_name = try activePlanNameAlloc(allocator, name, package_store.max_release_len); + defer allocator.free(active_plan_name); + + const plan = try loadPlan(name, active_plan_name); + var path_buf: [filesystem.max_path_len]u8 = undefined; + const path = try planPath(name, active_plan_name, &path_buf); + return renderWorkspacePlanAlloc(allocator, plan, path, active_plan_name, max_bytes); +} + +pub fn savePlan( + name: []const u8, + plan_name: []const u8, + suite_name: []const u8, + trust_bundle: []const u8, + display_width: u16, + display_height: u16, + channel_entries_spec: []const u8, + tick: u64, +) Error!void { + try validateWorkspaceName(name); + try validatePlanName(plan_name); + if (!try workspaceExists(name)) return error.WorkspaceNotFound; + try validateSuiteName(suite_name); + try validateTrustBundle(trust_bundle); + try validateDisplayMode(display_width, display_height); + + var plan = try initPlan(name, plan_name); + try copyComponent(plan.suite_name_storage[0..], &plan.suite_name_len, suite_name, error.InvalidWorkspace); + try copyComponent(plan.trust_bundle_storage[0..], &plan.trust_bundle_len, trust_bundle, error.InvalidWorkspace); + plan.display_width = display_width; + plan.display_height = display_height; + try parsePlanChannelEntriesSpec(&plan, channel_entries_spec); + + var workspace_dir_buf: [filesystem.max_path_len]u8 = undefined; + var plans_dir_buf: [filesystem.max_path_len]u8 = undefined; + var path_buf: [filesystem.max_path_len]u8 = undefined; + try filesystem.createDirPath(root_dir); + try filesystem.createDirPath(try workspaceDirPath(name, &workspace_dir_buf)); + try filesystem.createDirPath(try plansDirPath(name, &plans_dir_buf)); + var body_buffer: [max_plan_bytes]u8 = undefined; + const body = try renderWorkspacePlanBody(&plan, &body_buffer); + try filesystem.writeFile(try planPath(name, plan_name, &path_buf), body, tick); +} + +pub fn applyPlan(name: []const u8, plan_name: []const u8, tick: u64) Error!void { + const plan = try loadPlan(name, plan_name); + var workspace = try initWorkspace(plan.workspaceName()); + try copyComponent(workspace.suite_name_storage[0..], &workspace.suite_name_len, plan.suiteName(), error.InvalidWorkspace); + try copyComponent(workspace.trust_bundle_storage[0..], &workspace.trust_bundle_len, plan.trustBundle(), error.InvalidWorkspace); + workspace.display_width = plan.display_width; + workspace.display_height = plan.display_height; + workspace.entry_count = plan.entry_count; + @memcpy(workspace.entries[0..plan.entry_count], plan.entries[0..plan.entry_count]); + + var path_buffer: [filesystem.max_path_len]u8 = undefined; + var body_buffer: [max_workspace_bytes]u8 = undefined; + const body = try renderWorkspaceBody(&workspace, &body_buffer); + try filesystem.writeFile(try workspacePath(name, &path_buffer), body, tick); + try setActivePlan(name, plan_name, tick); +} + +pub fn deletePlan(name: []const u8, plan_name: []const u8, tick: u64) Error!void { + try validateWorkspaceName(name); + try validatePlanName(plan_name); + if (!try workspaceExists(name)) return error.WorkspaceNotFound; + + var path_buf: [filesystem.max_path_len]u8 = undefined; + filesystem.deleteFile(try planPath(name, plan_name, &path_buf), tick) catch |err| switch (err) { + error.FileNotFound => return error.WorkspacePlanNotFound, + else => return err, + }; + + var active_name_buf: [package_store.max_release_len]u8 = undefined; + const active_name = loadActivePlanNameScratch(name, &active_name_buf) catch |err| switch (err) { + error.WorkspaceActivePlanNotSet => null, + else => return err, + }; + if (active_name) |selected| { + if (std.mem.eql(u8, selected, plan_name)) { + clearActivePlan(name, tick) catch |err| switch (err) { + error.WorkspaceActivePlanNotSet => {}, + else => return err, + }; + } + } +} + pub fn releaseListAlloc(allocator: std.mem.Allocator, name: []const u8, max_bytes: usize) Error![]u8 { try validateWorkspaceName(name); if (!try workspaceExists(name)) return error.WorkspaceNotFound; @@ -325,6 +690,92 @@ pub fn saveWorkspace( try filesystem.writeFile(try workspacePath(name, &path_buffer), body, tick); } +pub fn saveSuite(suite_name: []const u8, entries_spec: []const u8, tick: u64) Error!void { + try validateWorkspaceSuiteName(suite_name); + + var suite = WorkspaceSuite{}; + try copyComponent(suite.suite_name_storage[0..], &suite.suite_name_len, suite_name, error.InvalidWorkspaceSuiteName); + try parseWorkspaceSuiteEntriesSpec(&suite, entries_spec); + if (suite.entry_count == 0) return error.WorkspaceSuiteEmpty; + + try filesystem.createDirPath(suite_root_dir); + var path_buffer: [filesystem.max_path_len]u8 = undefined; + var body_buffer: [max_workspace_suite_bytes]u8 = undefined; + const body = try renderWorkspaceSuiteBody(&suite, &body_buffer); + try filesystem.writeFile(try workspaceSuitePath(suite_name, &path_buffer), body, tick); +} + +pub fn snapshotSuiteRelease(suite_name: []const u8, release: []const u8, tick: u64) Error!void { + try validateWorkspaceSuiteName(suite_name); + try package_store.validateReleaseName(release); + if (!try workspaceSuiteExists(suite_name)) return error.WorkspaceSuiteNotFound; + if (try suiteReleaseExists(suite_name, release)) return error.WorkspaceSuiteReleaseAlreadyExists; + + const saved_seq = try nextSuiteReleaseSequence(suite_name); + try createSuiteReleaseDirectories(suite_name, release); + + var canonical_path_buffer: [filesystem.max_path_len]u8 = undefined; + var canonical_scratch: [max_workspace_suite_bytes]u8 = undefined; + var canonical_fba = std.heap.FixedBufferAllocator.init(&canonical_scratch); + const canonical_payload = filesystem.readFileAlloc(canonical_fba.allocator(), try workspaceSuitePath(suite_name, &canonical_path_buffer), canonical_scratch.len) catch |err| switch (err) { + error.FileNotFound => return error.WorkspaceSuiteNotFound, + else => return err, + }; + + var release_path_buffer: [filesystem.max_path_len]u8 = undefined; + try filesystem.writeFile(try suiteReleasePath(suite_name, release, &release_path_buffer), canonical_payload, tick); + try writeSuiteReleaseMetadata(suite_name, release, saved_seq, tick); +} + +pub fn activateSuiteRelease(suite_name: []const u8, release: []const u8, tick: u64) Error!void { + try validateWorkspaceSuiteName(suite_name); + try package_store.validateReleaseName(release); + if (!try suiteReleaseExists(suite_name, release)) return error.WorkspaceSuiteReleaseNotFound; + + var release_path_buffer: [filesystem.max_path_len]u8 = undefined; + var release_scratch: [max_workspace_suite_bytes]u8 = undefined; + var release_fba = std.heap.FixedBufferAllocator.init(&release_scratch); + const release_payload = filesystem.readFileAlloc(release_fba.allocator(), try suiteReleasePath(suite_name, release, &release_path_buffer), release_scratch.len) catch |err| switch (err) { + error.FileNotFound => return error.WorkspaceSuiteReleaseNotFound, + else => return err, + }; + _ = try parseWorkspaceSuitePayload(suite_name, release_payload); + + try filesystem.createDirPath(suite_root_dir); + var canonical_path_buffer: [filesystem.max_path_len]u8 = undefined; + try filesystem.writeFile(try workspaceSuitePath(suite_name, &canonical_path_buffer), release_payload, tick); +} + +pub fn deleteSuiteRelease(suite_name: []const u8, release: []const u8, tick: u64) Error!void { + try validateWorkspaceSuiteName(suite_name); + try package_store.validateReleaseName(release); + if (!try suiteReleaseExists(suite_name, release)) return error.WorkspaceSuiteReleaseNotFound; + + var release_dir_buffer: [filesystem.max_path_len]u8 = undefined; + try filesystem.deleteTree(try suiteReleaseDirPath(suite_name, release, &release_dir_buffer), tick); +} + +pub fn pruneSuiteReleases(suite_name: []const u8, keep: usize, tick: u64) Error!ReleasePruneResult { + try validateWorkspaceSuiteName(suite_name); + if (!try workspaceSuiteExists(suite_name)) return error.WorkspaceSuiteNotFound; + + var records: [filesystem.max_entries]ReleaseRecord = undefined; + const record_count = try collectSuiteReleaseRecords(suite_name, &records); + sortReleaseRecordsNewestFirst(records[0..record_count]); + + var deleted_count: u32 = 0; + var index = keep; + while (index < record_count) : (index += 1) { + try deleteSuiteRelease(suite_name, records[index].name(), tick); + deleted_count += 1; + } + + return .{ + .kept_count = @intCast(@min(keep, record_count)), + .deleted_count = deleted_count, + }; +} + pub fn snapshotWorkspaceRelease(name: []const u8, release: []const u8, tick: u64) Error!void { try validateWorkspaceName(name); try package_store.validateReleaseName(release); @@ -416,6 +867,15 @@ pub fn applyWorkspace(name: []const u8, tick: u64) Error!void { }; } +pub fn applySuite(suite_name: []const u8, tick: u64) Error!void { + const suite = try loadWorkspaceSuite(suite_name); + if (suite.entry_count == 0) return error.WorkspaceSuiteEmpty; + + for (suite.entries[0..suite.entry_count]) |entry| { + try applyWorkspace(entry.workspaceName(), tick); + } +} + pub fn deleteWorkspace(name: []const u8, tick: u64) Error!void { try validateWorkspaceName(name); var path_buffer: [filesystem.max_path_len]u8 = undefined; @@ -437,6 +897,40 @@ pub fn deleteWorkspace(name: []const u8, tick: u64) Error!void { error.FileNotFound => {}, else => return err, }; + var channels_buffer: [filesystem.max_path_len]u8 = undefined; + filesystem.deleteTree(channelsRootPath(name, &channels_buffer), tick) catch |err| switch (err) { + error.FileNotFound => {}, + else => return err, + }; + var plans_buffer: [filesystem.max_path_len]u8 = undefined; + filesystem.deleteTree(try plansDirPath(name, &plans_buffer), tick) catch |err| switch (err) { + error.FileNotFound => {}, + else => return err, + }; + clearActivePlan(name, tick) catch |err| switch (err) { + error.WorkspaceActivePlanNotSet => {}, + else => return err, + }; +} + +pub fn deleteSuite(suite_name: []const u8, tick: u64) Error!void { + var path_buffer: [filesystem.max_path_len]u8 = undefined; + filesystem.deleteFile(try workspaceSuitePath(suite_name, &path_buffer), tick) catch |err| switch (err) { + error.FileNotFound => return error.WorkspaceSuiteNotFound, + else => return err, + }; + + var releases_buffer: [filesystem.max_path_len]u8 = undefined; + filesystem.deleteTree(suiteReleasesRootPath(suite_name, &releases_buffer), tick) catch |err| switch (err) { + error.FileNotFound => {}, + else => return err, + }; + + var channels_buffer: [filesystem.max_path_len]u8 = undefined; + filesystem.deleteTree(suiteChannelsRootPath(suite_name, &channels_buffer), tick) catch |err| switch (err) { + error.FileNotFound => {}, + else => return err, + }; } pub fn suiteNameAlloc(allocator: std.mem.Allocator, name: []const u8, max_bytes: usize) Error![]u8 { @@ -608,6 +1102,40 @@ fn workspacePath(name: []const u8, buffer: *[filesystem.max_path_len]u8) Error![ return std.fmt.bufPrint(buffer, "{s}/{s}.txt", .{ root_dir, name }) catch error.InvalidPath; } +fn workspaceDirPath(name: []const u8, buffer: *[filesystem.max_path_len]u8) Error![]const u8 { + try validateWorkspaceName(name); + return std.fmt.bufPrint(buffer, "{s}/{s}", .{ root_dir, name }) catch error.InvalidPath; +} + +fn plansDirPath(name: []const u8, buffer: *[filesystem.max_path_len]u8) Error![]const u8 { + try validateWorkspaceName(name); + return std.fmt.bufPrint(buffer, "{s}/{s}/plans", .{ root_dir, name }) catch error.InvalidPath; +} + +fn planPath(name: []const u8, plan_name: []const u8, buffer: *[filesystem.max_path_len]u8) Error![]const u8 { + try validateWorkspaceName(name); + try validatePlanName(plan_name); + return std.fmt.bufPrint(buffer, "{s}/{s}/plans/{s}.txt", .{ root_dir, name, plan_name }) catch error.InvalidPath; +} + +fn activePlanPath(name: []const u8, buffer: *[filesystem.max_path_len]u8) Error![]const u8 { + try validateWorkspaceName(name); + return std.fmt.bufPrint(buffer, "{s}/{s}/active_plan.txt", .{ root_dir, name }) catch error.InvalidPath; +} + +fn workspaceSuitePath(name: []const u8, buffer: *[filesystem.max_path_len]u8) Error![]const u8 { + try validateWorkspaceSuiteName(name); + return std.fmt.bufPrint(buffer, "{s}/{s}.txt", .{ suite_root_dir, name }) catch error.InvalidPath; +} + +fn suiteChannelsRootPath(suite_name: []const u8, buffer: *[filesystem.max_path_len]u8) []const u8 { + return std.fmt.bufPrint(buffer, "{s}/{s}", .{ suite_channel_root_dir, suite_name }) catch unreachable; +} + +fn suiteReleasesRootPath(suite_name: []const u8, buffer: *[filesystem.max_path_len]u8) []const u8 { + return std.fmt.bufPrint(buffer, "{s}/{s}", .{ suite_release_root_dir, suite_name }) catch unreachable; +} + fn releasesRootPath(name: []const u8, buffer: *[filesystem.max_path_len]u8) []const u8 { return std.fmt.bufPrint(buffer, "{s}/{s}", .{ release_root_dir, name }) catch unreachable; } @@ -619,19 +1147,43 @@ fn channelsRootPath(name: []const u8, buffer: *[filesystem.max_path_len]u8) []co fn releaseDirPath(name: []const u8, release: []const u8, buffer: *[filesystem.max_path_len]u8) Error![]const u8 { try validateWorkspaceName(name); try package_store.validateReleaseName(release); - return std.fmt.bufPrint(buffer, "{s}/{s}/{s}", .{ release_root_dir, name, release }) catch error.InvalidPath; + return std.fmt.bufPrint(buffer, "{s}/{s}/{s}", .{ release_root_dir, name, release }) catch error.InvalidPath; +} + +fn releasePath(name: []const u8, release: []const u8, buffer: *[filesystem.max_path_len]u8) Error![]const u8 { + try validateWorkspaceName(name); + try package_store.validateReleaseName(release); + return std.fmt.bufPrint(buffer, "{s}/{s}/{s}/workspace.txt", .{ release_root_dir, name, release }) catch error.InvalidPath; +} + +fn releaseMetadataPath(name: []const u8, release: []const u8, buffer: *[filesystem.max_path_len]u8) Error![]const u8 { + try validateWorkspaceName(name); + try package_store.validateReleaseName(release); + return std.fmt.bufPrint(buffer, "{s}/{s}/{s}/release.txt", .{ release_root_dir, name, release }) catch error.InvalidPath; +} + +fn suiteReleaseDirPath(suite_name: []const u8, release: []const u8, buffer: *[filesystem.max_path_len]u8) Error![]const u8 { + try validateWorkspaceSuiteName(suite_name); + try package_store.validateReleaseName(release); + return std.fmt.bufPrint(buffer, "{s}/{s}/{s}", .{ suite_release_root_dir, suite_name, release }) catch error.InvalidPath; } -fn releasePath(name: []const u8, release: []const u8, buffer: *[filesystem.max_path_len]u8) Error![]const u8 { - try validateWorkspaceName(name); +fn suiteReleasePath(suite_name: []const u8, release: []const u8, buffer: *[filesystem.max_path_len]u8) Error![]const u8 { + try validateWorkspaceSuiteName(suite_name); try package_store.validateReleaseName(release); - return std.fmt.bufPrint(buffer, "{s}/{s}/{s}/workspace.txt", .{ release_root_dir, name, release }) catch error.InvalidPath; + return std.fmt.bufPrint(buffer, "{s}/{s}/{s}/suite.txt", .{ suite_release_root_dir, suite_name, release }) catch error.InvalidPath; } -fn releaseMetadataPath(name: []const u8, release: []const u8, buffer: *[filesystem.max_path_len]u8) Error![]const u8 { - try validateWorkspaceName(name); +fn suiteReleaseMetadataPath(suite_name: []const u8, release: []const u8, buffer: *[filesystem.max_path_len]u8) Error![]const u8 { + try validateWorkspaceSuiteName(suite_name); try package_store.validateReleaseName(release); - return std.fmt.bufPrint(buffer, "{s}/{s}/{s}/release.txt", .{ release_root_dir, name, release }) catch error.InvalidPath; + return std.fmt.bufPrint(buffer, "{s}/{s}/{s}/release.txt", .{ suite_release_root_dir, suite_name, release }) catch error.InvalidPath; +} + +fn suiteChannelPath(suite_name: []const u8, channel: []const u8, buffer: *[filesystem.max_path_len]u8) Error![]const u8 { + try validateWorkspaceSuiteName(suite_name); + try package_store.validateChannelName(channel); + return std.fmt.bufPrint(buffer, "{s}/{s}/{s}.txt", .{ suite_channel_root_dir, suite_name, channel }) catch error.InvalidPath; } fn channelPath(name: []const u8, channel: []const u8, buffer: *[filesystem.max_path_len]u8) Error![]const u8 { @@ -640,6 +1192,37 @@ fn channelPath(name: []const u8, channel: []const u8, buffer: *[filesystem.max_p return std.fmt.bufPrint(buffer, "{s}/{s}/{s}.txt", .{ channel_root_dir, name, channel }) catch error.InvalidPath; } +fn setActivePlan(name: []const u8, plan_name: []const u8, tick: u64) Error!void { + var active_path_buf: [filesystem.max_path_len]u8 = undefined; + try filesystem.writeFile(try activePlanPath(name, &active_path_buf), plan_name, tick); +} + +fn clearActivePlan(name: []const u8, tick: u64) Error!void { + var active_path_buf: [filesystem.max_path_len]u8 = undefined; + filesystem.deleteFile(try activePlanPath(name, &active_path_buf), tick) catch |err| switch (err) { + error.FileNotFound => return error.WorkspaceActivePlanNotSet, + else => return err, + }; +} + +fn activePlanNameAlloc(allocator: std.mem.Allocator, name: []const u8, max_bytes: usize) Error![]u8 { + var active_path_buf: [filesystem.max_path_len]u8 = undefined; + return filesystem.readFileAlloc(allocator, try activePlanPath(name, &active_path_buf), max_bytes) catch |err| switch (err) { + error.FileNotFound => error.WorkspaceActivePlanNotSet, + else => err, + }; +} + +fn loadActivePlanNameScratch(name: []const u8, buffer: *[package_store.max_release_len]u8) Error!?[]const u8 { + var fba = std.heap.FixedBufferAllocator.init(buffer); + const active_name = activePlanNameAlloc(fba.allocator(), name, buffer.len) catch |err| switch (err) { + error.WorkspaceActivePlanNotSet => return null, + else => return err, + }; + try validatePlanName(active_name); + return active_name; +} + pub fn statePath(name: []const u8, buffer: *[filesystem.max_path_len]u8) Error![]const u8 { try validateWorkspaceName(name); return std.fmt.bufPrint(buffer, "{s}/{s}/last_run.txt", .{ runtime_root_dir, name }) catch error.InvalidPath; @@ -665,6 +1248,49 @@ fn workspaceRunDirPath(name: []const u8, buffer: *[filesystem.max_path_len]u8) E return std.fmt.bufPrint(buffer, "{s}/{s}", .{ runtime_root_dir, name }) catch error.InvalidPath; } +fn renderWorkspaceSuiteEntriesAlloc( + allocator: std.mem.Allocator, + suite: WorkspaceSuite, + max_bytes: usize, +) Error![]u8 { + var out = std.ArrayList(u8).empty; + defer out.deinit(allocator); + + for (suite.entries[0..suite.entry_count]) |entry| { + try appendLine( + &out, + allocator, + max_bytes, + try std.fmt.allocPrint(allocator, "{s}\n", .{entry.workspaceName()}), + ); + } + + return out.toOwnedSlice(allocator); +} + +fn renderWorkspaceSuiteInfoAlloc( + allocator: std.mem.Allocator, + suite: WorkspaceSuite, + path: []const u8, + max_bytes: usize, +) Error![]u8 { + var out = std.ArrayList(u8).empty; + defer out.deinit(allocator); + + try appendLine(&out, allocator, max_bytes, try std.fmt.allocPrint(allocator, "suite={s}\n", .{suite.suiteName()})); + try appendLine(&out, allocator, max_bytes, try std.fmt.allocPrint(allocator, "path={s}\n", .{path})); + for (suite.entries[0..suite.entry_count]) |entry| { + try appendLine( + &out, + allocator, + max_bytes, + try std.fmt.allocPrint(allocator, "workspace={s}\n", .{entry.workspaceName()}), + ); + } + + return out.toOwnedSlice(allocator); +} + fn renderWorkspaceAlloc( allocator: std.mem.Allocator, workspace: Workspace, @@ -700,6 +1326,37 @@ fn renderWorkspaceAlloc( return out.toOwnedSlice(allocator); } +fn renderWorkspacePlanAlloc( + allocator: std.mem.Allocator, + plan: WorkspacePlan, + path: []const u8, + active_name: ?[]const u8, + max_bytes: usize, +) Error![]u8 { + var out = std.ArrayList(u8).empty; + defer out.deinit(allocator); + + if (active_name) |active| { + try appendLine(&out, allocator, max_bytes, try std.fmt.allocPrint(allocator, "active_plan={s}\n", .{active})); + } + try appendLine(&out, allocator, max_bytes, try std.fmt.allocPrint(allocator, "workspace={s}\n", .{plan.workspaceName()})); + try appendLine(&out, allocator, max_bytes, try std.fmt.allocPrint(allocator, "plan={s}\n", .{plan.planName()})); + try appendLine(&out, allocator, max_bytes, try std.fmt.allocPrint(allocator, "path={s}\n", .{path})); + try appendLine(&out, allocator, max_bytes, try std.fmt.allocPrint(allocator, "suite={s}\n", .{if (plan.suiteName().len == 0) "none" else plan.suiteName()})); + try appendLine(&out, allocator, max_bytes, try std.fmt.allocPrint(allocator, "trust_bundle={s}\n", .{if (plan.trustBundle().len == 0) "none" else plan.trustBundle()})); + try appendLine(&out, allocator, max_bytes, try std.fmt.allocPrint(allocator, "display={d}x{d}\n", .{ plan.display_width, plan.display_height })); + for (plan.entries[0..plan.entry_count]) |entry| { + try appendLine( + &out, + allocator, + max_bytes, + try std.fmt.allocPrint(allocator, "channel={s}:{s}:{s}\n", .{ entry.packageName(), entry.channelName(), entry.releaseName() }), + ); + } + + return out.toOwnedSlice(allocator); +} + fn appendLine(out: *std.ArrayList(u8), allocator: std.mem.Allocator, max_bytes: usize, line: []u8) Error!void { defer allocator.free(line); if (out.items.len + line.len > max_bytes) return error.ResponseTooLarge; @@ -720,6 +1377,28 @@ fn renderWorkspaceBody(workspace: *const Workspace, buffer: *[max_workspace_byte return buffer[0..used]; } +fn renderWorkspacePlanBody(plan: *const WorkspacePlan, buffer: *[max_plan_bytes]u8) Error![]const u8 { + var used: usize = 0; + + used += (std.fmt.bufPrint(buffer[used..], "suite={s}\n", .{if (plan.suiteName().len == 0) "none" else plan.suiteName()}) catch return error.ResponseTooLarge).len; + used += (std.fmt.bufPrint(buffer[used..], "trust_bundle={s}\n", .{if (plan.trustBundle().len == 0) "none" else plan.trustBundle()}) catch return error.ResponseTooLarge).len; + used += (std.fmt.bufPrint(buffer[used..], "display_width={d}\n", .{plan.display_width}) catch return error.ResponseTooLarge).len; + used += (std.fmt.bufPrint(buffer[used..], "display_height={d}\n", .{plan.display_height}) catch return error.ResponseTooLarge).len; + for (plan.entries[0..plan.entry_count]) |entry| { + used += (std.fmt.bufPrint(buffer[used..], "channel={s}:{s}:{s}\n", .{ entry.packageName(), entry.channelName(), entry.releaseName() }) catch return error.ResponseTooLarge).len; + } + + return buffer[0..used]; +} + +fn renderWorkspaceSuiteBody(suite: *const WorkspaceSuite, buffer: *[max_workspace_suite_bytes]u8) Error![]const u8 { + var used: usize = 0; + for (suite.entries[0..suite.entry_count]) |entry| { + used += (std.fmt.bufPrint(buffer[used..], "workspace={s}\n", .{entry.workspaceName()}) catch return error.ResponseTooLarge).len; + } + return buffer[0..used]; +} + fn loadWorkspace(name: []const u8) Error!Workspace { try validateWorkspaceName(name); var path_buffer: [filesystem.max_path_len]u8 = undefined; @@ -732,6 +1411,33 @@ fn loadWorkspace(name: []const u8) Error!Workspace { return parseWorkspacePayload(name, payload); } +fn loadPlan(name: []const u8, plan_name: []const u8) Error!WorkspacePlan { + try validateWorkspaceName(name); + try validatePlanName(plan_name); + if (!try workspaceExists(name)) return error.WorkspaceNotFound; + + var path_buffer: [filesystem.max_path_len]u8 = undefined; + var scratch: [max_plan_bytes]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&scratch); + const payload = filesystem.readFileAlloc(fba.allocator(), try planPath(name, plan_name, &path_buffer), max_plan_bytes) catch |err| switch (err) { + error.FileNotFound => return error.WorkspacePlanNotFound, + else => return err, + }; + return parseWorkspacePlanPayload(name, plan_name, payload); +} + +fn loadWorkspaceSuite(suite_name: []const u8) Error!WorkspaceSuite { + try validateWorkspaceSuiteName(suite_name); + var path_buffer: [filesystem.max_path_len]u8 = undefined; + var scratch: [max_workspace_suite_bytes]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&scratch); + const payload = filesystem.readFileAlloc(fba.allocator(), try workspaceSuitePath(suite_name, &path_buffer), max_workspace_suite_bytes) catch |err| switch (err) { + error.FileNotFound => return error.WorkspaceSuiteNotFound, + else => return err, + }; + return parseWorkspaceSuitePayload(suite_name, payload); +} + fn workspaceExists(name: []const u8) Error!bool { var path_buffer: [filesystem.max_path_len]u8 = undefined; _ = filesystem.statSummary(try workspacePath(name, &path_buffer)) catch |err| switch (err) { @@ -741,6 +1447,15 @@ fn workspaceExists(name: []const u8) Error!bool { return true; } +fn workspaceSuiteExists(name: []const u8) Error!bool { + var path_buffer: [filesystem.max_path_len]u8 = undefined; + _ = filesystem.statSummary(try workspaceSuitePath(name, &path_buffer)) catch |err| switch (err) { + error.FileNotFound => return false, + else => return err, + }; + return true; +} + fn releaseExists(name: []const u8, release: []const u8) Error!bool { var path_buffer: [filesystem.max_path_len]u8 = undefined; _ = filesystem.statSummary(try releasePath(name, release, &path_buffer)) catch |err| switch (err) { @@ -750,6 +1465,15 @@ fn releaseExists(name: []const u8, release: []const u8) Error!bool { return true; } +fn suiteReleaseExists(suite_name: []const u8, release: []const u8) Error!bool { + var path_buffer: [filesystem.max_path_len]u8 = undefined; + _ = filesystem.statSummary(try suiteReleasePath(suite_name, release, &path_buffer)) catch |err| switch (err) { + error.FileNotFound => return false, + else => return err, + }; + return true; +} + fn parseWorkspacePayload(name: []const u8, payload: []const u8) Error!Workspace { var workspace = Workspace{}; try copyComponent(workspace.workspace_name_storage[0..], &workspace.workspace_name_len, name, error.InvalidWorkspaceName); @@ -798,6 +1522,90 @@ fn parseWorkspacePayload(name: []const u8, payload: []const u8) Error!Workspace return workspace; } +fn parseWorkspacePlanPayload(name: []const u8, plan_name: []const u8, payload: []const u8) Error!WorkspacePlan { + var plan = try initPlan(name, plan_name); + + var have_width = false; + var have_height = false; + + var lines = std.mem.splitScalar(u8, payload, '\n'); + while (lines.next()) |raw_line| { + const line = std.mem.trim(u8, raw_line, "\r"); + if (line.len == 0) continue; + + if (std.mem.startsWith(u8, line, "suite=")) { + const value = line["suite=".len..]; + const suite_name = if (std.ascii.eqlIgnoreCase(value, "none")) "" else value; + try validateSuiteName(suite_name); + try copyComponent(plan.suite_name_storage[0..], &plan.suite_name_len, suite_name, error.InvalidWorkspace); + continue; + } + if (std.mem.startsWith(u8, line, "trust_bundle=")) { + const value = line["trust_bundle=".len..]; + const trust_name = if (std.ascii.eqlIgnoreCase(value, "none")) "" else value; + try validateTrustBundle(trust_name); + try copyComponent(plan.trust_bundle_storage[0..], &plan.trust_bundle_len, trust_name, error.InvalidWorkspace); + continue; + } + if (std.mem.startsWith(u8, line, "display_width=")) { + plan.display_width = std.fmt.parseInt(u16, line["display_width=".len..], 10) catch return error.InvalidWorkspace; + have_width = true; + continue; + } + if (std.mem.startsWith(u8, line, "display_height=")) { + plan.display_height = std.fmt.parseInt(u16, line["display_height=".len..], 10) catch return error.InvalidWorkspace; + have_height = true; + continue; + } + if (std.mem.startsWith(u8, line, "channel=")) { + try parsePlanChannelEntryLine(&plan, line["channel=".len..]); + continue; + } + return error.InvalidWorkspace; + } + + if (!have_width or !have_height) return error.InvalidWorkspace; + try validateDisplayMode(plan.display_width, plan.display_height); + return plan; +} + +fn parseWorkspaceSuitePayload(name: []const u8, payload: []const u8) Error!WorkspaceSuite { + var suite = WorkspaceSuite{}; + try copyComponent(suite.suite_name_storage[0..], &suite.suite_name_len, name, error.InvalidWorkspaceSuiteName); + + var lines = std.mem.splitScalar(u8, payload, '\n'); + while (lines.next()) |raw_line| { + const line = std.mem.trim(u8, raw_line, "\r"); + if (line.len == 0) continue; + if (std.mem.startsWith(u8, line, "workspace=")) { + try parseWorkspaceSuiteEntryLine(&suite, line["workspace=".len..]); + continue; + } + return error.InvalidWorkspaceSuite; + } + + if (suite.entry_count == 0) return error.WorkspaceSuiteEmpty; + return suite; +} + +fn parseWorkspaceSuiteEntriesSpec(suite: *WorkspaceSuite, spec: []const u8) Error!void { + if (spec.len == 0) return; + var iter = std.mem.tokenizeAny(u8, spec, " \t\r\n"); + while (iter.next()) |token| { + try parseWorkspaceSuiteEntryLine(suite, token); + } +} + +fn parseWorkspaceSuiteEntryLine(suite: *WorkspaceSuite, line: []const u8) Error!void { + try validateWorkspaceName(line); + if (!try workspaceExists(line)) return error.WorkspaceNotFound; + if (suite.entry_count >= max_workspace_entries) return error.WorkspaceEntryLimit; + + const entry = &suite.entries[suite.entry_count]; + try copyComponent(entry.workspace_name_storage[0..], &entry.workspace_name_len, line, error.InvalidWorkspaceSuite); + suite.entry_count += 1; +} + fn parseChannelEntriesSpec(workspace: *Workspace, spec: []const u8) Error!void { if (spec.len == 0) return; var iter = std.mem.tokenizeAny(u8, spec, " \t\r\n"); @@ -806,6 +1614,14 @@ fn parseChannelEntriesSpec(workspace: *Workspace, spec: []const u8) Error!void { } } +fn parsePlanChannelEntriesSpec(plan: *WorkspacePlan, spec: []const u8) Error!void { + if (spec.len == 0) return; + var iter = std.mem.tokenizeAny(u8, spec, " \t\r\n"); + while (iter.next()) |token| { + try parsePlanChannelEntryLine(plan, token); + } +} + fn parseChannelEntryLine(workspace: *Workspace, line: []const u8) Error!void { const first_sep = std.mem.indexOfScalar(u8, line, ':') orelse return error.InvalidWorkspaceEntry; const second_rel = std.mem.indexOfScalar(u8, line[first_sep + 1 ..], ':') orelse return error.InvalidWorkspaceEntry; @@ -833,6 +1649,33 @@ fn parseChannelEntryLine(workspace: *Workspace, line: []const u8) Error!void { workspace.entry_count += 1; } +fn parsePlanChannelEntryLine(plan: *WorkspacePlan, line: []const u8) Error!void { + const first_sep = std.mem.indexOfScalar(u8, line, ':') orelse return error.InvalidWorkspaceEntry; + const second_rel = std.mem.indexOfScalar(u8, line[first_sep + 1 ..], ':') orelse return error.InvalidWorkspaceEntry; + const second_sep = first_sep + 1 + second_rel; + + const package_name = line[0..first_sep]; + const channel_name = line[first_sep + 1 .. second_sep]; + const release_name = line[second_sep + 1 ..]; + if (package_name.len == 0 or channel_name.len == 0 or release_name.len == 0) return error.InvalidWorkspaceEntry; + if (std.mem.indexOfScalar(u8, release_name, ':') != null) return error.InvalidWorkspaceEntry; + + try package_store.validatePackageName(package_name); + try package_store.validateChannelName(channel_name); + try package_store.validateReleaseName(release_name); + + var entrypoint_buffer: [filesystem.max_path_len]u8 = undefined; + _ = try package_store.loadLaunchProfile(package_name, &entrypoint_buffer); + if (!try package_store.releaseExistsAlloc(package_name, release_name)) return error.PackageReleaseNotFound; + + if (plan.entry_count >= max_workspace_entries) return error.WorkspaceEntryLimit; + const entry = &plan.entries[plan.entry_count]; + try copyComponent(entry.package_name_storage[0..], &entry.package_name_len, package_name, error.InvalidWorkspaceEntry); + try copyComponent(entry.channel_name_storage[0..], &entry.channel_name_len, channel_name, error.InvalidWorkspaceEntry); + try copyComponent(entry.release_name_storage[0..], &entry.release_name_len, release_name, error.InvalidWorkspaceEntry); + plan.entry_count += 1; +} + fn validateWorkspaceName(name: []const u8) Error!void { if (name.len == 0 or name.len > max_name_len) return error.InvalidWorkspaceName; for (name) |char| { @@ -841,6 +1684,22 @@ fn validateWorkspaceName(name: []const u8) Error!void { } } +fn validateWorkspaceSuiteName(name: []const u8) Error!void { + if (name.len == 0 or name.len > max_name_len) return error.InvalidWorkspaceSuiteName; + for (name) |char| { + if (std.ascii.isAlphanumeric(char) or char == '-' or char == '_' or char == '.') continue; + return error.InvalidWorkspaceSuiteName; + } +} + +fn validatePlanName(name: []const u8) Error!void { + if (name.len == 0 or name.len > package_store.max_release_len) return error.InvalidWorkspacePlanName; + for (name) |char| { + if (std.ascii.isAlphanumeric(char) or char == '-' or char == '_' or char == '.') continue; + return error.InvalidWorkspacePlanName; + } +} + fn validateSuiteName(name: []const u8) Error!void { if (name.len == 0) return; var scratch: [512]u8 = undefined; @@ -872,6 +1731,19 @@ fn copyComponent(storage: []u8, len_ptr: anytype, value: []const u8, comptime er len_ptr.* = @as(u8, @intCast(value.len)); } +fn initWorkspace(name: []const u8) Error!Workspace { + var workspace = Workspace{}; + try copyComponent(workspace.workspace_name_storage[0..], &workspace.workspace_name_len, name, error.InvalidWorkspaceName); + return workspace; +} + +fn initPlan(name: []const u8, plan_name: []const u8) Error!WorkspacePlan { + var plan = WorkspacePlan{}; + try copyComponent(plan.workspace_name_storage[0..], &plan.workspace_name_len, name, error.InvalidWorkspaceName); + try copyComponent(plan.plan_name_storage[0..], &plan.plan_name_len, plan_name, error.InvalidWorkspacePlanName); + return plan; +} + fn readAutorunListAlloc(allocator: std.mem.Allocator, max_bytes: usize) Error![]u8 { return filesystem.readFileAlloc(allocator, autorun_list_path, max_bytes) catch |err| switch (err) { error.FileNotFound => allocator.dupe(u8, ""), @@ -894,6 +1766,13 @@ fn createReleaseDirectories(name: []const u8, release: []const u8) Error!void { try filesystem.createDirPath(try releaseDirPath(name, release, &release_dir_buffer)); } +fn createSuiteReleaseDirectories(suite_name: []const u8, release: []const u8) Error!void { + var releases_root_buffer: [filesystem.max_path_len]u8 = undefined; + var release_dir_buffer: [filesystem.max_path_len]u8 = undefined; + try filesystem.createDirPath(suiteReleasesRootPath(suite_name, &releases_root_buffer)); + try filesystem.createDirPath(try suiteReleaseDirPath(suite_name, release, &release_dir_buffer)); +} + fn writeReleaseMetadata(name: []const u8, release: []const u8, saved_seq: u32, saved_tick: u64) Error!void { var metadata_path_buffer: [filesystem.max_path_len]u8 = undefined; var body_buffer: [192]u8 = undefined; @@ -905,14 +1784,52 @@ fn writeReleaseMetadata(name: []const u8, release: []const u8, saved_seq: u32, s try filesystem.writeFile(try releaseMetadataPath(name, release, &metadata_path_buffer), body, saved_tick); } -fn parseReleaseMetadata(payload: []const u8) Error!ReleaseMetadata { +fn writeSuiteReleaseMetadata(suite_name: []const u8, release: []const u8, saved_seq: u32, saved_tick: u64) Error!void { + var metadata_path_buffer: [filesystem.max_path_len]u8 = undefined; + var body_buffer: [192]u8 = undefined; + const body = std.fmt.bufPrint( + &body_buffer, + "suite={s}\nrelease={s}\nsaved_seq={d}\nsaved_tick={d}\n", + .{ suite_name, release, saved_seq, saved_tick }, + ) catch return error.ResponseTooLarge; + try filesystem.writeFile(try suiteReleaseMetadataPath(suite_name, release, &metadata_path_buffer), body, saved_tick); +} + +fn parseReleaseMetadata(payload: []const u8) Error!ReleaseMetadata { + var metadata = ReleaseMetadata{}; + var lines = std.mem.splitScalar(u8, payload, '\n'); + while (lines.next()) |raw_line| { + const line = std.mem.trim(u8, raw_line, "\r"); + if (line.len == 0) continue; + if (std.mem.startsWith(u8, line, "workspace=")) { + metadata.name = line["workspace=".len..]; + continue; + } + if (std.mem.startsWith(u8, line, "release=")) { + metadata.release = line["release=".len..]; + continue; + } + if (std.mem.startsWith(u8, line, "saved_seq=")) { + metadata.saved_seq = std.fmt.parseInt(u32, line["saved_seq=".len..], 10) catch return error.InvalidWorkspace; + continue; + } + if (std.mem.startsWith(u8, line, "saved_tick=")) { + metadata.saved_tick = std.fmt.parseInt(u64, line["saved_tick=".len..], 10) catch return error.InvalidWorkspace; + continue; + } + return error.InvalidWorkspace; + } + return metadata; +} + +fn parseSuiteReleaseMetadata(payload: []const u8) Error!ReleaseMetadata { var metadata = ReleaseMetadata{}; var lines = std.mem.splitScalar(u8, payload, '\n'); while (lines.next()) |raw_line| { const line = std.mem.trim(u8, raw_line, "\r"); if (line.len == 0) continue; - if (std.mem.startsWith(u8, line, "workspace=")) { - metadata.name = line["workspace=".len..]; + if (std.mem.startsWith(u8, line, "suite=")) { + metadata.name = line["suite=".len..]; continue; } if (std.mem.startsWith(u8, line, "release=")) { @@ -920,14 +1837,14 @@ fn parseReleaseMetadata(payload: []const u8) Error!ReleaseMetadata { continue; } if (std.mem.startsWith(u8, line, "saved_seq=")) { - metadata.saved_seq = std.fmt.parseInt(u32, line["saved_seq=".len..], 10) catch return error.InvalidWorkspace; + metadata.saved_seq = std.fmt.parseInt(u32, line["saved_seq=".len..], 10) catch return error.InvalidWorkspaceSuite; continue; } if (std.mem.startsWith(u8, line, "saved_tick=")) { - metadata.saved_tick = std.fmt.parseInt(u64, line["saved_tick=".len..], 10) catch return error.InvalidWorkspace; + metadata.saved_tick = std.fmt.parseInt(u64, line["saved_tick=".len..], 10) catch return error.InvalidWorkspaceSuite; continue; } - return error.InvalidWorkspace; + return error.InvalidWorkspaceSuite; } return metadata; } @@ -942,6 +1859,16 @@ fn nextReleaseSequence(name: []const u8) Error!u32 { return max_seq + 1; } +fn nextSuiteReleaseSequence(suite_name: []const u8) Error!u32 { + var records: [filesystem.max_entries]ReleaseRecord = undefined; + const record_count = try collectSuiteReleaseRecords(suite_name, &records); + var max_seq: u32 = 0; + for (records[0..record_count]) |record| { + max_seq = @max(max_seq, record.saved_seq); + } + return max_seq + 1; +} + fn collectReleaseRecords(name: []const u8, records: *[filesystem.max_entries]ReleaseRecord) Error!usize { var releases_buffer: [filesystem.max_path_len]u8 = undefined; const releases_root = releasesRootPath(name, &releases_buffer); @@ -980,6 +1907,70 @@ fn collectReleaseRecords(name: []const u8, records: *[filesystem.max_entries]Rel return count; } +fn collectSuiteReleaseRecords(suite_name: []const u8, records: *[filesystem.max_entries]ReleaseRecord) Error!usize { + var releases_buffer: [filesystem.max_path_len]u8 = undefined; + const releases_root = suiteReleasesRootPath(suite_name, &releases_buffer); + + var count: usize = 0; + var idx: u32 = 0; + while (idx < filesystem.max_entries) : (idx += 1) { + const record = filesystem.entry(idx); + if (record.kind != abi.filesystem_kind_directory) continue; + const path = record.path[0..record.path_len]; + const release_name = directChildName(releases_root, path) orelse continue; + if (release_name.len == 0) continue; + if (findReleaseRecord(records[0..count], release_name) != null) continue; + + var saved_seq: u32 = 0; + var metadata_path_buffer: [filesystem.max_path_len]u8 = undefined; + const metadata_path = try suiteReleaseMetadataPath(suite_name, release_name, &metadata_path_buffer); + var metadata_scratch: [release_list_scan_max_bytes]u8 = undefined; + var metadata_fba = std.heap.FixedBufferAllocator.init(&metadata_scratch); + const metadata_raw = filesystem.readFileAlloc(metadata_fba.allocator(), metadata_path, metadata_scratch.len) catch |err| switch (err) { + error.FileNotFound => null, + else => return err, + }; + if (metadata_raw) |raw| { + const metadata = try parseSuiteReleaseMetadata(raw); + saved_seq = metadata.saved_seq; + } + + records[count] = .{ + .name_len = @intCast(release_name.len), + .saved_seq = saved_seq, + }; + @memcpy(records[count].name_storage[0..release_name.len], release_name); + count += 1; + } + return count; +} + +fn collectSuiteChannelRecords(suite_name: []const u8, records: *[filesystem.max_entries]ReleaseRecord) Error!usize { + var channels_root_buffer: [filesystem.max_path_len]u8 = undefined; + const channels_root = suiteChannelsRootPath(suite_name, &channels_root_buffer); + + var count: usize = 0; + var idx: u32 = 0; + while (idx < filesystem.max_entries) : (idx += 1) { + const record = filesystem.entry(idx); + if (record.kind != abi.filesystem_kind_file) continue; + const path = record.path[0..record.path_len]; + const channel_name_with_ext = directChildName(channels_root, path) orelse continue; + if (!std.mem.endsWith(u8, channel_name_with_ext, ".txt")) continue; + const channel_name = channel_name_with_ext[0 .. channel_name_with_ext.len - ".txt".len]; + if (channel_name.len == 0) continue; + if (findReleaseRecord(records[0..count], channel_name) != null) continue; + + records[count] = .{ + .name_len = @intCast(channel_name.len), + .saved_seq = 0, + }; + @memcpy(records[count].name_storage[0..channel_name.len], channel_name); + count += 1; + } + return count; +} + fn collectChannelRecords(name: []const u8, records: *[filesystem.max_entries]ReleaseRecord) Error!usize { var channels_root_buffer: [filesystem.max_path_len]u8 = undefined; const channels_root = channelsRootPath(name, &channels_root_buffer); @@ -1025,6 +2016,25 @@ fn readChannelTargetAlloc(allocator: std.mem.Allocator, name: []const u8, channe return raw; } +fn readSuiteChannelTargetAlloc(allocator: std.mem.Allocator, suite_name: []const u8, channel: []const u8, max_bytes: usize) Error![]u8 { + try validateWorkspaceSuiteName(suite_name); + try package_store.validateChannelName(channel); + if (!try workspaceSuiteExists(suite_name)) return error.WorkspaceSuiteNotFound; + + var channel_path_buffer: [filesystem.max_path_len]u8 = undefined; + const raw = filesystem.readFileAlloc(allocator, try suiteChannelPath(suite_name, channel, &channel_path_buffer), max_bytes) catch |err| switch (err) { + error.FileNotFound => return error.WorkspaceSuiteReleaseChannelNotFound, + else => return err, + }; + const trimmed = std.mem.trim(u8, raw, " \t\r\n"); + if (trimmed.len != raw.len) { + const normalized = try allocator.dupe(u8, trimmed); + allocator.free(raw); + return normalized; + } + return raw; +} + fn findReleaseRecord(records: []const ReleaseRecord, release_name: []const u8) ?usize { for (records, 0..) |record, index| { if (std.mem.eql(u8, record.name(), release_name)) return index; @@ -1286,6 +2296,115 @@ test "workspace runtime persists autorun registry and clears stale entries" { try std.testing.expectEqualStrings("sidecar\n", updated); } +test "workspace runtime manages workspace plans" { + storage_backend.resetForTest(); + filesystem.resetForTest(); + framebuffer_console.resetForTest(); + + try trust_store.installBundle("root-a", "root-a-cert", 1); + try trust_store.installBundle("root-b", "root-b-cert", 2); + try package_store.installScriptPackage("demo", "echo workspace-plan-r1", 3); + try package_store.snapshotPackageRelease("demo", "r1", 4); + try package_store.installScriptPackage("demo", "echo workspace-plan-r2", 5); + try package_store.snapshotPackageRelease("demo", "r2", 6); + try app_runtime.savePlan("demo", "boot", "", "", abi.display_connector_virtual, 1024, 768, false, 7); + try app_runtime.saveSuite("duo", "demo:boot", 8); + try saveWorkspace("ops", "duo", "root-a", 1024, 768, "demo:stable:r1", 9); + + try savePlan("ops", "golden", "duo", "root-a", 1024, 768, "demo:stable:r1", 10); + try savePlan("ops", "staging", "", "", 640, 400, "demo:stable:r2", 11); + + const plan_list = try planListAlloc(std.testing.allocator, "ops", 64); + defer std.testing.allocator.free(plan_list); + try std.testing.expectEqualStrings("golden\nstaging\n", plan_list); + + const plan_info = try planInfoAlloc(std.testing.allocator, "ops", "staging", 256); + defer std.testing.allocator.free(plan_info); + try std.testing.expect(std.mem.indexOf(u8, plan_info, "workspace=ops") != null); + try std.testing.expect(std.mem.indexOf(u8, plan_info, "plan=staging") != null); + try std.testing.expect(std.mem.indexOf(u8, plan_info, "suite=none") != null); + try std.testing.expect(std.mem.indexOf(u8, plan_info, "trust_bundle=none") != null); + try std.testing.expect(std.mem.indexOf(u8, plan_info, "display=640x400") != null); + try std.testing.expect(std.mem.indexOf(u8, plan_info, "channel=demo:stable:r2") != null); + + try applyPlan("ops", "golden", 12); + + const golden_active = try activePlanInfoAlloc(std.testing.allocator, "ops", 256); + defer std.testing.allocator.free(golden_active); + try std.testing.expect(std.mem.indexOf(u8, golden_active, "active_plan=golden") != null); + + const restored = try infoAlloc(std.testing.allocator, "ops", 512); + defer std.testing.allocator.free(restored); + try std.testing.expect(std.mem.indexOf(u8, restored, "suite=duo") != null); + try std.testing.expect(std.mem.indexOf(u8, restored, "trust_bundle=root-a") != null); + try std.testing.expect(std.mem.indexOf(u8, restored, "display=1024x768") != null); + try std.testing.expect(std.mem.indexOf(u8, restored, "channel=demo:stable:r1") != null); + + try applyPlan("ops", "staging", 13); + + const staging_active = try activePlanInfoAlloc(std.testing.allocator, "ops", 256); + defer std.testing.allocator.free(staging_active); + try std.testing.expect(std.mem.indexOf(u8, staging_active, "active_plan=staging") != null); + + const mutated = try infoAlloc(std.testing.allocator, "ops", 512); + defer std.testing.allocator.free(mutated); + try std.testing.expect(std.mem.indexOf(u8, mutated, "suite=none") != null); + try std.testing.expect(std.mem.indexOf(u8, mutated, "trust_bundle=none") != null); + try std.testing.expect(std.mem.indexOf(u8, mutated, "display=640x400") != null); + try std.testing.expect(std.mem.indexOf(u8, mutated, "channel=demo:stable:r2") != null); + + try deletePlan("ops", "golden", 14); + const final_list = try planListAlloc(std.testing.allocator, "ops", 64); + defer std.testing.allocator.free(final_list); + try std.testing.expectEqualStrings("staging\n", final_list); + + try deleteWorkspace("ops", 15); + try std.testing.expectError(error.WorkspaceNotFound, planListAlloc(std.testing.allocator, "ops", 64)); + if (filesystem.statSummary("/runtime/workspaces/ops/plans")) |_| { + return error.TestUnexpectedResult; + } else |err| switch (err) { + error.FileNotFound => {}, + else => return err, + } +} + +test "workspace runtime persists workspace plans on ata-backed storage" { + storage_backend.resetForTest(); + filesystem.resetForTest(); + framebuffer_console.resetForTest(); + ata_pio_disk.testEnableMockDevice(8192); + ata_pio_disk.testInstallMockMbrPartition(2048, 4096, 0x83); + defer ata_pio_disk.testDisableMockDevice(); + + try trust_store.installBundle("persisted-root", "persisted-cert", 1); + try package_store.installScriptPackage("persisted", "echo persisted-r1", 2); + try package_store.snapshotPackageRelease("persisted", "r1", 3); + try app_runtime.savePlan("persisted", "boot", "", "", abi.display_connector_virtual, 1280, 720, false, 4); + try app_runtime.saveSuite("persisted-suite", "persisted:boot", 5); + try saveWorkspace("persisted", "persisted-suite", "", 1024, 768, "persisted:stable:r1", 6); + try savePlan("persisted", "boot", "persisted-suite", "persisted-root", 1280, 720, "persisted:stable:r1", 7); + try applyPlan("persisted", "boot", 8); + + filesystem.resetForTest(); + framebuffer_console.resetForTest(); + + const plan_list = try planListAlloc(std.testing.allocator, "persisted", 64); + defer std.testing.allocator.free(plan_list); + try std.testing.expectEqualStrings("boot\n", plan_list); + + const active = try activePlanInfoAlloc(std.testing.allocator, "persisted", 256); + defer std.testing.allocator.free(active); + try std.testing.expect(std.mem.indexOf(u8, active, "active_plan=boot") != null); + try std.testing.expect(std.mem.indexOf(u8, active, "trust_bundle=persisted-root") != null); + try std.testing.expect(std.mem.indexOf(u8, active, "display=1280x720") != null); + + const restored = try infoAlloc(std.testing.allocator, "persisted", 512); + defer std.testing.allocator.free(restored); + try std.testing.expect(std.mem.indexOf(u8, restored, "suite=persisted-suite") != null); + try std.testing.expect(std.mem.indexOf(u8, restored, "trust_bundle=persisted-root") != null); + try std.testing.expect(std.mem.indexOf(u8, restored, "display=1280x720") != null); +} + test "workspace runtime snapshots activates deletes and prunes workspace releases" { storage_backend.resetForTest(); filesystem.resetForTest(); @@ -1402,3 +2521,311 @@ test "workspace runtime manages release channels" { try std.testing.expect(std.mem.indexOf(u8, restored_info, "display=1024x768") != null); try std.testing.expect(std.mem.indexOf(u8, restored_info, "channel=demo:stable:r1") != null); } + +test "workspace runtime manages workspace suites" { + storage_backend.resetForTest(); + filesystem.resetForTest(); + framebuffer_console.resetForTest(); + + try trust_store.installBundle("root-a", "root-a-cert", 1); + try trust_store.installBundle("root-b", "root-b-cert", 2); + try trust_store.selectBundle("root-a", 3); + + try package_store.installScriptPackage("demo", "echo demo-suite", 4); + try package_store.installScriptPackage("aux", "echo aux-suite", 5); + try app_runtime.savePlan("demo", "boot", "", "", abi.display_connector_virtual, 1024, 768, false, 6); + try app_runtime.savePlan("aux", "sidecar", "", "", abi.display_connector_virtual, 800, 600, false, 7); + try app_runtime.saveSuite("demo-suite", "demo:boot", 8); + try app_runtime.saveSuite("aux-suite", "aux:sidecar", 9); + try saveWorkspace("ops", "demo-suite", "root-a", 1024, 768, "", 10); + try saveWorkspace("sidecar", "aux-suite", "root-b", 800, 600, "", 11); + try saveSuite("crew", "ops sidecar", 12); + + const listing = try suiteListAlloc(std.testing.allocator, 128); + defer std.testing.allocator.free(listing); + try std.testing.expectEqualStrings("crew\n", listing); + + const info = try suiteInfoAlloc(std.testing.allocator, "crew", 256); + defer std.testing.allocator.free(info); + try std.testing.expect(std.mem.indexOf(u8, info, "suite=crew") != null); + try std.testing.expect(std.mem.indexOf(u8, info, "workspace=ops") != null); + try std.testing.expect(std.mem.indexOf(u8, info, "workspace=sidecar") != null); + + const entries = try suiteEntriesAlloc(std.testing.allocator, "crew", 64); + defer std.testing.allocator.free(entries); + try std.testing.expectEqualStrings("ops\nsidecar\n", entries); + + try applySuite("crew", 13); + const active_bundle = try trust_store.activeBundleNameAlloc(std.testing.allocator, trust_store.max_name_len); + defer std.testing.allocator.free(active_bundle); + try std.testing.expectEqualStrings("root-b", active_bundle); + const display_state = framebuffer_console.statePtr(); + try std.testing.expectEqual(@as(u16, 800), display_state.width); + try std.testing.expectEqual(@as(u16, 600), display_state.height); + + try deleteSuite("crew", 14); + const after_delete = try suiteListAlloc(std.testing.allocator, 64); + defer std.testing.allocator.free(after_delete); + try std.testing.expectEqualStrings("", after_delete); +} + +test "workspace runtime workspace suites persist on ata-backed storage" { + storage_backend.resetForTest(); + filesystem.resetForTest(); + framebuffer_console.resetForTest(); + ata_pio_disk.testEnableMockDevice(8192); + ata_pio_disk.testInstallMockMbrPartition(2048, 4096, 0x83); + defer ata_pio_disk.testDisableMockDevice(); + + try trust_store.installBundle("persist-root-a", "persist-a", 1); + try trust_store.installBundle("persist-root-b", "persist-b", 2); + try package_store.installScriptPackage("demo", "echo demo-persist", 3); + try package_store.installScriptPackage("aux", "echo aux-persist", 4); + try app_runtime.savePlan("demo", "boot", "", "", abi.display_connector_virtual, 1024, 768, false, 5); + try app_runtime.savePlan("aux", "sidecar", "", "", abi.display_connector_virtual, 1280, 720, false, 6); + try app_runtime.saveSuite("demo-suite", "demo:boot", 7); + try app_runtime.saveSuite("aux-suite", "aux:sidecar", 8); + try saveWorkspace("ops", "demo-suite", "persist-root-a", 1024, 768, "", 9); + try saveWorkspace("sidecar", "aux-suite", "persist-root-b", 1280, 720, "", 10); + try saveSuite("crew", "ops sidecar", 11); + + filesystem.resetForTest(); + framebuffer_console.resetForTest(); + + const listing = try suiteListAlloc(std.testing.allocator, 128); + defer std.testing.allocator.free(listing); + try std.testing.expectEqualStrings("crew\n", listing); + + const info = try suiteInfoAlloc(std.testing.allocator, "crew", 256); + defer std.testing.allocator.free(info); + try std.testing.expect(std.mem.indexOf(u8, info, "workspace=ops") != null); + try std.testing.expect(std.mem.indexOf(u8, info, "workspace=sidecar") != null); + + try applySuite("crew", 12); + try std.testing.expectEqual(@as(u32, 2048), ata_pio_disk.logicalBaseLba()); + + const active_bundle = try trust_store.activeBundleNameAlloc(std.testing.allocator, trust_store.max_name_len); + defer std.testing.allocator.free(active_bundle); + try std.testing.expectEqualStrings("persist-root-b", active_bundle); + + const display_state = framebuffer_console.statePtr(); + try std.testing.expectEqual(@as(u16, 1280), display_state.width); + try std.testing.expectEqual(@as(u16, 720), display_state.height); +} + +test "workspace runtime manages workspace suite releases" { + storage_backend.resetForTest(); + filesystem.resetForTest(); + framebuffer_console.resetForTest(); + + try trust_store.installBundle("root-a", "root-a-cert", 1); + try trust_store.installBundle("root-b", "root-b-cert", 2); + try package_store.installScriptPackage("demo", "echo demo-suite", 3); + try package_store.installScriptPackage("aux", "echo aux-suite", 4); + try app_runtime.savePlan("demo", "boot", "", "", abi.display_connector_virtual, 1024, 768, false, 5); + try app_runtime.savePlan("demo", "canary", "", "", abi.display_connector_virtual, 640, 400, false, 6); + try app_runtime.savePlan("aux", "sidecar", "", "", abi.display_connector_virtual, 800, 600, false, 7); + try app_runtime.saveSuite("demo-suite", "demo:boot", 8); + try app_runtime.saveSuite("aux-suite", "aux:sidecar", 9); + try saveWorkspace("ops", "demo-suite", "root-a", 1024, 768, "", 10); + try saveWorkspace("sidecar", "aux-suite", "root-b", 800, 600, "", 11); + try saveSuite("crew", "ops sidecar", 12); + try snapshotSuiteRelease("crew", "golden", 13); + + try saveWorkspace("ops", "demo-suite", "root-b", 640, 400, "", 14); + try saveSuite("crew", "ops", 15); + try snapshotSuiteRelease("crew", "staging", 16); + + const release_list = try suiteReleaseListAlloc(std.testing.allocator, "crew", 64); + defer std.testing.allocator.free(release_list); + try std.testing.expectEqualStrings("golden\nstaging\n", release_list); + + const release_info = try suiteReleaseInfoAlloc(std.testing.allocator, "crew", "staging", 512); + defer std.testing.allocator.free(release_info); + try std.testing.expect(std.mem.indexOf(u8, release_info, "suite=crew") != null); + try std.testing.expect(std.mem.indexOf(u8, release_info, "release=staging") != null); + try std.testing.expect(std.mem.indexOf(u8, release_info, "saved_seq=2") != null); + try std.testing.expect(std.mem.indexOf(u8, release_info, "workspace=ops") != null); + + try activateSuiteRelease("crew", "golden", 17); + const restored_suite = try suiteInfoAlloc(std.testing.allocator, "crew", 256); + defer std.testing.allocator.free(restored_suite); + try std.testing.expect(std.mem.indexOf(u8, restored_suite, "workspace=ops") != null); + try std.testing.expect(std.mem.indexOf(u8, restored_suite, "workspace=sidecar") != null); + + try deleteSuiteRelease("crew", "staging", 18); + const list_after_delete = try suiteReleaseListAlloc(std.testing.allocator, "crew", 64); + defer std.testing.allocator.free(list_after_delete); + try std.testing.expectEqualStrings("golden\n", list_after_delete); + + try saveWorkspace("ops", "demo-suite", "root-b", 640, 400, "", 19); + try saveSuite("crew", "ops", 20); + try snapshotSuiteRelease("crew", "fallback", 21); + const prune_result = try pruneSuiteReleases("crew", 1, 22); + try std.testing.expectEqual(@as(u32, 1), prune_result.kept_count); + try std.testing.expectEqual(@as(u32, 1), prune_result.deleted_count); + + const final_list = try suiteReleaseListAlloc(std.testing.allocator, "crew", 64); + defer std.testing.allocator.free(final_list); + try std.testing.expectEqualStrings("fallback\n", final_list); + + try deleteSuite("crew", 23); + try std.testing.expectError(error.WorkspaceSuiteNotFound, suiteReleaseListAlloc(std.testing.allocator, "crew", 64)); + if (filesystem.statSummary("/runtime/workspace-suite-releases/crew")) |_| { + return error.InvalidWorkspaceSuite; + } else |err| switch (err) { + error.FileNotFound => {}, + else => return err, + } +} + +test "workspace runtime workspace suite releases persist on ata-backed storage" { + storage_backend.resetForTest(); + filesystem.resetForTest(); + framebuffer_console.resetForTest(); + ata_pio_disk.testEnableMockDevice(8192); + ata_pio_disk.testInstallMockMbrPartition(2048, 4096, 0x83); + defer ata_pio_disk.testDisableMockDevice(); + + try trust_store.installBundle("persist-root-a", "persist-a", 1); + try trust_store.installBundle("persist-root-b", "persist-b", 2); + try package_store.installScriptPackage("demo", "echo demo-persist", 3); + try package_store.installScriptPackage("aux", "echo aux-persist", 4); + try app_runtime.savePlan("demo", "boot", "", "", abi.display_connector_virtual, 1024, 768, false, 5); + try app_runtime.savePlan("demo", "canary", "", "", abi.display_connector_virtual, 640, 400, false, 6); + try app_runtime.savePlan("aux", "sidecar", "", "", abi.display_connector_virtual, 1280, 720, false, 7); + try app_runtime.saveSuite("demo-suite", "demo:boot", 8); + try app_runtime.saveSuite("aux-suite", "aux:sidecar", 9); + try saveWorkspace("ops", "demo-suite", "persist-root-a", 1024, 768, "", 10); + try saveWorkspace("sidecar", "aux-suite", "persist-root-b", 1280, 720, "", 11); + try saveSuite("crew", "ops sidecar", 12); + try snapshotSuiteRelease("crew", "golden", 13); + try saveWorkspace("ops", "demo-suite", "persist-root-b", 640, 400, "", 14); + + filesystem.resetForTest(); + framebuffer_console.resetForTest(); + + const release_list = try suiteReleaseListAlloc(std.testing.allocator, "crew", 64); + defer std.testing.allocator.free(release_list); + try std.testing.expectEqualStrings("golden\n", release_list); + + const release_info = try suiteReleaseInfoAlloc(std.testing.allocator, "crew", "golden", 512); + defer std.testing.allocator.free(release_info); + try std.testing.expect(std.mem.indexOf(u8, release_info, "release=golden") != null); + try std.testing.expect(std.mem.indexOf(u8, release_info, "saved_seq=1") != null); + try std.testing.expect(std.mem.indexOf(u8, release_info, "workspace=ops") != null); + try std.testing.expect(std.mem.indexOf(u8, release_info, "workspace=sidecar") != null); + + try activateSuiteRelease("crew", "golden", 15); + const restored_suite = try suiteInfoAlloc(std.testing.allocator, "crew", 256); + defer std.testing.allocator.free(restored_suite); + try std.testing.expect(std.mem.indexOf(u8, restored_suite, "workspace=ops") != null); + try std.testing.expect(std.mem.indexOf(u8, restored_suite, "workspace=sidecar") != null); +} + +test "workspace runtime manages workspace suite release channels" { + storage_backend.resetForTest(); + filesystem.resetForTest(); + framebuffer_console.resetForTest(); + + try trust_store.installBundle("root-a", "root-a-cert", 1); + try trust_store.installBundle("root-b", "root-b-cert", 2); + try package_store.installScriptPackage("demo", "echo demo-suite", 3); + try package_store.installScriptPackage("aux", "echo aux-suite", 4); + try app_runtime.savePlan("demo", "boot", "", "", abi.display_connector_virtual, 1024, 768, false, 5); + try app_runtime.savePlan("demo", "canary", "", "", abi.display_connector_virtual, 640, 400, false, 6); + try app_runtime.savePlan("aux", "sidecar", "", "", abi.display_connector_virtual, 800, 600, false, 7); + try app_runtime.saveSuite("demo-suite", "demo:boot", 8); + try app_runtime.saveSuite("aux-suite", "aux:sidecar", 9); + try saveWorkspace("ops", "demo-suite", "root-a", 1024, 768, "", 10); + try saveWorkspace("sidecar", "aux-suite", "root-b", 800, 600, "", 11); + try saveSuite("crew", "ops sidecar", 12); + try snapshotSuiteRelease("crew", "golden", 13); + + try saveWorkspace("ops", "demo-suite", "root-b", 640, 400, "", 14); + try saveSuite("crew", "ops", 15); + try snapshotSuiteRelease("crew", "staging", 16); + + try setSuiteReleaseChannel("crew", "stable", "golden", 17); + + const channel_list = try suiteChannelListAlloc(std.testing.allocator, "crew", 64); + defer std.testing.allocator.free(channel_list); + try std.testing.expectEqualStrings("stable\n", channel_list); + + const channel_info = try suiteChannelInfoAlloc(std.testing.allocator, "crew", "stable", 128); + defer std.testing.allocator.free(channel_info); + try std.testing.expect(std.mem.indexOf(u8, channel_info, "suite=crew") != null); + try std.testing.expect(std.mem.indexOf(u8, channel_info, "channel=stable") != null); + try std.testing.expect(std.mem.indexOf(u8, channel_info, "release=golden") != null); + + try setSuiteReleaseChannel("crew", "stable", "staging", 18); + try activateSuiteReleaseChannel("crew", "stable", 19); + + const staging_suite = try suiteInfoAlloc(std.testing.allocator, "crew", 256); + defer std.testing.allocator.free(staging_suite); + try std.testing.expect(std.mem.indexOf(u8, staging_suite, "workspace=ops") != null); + try std.testing.expect(std.mem.indexOf(u8, staging_suite, "workspace=sidecar") == null); + + try setSuiteReleaseChannel("crew", "stable", "golden", 20); + try activateSuiteReleaseChannel("crew", "stable", 21); + + const restored_suite = try suiteInfoAlloc(std.testing.allocator, "crew", 256); + defer std.testing.allocator.free(restored_suite); + try std.testing.expect(std.mem.indexOf(u8, restored_suite, "workspace=ops") != null); + try std.testing.expect(std.mem.indexOf(u8, restored_suite, "workspace=sidecar") != null); + + try deleteSuite("crew", 22); + try std.testing.expectError(error.WorkspaceSuiteNotFound, suiteChannelListAlloc(std.testing.allocator, "crew", 64)); + if (filesystem.statSummary("/runtime/workspace-suite-release-channels/crew")) |_| { + return error.InvalidWorkspaceSuite; + } else |err| switch (err) { + error.FileNotFound => {}, + else => return err, + } +} + +test "workspace runtime workspace suite release channels persist on ata-backed storage" { + storage_backend.resetForTest(); + filesystem.resetForTest(); + framebuffer_console.resetForTest(); + ata_pio_disk.testEnableMockDevice(8192); + ata_pio_disk.testInstallMockMbrPartition(2048, 4096, 0x83); + defer ata_pio_disk.testDisableMockDevice(); + + try trust_store.installBundle("persist-root-a", "persist-a", 1); + try trust_store.installBundle("persist-root-b", "persist-b", 2); + try package_store.installScriptPackage("demo", "echo demo-persist", 3); + try package_store.installScriptPackage("aux", "echo aux-persist", 4); + try app_runtime.savePlan("demo", "boot", "", "", abi.display_connector_virtual, 1024, 768, false, 5); + try app_runtime.savePlan("demo", "canary", "", "", abi.display_connector_virtual, 640, 400, false, 6); + try app_runtime.savePlan("aux", "sidecar", "", "", abi.display_connector_virtual, 1280, 720, false, 7); + try app_runtime.saveSuite("demo-suite", "demo:boot", 8); + try app_runtime.saveSuite("aux-suite", "aux:sidecar", 9); + try saveWorkspace("ops", "demo-suite", "persist-root-a", 1024, 768, "", 10); + try saveWorkspace("sidecar", "aux-suite", "persist-root-b", 1280, 720, "", 11); + try saveSuite("crew", "ops sidecar", 12); + try snapshotSuiteRelease("crew", "golden", 13); + try saveWorkspace("ops", "demo-suite", "persist-root-b", 640, 400, "", 14); + try saveSuite("crew", "ops", 15); + try snapshotSuiteRelease("crew", "staging", 16); + try setSuiteReleaseChannel("crew", "stable", "golden", 17); + + filesystem.resetForTest(); + framebuffer_console.resetForTest(); + + const channel_list = try suiteChannelListAlloc(std.testing.allocator, "crew", 64); + defer std.testing.allocator.free(channel_list); + try std.testing.expectEqualStrings("stable\n", channel_list); + + const channel_info = try suiteChannelInfoAlloc(std.testing.allocator, "crew", "stable", 128); + defer std.testing.allocator.free(channel_info); + try std.testing.expect(std.mem.indexOf(u8, channel_info, "release=golden") != null); + + try setSuiteReleaseChannel("crew", "stable", "staging", 18); + try activateSuiteReleaseChannel("crew", "stable", 19); + + const staging_suite = try suiteInfoAlloc(std.testing.allocator, "crew", 256); + defer std.testing.allocator.free(staging_suite); + try std.testing.expect(std.mem.indexOf(u8, staging_suite, "workspace=ops") != null); + try std.testing.expect(std.mem.indexOf(u8, staging_suite, "workspace=sidecar") == null); +} diff --git a/src/baremetal_main.zig b/src/baremetal_main.zig index a6745cc1..baba9896 100644 --- a/src/baremetal_main.zig +++ b/src/baremetal_main.zig @@ -3439,6 +3439,28 @@ fn runRtl8139TcpProbe() Rtl8139TcpProbeError!void { const app_plan_delete_request_id: u32 = 106; const app_plan_list_after_delete_request_id: u32 = 107; const app_plan_canary_save_request_id: u32 = 203; + const workspace_suite_name = "crew"; + const workspace_suite_save_request_id: u32 = 204; + const workspace_suite_list_request_id: u32 = 205; + const workspace_suite_info_request_id: u32 = 206; + const workspace_suite_apply_request_id: u32 = 207; + const workspace_suite_run_request_id: u32 = 208; + const workspace_suite_delete_request_id: u32 = 209; + const workspace_suite_list_after_delete_request_id: u32 = 210; + const workspace_suite_release_save_golden_request_id: u32 = 211; + const workspace_suite_release_save_staging_request_id: u32 = 212; + const workspace_suite_release_list_request_id: u32 = 213; + const workspace_suite_release_info_request_id: u32 = 214; + const workspace_suite_release_activate_request_id: u32 = 215; + const workspace_suite_release_delete_request_id: u32 = 216; + const workspace_suite_release_save_fallback_request_id: u32 = 217; + const workspace_suite_release_prune_request_id: u32 = 218; + const workspace_suite_release_list_final_request_id: u32 = 219; + const workspace_suite_channel_set_request_id: u32 = 220; + const workspace_suite_channel_list_request_id: u32 = 221; + const workspace_suite_channel_info_request_id: u32 = 222; + const workspace_suite_channel_activate_request_id: u32 = 223; + const workspace_suite_channel_restored_info_request_id: u32 = 224; const app_suite_name = "duo"; const app_suite_aux_plan_name = "sidecar"; const app_suite_aux_plan_save_request_id: u32 = 108; @@ -3490,6 +3512,17 @@ fn runRtl8139TcpProbe() Rtl8139TcpProbeError!void { const workspace_channel_activate_golden_request_id: u32 = 152; const workspace_channel_staging_info_request_id: u32 = 153; const workspace_channel_restored_info_request_id: u32 = 154; + const workspace_plan_save_golden_request_id: u32 = 225; + const workspace_plan_save_staging_request_id: u32 = 226; + const workspace_plan_list_request_id: u32 = 227; + const workspace_plan_info_request_id: u32 = 228; + const workspace_plan_apply_staging_request_id: u32 = 229; + const workspace_plan_active_request_id: u32 = 230; + const workspace_plan_staging_info_request_id: u32 = 231; + const workspace_plan_apply_golden_request_id: u32 = 232; + const workspace_plan_restored_info_request_id: u32 = 233; + const workspace_plan_delete_staging_request_id: u32 = 234; + const workspace_plan_list_final_request_id: u32 = 235; const app_suite_release_save_golden_request_id: u32 = 155; const app_suite_mutate_staging_request_id: u32 = 156; const app_suite_release_save_staging_request_id: u32 = 157; @@ -3820,9 +3853,9 @@ fn runRtl8139TcpProbe() Rtl8139TcpProbeError!void { const service_response_long = tool_service.handleFramedRequest( service_fba.allocator(), scratch.packet_storage.payload[0..scratch.packet_storage.payload_len], - 2048, + 4096, 256, - 2048, + 4096, ) catch return error.ToolServiceFailed; if (!std.mem.startsWith(u8, service_response_long, "RESP 3 ")) return error.ToolServiceResponseMismatch; if (std.mem.indexOf(u8, service_response_long, "OpenClaw bare-metal builtins:") == null) return error.ToolServiceResponseMismatch; @@ -7083,6 +7116,243 @@ fn runRtl8139TcpProbe() Rtl8139TcpProbeError!void { ); if (!std.mem.eql(u8, workspace_info_response, workspace_info_response_expected)) return error.ToolServiceResponseMismatch; + var workspace_plan_save_golden_request_buffer: [160]u8 = undefined; + const workspace_plan_save_golden_request = std.fmt.bufPrint( + &workspace_plan_save_golden_request_buffer, + "REQ {d} WORKSPACEPLANSAVE {s} golden {s} {s} 1024 768 {s}:{s}:{s}", + .{ workspace_plan_save_golden_request_id, workspace_name, app_suite_name, trust_backup_bundle_name, package_name, package_channel_name, package_release_three_name }, + ) catch return error.ToolServiceFailed; + const workspace_plan_save_golden_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_plan_save_golden_request, + 512, + 256, + 512, + ); + if (!std.mem.eql(u8, workspace_plan_save_golden_response, "RESP 225 29\nWORKSPACEPLANSAVE ops golden\n")) return error.ToolServiceResponseMismatch; + + var workspace_plan_save_staging_request_buffer: [160]u8 = undefined; + const workspace_plan_save_staging_request = std.fmt.bufPrint( + &workspace_plan_save_staging_request_buffer, + "REQ {d} WORKSPACEPLANSAVE {s} staging none none 640 400 {s}:{s}:{s}", + .{ workspace_plan_save_staging_request_id, workspace_name, package_name, package_channel_name, package_release_three_name }, + ) catch return error.ToolServiceFailed; + const workspace_plan_save_staging_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_plan_save_staging_request, + 512, + 256, + 512, + ); + if (!std.mem.eql(u8, workspace_plan_save_staging_response, "RESP 226 30\nWORKSPACEPLANSAVE ops staging\n")) return error.ToolServiceResponseMismatch; + + var workspace_plan_list_request_buffer: [96]u8 = undefined; + const workspace_plan_list_request = std.fmt.bufPrint( + &workspace_plan_list_request_buffer, + "REQ {d} WORKSPACEPLANLIST {s}", + .{ workspace_plan_list_request_id, workspace_name }, + ) catch return error.ToolServiceFailed; + const workspace_plan_list_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_plan_list_request, + 512, + 256, + 512, + ); + if (!std.mem.eql(u8, workspace_plan_list_response, "RESP 227 15\ngolden\nstaging\n")) return error.ToolServiceResponseMismatch; + + var workspace_plan_info_request_buffer: [112]u8 = undefined; + const workspace_plan_info_request = std.fmt.bufPrint( + &workspace_plan_info_request_buffer, + "REQ {d} WORKSPACEPLANINFO {s} staging", + .{ workspace_plan_info_request_id, workspace_name }, + ) catch return error.ToolServiceFailed; + const workspace_plan_info_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_plan_info_request, + 512, + 256, + 512, + ); + if (!std.mem.startsWith(u8, workspace_plan_info_response, "RESP 228 ")) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_plan_info_response, "workspace=ops") == null) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_plan_info_response, "plan=staging") == null) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_plan_info_response, "suite=none") == null) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_plan_info_response, "trust_bundle=none") == null) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_plan_info_response, "display=640x400") == null) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_plan_info_response, "channel=demo:stable:r3") == null) return error.ToolServiceResponseMismatch; + + var workspace_plan_apply_staging_request_buffer: [112]u8 = undefined; + const workspace_plan_apply_staging_request = std.fmt.bufPrint( + &workspace_plan_apply_staging_request_buffer, + "REQ {d} WORKSPACEPLANAPPLY {s} staging", + .{ workspace_plan_apply_staging_request_id, workspace_name }, + ) catch return error.ToolServiceFailed; + const workspace_plan_apply_staging_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_plan_apply_staging_request, + 512, + 256, + 512, + ); + if (!std.mem.eql(u8, workspace_plan_apply_staging_response, "RESP 229 31\nWORKSPACEPLANAPPLY ops staging\n")) return error.ToolServiceResponseMismatch; + + var workspace_plan_active_request_buffer: [96]u8 = undefined; + const workspace_plan_active_request = std.fmt.bufPrint( + &workspace_plan_active_request_buffer, + "REQ {d} WORKSPACEPLANACTIVE {s}", + .{ workspace_plan_active_request_id, workspace_name }, + ) catch return error.ToolServiceFailed; + const workspace_plan_active_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_plan_active_request, + 512, + 256, + 512, + ); + if (!std.mem.startsWith(u8, workspace_plan_active_response, "RESP 230 ")) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_plan_active_response, "active_plan=staging") == null) return error.ToolServiceResponseMismatch; + + var workspace_plan_staging_info_request_buffer: [96]u8 = undefined; + const workspace_plan_staging_info_request = std.fmt.bufPrint( + &workspace_plan_staging_info_request_buffer, + "REQ {d} WORKSPACEINFO {s}", + .{ workspace_plan_staging_info_request_id, workspace_name }, + ) catch return error.ToolServiceFailed; + const workspace_plan_staging_info_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_plan_staging_info_request, + 512, + 256, + 512, + ); + if (!std.mem.startsWith(u8, workspace_plan_staging_info_response, "RESP 231 ")) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_plan_staging_info_response, "suite=none") == null) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_plan_staging_info_response, "trust_bundle=none") == null) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_plan_staging_info_response, "display=640x400") == null) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_plan_staging_info_response, "channel=demo:stable:r3") == null) return error.ToolServiceResponseMismatch; + + var workspace_plan_apply_golden_request_buffer: [112]u8 = undefined; + const workspace_plan_apply_golden_request = std.fmt.bufPrint( + &workspace_plan_apply_golden_request_buffer, + "REQ {d} WORKSPACEPLANAPPLY {s} golden", + .{ workspace_plan_apply_golden_request_id, workspace_name }, + ) catch return error.ToolServiceFailed; + const workspace_plan_apply_golden_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_plan_apply_golden_request, + 512, + 256, + 512, + ); + if (!std.mem.eql(u8, workspace_plan_apply_golden_response, "RESP 232 30\nWORKSPACEPLANAPPLY ops golden\n")) return error.ToolServiceResponseMismatch; + + var workspace_plan_restored_info_response_buffer: [640]u8 = undefined; + const workspace_plan_restored_info_response_expected = std.fmt.bufPrint( + &workspace_plan_restored_info_response_buffer, + "RESP {d} {d}\n{s}", + .{ workspace_plan_restored_info_request_id, workspace_info_payload_expected.len, workspace_info_payload_expected }, + ) catch return error.ToolServiceFailed; + var workspace_plan_restored_info_request_buffer: [96]u8 = undefined; + const workspace_plan_restored_info_request = std.fmt.bufPrint( + &workspace_plan_restored_info_request_buffer, + "REQ {d} WORKSPACEINFO {s}", + .{ workspace_plan_restored_info_request_id, workspace_name }, + ) catch return error.ToolServiceFailed; + const workspace_plan_restored_info_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_plan_restored_info_request, + 512, + 256, + 512, + ); + if (!std.mem.eql(u8, workspace_plan_restored_info_response, workspace_plan_restored_info_response_expected)) return error.ToolServiceResponseMismatch; + + var workspace_plan_delete_staging_request_buffer: [112]u8 = undefined; + const workspace_plan_delete_staging_request = std.fmt.bufPrint( + &workspace_plan_delete_staging_request_buffer, + "REQ {d} WORKSPACEPLANDELETE {s} staging", + .{ workspace_plan_delete_staging_request_id, workspace_name }, + ) catch return error.ToolServiceFailed; + const workspace_plan_delete_staging_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_plan_delete_staging_request, + 512, + 256, + 512, + ); + if (!std.mem.eql(u8, workspace_plan_delete_staging_response, "RESP 234 32\nWORKSPACEPLANDELETE ops staging\n")) return error.ToolServiceResponseMismatch; + + var workspace_plan_list_final_request_buffer: [96]u8 = undefined; + const workspace_plan_list_final_request = std.fmt.bufPrint( + &workspace_plan_list_final_request_buffer, + "REQ {d} WORKSPACEPLANLIST {s}", + .{ workspace_plan_list_final_request_id, workspace_name }, + ) catch return error.ToolServiceFailed; + const workspace_plan_list_final_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_plan_list_final_request, + 512, + 256, + 512, + ); + if (!std.mem.eql(u8, workspace_plan_list_final_response, "RESP 235 7\ngolden\n")) return error.ToolServiceResponseMismatch; + var workspace_release_golden_save_request_buffer: [96]u8 = undefined; const workspace_release_golden_save_request = std.fmt.bufPrint( &workspace_release_golden_save_request_buffer, @@ -7758,6 +8028,643 @@ fn runRtl8139TcpProbe() Rtl8139TcpProbeError!void { ); if (!std.mem.eql(u8, workspace_autorun_save_response, "RESP 127 19\nWORKSPACESAVE ops2\n")) return error.ToolServiceResponseMismatch; + var workspace_suite_save_request_buffer: [160]u8 = undefined; + const workspace_suite_save_request = std.fmt.bufPrint( + &workspace_suite_save_request_buffer, + "REQ {d} WORKSPACESUITESAVE {s} {s} {s}", + .{ workspace_suite_save_request_id, workspace_suite_name, workspace_name, workspace_autorun_name }, + ) catch return error.ToolServiceFailed; + const workspace_suite_save_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_save_request, + 512, + 256, + 512, + ); + var workspace_suite_save_response_expected_buffer: [96]u8 = undefined; + const workspace_suite_save_payload_expected = "WORKSPACESUITESAVE crew\n"; + const workspace_suite_save_response_expected = std.fmt.bufPrint( + &workspace_suite_save_response_expected_buffer, + "RESP {d} {d}\n{s}", + .{ workspace_suite_save_request_id, workspace_suite_save_payload_expected.len, workspace_suite_save_payload_expected }, + ) catch return error.ToolServiceFailed; + if (!std.mem.eql(u8, workspace_suite_save_response, workspace_suite_save_response_expected)) return error.ToolServiceResponseMismatch; + + var workspace_suite_path_buffer: [filesystem.max_path_len]u8 = undefined; + const workspace_suite_path = std.fmt.bufPrint( + &workspace_suite_path_buffer, + "/runtime/workspace-suites/{s}.txt", + .{workspace_suite_name}, + ) catch return error.ToolServiceFailed; + service_fba = std.heap.FixedBufferAllocator.init(&scratch.service_scratch); + const workspace_suite_readback = filesystem.readFileAlloc(service_fba.allocator(), workspace_suite_path, 64) catch return error.ToolServiceFailed; + if (!std.mem.eql(u8, workspace_suite_readback, "workspace=ops\nworkspace=ops2\n")) return error.ToolServiceResponseMismatch; + + var workspace_suite_list_request_buffer: [96]u8 = undefined; + const workspace_suite_list_request = std.fmt.bufPrint( + &workspace_suite_list_request_buffer, + "REQ {d} WORKSPACESUITELIST", + .{workspace_suite_list_request_id}, + ) catch return error.ToolServiceFailed; + const workspace_suite_list_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_list_request, + 512, + 256, + 512, + ); + if (!std.mem.eql(u8, workspace_suite_list_response, "RESP 205 5\ncrew\n")) return error.ToolServiceResponseMismatch; + + var workspace_suite_info_request_buffer: [128]u8 = undefined; + const workspace_suite_info_request = std.fmt.bufPrint( + &workspace_suite_info_request_buffer, + "REQ {d} WORKSPACESUITEINFO {s}", + .{ workspace_suite_info_request_id, workspace_suite_name }, + ) catch return error.ToolServiceFailed; + const workspace_suite_info_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_info_request, + 512, + 256, + 512, + ); + if (!std.mem.startsWith(u8, workspace_suite_info_response, "RESP 206 ")) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_info_response, "suite=crew") == null) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_info_response, "workspace=ops") == null) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_info_response, "workspace=ops2") == null) return error.ToolServiceResponseMismatch; + + var workspace_suite_apply_request_buffer: [128]u8 = undefined; + const workspace_suite_apply_request = std.fmt.bufPrint( + &workspace_suite_apply_request_buffer, + "REQ {d} WORKSPACESUITEAPPLY {s}", + .{ workspace_suite_apply_request_id, workspace_suite_name }, + ) catch return error.ToolServiceFailed; + const workspace_suite_apply_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_apply_request, + 512, + 256, + 512, + ); + var workspace_suite_apply_response_expected_buffer: [96]u8 = undefined; + const workspace_suite_apply_payload_expected = "WORKSPACESUITEAPPLY crew\n"; + const workspace_suite_apply_response_expected = std.fmt.bufPrint( + &workspace_suite_apply_response_expected_buffer, + "RESP {d} {d}\n{s}", + .{ workspace_suite_apply_request_id, workspace_suite_apply_payload_expected.len, workspace_suite_apply_payload_expected }, + ) catch return error.ToolServiceFailed; + if (!std.mem.eql(u8, workspace_suite_apply_response, workspace_suite_apply_response_expected)) return error.ToolServiceResponseMismatch; + + service_fba = std.heap.FixedBufferAllocator.init(&scratch.service_scratch); + const workspace_suite_active_bundle = trust_store.activeBundleNameAlloc(service_fba.allocator(), trust_store.max_name_len) catch return error.ToolServiceFailed; + if (!std.mem.eql(u8, workspace_suite_active_bundle, trust_backup_bundle_name)) return error.ToolServiceResponseMismatch; + + var workspace_suite_run_request_buffer: [128]u8 = undefined; + const workspace_suite_run_request = std.fmt.bufPrint( + &workspace_suite_run_request_buffer, + "REQ {d} WORKSPACESUITERUN {s}", + .{ workspace_suite_run_request_id, workspace_suite_name }, + ) catch return error.ToolServiceFailed; + var workspace_suite_run_payload_buffer: [256]u8 = undefined; + const workspace_suite_run_payload_expected = std.fmt.bufPrint( + &workspace_suite_run_payload_buffer, + "{s}{s}{s}{s}", + .{ package_run_stdout, autorun_run_stdout, package_run_stdout, autorun_run_stdout }, + ) catch return error.ToolServiceFailed; + const workspace_suite_run_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_run_request, + 512, + 256, + 512, + ); + if (!std.mem.startsWith(u8, workspace_suite_run_response, "RESP 208 ")) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_run_response, workspace_suite_run_payload_expected) == null) return error.ToolServiceResponseMismatch; + + var workspace_suite_release_golden_request_buffer: [128]u8 = undefined; + const workspace_suite_release_golden_request = std.fmt.bufPrint( + &workspace_suite_release_golden_request_buffer, + "REQ {d} WORKSPACESUITERELEASESAVE {s} golden", + .{ workspace_suite_release_save_golden_request_id, workspace_suite_name }, + ) catch return error.ToolServiceFailed; + const workspace_suite_release_golden_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_release_golden_request, + 512, + 256, + 512, + ); + if (!std.mem.startsWith(u8, workspace_suite_release_golden_response, "RESP 211 ")) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_release_golden_response, "WORKSPACESUITERELEASESAVE crew golden\n") == null) return error.ToolServiceResponseMismatch; + + var workspace_suite_release_root_path_buffer: [filesystem.max_path_len]u8 = undefined; + const workspace_suite_release_root_path = std.fmt.bufPrint( + &workspace_suite_release_root_path_buffer, + "/runtime/workspace-suite-releases/{s}", + .{workspace_suite_name}, + ) catch return error.ToolServiceFailed; + var workspace_suite_release_golden_path_buffer: [filesystem.max_path_len]u8 = undefined; + const workspace_suite_release_golden_path = std.fmt.bufPrint( + &workspace_suite_release_golden_path_buffer, + "/runtime/workspace-suite-releases/{s}/golden/suite.txt", + .{workspace_suite_name}, + ) catch return error.ToolServiceFailed; + var workspace_suite_release_golden_metadata_path_buffer: [filesystem.max_path_len]u8 = undefined; + const workspace_suite_release_golden_metadata_path = std.fmt.bufPrint( + &workspace_suite_release_golden_metadata_path_buffer, + "/runtime/workspace-suite-releases/{s}/golden/release.txt", + .{workspace_suite_name}, + ) catch return error.ToolServiceFailed; + + service_fba = std.heap.FixedBufferAllocator.init(&scratch.service_scratch); + const workspace_suite_release_golden_readback = filesystem.readFileAlloc(service_fba.allocator(), workspace_suite_release_golden_path, 128) catch return error.ToolServiceFailed; + if (!std.mem.eql(u8, workspace_suite_release_golden_readback, "workspace=ops\nworkspace=ops2\n")) return error.ToolServiceResponseMismatch; + + service_fba = std.heap.FixedBufferAllocator.init(&scratch.service_scratch); + const workspace_suite_release_golden_metadata_readback = filesystem.readFileAlloc(service_fba.allocator(), workspace_suite_release_golden_metadata_path, 128) catch return error.ToolServiceFailed; + if (std.mem.indexOf(u8, workspace_suite_release_golden_metadata_readback, "release=golden") == null) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_release_golden_metadata_readback, "saved_seq=1") == null) return error.ToolServiceResponseMismatch; + + var workspace_suite_mutate_request_buffer: [160]u8 = undefined; + const workspace_suite_mutate_request = std.fmt.bufPrint( + &workspace_suite_mutate_request_buffer, + "REQ {d} WORKSPACESAVE {s} {s} none 640 400 {s}:{s}:{s}", + .{ workspace_release_mutate_request_id, workspace_name, app_suite_name, package_name, package_channel_name, package_release_three_name }, + ) catch return error.ToolServiceFailed; + const workspace_suite_mutate_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_mutate_request, + 512, + 256, + 512, + ); + if (!std.mem.eql(u8, workspace_suite_mutate_response, "RESP 145 18\nWORKSPACESAVE ops\n")) return error.ToolServiceResponseMismatch; + + var workspace_suite_overwrite_request_buffer: [128]u8 = undefined; + const workspace_suite_overwrite_request = std.fmt.bufPrint( + &workspace_suite_overwrite_request_buffer, + "REQ {d} WORKSPACESUITESAVE {s} {s}", + .{ workspace_suite_save_request_id, workspace_suite_name, workspace_name }, + ) catch return error.ToolServiceFailed; + const workspace_suite_overwrite_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_overwrite_request, + 512, + 256, + 512, + ); + if (!std.mem.eql(u8, workspace_suite_overwrite_response, workspace_suite_save_response_expected)) return error.ToolServiceResponseMismatch; + + var workspace_suite_release_staging_request_buffer: [128]u8 = undefined; + const workspace_suite_release_staging_request = std.fmt.bufPrint( + &workspace_suite_release_staging_request_buffer, + "REQ {d} WORKSPACESUITERELEASESAVE {s} staging", + .{ workspace_suite_release_save_staging_request_id, workspace_suite_name }, + ) catch return error.ToolServiceFailed; + const workspace_suite_release_staging_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_release_staging_request, + 512, + 256, + 512, + ); + if (!std.mem.startsWith(u8, workspace_suite_release_staging_response, "RESP 212 ")) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_release_staging_response, "WORKSPACESUITERELEASESAVE crew staging\n") == null) return error.ToolServiceResponseMismatch; + + var workspace_suite_release_staging_path_buffer: [filesystem.max_path_len]u8 = undefined; + const workspace_suite_release_staging_path = std.fmt.bufPrint( + &workspace_suite_release_staging_path_buffer, + "/runtime/workspace-suite-releases/{s}/staging/suite.txt", + .{workspace_suite_name}, + ) catch return error.ToolServiceFailed; + var workspace_suite_release_staging_metadata_path_buffer: [filesystem.max_path_len]u8 = undefined; + const workspace_suite_release_staging_metadata_path = std.fmt.bufPrint( + &workspace_suite_release_staging_metadata_path_buffer, + "/runtime/workspace-suite-releases/{s}/staging/release.txt", + .{workspace_suite_name}, + ) catch return error.ToolServiceFailed; + + service_fba = std.heap.FixedBufferAllocator.init(&scratch.service_scratch); + const workspace_suite_release_staging_readback = filesystem.readFileAlloc(service_fba.allocator(), workspace_suite_release_staging_path, 128) catch return error.ToolServiceFailed; + if (!std.mem.eql(u8, workspace_suite_release_staging_readback, "workspace=ops\n")) return error.ToolServiceResponseMismatch; + + service_fba = std.heap.FixedBufferAllocator.init(&scratch.service_scratch); + const workspace_suite_release_staging_metadata_readback = filesystem.readFileAlloc(service_fba.allocator(), workspace_suite_release_staging_metadata_path, 128) catch return error.ToolServiceFailed; + if (std.mem.indexOf(u8, workspace_suite_release_staging_metadata_readback, "release=staging") == null) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_release_staging_metadata_readback, "saved_seq=2") == null) return error.ToolServiceResponseMismatch; + + var workspace_suite_release_list_request_buffer: [128]u8 = undefined; + const workspace_suite_release_list_request = std.fmt.bufPrint( + &workspace_suite_release_list_request_buffer, + "REQ {d} WORKSPACESUITERELEASELIST {s}", + .{ workspace_suite_release_list_request_id, workspace_suite_name }, + ) catch return error.ToolServiceFailed; + const workspace_suite_release_list_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_release_list_request, + 512, + 256, + 512, + ); + if (!std.mem.eql(u8, workspace_suite_release_list_response, "RESP 213 15\ngolden\nstaging\n")) return error.ToolServiceResponseMismatch; + + var workspace_suite_release_info_request_buffer: [160]u8 = undefined; + const workspace_suite_release_info_request = std.fmt.bufPrint( + &workspace_suite_release_info_request_buffer, + "REQ {d} WORKSPACESUITERELEASEINFO {s} staging", + .{ workspace_suite_release_info_request_id, workspace_suite_name }, + ) catch return error.ToolServiceFailed; + const workspace_suite_release_info_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_release_info_request, + 512, + 256, + 512, + ); + if (!std.mem.startsWith(u8, workspace_suite_release_info_response, "RESP 214 ")) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_release_info_response, "suite=crew") == null) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_release_info_response, "release=staging") == null) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_release_info_response, "saved_seq=2") == null) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_release_info_response, "workspace=ops") == null) return error.ToolServiceResponseMismatch; + + var workspace_suite_release_activate_request_buffer: [128]u8 = undefined; + const workspace_suite_release_activate_request = std.fmt.bufPrint( + &workspace_suite_release_activate_request_buffer, + "REQ {d} WORKSPACESUITERELEASEACTIVATE {s} golden", + .{ workspace_suite_release_activate_request_id, workspace_suite_name }, + ) catch return error.ToolServiceFailed; + const workspace_suite_release_activate_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_release_activate_request, + 512, + 256, + 512, + ); + if (!std.mem.startsWith(u8, workspace_suite_release_activate_response, "RESP 215 ")) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_release_activate_response, "WORKSPACESUITERELEASEACTIVATE crew golden\n") == null) return error.ToolServiceResponseMismatch; + + var workspace_suite_restored_info_request_buffer: [128]u8 = undefined; + const workspace_suite_restored_info_request = std.fmt.bufPrint( + &workspace_suite_restored_info_request_buffer, + "REQ {d} WORKSPACESUITEINFO {s}", + .{ workspace_suite_info_request_id, workspace_suite_name }, + ) catch return error.ToolServiceFailed; + const workspace_suite_restored_info_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_restored_info_request, + 512, + 256, + 512, + ); + if (!std.mem.startsWith(u8, workspace_suite_restored_info_response, "RESP 206 ")) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_restored_info_response, "workspace=ops") == null) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_restored_info_response, "workspace=ops2") == null) return error.ToolServiceResponseMismatch; + + var workspace_suite_release_delete_request_buffer: [128]u8 = undefined; + const workspace_suite_release_delete_request = std.fmt.bufPrint( + &workspace_suite_release_delete_request_buffer, + "REQ {d} WORKSPACESUITERELEASEDELETE {s} staging", + .{ workspace_suite_release_delete_request_id, workspace_suite_name }, + ) catch return error.ToolServiceFailed; + const workspace_suite_release_delete_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_release_delete_request, + 512, + 256, + 512, + ); + if (!std.mem.startsWith(u8, workspace_suite_release_delete_response, "RESP 216 ")) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_release_delete_response, "WORKSPACESUITERELEASEDELETE crew staging\n") == null) return error.ToolServiceResponseMismatch; + + if (filesystem.statSummary(workspace_suite_release_staging_path)) |_| { + return error.ToolServiceResponseMismatch; + } else |err| switch (err) { + error.FileNotFound => {}, + else => return error.ToolServiceFailed, + } + + var workspace_suite_release_fallback_request_buffer: [128]u8 = undefined; + const workspace_suite_release_fallback_request = std.fmt.bufPrint( + &workspace_suite_release_fallback_request_buffer, + "REQ {d} WORKSPACESUITERELEASESAVE {s} fallback", + .{ workspace_suite_release_save_fallback_request_id, workspace_suite_name }, + ) catch return error.ToolServiceFailed; + const workspace_suite_release_fallback_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_release_fallback_request, + 512, + 256, + 512, + ); + if (!std.mem.startsWith(u8, workspace_suite_release_fallback_response, "RESP 217 ")) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_release_fallback_response, "WORKSPACESUITERELEASESAVE crew fallback\n") == null) return error.ToolServiceResponseMismatch; + + var workspace_suite_release_prune_request_buffer: [128]u8 = undefined; + const workspace_suite_release_prune_request = std.fmt.bufPrint( + &workspace_suite_release_prune_request_buffer, + "REQ {d} WORKSPACESUITERELEASEPRUNE {s} 1", + .{ workspace_suite_release_prune_request_id, workspace_suite_name }, + ) catch return error.ToolServiceFailed; + const workspace_suite_release_prune_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_release_prune_request, + 512, + 256, + 512, + ); + if (!std.mem.startsWith(u8, workspace_suite_release_prune_response, "RESP 218 ")) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_release_prune_response, "WORKSPACESUITERELEASEPRUNE crew keep=1 deleted=1 kept=1\n") == null) return error.ToolServiceResponseMismatch; + + if (filesystem.statSummary(workspace_suite_release_golden_path)) |_| { + return error.ToolServiceResponseMismatch; + } else |err| switch (err) { + error.FileNotFound => {}, + else => return error.ToolServiceFailed, + } + + var workspace_suite_release_list_final_request_buffer: [128]u8 = undefined; + const workspace_suite_release_list_final_request = std.fmt.bufPrint( + &workspace_suite_release_list_final_request_buffer, + "REQ {d} WORKSPACESUITERELEASELIST {s}", + .{ workspace_suite_release_list_final_request_id, workspace_suite_name }, + ) catch return error.ToolServiceFailed; + const workspace_suite_release_list_final_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_release_list_final_request, + 512, + 256, + 512, + ); + if (!std.mem.eql(u8, workspace_suite_release_list_final_response, "RESP 219 9\nfallback\n")) return error.ToolServiceResponseMismatch; + + var workspace_suite_channel_set_request_buffer: [128]u8 = undefined; + const workspace_suite_channel_set_request = std.fmt.bufPrint( + &workspace_suite_channel_set_request_buffer, + "REQ {d} WORKSPACESUITECHANNELSET {s} stable fallback", + .{ workspace_suite_channel_set_request_id, workspace_suite_name }, + ) catch return error.ToolServiceFailed; + const workspace_suite_channel_set_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_channel_set_request, + 512, + 256, + 512, + ); + if (!std.mem.startsWith(u8, workspace_suite_channel_set_response, "RESP 220 ")) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_channel_set_response, "WORKSPACESUITECHANNELSET crew stable fallback\n") == null) return error.ToolServiceResponseMismatch; + + var workspace_suite_channel_path_buffer: [filesystem.max_path_len]u8 = undefined; + const workspace_suite_channel_path = std.fmt.bufPrint( + &workspace_suite_channel_path_buffer, + "/runtime/workspace-suite-release-channels/{s}/stable.txt", + .{workspace_suite_name}, + ) catch return error.ToolServiceFailed; + service_fba = std.heap.FixedBufferAllocator.init(&scratch.service_scratch); + const workspace_suite_channel_readback = filesystem.readFileAlloc(service_fba.allocator(), workspace_suite_channel_path, 64) catch return error.ToolServiceFailed; + if (!std.mem.eql(u8, std.mem.trim(u8, workspace_suite_channel_readback, "\r\n\t "), "fallback")) return error.ToolServiceResponseMismatch; + + var workspace_suite_channel_list_request_buffer: [128]u8 = undefined; + const workspace_suite_channel_list_request = std.fmt.bufPrint( + &workspace_suite_channel_list_request_buffer, + "REQ {d} WORKSPACESUITECHANNELLIST {s}", + .{ workspace_suite_channel_list_request_id, workspace_suite_name }, + ) catch return error.ToolServiceFailed; + const workspace_suite_channel_list_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_channel_list_request, + 512, + 256, + 512, + ); + if (!std.mem.eql(u8, workspace_suite_channel_list_response, "RESP 221 7\nstable\n")) return error.ToolServiceResponseMismatch; + + var workspace_suite_channel_info_request_buffer: [128]u8 = undefined; + const workspace_suite_channel_info_request = std.fmt.bufPrint( + &workspace_suite_channel_info_request_buffer, + "REQ {d} WORKSPACESUITECHANNELINFO {s} stable", + .{ workspace_suite_channel_info_request_id, workspace_suite_name }, + ) catch return error.ToolServiceFailed; + const workspace_suite_channel_info_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_channel_info_request, + 512, + 256, + 512, + ); + if (!std.mem.startsWith(u8, workspace_suite_channel_info_response, "RESP 222 ")) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_channel_info_response, "suite=crew") == null) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_channel_info_response, "channel=stable") == null) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_channel_info_response, "release=fallback") == null) return error.ToolServiceResponseMismatch; + + var workspace_suite_channel_activate_request_buffer: [128]u8 = undefined; + const workspace_suite_channel_activate_request = std.fmt.bufPrint( + &workspace_suite_channel_activate_request_buffer, + "REQ {d} WORKSPACESUITECHANNELACTIVATE {s} stable", + .{ workspace_suite_channel_activate_request_id, workspace_suite_name }, + ) catch return error.ToolServiceFailed; + const workspace_suite_channel_activate_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_channel_activate_request, + 512, + 256, + 512, + ); + if (!std.mem.startsWith(u8, workspace_suite_channel_activate_response, "RESP 223 ")) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_channel_activate_response, "WORKSPACESUITECHANNELACTIVATE crew stable\n") == null) return error.ToolServiceResponseMismatch; + + var workspace_suite_channel_restored_info_request_buffer: [128]u8 = undefined; + const workspace_suite_channel_restored_info_request = std.fmt.bufPrint( + &workspace_suite_channel_restored_info_request_buffer, + "REQ {d} WORKSPACESUITEINFO {s}", + .{ workspace_suite_channel_restored_info_request_id, workspace_suite_name }, + ) catch return error.ToolServiceFailed; + const workspace_suite_channel_restored_info_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_channel_restored_info_request, + 512, + 256, + 512, + ); + if (!std.mem.startsWith(u8, workspace_suite_channel_restored_info_response, "RESP 224 ")) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_channel_restored_info_response, "workspace=ops") == null) return error.ToolServiceResponseMismatch; + if (std.mem.indexOf(u8, workspace_suite_channel_restored_info_response, "workspace=sidecar") != null) return error.ToolServiceResponseMismatch; + + var workspace_suite_delete_request_buffer: [128]u8 = undefined; + const workspace_suite_delete_request = std.fmt.bufPrint( + &workspace_suite_delete_request_buffer, + "REQ {d} WORKSPACESUITEDELETE {s}", + .{ workspace_suite_delete_request_id, workspace_suite_name }, + ) catch return error.ToolServiceFailed; + const workspace_suite_delete_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_delete_request, + 512, + 256, + 512, + ); + var workspace_suite_delete_response_expected_buffer: [96]u8 = undefined; + const workspace_suite_delete_payload_expected = "WORKSPACESUITEDELETE crew\n"; + const workspace_suite_delete_response_expected = std.fmt.bufPrint( + &workspace_suite_delete_response_expected_buffer, + "RESP {d} {d}\n{s}", + .{ workspace_suite_delete_request_id, workspace_suite_delete_payload_expected.len, workspace_suite_delete_payload_expected }, + ) catch return error.ToolServiceFailed; + if (!std.mem.eql(u8, workspace_suite_delete_response, workspace_suite_delete_response_expected)) return error.ToolServiceResponseMismatch; + + var workspace_suite_list_after_delete_request_buffer: [96]u8 = undefined; + const workspace_suite_list_after_delete_request = std.fmt.bufPrint( + &workspace_suite_list_after_delete_request_buffer, + "REQ {d} WORKSPACESUITELIST", + .{workspace_suite_list_after_delete_request_id}, + ) catch return error.ToolServiceFailed; + const workspace_suite_list_after_delete_response = try exchangeTcpProbeServiceRequest( + eth, + scratch, + client_b, + &server_b, + source_ip, + destination_ip, + workspace_suite_list_after_delete_request, + 512, + 256, + 512, + ); + if (!std.mem.eql(u8, workspace_suite_list_after_delete_response, "RESP 210 0\n")) return error.ToolServiceResponseMismatch; + + if (filesystem.statSummary(workspace_suite_path)) |_| { + return error.ToolServiceResponseMismatch; + } else |err| switch (err) { + error.FileNotFound => {}, + else => return error.ToolServiceFailed, + } + if (filesystem.statSummary(workspace_suite_release_root_path)) |_| { + return error.ToolServiceResponseMismatch; + } else |err| switch (err) { + error.FileNotFound => {}, + else => return error.ToolServiceFailed, + } + if (filesystem.statSummary("/runtime/workspace-suite-release-channels/crew")) |_| { + return error.ToolServiceResponseMismatch; + } else |err| switch (err) { + error.FileNotFound => {}, + else => return error.ToolServiceFailed, + } + var workspace_autorun_batch_request_buffer: [256]u8 = undefined; const workspace_autorun_batch_request = std.fmt.bufPrint( &workspace_autorun_batch_request_buffer, @@ -9167,7 +10074,7 @@ fn runToolExecProbe() ToolExecProbeError!void { const allocator = fba.allocator(); const io: std.Io = undefined; - var help = pal_proc.runCaptureFreestanding(allocator, io, &.{"help"}, 1000, 2048, 128) catch |err| switch (err) { + var help = pal_proc.runCaptureFreestanding(allocator, io, &.{"help"}, 1000, 4096, 128) catch |err| switch (err) { error.OutOfMemory => return error.AllocatorExhausted, else => return error.HelpRunFailed, }; diff --git a/src/gateway/dispatcher.zig b/src/gateway/dispatcher.zig index 6a2019f2..81062a82 100644 --- a/src/gateway/dispatcher.zig +++ b/src/gateway/dispatcher.zig @@ -13894,6 +13894,10 @@ test "dispatch browser.open and send aliases follow existing runtime paths" { test "dispatch file.write and file.read lifecycle updates status counters" { const allocator = std.testing.allocator; const io = std.Io.Threaded.global_single_threaded.io(); + var cfg = config.defaults(); + cfg.state_path = "memory://dispatcher-file-lifecycle"; + setConfig(cfg); + defer setConfig(config.defaults()); var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();