Skip to content

feat(api): read and write a workspace's project trust over the Runtime API - #6959

Merged
Hmbown merged 5 commits into
codewhale-hq:mainfrom
gaord:feat/workspace-trust-api
Oct 14, 2026
Merged

Hmbown merged 5 commits into
codewhale-hq:mainfrom
gaord:feat/workspace-trust-api

Conversation

@gaord

@gaord gaord commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

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 the
TUI — 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/status now reports workspace_trusted next to the git
    facts it already carried.
  • POST /v1/workspace/trust with {"trusted": true|false} records the
    decision.

The route is deliberately the only writer a client gets. It 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: the file is the user's, and a second writer
would be a second source of truth for a security decision.

Three details worth a reviewer's attention:

  • The workspace is the server's own (state.workspace). The route accepts no
    path, so a client can trust the project it is attached to and nothing else.
  • The response's workspace_trusted is read back through the gate after the
    write rather than echoing the request. If a write ever landed somewhere
    is_workspace_trusted does not look, the response reports false instead of
    claiming success, and config_path names the document that now holds it.
  • The effect is not retroactive, and the message says so instead of implying
    otherwise: 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 in workspace_trust.rs) is a
different mechanism and is untouched, as is a thread's trust_mode / Full
Access — 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

cargo fmt --all -- --check
cargo clippy -p codewhale-tui --all-targets --all-features --locked -- \
  -D warnings -A clippy::uninlined_format_args -A clippy::too_many_arguments \
  -A clippy::unnecessary_map_or -A clippy::collapsible_if -A clippy::assertions_on_constants
RUST_MIN_STACK=8388608 cargo test -p codewhale-tui --lib -- runtime_api::   # 456 passed
cargo test -p codewhale-tui --test feature_registry                        # 2 passed

Three new tests in runtime_api::tests, against the real router (they run the
product stack like their neighbours):

  • workspace_trust_is_reported_and_written_through_the_api — the round trip in
    both 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 an
    unknown field are both 422, and the document is byte-identical afterwards.
    deny_unknown_fields is load-bearing here: a permissive parse would default
    trusted to false and 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's
    id (env-holder-ThreadId(N)/config.toml), and with_test_state_path counts as
    a 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_blocking calls landed on one pooled worker; on CI they did not. I
    confirmed the mechanism with a throwaway probe before changing anything.
  • Sealing the home instead deadlocks: the route resolves its config on a
    spawn_blocking worker, which is not a member of the test's env scope, so it
    blocks on lock_test_env while the sealing test waits on the server. libtest
    has 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=8 with no failures.

Running these locally needs RUST_MIN_STACK (CI sets 8 MiB), otherwise an
unrelated 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
--config a runtime may have been launched with. A server started with
--config /custom/config.toml therefore records trust in the default document.
That is exactly what the TUI's /trust on --save does, so read, write and gate
all 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 --config
with 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.

@gaord
gaord requested a review from Hmbown as a code owner October 11, 2026 21:06

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

…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.
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
@Hmbown

Hmbown commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

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
@Hmbown

Hmbown commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator

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 the
TUI — 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/status now reports workspace_trusted next to the git
    facts it already carried.
  • POST /v1/workspace/trust with {"trusted": true|false} records the
    decision.

The route is deliberately the only writer a client gets. It 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: the file is the user's, and a second writer
would be a second source of truth for a security decision.

Three details worth a reviewer's attention:

  • The workspace is the server's own (state.workspace). The route accepts no
    path, so a client can trust the project it is attached to and nothing else.
  • The response's workspace_trusted is read back through the gate after the
    write rather than echoing the request. If a write ever landed somewhere
    is_workspace_trusted does not look, the response reports false instead of
    claiming success, and config_path names the document that now holds it.
  • The effect is not retroactive, and the message says so instead of implying
    otherwise: 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 in workspace_trust.rs) is a
different mechanism and is untouched, as is a thread's trust_mode / Full
Access — 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

cargo fmt --all -- --check
cargo clippy -p codewhale-tui --all-targets --all-features --locked -- \
  -D warnings -A clippy::uninlined_format_args -A clippy::too_many_arguments \
  -A clippy::unnecessary_map_or -A clippy::collapsible_if -A clippy::assertions_on_constants
RUST_MIN_STACK=8388608 cargo test -p codewhale-tui --lib -- runtime_api::   # 456 passed
cargo test -p codewhale-tui --test feature_registry                        # 2 passed

Three new tests in runtime_api::tests, against the real router (they run the
product stack like their neighbours):

  • workspace_trust_is_reported_and_written_through_the_api — the round trip in
    both 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 an
    unknown field are both 422, and the document is byte-identical afterwards.
    deny_unknown_fields is load-bearing here: a permissive parse would default
    trusted to false and 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's
    id (env-holder-ThreadId(N)/config.toml), and with_test_state_path counts as
    a 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_blocking calls landed on one pooled worker; on CI they did not. I
    confirmed the mechanism with a throwaway probe before changing anything.
  • Sealing the home instead deadlocks: the route resolves its config on a
    spawn_blocking worker, which is not a member of the test's env scope, so it
    blocks on lock_test_env while the sealing test waits on the server. libtest
    has 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=8 with no failures.

Running these locally needs RUST_MIN_STACK (CI sets 8 MiB), otherwise an
unrelated 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
--config a runtime may have been launched with. A server started with
--config /custom/config.toml therefore records trust in the default document.
That is exactly what the TUI's /trust on --save does, so read, write and gate
all 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 --config
with 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 is still pending; no full CI, live authorization or packaged acceptance is claimed. The source branch/original authored commits and tests remain. This PR is not yet merged or included inECD; no release publication or issue closure.

Thank you so much!!!

@Hmbown
Hmbown merged commit 947f670 into codewhale-hq:main Oct 14, 2026
28 checks passed
Hmbown pushed a commit that referenced this pull request Oct 15, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants