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
21 changes: 21 additions & 0 deletions docs/evidence/host-command-activity/README.md
Original file line number Diff line number Diff line change
@@ -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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/evidence/host-command-activity/after.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/evidence/host-command-activity/before.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -68418,7 +68418,8 @@
},
"network": {
"enum": [
"disabled"
"disabled",
"host"
],
"type": "string"
},
Expand Down
7 changes: 4 additions & 3 deletions internal/browserruntime/ui_evidence_runtime_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion internal/httpapi/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
2 changes: 2 additions & 0 deletions internal/httpapi/openapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"} {
Expand Down
14 changes: 11 additions & 3 deletions web/src/api/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [] }] } } }],
};
Expand All @@ -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");
Expand All @@ -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");
});
Expand Down
2 changes: 1 addition & 1 deletion web/src/api/client/responses/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))) &&
Expand Down
2 changes: 1 addition & 1 deletion web/src/api/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading