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: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,12 @@ Restart `pnpm dev` after changing them. Keep credentials server-side. A direct
Core URL in the connection dialog is only for a compatible Core that explicitly
allows the Web origin, methods, and headers through CORS.

Do not have a Core running yet? Follow [Connecting Agent Core](docs/core-connection.md).
That guide contains the complete PostgreSQL, caller key, device, daemon, native
harness, `CODEX_HOME`, security, verification, and shutdown procedures.
Do not have a Core running yet? Use the immutable
[current Parsar setup guide](https://github.com/MiniMax-AI-Dev/parsar/blob/d91ba48ac6c49cfdf6f08d7687b9be76ba6d53ee/services/agents-api/README.md#standalone-http-service).
The repository's [legacy Web connection runbook](docs/core-connection.md) is pinned
to the older revision stated at its top; revalidate its PostgreSQL, caller-key,
device, daemon, native-harness, `CODEX_HOME`, verification, and shutdown steps before
applying them to a newer Core.

## First use

Expand Down Expand Up @@ -110,16 +113,18 @@ See [Architecture](docs/architecture.md) for the full component and trust bounda
| --- | --- |
| Web cannot reach Core | Confirm the Core address and `AGENTS_API_PROXY_TARGET`, then restart Vite |
| `401 invalid_api_key` | The plaintext caller bearer must match the current Core key binding |
| `503 execution_unavailable` / `Execution is not enabled` | Core is reachable but has no enabled execution path; connect its configured executor/daemon |
| `503 execution_unavailable` / `Execution is not enabled` | Core rejected execution; inspect its safe error plus runtime and ownership state. A worker, executor, or daemon may be unconfigured or disconnected, or an execution lease may have been lost |
| Agent saves but its model fails | Use a model ID and provider credential supported by the connected runtime |

`/healthz` proves HTTP liveness only, not chat readiness. See the
[full troubleshooting guide](docs/core-connection.md#troubleshooting) before retrying
an uncertain request.
`/healthz` proves HTTP liveness only, not chat readiness. Check durable Core state and
the current pinned Parsar guide before retrying an uncertain request; the
[legacy 043 troubleshooting snapshot](docs/core-connection.md#troubleshooting) is
historical context only.

## Documentation

- [Connect Agent Core](docs/core-connection.md) — full local and deployment setup
- [Current Parsar Core setup](https://github.com/MiniMax-AI-Dev/parsar/blob/d91ba48ac6c49cfdf6f08d7687b9be76ba6d53ee/services/agents-api/README.md#standalone-http-service) — immutable current upstream guide
- [Legacy Web connection runbook](docs/core-connection.md) — historical `0438880` snapshot; revalidate before use
- [Protocol coverage](docs/protocol-coverage.md) — exact supported API surface
- [Architecture](docs/architecture.md) — ownership, runtime, and trust boundaries
- [Roadmap](docs/roadmap.md) — planned Web and Core integrations
Expand Down
16 changes: 10 additions & 6 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,11 @@ AGENTS_API_PROXY_TOKEN_FILE=/absolute/private/path/to/web-token
修改后重启 `pnpm dev`。凭据应保留在服务端。只有兼容 Core 通过 CORS
明确允许 Web 的源、方法和请求头时,才能在连接对话框中使用 Core 直连 URL。

还没有运行中的 Core?请参阅[连接 Agent Core](docs/core-connection.md)。
该文档包含完整的 PostgreSQL、调用方凭据、执行设备、daemon、原生执行适配层(harness)、
`CODEX_HOME`、安全、验证和停止流程。
还没有运行中的 Core?请使用不可变的
[当前 Parsar 配置指南](https://github.com/MiniMax-AI-Dev/parsar/blob/d91ba48ac6c49cfdf6f08d7687b9be76ba6d53ee/services/agents-api/README.md#standalone-http-service)。
仓库内的[旧版 Web 连接手册](docs/core-connection.md)固定在文首标注的旧 revision;
将其中 PostgreSQL、调用方凭据、执行设备、daemon、原生执行适配层(harness)、
`CODEX_HOME`、验证和停止流程用于更新版 Core 前必须重新核对。

## 第一次使用

Expand Down Expand Up @@ -108,15 +110,17 @@ WebSocket 当作 API URL。
| --- | --- |
| Web 无法访问 Core | 确认 Core 地址和 `AGENTS_API_PROXY_TARGET`,然后重启 Vite |
| `401 invalid_api_key` | 明文调用方 Bearer 凭据必须与 Core 当前的密钥绑定匹配 |
| `503 execution_unavailable` / `Execution is not enabled` | Core 可以访问,但没有已启用的执行链路;请连接其配置的 executor/daemon |
| `503 execution_unavailable` / `Execution is not enabled` | Core 拒绝执行;请检查其安全错误、运行时和 ownership 状态。worker、executor 或 daemon 可能未配置或已断连,也可能丢失了执行 lease |
| Agent 保存成功但模型运行失败 | 使用已连接运行时支持的 model ID 和提供商凭据 |

`/healthz` 只能证明 HTTP 存活,不能证明聊天已就绪。重试结果不确定的请求前,
请先查看[完整故障排查](docs/core-connection.md#troubleshooting)。
请先核对 Core 持久状态和当前固定版本的 Parsar 指南;
[旧版 043 故障排查快照](docs/core-connection.md#troubleshooting)仅供历史参考。

## 文档入口

- [连接 Agent Core](docs/core-connection.md) — 完整本地和部署配置
- [当前 Parsar Core 配置](https://github.com/MiniMax-AI-Dev/parsar/blob/d91ba48ac6c49cfdf6f08d7687b9be76ba6d53ee/services/agents-api/README.md#standalone-http-service) — 不可变的当前上游指南
- [旧版 Web 连接手册](docs/core-connection.md) — 历史 `0438880` 快照,使用前必须重新核对
- [协议覆盖范围](docs/protocol-coverage.md) — 准确的已支持 API 范围
- [架构说明](docs/architecture.md) — 所有权、运行时和信任边界
- [路线图](docs/roadmap.md) — 计划中的 Web 和 Core 集成
Expand Down
33 changes: 30 additions & 3 deletions apps/web/e2e/agents-lifecycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,8 +503,7 @@ test("keeps the Agent ledger and dialogs usable at 390 px in light and dark mode
expect(metrics.ledger?.right).toBeLessThanOrEqual(390);
await attachScreenshot(page, testInfo, "narrow-light-agent-ledger");

await page.getByRole("button", { name: "Environments", exact: true }).click();
await expect(page.getByRole("heading", { name: "Environments Observed" })).toBeVisible();
await expect(page.getByRole("button", { name: "Environments", exact: true })).toHaveCount(0);
const sessionsNavigation = page.getByRole("button", { name: "Sessions", exact: true });
await sessionsNavigation.click();
await expect(sessionsNavigation).toHaveAttribute("aria-current", "page");
Expand All @@ -515,6 +514,9 @@ test("keeps the Agent ledger and dialogs usable at 390 px in light and dark mode
await globalCreate.click();
const createPanel = page.getByRole("menu", { name: "Create" });
await expect(createPanel).toBeVisible();
await expect(createPanel.getByRole("menuitem")).toHaveCount(2);
await expect(createPanel.getByRole("menuitem", { name: /Environment template/i })).toHaveCount(0);
await expect(createPanel.getByRole("menuitem", { name: /Environment key/i })).toHaveCount(0);
const createPanelBox = await createPanel.boundingBox();
expect(createPanelBox).not.toBeNull();
expect(createPanelBox?.x ?? -1).toBeGreaterThanOrEqual(0);
Expand Down Expand Up @@ -1043,7 +1045,10 @@ test("renders self-hosted Environment and Workspace state safely across reconnec
await expect(panel.getByRole("link", { name: "Launcher setup" })).toBeVisible();
await expect(page.getByRole("region", { name: "Environment connection required" })).toBeVisible();
await expect(page.getByRole("region", { name: "Function result required" })).toBeVisible();
await expect(page.getByLabel("Function result or error")).toBeVisible();
await expect(page.getByLabel("Function result or error")).toBeEnabled();
await expect(page.getByRole("button", { name: "Return error" })).toBeEnabled();
await expect(page.getByRole("button", { name: "Submit result" })).toBeDisabled();
await expect(page.getByRole("button", { name: "Cancel active Turn" })).toBeEnabled();
await expect(page.locator("body")).not.toContainText("launcher:private");
await expect(page.locator("body")).not.toContainText("executor_token=secret");
await expect(page.locator('a[href^="file:"]')).toHaveCount(0);
Expand Down Expand Up @@ -1404,3 +1409,25 @@ test("manually retries uncertain sends with the original key only while the payl
expect(sends[5]?.idempotencyKey).not.toBe(sends[4]?.idempotencyKey);
await attachScreenshot(page, testInfo, "desktop-send-recovery");
});

test("keeps cancellation available for an Environment-only required action", async ({ page, request }) => {
await resetFixture(request);
await controlFixture(request, { environmentScenario: 6 });
await page.goto("/");
await expect(page.getByText("listening", { exact: true })).toBeVisible();
await expect(page.getByRole("region", { name: "Environment connection required" })).toBeVisible();
await expect(page.getByRole("region", { name: "Function result required" })).toHaveCount(0);
const writesBefore = (await fixtureRequests(request)).filter(
(entry) => entry.method === "POST" && entry.path.endsWith("/events"),
).length;
const cancel = page.getByRole("button", { name: "Cancel active Turn" });
await expect(cancel).toBeEnabled();
await cancel.click();
await expect.poll(async () => (await fixtureRequests(request)).filter(
(entry) => entry.method === "POST" && entry.path.endsWith("/events"),
).length).toBe(writesBefore + 1);
const writes = (await fixtureRequests(request)).filter(
(entry) => entry.method === "POST" && entry.path.endsWith("/events"),
);
expect(writes.at(-1)?.body).toEqual({ events: [{ type: "agent.session.input.cancel" }] });
});
8 changes: 5 additions & 3 deletions apps/web/e2e/core-connection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ test("announces loading, authenticated access, and each safe failure state from
methods.push(route.request().method());
const reply = replies.shift();
if (!reply) return route.abort("failed");
await new Promise((resolve) => setTimeout(resolve, 50));
await new Promise((resolve) => setTimeout(resolve, 150));
if ("abort" in reply) return route.abort("failed");
return route.fulfill({
status: reply.status,
Expand Down Expand Up @@ -333,7 +333,8 @@ test("announces loading, authenticated access, and each safe failure state from
await expect(loading).toContainText("Testing Core connection…");
const terminal = dialog.getByRole(expected.role);
await expect(terminal).toContainText(expected.text);
await expect(terminal).toContainText("Execution readiness: Unknown / not verified");
await expect(terminal).toContainText("Chat uses the current Agents API contract");
await expect(terminal).toContainText("does not start a Turn or verify its runtime dependencies");
if (expected.absentText) await expect(terminal).not.toContainText(expected.absentText);
}

Expand Down Expand Up @@ -373,7 +374,8 @@ test("turns a stalled probe into one bounded unreachable result", async ({ page,
(window as ProbeInstrumentationWindow).__stalledProbeCallCount ?? 0
))).toBe(1);
await expect(dialog.getByRole("alert")).toContainText("Core unreachable", { timeout: 7_500 });
await expect(dialog.getByRole("alert")).toContainText("Execution readiness: Unknown / not verified");
await expect(dialog.getByRole("alert")).toContainText("Chat uses the current Agents API contract");
await expect(dialog.getByRole("alert")).toContainText("does not start a Turn or verify its runtime dependencies");
expect(await page.evaluate(() => (window as ProbeInstrumentationWindow).__stalledProbeCallCount)).toBe(1);
});

Expand Down
16 changes: 10 additions & 6 deletions apps/web/e2e/fixture-core.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -253,19 +253,23 @@ function applyEnvironmentScenario(value) {
const session = state.sessions[0];
if (!session) return;
const hostileRemote = "https://launcher:private@executor.example.test/connect?executor_token=secret#credential";
if (value === 1 || value === 4 || value === 5) {
if (value === 1 || value === 4 || value === 5 || value === 6) {
session.environment = {
type: "self_hosted",
id: value === 5 ? canonicalEnvironmentUuid.toUpperCase() : "environment_fixture",
remote_url: hostileRemote,
workspace_directory: `/workspace/<script>safe</script>/${"long/".repeat(45)}project`,
capability_directories: ["/capabilities/read-only", `/capabilities/${"wide/".repeat(55)}`],
};
session.status = value === 1 ? "requires_action" : "idle";
session.required_actions = value === 1 ? [
{ type: "environment_connection", environment_id: "environment_fixture" },
{ type: "function_call", call_id: "call_fixture", turn_id: "turn_fixture", name: "confirm", arguments: { safe: true } },
] : [];
session.status = value === 1 || value === 6 ? "requires_action" : "idle";
session.required_actions = value === 1
? [
{ type: "environment_connection", environment_id: "environment_fixture" },
{ type: "function_call", call_id: "call_fixture", turn_id: "turn_fixture", name: "confirm", arguments: { safe: true } },
]
: value === 6
? [{ type: "environment_connection", environment_id: "environment_fixture" }]
: [];
return;
}
if (value === 2) {
Expand Down
11 changes: 0 additions & 11 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {
requestAgentDetail,
requestAgentUpdate,
} from "./features/agents/agent-actions";
import { EnvironmentsView } from "./features/environments/EnvironmentsView";
import {
SessionsView,
type SessionDetailState,
Expand Down Expand Up @@ -1264,16 +1263,6 @@ export function App() {
onUpdate={updateAgent}
/>
) : null}
{view === "environments" ? (
<EnvironmentsView
sessions={sessions}
observations={environmentObservations}
onOpenSession={(sessionId) => {
setSelectedId(sessionId);
setView("sessions");
}}
/>
) : null}
{view === "system" ? <SystemView /> : null}
</div>
</main>
Expand Down
17 changes: 9 additions & 8 deletions apps/web/src/components/ConnectionModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,12 @@ describe("Agent Core connection modes", () => {
const markup = renderModal("/v1", true);

expect(markup).toContain("Operator-owned setup");
expect(markup).toContain("Connection guide");
expect(markup).toContain("Troubleshooting");
expect(markup).toContain("Legacy Web guide · 043 snapshot");
expect(markup).toContain("Legacy troubleshooting · 043 snapshot");
expect(markup).toContain("Parsar Core setup");
expect(markup).toContain("98c5b3312ad33e1fae8b94283a011eb3e5f4ee2c");
expect(markup).toContain("0438880ab21aa16d05cb91a4c7f91cc0abc12358");
expect(markup).toContain("d91ba48ac6c49cfdf6f08d7687b9be76ba6d53ee");
expect(markup).not.toContain("0438880ab21aa16d05cb91a4c7f91cc0abc12358");
expect(markup).not.toContain("f7cdf591396529880d80f8211fc7a0f4768fdf46");
expect(markup).not.toContain("8cc2898ca42b272cb3771234ee6a0ad0d2e932ba");
expect(markup).not.toContain("7409e00ca25311805a9f8f0d03614f820e407642");
Expand All @@ -104,14 +105,14 @@ describe("Agent Core connection modes", () => {
expect(markup).not.toContain("AGENTS_API_DAEMON_WS_URL");
});

it("states the GET-only probe boundary and unknown execution readiness", () => {
it("states the GET-only probe boundary without disabling the current chat contract", () => {
const markup = renderModal("/v1", true);

expect(markup).toContain("Test connection");
expect(markup).toContain("with one GET");
expect(markup).toContain("never creates an Agent, Session, Turn, or Item");
expect(markup).toContain("Execution readiness remains Unknown / not verified");
expect(markup).toContain("does not prove a daemon, model, or provider is ready");
expect(markup).toContain("Chat uses the current Core events contract");
expect(markup).toContain("a real request can still fail");
});
});

Expand Down Expand Up @@ -172,8 +173,8 @@ describe("Connection probe status", () => {
const markup = renderToStaticMarkup(<ConnectionProbeStatus state={state} />);

expect(markup).toContain(expected);
expect(markup).toContain("Execution readiness: Unknown / not verified");
expect(markup).not.toContain("Execution readiness: Ready");
expect(markup).toContain("Chat uses the current Agents API contract");
expect(markup).toContain("does not start a Turn or verify its runtime dependencies");
});

it("uses an alert for failures and a polite status for authenticated access", () => {
Expand Down
10 changes: 5 additions & 5 deletions apps/web/src/components/ConnectionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export type ConnectionProbeState =
| { status: "complete"; result: CoreProbeResult };

const webBaseline = "98c5b3312ad33e1fae8b94283a011eb3e5f4ee2c";
const parsarBaseline = "0438880ab21aa16d05cb91a4c7f91cc0abc12358";
const parsarBaseline = "d91ba48ac6c49cfdf6f08d7687b9be76ba6d53ee";
const operatorGuideUrl = `https://github.com/MiniMax-AI-Dev/agents-core-web/blob/${webBaseline}/docs/core-connection.md`;
const troubleshootingUrl = `${operatorGuideUrl}#troubleshooting`;
const parsarCoreSetupUrl = `https://github.com/MiniMax-AI-Dev/parsar/blob/${parsarBaseline}/services/agents-api/README.md#standalone-http-service`;
Expand Down Expand Up @@ -102,7 +102,7 @@ export function ConnectionProbeStatus({ state }: { state: ConnectionProbeState }
>
<strong>{copy.title}</strong>
<p>{copy.detail}</p>
<small>Execution readiness: Unknown / not verified.</small>
<small>Chat uses the current Agents API contract. This read-only probe does not start a Turn or verify its runtime dependencies.</small>
</div>
);
}
Expand Down Expand Up @@ -316,11 +316,11 @@ export function ConnectionModal({
</div>
<div className="connection-guide-links">
<a className="connection-guide-link" href={operatorGuideUrl} target="_blank" rel="noreferrer">
Connection guide
Legacy Web guide · 043 snapshot
<ExternalLink size={12} strokeWidth={1.5} aria-hidden="true" />
</a>
<a className="connection-guide-link" href={troubleshootingUrl} target="_blank" rel="noreferrer">
Troubleshooting
Legacy troubleshooting · 043 snapshot
<ExternalLink size={12} strokeWidth={1.5} aria-hidden="true" />
</a>
<a className="connection-guide-link" href={parsarCoreSetupUrl} target="_blank" rel="noreferrer">
Expand All @@ -331,7 +331,7 @@ export function ConnectionModal({
</section>
<div className="notice neutral">
<Info size={14} strokeWidth={1.5} aria-hidden="true" />
Execution readiness remains Unknown / not verified. HTTP access does not prove a daemon, model, or provider is ready.
Test connection verifies Agents API access only. Chat uses the current Core events contract; a real request can still fail when its worker, executor, model, or provider is unavailable.
</div>
</Modal>
);
Expand Down
11 changes: 6 additions & 5 deletions apps/web/src/components/CreateMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest";
import { CreateMenuContent } from "./CreateMenu";

describe("Create menu", () => {
it("keeps supported actions separate from explicit Core-unavailable resources", () => {
it("shows only actions supported by the current Core contract", () => {
const html = renderToStaticMarkup(
<CreateMenuContent
canCreateAgent
Expand All @@ -15,10 +15,11 @@ describe("Create menu", () => {
);

expect(html).toContain('role="menu"');
expect(html).toContain("Environment template");
expect(html).toContain("Core exposes no template API");
expect(html).toContain("operator-owned, never browser-managed");
expect(html.match(/aria-disabled="true"/g)).toHaveLength(2);
expect(html).toContain("Agent");
expect(html).toContain("Start Session");
expect(html).not.toContain("Environment template");
expect(html).not.toContain("Environment key");
expect(html).not.toContain('aria-disabled="true"');
expect(html).not.toContain("executor_token");
});
});
Loading
Loading