Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions docs/FRONTEND_CAPABILITY_ALIGNMENT_287.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Frontend permission and capability alignment (#287)

## Problem and implementation

The backend readiness projection returns Ask / Auto / Full, while the frontend
validator required five retired writable choices. A valid HTTP 200 response was
therefore rejected before execution settings and review checks could render.
The validator and current fixtures now use the canonical three-choice sequence;
missing, duplicate, unknown, reordered and legacy readiness groups are rejected.
Historical Run/Thread permission snapshots retain their existing read support.

`TestRunCapabilityReadinessHTTPCurrentFixtureMatchesGoProjection` creates an
isolated SQLite Run and obtains the response through the real Go HTTP handler.
Only generated request and Run identities are normalized. Every public field is
compared with the committed current fixture during normal Go tests. APIClient
tests parse that same fixture, and optionally the freshly exported response via
`TRAVERSE_TEST_READINESS_OUTPUT`. The original dated five-choice capture remains
unchanged as a negative current-contract specimen.

Execution settings, Debug activation and CDP descriptions distinguish approval
preference from runtime capability. Direct URL fetch status uses the stored
network Scope, including a historical `public_https` target when actually present;
Full or old Debug values do not manufacture a network grant.

Task review now has a first-level **更多交付工具** entry for batch delivery,
advanced Git, GitHub review and UI evidence. Each existing panel loads on demand
with the selected Run identity. Exact Git/GitHub review results survive a visit to
that Run's approval or delivery checks; changing Run clears retained review.
A missing selected historical Run fails closed instead of selecting the current
Run. No execution endpoint or startup default changed.

UI evidence distinguishes missing control credentials from disabled UI evidence,
Run execution and browser-CDP capabilities. An unavailable history reader keeps
history unknown. Batch validation explains its independent startup requirements.
These instructions expose existing configuration paths; they do not provide new
runtime activation APIs or bypass backend checks.

## Verification and limits

- Full frontend suite: 175 files / 1,676 tests passed before the final UI-reader
404 cases and CDP wording follow-up. The affected final slice passed 23 tests,
including six new UI-reader error/history cases. TypeScript, production build
and generated API checks passed; the existing large-chunk build warning remains.
- The Go HTTP/application readiness tests passed. A fresh Go export was also
consumed by the real TypeScript APIClient. Fixed declared Local adapter facts
make the fixture portable; this is not evidence of a real OS sandbox launch.
- The complete HTTP API package passed locally, along with its vet check and the
release/documentation contract packages.
- Read-only cross-checks covered exact network scope, live Full requirements,
Run/Thread isolation and retention of precise approval targets.
- Chromium used a separate loopback Go API, fresh application home and local
fixture workspace. The acceptance Run was seeded through `RunService` without
a model call. Its actual three-choice HTTP response rendered execution settings;
the four tool entries and disabled UI-evidence reader state were inspected at
desktop width. The tool selector was also checked at a 390-pixel viewport.

No external model, GitHub review write, repository mutation, host validation,
managed browser startup or native Desktop restart was performed during the
browser check. It verifies entry points and current response consumption, not a
complete delivery or native-runtime acceptance run. Local screenshots and logs
remain in ignored `output/playwright/` and `build/permission-alignment/`.
7 changes: 7 additions & 0 deletions docs/PROJECT_MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
> The nightly full matrix and blocking vulnerability audit remain enabled;
> see [CI security baseline](ci.md#go-security-baseline) for scan evidence limits.

> 2026-10-07 permission/capability alignment (#287): the frontend consumes the
> current Ask / Auto / Full readiness contract, with a Go HTTP fixture checked by
> both Go and the real APIClient. Task review exposes existing Run-bound delivery
> tools and explains independent startup gates. Historical permission readers and
> all Go authority checks remain in place. See
> [implementation and verification scope](FRONTEND_CAPABILITY_ALIGNMENT_287.md).

> 2026-10-07 extension onboarding checkpoint (#276): shared Web/Desktop settings
> stage manual MCP descriptors, import pinned Plugin ZIPs and stage/review LSP
> configuration through Go-owned services. Backend capability flags, source/scope,
Expand Down
15 changes: 15 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,21 @@ Schema v86 separates execution interaction intent from general runtime authority

Current execution preferences are `ask|auto|full`. Ask and Auto use the common per-operation authorizer and exact approvals when required; Full requires explicit confirmation and live activation in the current process. The Standard Code preset selects Ask. Commands use the same `command-runtime.v2` protocol with an independently ready Local or explicit Docker sandbox adapter, or an explicitly installed host adapter. A missing sandbox never falls back to host execution. `--enable-workspace-sandbox` opens the Local gate only after its real AppContainer/WFP/Job/ACL readiness proof; Docker remains independently enabled. Host Ask/Auto calls require exact durable approval, while host Full requires live activation. Saved permission rows cannot restore either authority. A permission revision change fences old calls and Jobs. See [Command Runtime adapter split](architecture/command-runtime-adapter-split.md) and [retirement decision](adr/0165-retire-legacy-command-execution.md).

The current Web/Desktop permission selector and readiness projection use only
Ask / Auto / Full; the five legacy values remain historical read formats. The
execution environment, Debug interaction/runtime, exact URL fetch scope and
browser-CDP permissions are separate controls. A Debug restart does not restore
Full activation or enable independent batch-validation/UI-evidence capabilities.

In a task, open **审阅改动 → 更多交付工具** to select batch delivery, advanced Git,
GitHub review or UI evidence. Select the execution record before opening a tool;
its reads, review and approval flow remain bound to that Run. Switching records
clears the retained tool review. Missing startup capabilities show their existing
configuration requirements and keep execution disabled. UI evidence history is
still read independently of write capability; an unavailable reader is shown as
unknown history, never as an empty evidence ledger. See
[the #287 acceptance record](FRONTEND_CAPABILITY_ALIGNMENT_287.md).

`run capability-readiness` and `GET /api/v1/runs/{run_id}/capability-readiness`
return the same Go-owned `run_capability_readiness.v1` projection used by Desktop.
Each Permission, Profile, Interaction, browser-CDP, and Standard Code option reports
Expand Down
99 changes: 99 additions & 0 deletions internal/httpapi/capability_readiness_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package httpapi

import (
"encoding/json"
"net/http"
"os"
"path/filepath"
"reflect"
"strings"
"testing"

"cyberagent-workbench/internal/application"
"cyberagent-workbench/internal/commandruntimeadapter"
"cyberagent-workbench/internal/domain"
"cyberagent-workbench/internal/store"
)

func TestRunCapabilityReadinessHTTPProjectsSameStableGoFacts(t *testing.T) {
Expand Down Expand Up @@ -48,6 +55,98 @@ func TestRunCapabilityReadinessHTTPProjectsSameStableGoFacts(t *testing.T) {
http.StatusBadRequest, "INVALID_ARGUMENT")
}

func TestRunCapabilityReadinessHTTPCurrentFixtureMatchesGoProjection(t *testing.T) {
state, err := store.Open(filepath.Join(t.TempDir(), "readiness-contract.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = state.Close() })
runs := application.NewRunService(state)
_, run, err := runs.Create(t.Context(), application.CreateRunRequest{
Goal: "read paused Plan readiness", Profile: "code", Surface: "code",
Phase: "plan", Budget: domain.Budget{MaxTurns: 2},
})
if err != nil {
t.Fatal(err)
}
if _, err := application.NewRunExecutionProfileService(state).Change(t.Context(),
application.ChangeRunExecutionProfileRequest{RunID: run.ID, Profile: "local",
OperationKey: "readiness-contract-local-profile", RequestedBy: "test_operator",
Reason: "project selected installed adapter without execution"}); err != nil {
t.Fatal(err)
}
if _, err := runs.Start(t.Context(), run.ID); err != nil {
t.Fatal(err)
}
if _, err := runs.Pause(t.Context(), run.ID); err != nil {
t.Fatal(err)
}
capabilities := domain.ExecutionPermissionRuntimeCapabilities{
OperatorApprovalEnabled: true, WorkspaceSandboxEnabled: true,
}
adapters := []commandruntimeadapter.Identity{commandruntimeadapter.SandboxedWorkspace(
application.CommandRuntimeLocalSandboxBackend, "readiness-fixture-local", strings.Repeat("a", 64))}
runtime := application.CapabilityReadinessRuntime{
RunControlEnabled: true, ExecutionPermissionControlEnabled: true,
ExecutionPermissionCapabilities: capabilities,
LocalSandboxInstalled: true, LocalSandboxProven: true, LocalBackendReady: true,
CommandRuntimeAdapters: adapters,
}
api, err := New(state, Config{
AccessToken: testAccessToken, ControlToken: testControlToken,
RunControlEnabled: true, ExecutionPermissionControlEnabled: true,
ExecutionPermissionCapabilities: capabilities, CommandRuntimeAdapters: adapters,
CapabilityReadinessRuntime: &runtime,
})
if err != nil {
t.Fatal(err)
}
response := performSessionMessageRequest(t, api, http.MethodGet,
"/api/v1/runs/"+run.ID+"/capability-readiness", testAccessToken, "", "", nil)
var view RunCapabilityReadinessView
decodeDataStatus(t, response, http.StatusOK, &view)
if len(view.Permissions) != 3 || view.Permissions[0].Value != "ask" ||
view.Permissions[1].Value != "auto" || view.Permissions[2].Value != "full" ||
!view.Permissions[0].Selected || view.CapabilityGrant ||
!view.CommandRuntime.AdapterInstalled || !view.CommandRuntime.AdapterReady ||
view.CommandRuntime.CurrentRunGranted || view.CommandRuntime.AdapterKind != "sandboxed_workspace" ||
view.CommandRuntime.Backend != application.CommandRuntimeLocalSandboxBackend {
t.Fatalf("current readiness contract changed: %#v", view)
}

// Capture the real HTTP envelope, normalizing only generated request/Run IDs.
// Compare every public field in ordinary Go runs so a manually maintained TS
// specimen cannot hide a backend contract change. The optional export follows
// the existing TRAVERSE_TEST_*_OUTPUT cross-language acceptance convention.
var captured map[string]any
if err := json.Unmarshal(response.Body.Bytes(), &captured); err != nil {
t.Fatal(err)
}
captured["request_id"] = "req-readiness-current"
captured["data"].(map[string]any)["run_id"] = "run-readiness-current"
data, err := json.MarshalIndent(captured, "", " ")
if err != nil {
t.Fatal(err)
}
if output := os.Getenv("TRAVERSE_TEST_READINESS_OUTPUT"); output != "" {
if err := os.WriteFile(output, append(data, '\n'), 0600); err != nil {
t.Fatal(err)
}
}
fixturePath := filepath.Join("..", "..", "web", "src", "test", "fixtures", "readiness-paused-plan-current.json")
fixture, err := os.ReadFile(fixturePath)
if err != nil {
t.Fatal(err)
}
var expected map[string]any
if err := json.Unmarshal(fixture, &expected); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(captured, expected) {
t.Fatalf("current readiness fixture drifted from the Go HTTP response; regenerate with TRAVERSE_TEST_READINESS_OUTPUT=%s\n%s", fixturePath, data)
}
}

func readinessHTTPOption(t *testing.T, options []CapabilityReadinessOptionView,
value string,
) CapabilityReadinessOptionView {
Expand Down
Loading
Loading