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
70 changes: 37 additions & 33 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ Reuse existing resource deletion and serialization code. Cross-Project copying w
removed. `admin_resource_owners` alone feeds the historical `source:"admin_copy"`
provenance read; `admin_asset_copies` is retained only because tables are not
dropped. Unknown historical provenance remains unknown. No secrets or request
bodies enter logs. A forwarded console actor name is only a label, never an
authorization input.
bodies enter logs. The console's fixed actor label (`console`) is only an audit
display label, never an authorization input.

When requirements conflict, object ownership is unclear, or a design would need
parallel compatibility paths, raise the issue with a concrete recommendation and
Expand Down Expand Up @@ -731,10 +731,10 @@ does not qualify its isolation or enable public creation.
### Hosted sandbox nodes and optional suspension

Default installation includes Core, Web and PostgreSQL but no execution node.
It always creates a separate deployment administrator credential. Core receives
only its digest; the paired console server receives the private token and injects
it only on approved management routes after console login and same-origin checks.
The browser never receives that token. Node and daemon connections use `/api/v1`
It always creates the Core key. Core receives only its digest; the paired console
server receives the private key, uses it for sign-in and injects it only on
approved management routes after console login and same-origin checks. The
browser never receives that key. Node and daemon connections use `/api/v1`
with their own credentials; the reverse proxy sends them directly to Core, never
through Web. Zero-node Core receives neither the Docker socket nor KVM.
The Web and deployment administrator API select one provider, public Core origin,
Expand Down Expand Up @@ -825,7 +825,7 @@ as a user service; it performs no SSH installation, Session creation or model ca

The [Hosted Sandbox Manager](services/agents-api/HOSTED-SANDBOX-MANAGER.md) is a
deployment-level admin surface, separate from Project credentials. The paired
console's administrator token stays on its server. Enrollment credentials authorize
console's Core key stays on its server. Enrollment credentials authorize
initial node configuration reads and registration; durable node credentials authorize
retained configuration reads and node transport. Project keys can read a narrow node
directory and their own Session placement, never global allocations.
Expand Down Expand Up @@ -1830,7 +1830,7 @@ installer. Its initial default is 2 CPUs and 4096 MiB; microsandbox also request
installer defaults. Node configuration and identity live under
`~/.parsar/nodes/<installation-id>/`; microsandbox uses its separate short private
Runtime home. Zero-node installs create no node identity state but retain the paired
administrator credential for first setup.
Core key for first setup.

One Runtime image contains the existing daemon, shared helpers and three native
harness packages. Their differences remain in the adapters. Core keeps exclusive
Expand All @@ -1843,21 +1843,22 @@ 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 proxies only finite
administrator and sandbox-management routes after console login. It requires a
private `CORE_CONSOLE_ADMIN_TOKEN_FILE` and holds no project caller credential.
Every `/v1` and `/api/v1` request returns 404, including explicit Bearer and
WebSocket requests; Web forwards no node or daemon transport. The installer
mounts only the administrator key into Web and only its digest into Core. The
browser receives safe capability flags, never that key. The deployment's TLS
reverse proxy routes `/v1` (applications) and `/api/v1` (nodes and Runtime
daemons, with their own credentials) directly to Core and everything else to Web,
except the Project-authenticated `/core/v1/environments/*/executor-credentials`
routes, which also go straight to Core.
administrator and sandbox-management routes after console login. It requires the
private Core key file named by `CORE_CONSOLE_CORE_KEY_FILE` and holds no project
caller credential. Every `/v1` and `/api/v1` request returns 404, including
explicit Bearer and WebSocket requests; Web forwards no node or daemon transport.
The installer mounts only the Core key into Web and only its digest
(`AGENTS_API_CORE_KEY_DIGESTS_FILE`) into Core. The browser receives safe
configuration, never that key. The deployment's TLS reverse proxy routes `/v1`
(applications) and `/api/v1` (nodes and Runtime daemons, with their own
credentials) directly to Core and everything else to Web, except the
Project-authenticated `/core/v1/environments/*/executor-credentials` routes, which
also go straight to Core.
Nodes and Core come from one distribution; older nodes using the removed
`/core/v1/sandbox` node paths cannot connect and are replaced through the drained
upgrade and re-enrollment workflow.

The Web manager offers no manual administrator-key fallback. A console without
The Web manager offers no manual Core key entry outside sign-in. A console without
paired management configuration shows setup guidance; direct remote project API
connections do not silently administer the console's configured deployment.
Chinese/English sandbox text, status and diagnostic formatting live in the shared
Expand All @@ -1883,20 +1884,23 @@ verification, and passes the enrollment credential only to the installer process
Management 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. 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.

Account mode is explicit (`CORE_CONSOLE_AUTH_MODE=account`) and requires a private
writable state directory. The first visitor registers the sole administrator with
a username and password; no initialization key is required. The atomic durable
account record stores a password hash; corruption or missing registered account
state 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 static node
installation payload. Authentication requests use same-origin JSON POSTs with
bounded bodies and bounded password-hash work.
or execution loop. Signing in with the Core key grants the complete console
surface; do not introduce Web accounts, roles, invitations or per-project Web
identities. Agent API caller keys remain independent of the Core key and cookie.

Web signs in only with the Core key (`POST /console/auth/login` with
`{"core_key":"…"}`), compared in constant time with the console's configured key
and never logged or echoed. There are no accounts, passwords, first-run setup or
Basic authentication; `CORE_CONSOLE_AUTH_MODE`, `CORE_CONSOLE_STATE_DIR`,
`CORE_CONSOLE_PASSWORD_FILE` and the renamed `CORE_CONSOLE_ADMIN_TOKEN_FILE` fail
startup. Cookie sessions are in memory, bounded, HttpOnly, SameSite Strict and
Secure for HTTPS origins; a restart or Core key rotation requires sign-in again.
Unauthenticated access is limited to the static login UI, finite console
authentication routes and the static node installation payload. Sign-in uses
same-origin JSON POSTs with bounded bodies and bounded concurrent work. Only
failed attempts are rate limited, so the correct key always signs in; Web and the
installer therefore require Core keys of at least 32 characters. See the
[Core key operations guide](docs/getting-started/operations.md#core-key).

Projects and application API keys live in Core PostgreSQL. Project creation owns
its scope and shared principal; key issuance, revocation and Project archive share
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ existing Sessions retain their node across disconnects and resume.
current management changes. See the [installation guide](docs/getting-started/install.md)
for obtaining/building a matching bundle and the host/network prerequisites.
2. **Sign in to Web.** Open the console address printed by the installer and
register your administrator account with a username and password. Keep them safe.
Existing installations retain their `admin` / `console.password` login.
The console connects to Core automatically. Create a Project in Web, then
sign in with the [Core key](docs/getting-started/operations.md#core-key) from
`~/.parsar/core/admin/core.key`. Keep it private. The console connects to Core
automatically. Create a Project in Web, then
issue a key within it for your application. Save the one-time plaintext
response privately; Core stores its digest. Rotate by issuing another key in the
same Project and revoking the old one.
Expand Down
10 changes: 7 additions & 3 deletions apps/web/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -580,9 +580,13 @@ the Parsar mark on a tile with a travelling border beam, and two orbits of
Agents, Sessions, Skills, Vaults, files, templates and machines around it; the
Parsar mark is itself nodes on a ring. Brand copy sits bottom-left in solid
ink; it is a paragraph, not a heading, because the panel's title names the task.
First run is three steps with numbered progress in the panel header: the
administrator account, the first project and its key (shown once; the example
request types itself into a terminal), and a three-chapter tour — Monitor,
Signing in asks for one thing, the deployment's Core key, in a single password
field; where the key is kept and what it can do sit behind a help tip beside the
label, and a refused key, too many attempts or an unavailable console is an error
beside the field. When no project exists yet, first run follows on the same stage
in two steps with numbered progress in the panel header: the first project and its
project API key (shown once; the example request types itself into a terminal),
and a three-chapter tour — Monitor,
Resources, Platform — whose stage shows a real dark screenshot of those pages,
tilted towards the panel. Steps change inside a View Transition: the stage holds
still while the panel slides and blurs across; opening the console dissolves
Expand Down
28 changes: 17 additions & 11 deletions apps/web/PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,17 @@ workbench.

## Operating Context

- Paired console (`services/core-console`): a single local administrator account
(legacy installations keep Basic authentication). The browser holds only the
console sign-in; the console server holds the deployment administrator
credential and forwards the Web API (`/core/v1/admin/**`) and sandbox
administration (`/core/v1/sandbox/**`). The console never calls `/v1`.
- The console account is not an Agents API identity. An administrator who wants to
call the Agents API issues a key in a project like any other caller.
- Paired console (`services/core-console`): the administrator signs in with the
deployment's Core key, the administration credential the installer writes to
`admin/core.key` under the installation directory (by default
`~/.parsar/core/admin/core.key`; keeping and rotating it is described in
[Core key](../../docs/getting-started/operations.md#core-key)). There are no
console accounts or usernames. The browser sends the key only to sign in and
keeps only the session cookie; the console server holds the Core key and forwards
the Web API (`/core/v1/admin/**`) and sandbox administration
(`/core/v1/sandbox/**`). The console never calls `/v1`.
- The Core key is not an Agents API identity and cannot call `/v1`. An administrator
who wants to call the Agents API issues a project API key like any other caller.
- `/console/config` reports whether sandbox administration is available; without
it the Nodes page explains that it is not configured and the fleet figures show
as unavailable.
Expand Down Expand Up @@ -85,13 +89,15 @@ workbench.
many sleep (Core's retained minus active) on the Nodes list, a node's page, Sandbox
metrics and Overview; a node's allocations show how long each has been suspended and
about when Core reclaims it. Docker never suspends and shows none of it.
- **First run**: after the administrator account is created and while no project
exists, full-screen steps outside the shell create the first project (default name
`Default`) and its first key, show the plaintext once with an example request,
- **First run**: after signing in with the Core key while no project exists,
full-screen steps outside the shell create the first project (default name
`Default`) and its first project API key, show the plaintext once with an example request,
then give a three-chapter tour of the console (Monitor, Resources, Platform)
before opening it. Signing in uses the same stage.
- Terminology: API terms stay in English in the Chinese UI (Agent, Session, Turn,
Skill, Vault, Credential, API key).
Skill, Vault, Credential, API key). The sign-in credential is the Core key
("Core Key"); keys issued in a project for applications are project API keys
("项目 API Key").

## Capabilities and Constraints

Expand Down
3 changes: 2 additions & 1 deletion apps/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ described in [DESIGN.md](DESIGN.md) and its product scope in [PRODUCT.md](PRODUC

Browser management requests use same-origin `/core/v1/admin` through `AdminClient`,
plus the existing sandbox management client for allowed `/core/v1/sandbox` routes.
The console authenticates the browser and keeps the deployment credential server-side.
The administrator signs in with the deployment's Core key; the console keeps the key
server-side and gives the browser only a session cookie.
Applications use their own Project keys directly against Core's public `/v1` API.
The production console returns 404 for `/v1`, even with an explicit Bearer token.

Expand Down
57 changes: 32 additions & 25 deletions apps/web/e2e/access.spec.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,48 @@
import { expect, test } from "@playwright/test";
import { expect, test, type Page } from "@playwright/test";

import { expectManagementBoundary, resetFixture } from "./console";
import { expectManagementBoundary, FIXTURE_CORE_KEY, resetFixture } from "./console";

test.afterEach(async ({ request }) => expectManagementBoundary(request));

test("creates the administrator, keeps no credential in the browser, and signs out and back in", async ({ page, request }) => {
await resetFixture(request, "setup");
async function signIn(page: Page, coreKey: string) {
await page.getByLabel("Core key", { exact: true }).fill(coreKey);
await page.getByRole("button", { name: "Sign in" }).click();
}

const browserStorage = (page: Page) => page.evaluate(() => JSON.stringify({ ...window.localStorage, ...window.sessionStorage }));

test("signs in with the Core key, keeps it out of the browser, and signs out and back in", async ({ page, request }) => {
await resetFixture(request, "login");
await page.addInitScript(() => window.localStorage.setItem("agents-core-web.language", "en"));
await page.goto("/");

await expect(page.getByRole("heading", { name: "Create your administrator account" })).toBeVisible();
await page.locator("input[name=username]").fill("admin");
await page.locator("input[name=password]").fill("correct horse battery");
await page.locator("input[name=confirm]").fill("correct horse battery");
await page.getByRole("button", { name: "Create administrator account" }).click();
await expect(page.getByRole("heading", { name: "Sign in to Parsar Core" })).toBeVisible();
await signIn(page, "not-the-core-key");
await expect(page.getByRole("alert")).toHaveText("This Core key is not correct. Check it and try again.");
await signIn(page, FIXTURE_CORE_KEY);
await expect(page.getByRole("heading", { name: "Overview", level: 1 })).toBeVisible();
expect(await browserStorage(page)).not.toContain(FIXTURE_CORE_KEY);

const stored = await page.evaluate(() => JSON.stringify({ ...window.localStorage, ...window.sessionStorage }));
expect(stored).not.toContain("correct horse battery");
await page.getByRole("button", { name: "Sign out" }).click();
await expect(page.getByRole("heading", { name: "Sign in to Parsar Core" })).toBeVisible();
await signIn(page, FIXTURE_CORE_KEY);
await expect(page.getByRole("heading", { name: "Overview", level: 1 })).toBeVisible();

// Repeated wrong keys: the console says how long to wait, from Retry-After.
await page.getByRole("button", { name: "Sign out" }).click();
await expect(page.getByRole("heading", { name: "Welcome back" })).toBeVisible();
await page.locator("input[name=username]").fill("admin");
await page.locator("input[name=password]").fill("wrong password!!");
await page.getByRole("button", { name: "Sign in" }).click();
await expect(page.getByRole("alert")).toBeVisible();
await page.locator("input[name=password]").fill("correct horse battery");
await page.getByRole("button", { name: "Sign in" }).click();
for (let attempt = 0; attempt < 3; attempt++) await signIn(page, "not-the-core-key");
await expect(page.getByRole("alert")).toHaveText("Too many attempts. Try again in 30 seconds.");
// Failed attempts never lock out the right key.
await signIn(page, FIXTURE_CORE_KEY);
await expect(page.getByRole("heading", { name: "Overview", level: 1 })).toBeVisible();
expect(await browserStorage(page)).not.toContain(FIXTURE_CORE_KEY);
});

test("sets up a fresh install: first project, a key shown once, the tour, then the console", async ({ page, request }) => {
await resetFixture(request, "setup", { fresh: true });
test("sets up a fresh install: Core key sign-in, first project, a key shown once, the tour, then the console", async ({ page, request }) => {
await resetFixture(request, "login", { fresh: true });
await page.addInitScript(() => window.localStorage.setItem("agents-core-web.language", "en"));
await page.goto("/");
await page.locator("input[name=username]").fill("admin");
await page.locator("input[name=password]").fill("correct horse battery");
await page.locator("input[name=confirm]").fill("correct horse battery");
await page.getByRole("button", { name: "Create administrator account" }).click();
await signIn(page, FIXTURE_CORE_KEY);

await expect(page.getByRole("heading", { name: "Create your first project" })).toBeVisible();
await page.locator("input[name=key-name]").fill("my-app");
Expand All @@ -49,5 +54,7 @@ test("sets up a fresh install: first project, a key shown once, the tour, then t
await expect(page.getByRole("heading", { name: "Is it healthy, and where does it fail?" })).toBeVisible();
await page.getByRole("button", { name: "Skip" }).click();
await expect(page.getByRole("heading", { name: "Overview", level: 1 })).toBeVisible();
expect(await page.evaluate(() => JSON.stringify({ ...window.localStorage, ...window.sessionStorage }))).not.toContain("fixture-secret");
const stored = await browserStorage(page);
expect(stored).not.toContain("fixture-secret");
expect(stored).not.toContain(FIXTURE_CORE_KEY);
});
Loading
Loading