diff --git a/docs/evidence/host-command-activity/README.md b/docs/evidence/host-command-activity/README.md new file mode 100644 index 00000000..3726058b --- /dev/null +++ b/docs/evidence/host-command-activity/README.md @@ -0,0 +1,21 @@ +# Host command activity response fix + +Captured in Chrome on Windows, 1280 × 900, using a built production web bundle and the production Go API with a disposable SQLite database. The offline provider invoked the real `command_runtime` tool under explicitly enabled Full access and the local execution profile. PowerShell printed three fixed verification lines; it exited successfully. No response mocking or DOM edits were used. + +Before: `52c7da77fd83251b61d1696735a3d98f521ce37f` web bundle. After: this fix on the same baseline. Both images use the same saved task and command execution: + +- Thread: `thread-run-20261021192104-36fa7ee25ab4` +- Activity: `toolu_391dc09ee2e7d7d1678084ed` +- Actual API: HTTP 200, `network: "host"`, `status: "completed"`, exit 0, one 87-byte stdout artifact. + +| Before | After | +| --- | --- | +| ![Execution detail rejected](before.png) | ![Execution detail and output loaded](after.png) | + +The before page displayed “执行详情加载失败。” even after Retry. The after page displayed all three output lines and opened the saved stdout: + +![Saved stdout opened](after-saved-output.png) + +Validation: the added host response and OpenAPI enum assertions failed against the old implementation, then passed after the fix. The disabled network case, unknown network rejection, and raw-field rejection remain covered. Four focused API client cases, the three OpenAPI document/golden/live-route tests, and the production TypeScript/Vite build passed. API TypeScript was regenerated from the Go-produced OpenAPI document. The change expands response parsing to an already supported runtime value; command execution authorization is unchanged. + +Screenshots demonstrate the Windows browser flow. The packaged desktop window was not run. diff --git a/docs/evidence/host-command-activity/after-saved-output.png b/docs/evidence/host-command-activity/after-saved-output.png new file mode 100644 index 00000000..bdde22c1 Binary files /dev/null and b/docs/evidence/host-command-activity/after-saved-output.png differ diff --git a/docs/evidence/host-command-activity/after.png b/docs/evidence/host-command-activity/after.png new file mode 100644 index 00000000..35828a1d Binary files /dev/null and b/docs/evidence/host-command-activity/after.png differ diff --git a/docs/evidence/host-command-activity/before.png b/docs/evidence/host-command-activity/before.png new file mode 100644 index 00000000..e3774f1c Binary files /dev/null and b/docs/evidence/host-command-activity/before.png differ diff --git a/docs/openapi.json b/docs/openapi.json index bc9d0939..7c8145b2 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -68418,7 +68418,8 @@ }, "network": { "enum": [ - "disabled" + "disabled", + "host" ], "type": "string" }, diff --git a/internal/browserruntime/ui_evidence_runtime_windows_test.go b/internal/browserruntime/ui_evidence_runtime_windows_test.go index 8a228c9f..933190bf 100644 --- a/internal/browserruntime/ui_evidence_runtime_windows_test.go +++ b/internal/browserruntime/ui_evidence_runtime_windows_test.go @@ -143,14 +143,15 @@ func TestInstalledEdgeUIEvidenceHeadlessMatrixAndRegression(t *testing.T) { endpoint := waitForUIEvidenceSmokeEndpoint(t, profilePath, process, artifactDirectory) - connection, err := dialRestrictedCDP(t.Context(), endpoint) + // Connect and initialize under the same bounded startup deadline. + operationContext, cancel := context.WithTimeout(t.Context(), 45*time.Second) + defer cancel() + connection, err := dialRestrictedCDP(operationContext, endpoint) if err != nil { t.Fatal(err) } client := &restrictedCDPClient{conn: connection, scope: scope, maxRequests: 500, uiEvidence: true} - operationContext, cancel := context.WithTimeout(t.Context(), 45*time.Second) - defer cancel() if err := client.initialize(operationContext); err != nil { _ = connection.Close() t.Fatalf("initialize real Edge restricted CDP: %v", err) diff --git a/internal/httpapi/openapi.go b/internal/httpapi/openapi.go index 406fca61..a44256d0 100644 --- a/internal/httpapi/openapi.go +++ b/internal/httpapi/openapi.go @@ -3551,7 +3551,7 @@ var openAPIFieldEnums = map[string][]string{ "ThreadActivityToolDetailView.agent_role": {string(domain.AgentRoleRoot), string(domain.AgentRoleSpecialist), "unknown"}, "ThreadActivityToolDetailView.status": {string(domain.SupervisorToolPending), string(domain.SupervisorToolCompleted), string(domain.SupervisorToolDenied), string(domain.SupervisorToolFailed)}, "ThreadActivityCommandDetailView.execution_environment": {"Workspace Sandbox", "Host · Full Access", "Legacy execution boundary"}, - "ThreadActivityCommandDetailView.network": {"disabled"}, + "ThreadActivityCommandDetailView.network": {"disabled", "host"}, "ThreadActivityCommandDetailView.status": {string(domain.SupervisorToolPending), string(domain.SupervisorToolCompleted), string(domain.SupervisorToolDenied), string(domain.SupervisorToolFailed), string(runner.CommandRuntimeJobPrepared), string(runner.CommandRuntimeJobRunning), string(runner.CommandRuntimeJobStopping), string(runner.CommandRuntimeJobCompleted), string(runner.CommandRuntimeJobFailed), string(runner.CommandRuntimeJobTimedOut), string(runner.CommandRuntimeJobCancelled), string(runner.CommandRuntimeJobKilled), string(runner.CommandRuntimeJobInterrupted)}, "IndexView.api_version": {Version}, "ProjectActionsView.protocol_version": {application.ProjectActionsProtocolVersion}, diff --git a/internal/httpapi/openapi_test.go b/internal/httpapi/openapi_test.go index 65c24e73..dbf8db40 100644 --- a/internal/httpapi/openapi_test.go +++ b/internal/httpapi/openapi_test.go @@ -189,6 +189,8 @@ func TestOpenAPIDocumentIsDeterministicCapabilitySeparatedAndSecretFree(t *testi } assertOpenAPISchemaOptional(t, document.Components.Schemas, "ThreadTranscriptItemView", "activity_detail_ref") + assertOpenAPIEnum(t, document.Components.Schemas, "ThreadActivityCommandDetailView", "network", + []string{string(runner.CommandRuntimeNetworkDisabled), string(runner.CommandRuntimeNetworkHost)}) for _, field := range []string{"pid", "process_id", "executable", "executable_path", "profile", "profile_path", "devtools_endpoint", "websocket_url", "token", "execution_fence", "authorization_fingerprint"} { diff --git a/web/src/api/client.test.ts b/web/src/api/client.test.ts index 01a7c281..f547ab5b 100644 --- a/web/src/api/client.test.ts +++ b/web/src/api/client.test.ts @@ -1912,15 +1912,15 @@ describe("APIClient", () => { .rejects.toThrow("transcript provenance is invalid"); }); - it("lazily reads only the strict safe Thread command activity projection", async () => { + it.each(["disabled", "host"])("lazily reads the strict Thread command activity projection (%s network)", async (network) => { const detail = { version: "thread_activity_detail.v2", activity_ref: "call-command-1", run_id: "run-created", tools: [{ name: "command_runtime", label: "运行命令", agent_id: "agent-root", agent_role: "root", agent_label: "Root Agent", status: "completed", started_at: "2026-08-24T00:01:00Z", completed_at: "2026-08-24T00:01:02Z", duration_milliseconds: 2_000, detail: { kind: "command", command: { commands: [{ command: "pnpm test session", - working_directory: "packages/core", execution_environment: "Workspace Sandbox", - network: "disabled", status: "completed", exit_code: 0, + working_directory: "packages/core", execution_environment: network === "host" ? "Host · Full Access" : "Workspace Sandbox", + network, status: "completed", exit_code: 0, duration_milliseconds: 2_000, stdout_preview: "42 tests passed", stderr_preview: "", truncated: false, artifacts: [] }] } } }], }; @@ -1931,6 +1931,12 @@ describe("APIClient", () => { .mockResolvedValueOnce(new Response(JSON.stringify({ version: "api.v1", request_id: "req-activity-detail-forged", data: { ...detail, tools: [{ ...detail.tools[0], environment: { API_KEY: "secret" } }] }, + }), { status: 200, headers: { "Content-Type": "application/json" } })) + .mockResolvedValueOnce(new Response(JSON.stringify({ + version: "api.v1", request_id: "req-activity-detail-unknown-network", + data: { ...detail, tools: [{ ...detail.tools[0], detail: { kind: "command", command: { + commands: [{ ...detail.tools[0].detail.command.commands[0], network: "allowlist" }], + } } }] }, }), { status: 200, headers: { "Content-Type": "application/json" } })); vi.stubGlobal("fetch", fetchMock); const client = new APIClient("read-secret", "/api/v1", "control-secret"); @@ -1939,6 +1945,8 @@ describe("APIClient", () => { .resolves.toEqual(detail); await expect(client.threadActivityDetail("thread-created", "call-command-1")) .rejects.toThrow("tool detail is invalid"); + await expect(client.threadActivityDetail("thread-created", "call-command-1")) + .rejects.toThrow("tool detail is invalid"); expect(fetchMock.mock.calls[0]?.[0]) .toBe("/api/v1/threads/thread-created/activities/call-command-1"); }); diff --git a/web/src/api/client/responses/activity.ts b/web/src/api/client/responses/activity.ts index 22c6cb09..d10bd8b0 100644 --- a/web/src/api/client/responses/activity.ts +++ b/web/src/api/client/responses/activity.ts @@ -94,7 +94,7 @@ export function validThreadActivityCommand(value: unknown): boolean { validThreadActivityString(value.command, 4_096) && validThreadActivityString(value.working_directory, 1_024) && threadActivityEnvironments.includes(String(value.execution_environment)) && - value.network === "disabled" && + (value.network === "disabled" || value.network === "host") && threadActivityCommandStatuses.includes(String(value.status)) && (value.exit_code === undefined || (typeof value.exit_code === "number" && Number.isSafeInteger(value.exit_code))) && diff --git a/web/src/api/schema.d.ts b/web/src/api/schema.d.ts index edb57dfd..97ae2278 100644 --- a/web/src/api/schema.d.ts +++ b/web/src/api/schema.d.ts @@ -14256,7 +14256,7 @@ export interface components { /** Format: int32 */ exit_code?: number; /** @enum {string} */ - network: "disabled"; + network: "disabled" | "host"; /** @enum {string} */ status: "pending" | "completed" | "denied" | "failed" | "prepared" | "running" | "stopping" | "completed" | "failed" | "timed_out" | "cancelled" | "killed" | "interrupted"; stderr_preview: string;