Repository navigation
feat(api): read and write a workspace's project trust over the Runtime API - #6959
Conversation
…time API
A client that is not the TUI has no way to see, or settle, whether a workspace
is trusted — and everything the engine skips for an untrusted one is silent.
The TUI reports it through `/trust on --save`, which needs a terminal a user may
not have: an editor extension cannot open one for them without doing the job
badly.
`GET /v1/workspace/status` now reports `workspace_trusted` alongside the git
facts it already carried, and `POST /v1/workspace/trust` with `{"trusted":
true|false}` records the decision. The write calls `config::set_workspace_trust`
— the same function behind `/trust on --save` — so both front ends write the one
`[projects."<workspace>"] trust_level` record that `is_workspace_trusted`, the
gate the engine actually consults, reads back. A client never edits
`config.toml` itself: the file is the user's, and a second writer would be a
second source of truth for a security decision.
The route takes no path. The workspace is the server's own, so a client can
trust the project it is attached to and nothing else. The response's
`workspace_trusted` is the verdict read back through the gate after the write
rather than an echo of the request, and `config_path` names the document that
now holds it — a write that landed somewhere the gate does not look reports
`false` instead of claiming success.
Three tests. The HTTP pair covers the round trip in both directions, that the
named document really carries the record, and that a malformed body is refused
before anything is written (`deny_unknown_fields` is load-bearing: a permissive
parse would supply `false` and silently revoke the workspace). The third asserts
the status projection follows the gate, sealed on one thread.
That third test exists because the first version of the other two was wrong in a
way only CI showed. They asserted the write via a *second* request, and failed on
Linux while passing here. The cause is in the harness, not the route:
`unsealed_test_state_root` returns a root keyed by the resolving thread's id, and
`with_test_state_path` counts as a holder while it resolves, so two requests
served by different blocking workers legitimately read and write two different
test config files. On a laptop both landed on one pooled worker; on CI they did
not. Sealing the home instead deadlocks — the route resolves on a blocking
worker, which is not an env-scope member and blocks on the lock a sealing test
holds while it awaits the server. So the HTTP tests now assert what one request
can prove on its own (the read-back inside that request), and the cross-request
property moved to the sealed, single-threaded test where it is deterministic.
Docs: `docs/RUNTIME_API.md` and its `zh_hans` translation, in this change.
d661bd7 to
117aefe
Compare
Keep the contributor API and its original authored commit on its own PR. Trace actual project-context and exact hook-approval consumers: persisted trust does not remove project instructions/history, and hooks require separate digest approval. Correct both response messages and EN/ZH owning docs; remove the two now-false explanatory comment blocks; update the existing runtime-api registry summary and default-config limitation. Source review, Rustfmt parse/format check and TOML registry validation passed. No product test was run for this branch correction; fresh exact-head hosted CI and Runtime authorization/trust acceptance remain required. No new agent test or code comment. Maintainer update brought actual main fixes with original commits preserved. Refs codewhale-hq#6959 Refs codewhale-hq#6968
|
Thank you @gaord for making workspace trust inspectable and writable by non-TUI clients through the engine’s existing config authority, and for the three tests covering round-trip, malformed bodies and bootstrap projection. Your #6959 merged as its own PR at 947f670fee, preserving your commit, tests and branch. Exact own-head CI passed: macOS19,038/0failed/33skipped, Ubuntu19,035/0/34, Windows18,304passed(oneleaky)/0/27 and Lint; all three trust cases and owner recovery passed everywhere. Web772Vitest/facts/docs/build passed. The API response and docs keep the separate hook-digest approval, default-config and MCP-restart limits explicit. This is on main for the 0.10.2 candidate; final assembled-source and package acceptance still remain. |
Shorten only the existing runtime-api summary from145 to116characters, preserving persisted trust, scoped tokens and account-bound turns. The previous own-head Ubuntu run failed only feature_registry_matches_code: Summary19035testsrun19034passed,1failed,34skipped. The original contributor trust/auth/owner cases passed in that failed run; it remains preserved. Existing reusable feature-registry binary with the exact replacement on disk: test result: ok.1passed;0failed;0ignored;0measured;1filteredout. TOML parse and exact one-field source comparison passed. This local registry proof is not a fresh production binary or Windows acceptance; new exact-head hostedCI remains required. No new test/comment or waiver. Original contributor PR/commits/branch retained. Refs codewhale-hq#6959
…ource Regenerate the existing agent-docs and feature JSON projections with the unchanged derive-facts generator at exact3a1eae source. Only the Runtime API owning-doc content and its116-character registry summary change semantically; facts.generated.ts remains byte-identical. Retain the contributor API, authored commits and tests on its own PR. Actual old Web job115628347590 failed check-facts because generated agent docs were stale;772Vitest tests passed in that failed workflow. Immutable139source payloads were independently Git-blob/SHA256 audited. The exact existing checker reproduced the stale-doc failure before generation and then reported [check-facts] OK — committed facts.generated.ts matches workspace. This is pure source generation/checker acceptance, not fullweb/native/Windows/dist qualification; fresh exact-head hosted gates remain required. No new product test/comment, no disabled gate, upload or deployment. Refs codewhale-hq#6959
Thank you so much!!! |
Folds 26 main commits (incl. #6924 Runtime stores/workspace drivers, #6977, #6943, #6959, #6958, #6956) into PR #6968's lane. The only conflicts were web/data/agent-docs.generated.json and features.generated.json, regenerated with web/scripts/derive-facts.mjs (check-facts OK, version 0.10.2). Verified: scripts/dev-cargo.sh check --locked -p codewhale-tui --all-targets finished in 9m00s with no errors. nextest -p codewhale-tui --lib on the overlap filter (runtime_api|rlm::turn|runtime_threads|workspace_lease|snapshot): 902 passed, 0 failed, 14427 skipped (80.1s). Full suite left to hosted CI. Branch exception: founder-authorized 2026-10-15 for this lane, recorded in CW/coordination/BRANCH_EXCEPTIONS.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NBKbhx19BRbZtvWK8rVKjz
What and why
Everything the engine skips for an untrusted workspace is silent, and the only
way to settle it was a terminal and
/trust on --save. A client that is not theTUI — the VS Code extension I am building against this API — had no way to see
the verdict or record it, so a user could sit in front of a project whose
trust-sensitive skills, commands, hooks and MCP configuration were quietly not
loading, with nothing in the UI to act on.
GET /v1/workspace/statusnow reportsworkspace_trustednext to the gitfacts it already carried.
POST /v1/workspace/trustwith{"trusted": true|false}records thedecision.
The route is deliberately the only writer a client gets. It calls
config::set_workspace_trust, the same function behind/trust on --save, soboth front ends write the one
[projects."<workspace>"] trust_levelrecord thatis_workspace_trusted— the gate the engine actually consults — reads back. Aclient never edits
config.toml: the file is the user's, and a second writerwould be a second source of truth for a security decision.
Three details worth a reviewer's attention:
state.workspace). The route accepts nopath, so a client can trust the project it is attached to and nothing else.
workspace_trustedis read back through the gate after thewrite rather than echoing the request. If a write ever landed somewhere
is_workspace_trusteddoes not look, the response reportsfalseinstead ofclaiming success, and
config_pathnames the document that now holds it.messagesays so instead of implyingotherwise: skills and commands consult the persisted decision at their
existing boundaries; project hooks also need exact-file digest approval,
and project MCP connections are rebuilt at the next engine start. Project
instructions and existing MCP connections are not removed by this POST.
/trust add <path>(the external-path list inworkspace_trust.rs) is adifferent mechanism and is untouched, as is a thread's
trust_mode/ FullAccess — the docs now spell out which is which.
Issue
No-Issue: no tracking issue; this is the engine half of a client-side gap found while building the VS Code extension against the Runtime API.
Contributor-reported original local verification
Three new tests in
runtime_api::tests, against the real router (they run theproduct stack like their neighbours):
workspace_trust_is_reported_and_written_through_the_api— the round trip inboth directions, that the response names the server's own workspace, and that
the named document really carries
projects."<workspace>".trust_level = "trusted"/"untrusted".workspace_trust_route_refuses_malformed_bodies— a missing field and anunknown field are both 422, and the document is byte-identical afterwards.
deny_unknown_fieldsis load-bearing here: a permissive parse would defaulttrustedtofalseand silently revoke the workspace.workspace_bootstrap_status_reports_the_trust_gate— sealed, on one thread:the status projection follows the gate, up and back down.
The first version of the HTTP tests was wrong, and CI caught what a laptop
could not. They asserted the write through a second request and failed on
Test (ubuntu-latest)while passing locally. The cause is in the test harness,not the route, and it is worth writing down because it is easy to rediscover the
hard way:
unsealed_test_state_root()returns a root keyed by the resolving thread'sid (
env-holder-ThreadId(N)/config.toml), andwith_test_state_pathcounts asa holder while it resolves — so any thread that resolves the user config gets
its own test config document. Two requests served by different blocking workers
therefore legitimately read and write two different files. On my machine both
spawn_blockingcalls landed on one pooled worker; on CI they did not. Iconfirmed the mechanism with a throwaway probe before changing anything.
spawn_blockingworker, which is not a member of the test's env scope, so itblocks on
lock_test_envwhile the sealing test waits on the server. libtesthas no per-test timeout, so the binary hangs rather than failing.
So the HTTP tests now assert only what one request can prove on its own — and
that is the stronger half anyway, since the read-back happens inside that
request — and the cross-request property moved to the sealed, single-threaded
test where it is deterministic. Re-ran the three 25× plus 12× under
--test-threads=8with no failures.Running these locally needs
RUST_MIN_STACK(CI sets 8 MiB), otherwise anunrelated debug-build stack overflow aborts the suite before they run.
Known limitation (not addressed here)
The route resolves the config path through
config::set_workspace_trust/is_workspace_trusted, which use the default discovery path — not the--configa runtime may have been launched with. A server started with--config /custom/config.tomltherefore records trust in the default document.That is exactly what the TUI's
/trust on --savedoes, so read, write and gateall agree; making the gate honour a per-runtime override would mean threading a
path through its ~15 call sites, which is a separate change. Flagging it rather
than fixing it half-way, which would be worse: a writer honouring
--configwith a gate reading the default path is precisely the disagreement the response's
read-back exists to refuse. The consuming extension launches the engine without
--config, so it is not affected today.Current maintainer correction and gate
The original contribution and its three tests remain. Current main was merged into the original branch without rewriting history. 10d047c1 corrected both API messages and owning English/Chinese docs to actual trust/context/hook/config boundaries. Default-config discovery remains explicit; project trust does not alter Full Access or external-path grants.
Two maintainer integration errors were then corrected: 3a1eae9f keeps the existing feature summary within its120-character contract; 97aac3e regenerates its documentation and feature projections with the existing generator. The earlier Ubuntu registry failure and stale-generated-doc web failure remain recorded.
Current exact-head Web38496306483 passed772Vitest tests, facts/docs validation and build. Native CI38496306482 completed: macOS19,038/0failed/33skipped, Ubuntu19,035/0/34, Windows18,304passed(oneleaky)/0/27 and Lint. All three trust cases and owner recovery passed on every platform; unique full-suite ordinals and exact source were independently checked. Web772Vitest/facts/docs/build passed. No live authorization or final packaged acceptance is claimed. The source branch/original authored commits and tests remain. This PR merged as itself at 947f670fee. Original author commits, three tests and branch are preserved. It is now on main, but not yet folded into the ECD release branch. No release publication or issue closure.