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
66 changes: 63 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -1666,8 +1666,9 @@ credential encryption key. Provider identity/backend namespace and native histor
must not change on a repeated install.

`services/core-console` serves the production Web build and forwards public `/v1`
requests to one configured Core using its project bearer, after console Basic
authentication. The paired console uses the same login for allowlisted sandbox
requests to one configured Core using its project bearer after administrator
authentication. New installations use a console-local single administrator account;
existing installations without explicit account mode retain Basic authentication. The paired console uses the same login for allowlisted sandbox
management routes and supplies its private server-side administrator token from
`CORE_CONSOLE_SANDBOX_ADMIN_TOKEN_FILE`. The browser receives only capability
flags through `/console/config`, never the deployment bearer. Project API keys
Expand Down Expand Up @@ -1701,7 +1702,66 @@ verification, and passes the enrollment credential only to the installer process
Both proxy paths retain fixed-origin, cross-site, safe-path, redirect and Upgrade
restrictions through the standard Go reverse proxy with streaming/cancellation.
The console implements no product identity, resource semantics, Runtime discovery
or execution loop.
or execution loop. Its local administrator account grants the complete console
surface; do not introduce Web roles, invitations or per-project Web identities.
Agent API caller keys remain independent of the administrator password and cookie.
Explicit, unambiguous caller Bearer requests to public `/v1` routes pass through
unchanged to Core, without borrowing the console's caller or administrator key.
The same origin, path, method and transport restrictions still apply.

Account mode is explicit (`CORE_CONSOLE_AUTH_MODE=account`) and requires a private
setup-key file and a private writable state directory. Only the installation's
one-time setup credential can claim the administrator account. The atomic durable
account record stores a password hash; corruption or a missing required credential
must never reopen registration. Account creation is race-safe. Cookie sessions are
bounded, HttpOnly, SameSite Strict and Secure for HTTPS origins; a restart requires
sign-in again, without deleting the account. Unauthenticated access is limited to
the static login UI, finite console authentication routes and the existing
independently authenticated node/project transports. Authentication requests use
same-origin JSON POSTs with bounded bodies and bounded password-hash work.

Console-managed Agent API keys live in Core PostgreSQL, separate from console
login state and model credentials. Only deployment administrator authentication
can create, list or revoke them through the Core management extension. The console
bridge derives the static parent binding digest from its private caller token;
never accept a browser-supplied parent, tenant or subject. A static parent digest
is a selector, not authentication. Freeze the complete configured principal with
each derived key, and reject it whenever the current static binding is missing or
changed. Never derive keys from another dynamic key. The issuer returns a random
secret once and stores only its digest and safe metadata. Reads never return key
material. Check revocation on each dynamic-key request without an auth cache;
database failures fail closed. Static configured-key authentication remains
independent of this lookup. Keep key management outside public `/v1` resources.

A caller-supplied creation UUID identifies a single key issuance. A repeated UUID
returns conflict without replaying or rotating a secret. After an uncertain create,
read the safe list and explicitly revoke an inaccessible key before replacing it;
any explicit retry uses the same UUID. The first-run UI reminds the operator to
save the key and use it for subsequent Agent API calls. Never persist a displayed
key in browser storage or carry it into the request code or URL. The console has
one administrator role and no project/role editor.
The `/console/config` `api_keys` capability controls whether key management is
available. Paired consoles require an active saved key before continuing from the
access step. Web-only consoles with `api_keys: false` instead explain how to use
an existing Core key and allow the introduction to continue without key-management
requests. A failed or malformed capability read must not imply either capability.

First-run Home is a skippable/replayable console introduction after account setup.
It does not change public Core resource semantics or block ordinary administration.
Keep new onboarding state and components outside the oversized `App.tsx`. Persist
only non-secret presentation progress; password, setup key and model provider key
must not enter browser storage or generated code samples. Creating a saved Agent
is an explicit write through the existing API. Reconcile uncertain results before
another write, and associate external examples with their exact metadata marker,
not arbitrary new resources or name matches. The request workbench generates code
from its real form fields; local execution obtains caller and model keys separately.
Generated examples reject redirects so credentials stay at the selected API origin.
A registered host supplies sandbox resources only for hosted Sessions; self-hosted
execution remains application-managed. Reuse the existing enrollment and topology
contracts. Show actual confirmed resources, no simulated work or Agent-to-node
ownership. Use nonlinear motion for transitions and success emphasis, preserve
keyboard focus, and honor reduced motion. Compatibility scope and caveats belong
in documentation, not in the introduction.
The console has neither KVM nor Docker authority; its static root contains no
secrets. Installation exposes only loopback API/console ports. Remote exposure
requires an operator-configured HTTPS/access boundary. Web-only mode can connect
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ existing Sessions retain their node across disconnects and resume.
bundles are not published yet; see the [installation guide](docs/getting-started/install.md)
for building a bundle and the host/network prerequisites.
2. **Sign in to Web.** Open the console address printed by the installer. Use
`admin` and the password in `~/.parsar/core/config/console.password`.
The paired console needs no additional API key setup.
the one-time key in `~/.parsar/core/config/console.setup.key` to register your
administrator account, then keep your chosen username and password safe.
Existing installations retain their `admin` / `console.password` login.
The console connects to Core automatically. During first-run, create and save
an Agent API key for requests from your own machine or application.
3. **Add a node.** Open **Hosted Sandbox Manager**, choose Docker or microsandbox,
and initialize the deployment. The paired console address is used by default;
advanced network settings allow a different reachable HTTPS origin. Select
Expand Down
77 changes: 77 additions & 0 deletions apps/web/e2e/first-run-keyboard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { expect, test } from "@playwright/test";

const fixtureBaseUrl = `http://127.0.0.1:${process.env.AGENTS_FIXTURE_PORT ?? 18092}`;

test.beforeEach(async ({ page, request }) => {
expect((await request.post(`${fixtureBaseUrl}/__fixture/reset`)).ok()).toBe(true);
await page.emulateMedia({ reducedMotion: "reduce" });
await page.addInitScript(() => localStorage.setItem("agents-core-web.locale", "en"));
await page.route("**/console/auth", (route) => route.fulfill({ json: { mode: "authenticated", username: "keyboard-admin" } }));
await page.route("**/console/config", (route) => route.fulfill({ json: { api_keys: true, sandbox_admin: true, node_installer: true, node_installer_sha256: "a".repeat(64) } }));
await page.route("**/console/api-keys", (route) => route.fulfill({ json: { data: [{
id: "fb533e99-524f-4e44-94bc-8f6e571646a7", name: "Existing key", prefix: "pc_fixture",
created_at: "2026-09-24T00:00:00Z", revoked_at: null,
}] } }));
});

test("moves focus from disappearing step actions to the new heading and retains chapter navigation focus", async ({ page }) => {
await page.goto("/");
const accessHeading = page.getByRole("heading", { name: "Keep your sign-in details.", exact: true });
await expect(accessHeading).toBeVisible();
await expect(accessHeading).not.toBeFocused();
const continueButton = page.getByRole("button", { name: "I've saved it. Continue", exact: true });
await expect(continueButton).toBeEnabled();
await continueButton.focus();
await page.keyboard.press("Enter");
const machineHeading = page.getByRole("heading", { name: "Connect your own machine.", exact: true });
await expect(machineHeading).toBeFocused();
await expect(page.getByRole("button", { name: "Refresh sandbox state", exact: true })).toBeEnabled();
await page.keyboard.press("Tab");
await expect(page.getByRole("button", { name: "Refresh sandbox state", exact: true })).toBeFocused();

await page.getByRole("button", { name: "Skip for now", exact: true }).focus();
await page.keyboard.press("Enter");
const requestHeading = page.getByRole("heading", { name: "Make your first API request.", exact: true });
await expect(requestHeading).toBeFocused();
await page.keyboard.press("Tab");
await expect(page.getByLabel("Agent name", { exact: true })).toBeFocused();

const machineChapter = page.getByRole("button", { name: /Your machines/ });
await machineChapter.focus();
await page.keyboard.press("Enter");
await expect(machineHeading).toBeVisible();
await expect(machineChapter).toBeFocused();
await page.getByRole("button", { name: "Continue to the API", exact: true }).focus();
await page.keyboard.press("Enter");
await expect(requestHeading).toBeFocused();

await page.getByRole("button", { name: "Back", exact: true }).focus();
await page.keyboard.press("Enter");
await expect(machineHeading).toBeFocused();
await page.getByRole("button", { name: "Back", exact: true }).focus();
await page.keyboard.press("Enter");
await expect(accessHeading).toBeFocused();
});

test("does not move retained navigation focus when polling discovers a newly created Agent", async ({ page, request }) => {
await page.goto("/");
const chapter = page.getByRole("button", { name: /Your first Agent/ });
await expect(chapter).toBeEnabled();
await chapter.focus();
await page.keyboard.press("Enter");
await expect(chapter).toBeFocused();
await page.getByLabel("Set a model provider for this Agent", { exact: true }).uncheck();
const code = await page.locator(".first-request-code pre").innerText();
const marker = code.match(/"core_home_example": "([^"]+)"/)?.[1];
expect(marker).toBeTruthy();
await chapter.focus();
const created = await request.post(`${fixtureBaseUrl}/v1/agents`, {
headers: { "OpenAI-Beta": "agents=v1" },
data: { name: "Keyboard Agent", model: "fixture/keyboard-model", metadata: { core_home_example: marker } },
});
expect(created.status()).toBe(201);
const agent = await created.json() as { id: string };
await expect(page.locator(".first-agent-card")).toContainText(agent.id);
await expect(chapter).toBeFocused();
await expect(page.getByRole("heading", { name: "Make your first API request.", exact: true })).not.toBeFocused();
});
45 changes: 45 additions & 0 deletions apps/web/e2e/first-run-recovery.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { expect, test } from "@playwright/test";

const fixtureBaseUrl = `http://127.0.0.1:${process.env.AGENTS_FIXTURE_PORT ?? 18092}`;

test("allows editing a copied request without sending another write or changing its marker", async ({ page, request }) => {
expect((await request.post(`${fixtureBaseUrl}/__fixture/reset`)).ok()).toBe(true);
await page.route("**/console/auth", (route) => route.fulfill({ json: { mode: "authenticated", username: "recovery-admin" } }));
await page.addInitScript(() => {
localStorage.setItem("agents-core-web.locale", "en");
localStorage.setItem(`parsar.introduction.v1:${location.origin}:recovery-admin`, JSON.stringify({ step: 2, dismissed: false }));
});
await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);
await page.goto("/");
await page.getByLabel("Agent name", { exact: true }).fill("Original external request");
await page.getByLabel("Provider base URL", { exact: true }).fill("https://models.example/v1");
await page.getByLabel("Core API base URL", { exact: true }).fill(`${fixtureBaseUrl}/v1`);
const firstCode = await page.locator(".first-request-code pre").innerText();
const marker = firstCode.match(/"core_home_example": "([^"]+)"/)?.[1];
expect(marker).toBeTruthy();
await page.getByRole("button", { name: "Copy request", exact: true }).click();
await expect(page.getByText("Waiting for your request…", { exact: true })).toBeVisible();
await expect(page.getByLabel("Agent name", { exact: true })).toBeEnabled();
await page.getByLabel("Agent name", { exact: true }).fill("Corrected external request");
await page.getByLabel("Model ID", { exact: true }).fill("fixture/corrected-model");
await page.getByLabel(/^Model API key/).fill("fixture-memory-only-key");
await expect(page.getByRole("button", { name: "Run here", exact: true })).toBeDisabled();
await page.getByRole("button", { name: "Copy request", exact: true }).click();
const correctedCode = await page.evaluate(() => navigator.clipboard.readText());
expect(correctedCode).toContain('"name": "Corrected external request"');
expect(correctedCode).toContain('"model": "fixture/corrected-model"');
expect(correctedCode.match(/"core_home_example": "([^"]+)"/)?.[1]).toBe(marker);
expect(correctedCode).not.toContain("fixture-memory-only-key");
const before = await (await request.get(`${fixtureBaseUrl}/__fixture/requests`)).json() as Array<{ method: string; path: string }>;
expect(before.filter((entry) => entry.method === "POST" && entry.path === "/v1/agents")).toHaveLength(0);
const response = await request.post(`${fixtureBaseUrl}/v1/agents`, {
headers: { "OpenAI-Beta": "agents=v1" },
data: { name: "Corrected external request", model: "fixture/corrected-model", metadata: { core_home_example: marker } },
});
expect(response.status()).toBe(201);
const agent = await response.json() as { id: string };
await page.getByRole("button", { name: "Check result", exact: true }).click();
await expect(page.locator(".first-agent-card")).toContainText(agent.id);
await expect(page.locator(".first-agent-card")).toContainText("Corrected external request");
await expect(page.getByLabel("Agent name", { exact: true })).toBeDisabled();
});
Loading
Loading