Skip to content

Commit a6cbbda

Browse files
committed
Merge origin/main (Core-key sign-in #124, allocation phase time #123, #125)
Keep #124's Core-key sign-in, fixed console actor label and configuration names; the console still forwards every signed-in /core/v1 request by the prefix rule instead of the removed allowlists. Installer, console tests and docs take #124's wording with /core/v1 paths in place of /core/v1/admin. #123's allocation test uses automatic placement, since P4 removed explicit node selection.
2 parents 4900b8c + 44d0caa commit a6cbbda

96 files changed

Lines changed: 1286 additions & 1234 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CONTRIBUTING.md‎

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ Reuse existing resource deletion and serialization code. Cross-Project copying w
5454
removed. `admin_resource_owners` alone feeds the historical `source:"admin_copy"`
5555
provenance read; `admin_asset_copies` is retained only because tables are not
5656
dropped. Unknown historical provenance remains unknown. No secrets or request
57-
bodies enter logs. A forwarded console actor name is only a label, never an
58-
authorization input.
57+
bodies enter logs. The console's fixed actor label (`console`) is only an audit
58+
display label, never an authorization input.
5959

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

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

827827
The [Hosted Sandbox Manager](services/agents-api/HOSTED-SANDBOX-MANAGER.md) is a
828828
deployment-level admin surface, separate from Project credentials. The paired
829-
console's administrator token stays on its server. Enrollment credentials authorize
829+
console's Core key stays on its server. Enrollment credentials authorize
830830
initial node configuration reads and registration; durable node credentials authorize
831831
retained configuration reads and node transport. Project keys cannot read nodes,
832832
placement or allocations.
@@ -1833,7 +1833,7 @@ installer. Its initial default is 2 CPUs and 4096 MiB; microsandbox also request
18331833
installer defaults. Node configuration and identity live under
18341834
`~/.parsar/nodes/<installation-id>/`; microsandbox uses its separate short private
18351835
Runtime home. Zero-node installs create no node identity state but retain the paired
1836-
administrator credential for first setup.
1836+
Core key for first setup.
18371837

18381838
One Runtime image contains the existing daemon, shared helpers and three native
18391839
harness packages. Their differences remain in the adapters. Core keeps exclusive
@@ -1847,21 +1847,21 @@ must not change on a repeated install.
18471847

18481848
`services/core-console` serves the production Web build and, after console login
18491849
and same-origin checks, forwards every `/core/v1` request with the Core key; Core
1850-
decides whether the route exists. It requires a
1851-
private `CORE_CONSOLE_ADMIN_TOKEN_FILE` and holds no project caller credential.
1852-
Every `/v1` and `/api/v1` request returns 404, including explicit Bearer and
1853-
WebSocket requests; Web forwards no node or daemon transport. The installer
1854-
mounts only the administrator key into Web and only its digest into Core. The
1855-
browser receives safe capability flags, never that key. The deployment's TLS
1856-
reverse proxy routes `/v1` (applications) and `/api/v1` (nodes and Runtime
1850+
decides whether the route exists. It requires the private Core key file named by
1851+
`CORE_CONSOLE_CORE_KEY_FILE` and holds no project caller credential. Every `/v1`
1852+
and `/api/v1` request returns 404, including explicit Bearer and WebSocket
1853+
requests; Web forwards no node or daemon transport. The installer mounts only the
1854+
Core key into Web and only its digest (`AGENTS_API_CORE_KEY_DIGESTS_FILE`) into
1855+
Core. The browser receives safe configuration, never that key. The deployment's
1856+
TLS reverse proxy routes `/v1` (applications) and `/api/v1` (nodes and Runtime
18571857
daemons, with their own credentials) directly to Core and everything else,
18581858
including `/core/v1`, to Web. Operator scripts call `/core/v1` on Core's loopback
18591859
port.
18601860
Nodes and Core come from one distribution; older nodes using the removed
18611861
`/core/v1/sandbox` node paths cannot connect and are replaced through the drained
18621862
upgrade and re-enrollment workflow.
18631863

1864-
The Web manager offers no manual administrator-key fallback. A console without
1864+
The Web manager offers no manual Core key entry outside sign-in. A console without
18651865
paired management configuration shows setup guidance; direct remote project API
18661866
connections do not silently administer the console's configured deployment.
18671867
Chinese/English sandbox text, status and diagnostic formatting live in the shared
@@ -1888,20 +1888,23 @@ The `/core/v1` proxy retains fixed-origin, cross-site, safe-path, redirect and U
18881888
restrictions through the standard Go reverse proxy with streaming/cancellation;
18891889
literal or encoded dot segments can never move a request out of `/core/v1`.
18901890
The console implements no product identity, resource semantics, Runtime discovery
1891-
or execution loop. Its local administrator account grants the complete console
1892-
surface; do not introduce Web roles, invitations or per-project Web identities.
1893-
Agent API caller keys remain independent of the administrator password and cookie.
1894-
1895-
Account mode is explicit (`CORE_CONSOLE_AUTH_MODE=account`) and requires a private
1896-
writable state directory. The first visitor registers the sole administrator with
1897-
a username and password; no initialization key is required. The atomic durable
1898-
account record stores a password hash; corruption or missing registered account
1899-
state must never reopen registration. Account creation is race-safe. Cookie sessions are
1900-
bounded, HttpOnly, SameSite Strict and Secure for HTTPS origins; a restart requires
1901-
sign-in again, without deleting the account. Unauthenticated access is limited to
1902-
the static login UI, finite console authentication routes and the static node
1903-
installation payload. Authentication requests use same-origin JSON POSTs with
1904-
bounded bodies and bounded password-hash work.
1891+
or execution loop. Signing in with the Core key grants the complete console
1892+
surface; do not introduce Web accounts, roles, invitations or per-project Web
1893+
identities. Agent API caller keys remain independent of the Core key and cookie.
1894+
1895+
Web signs in only with the Core key (`POST /console/auth/login` with
1896+
`{"core_key":"…"}`), compared in constant time with the console's configured key
1897+
and never logged or echoed. There are no accounts, passwords, first-run setup or
1898+
Basic authentication; `CORE_CONSOLE_AUTH_MODE`, `CORE_CONSOLE_STATE_DIR`,
1899+
`CORE_CONSOLE_PASSWORD_FILE` and the renamed `CORE_CONSOLE_ADMIN_TOKEN_FILE` fail
1900+
startup. Cookie sessions are in memory, bounded, HttpOnly, SameSite Strict and
1901+
Secure for HTTPS origins; a restart or Core key rotation requires sign-in again.
1902+
Unauthenticated access is limited to the static login UI, finite console
1903+
authentication routes and the static node installation payload. Sign-in uses
1904+
same-origin JSON POSTs with bounded bodies and bounded concurrent work. Only
1905+
failed attempts are rate limited, so the correct key always signs in; Web and the
1906+
installer therefore require Core keys of at least 32 characters. See the
1907+
[Core key operations guide](docs/getting-started/operations.md#core-key).
19051908

19061909
Projects and application API keys live in Core PostgreSQL. Project creation owns
19071910
its scope and shared principal; key issuance, revocation and Project archive share

‎README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ disconnects and resume.
3838
current management changes. See the [installation guide](docs/getting-started/install.md)
3939
for obtaining/building a matching bundle and the host/network prerequisites.
4040
2. **Sign in to Web.** Open the console address printed by the installer and
41-
register your administrator account with a username and password. Keep them safe.
42-
Existing installations retain their `admin` / `console.password` login.
43-
The console connects to Core automatically. Create a Project in Web, then
41+
sign in with the [Core key](docs/getting-started/operations.md#core-key) from
42+
`~/.parsar/core/admin/core.key`. Keep it private. The console connects to Core
43+
automatically. Create a Project in Web, then
4444
issue a key within it for your application. Save the one-time plaintext
4545
response privately; Core stores its digest. Rotate by issuing another key in the
4646
same Project and revoking the old one.

‎apps/web/DESIGN.md‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -580,9 +580,13 @@ the Parsar mark on a tile with a travelling border beam, and two orbits of
580580
Agents, Sessions, Skills, Vaults, files, templates and machines around it; the
581581
Parsar mark is itself nodes on a ring. Brand copy sits bottom-left in solid
582582
ink; it is a paragraph, not a heading, because the panel's title names the task.
583-
First run is three steps with numbered progress in the panel header: the
584-
administrator account, the first project and its key (shown once; the example
585-
request types itself into a terminal), and a three-chapter tour — Monitor,
583+
Signing in asks for one thing, the deployment's Core key, in a single password
584+
field; where the key is kept and what it can do sit behind a help tip beside the
585+
label, and a refused key, too many attempts or an unavailable console is an error
586+
beside the field. When no project exists yet, first run follows on the same stage
587+
in two steps with numbered progress in the panel header: the first project and its
588+
project API key (shown once; the example request types itself into a terminal),
589+
and a three-chapter tour — Monitor,
586590
Resources, Platform — whose stage shows a real dark screenshot of those pages,
587591
tilted towards the panel. Steps change inside a View Transition: the stage holds
588592
still while the panel slides and blurs across; opening the console dissolves

‎apps/web/PRODUCT.md‎

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,17 @@ workbench.
4040

4141
## Operating Context
4242

43-
- Paired console (`services/core-console`): a single local administrator account
44-
(legacy installations keep Basic authentication). The browser holds only the
45-
console sign-in; the console server holds the deployment administrator
46-
credential and forwards the Web API (`/core/v1/admin/**`) and sandbox
47-
administration (`/core/v1/sandbox/**`). The console never calls `/v1`.
48-
- The console account is not an Agents API identity. An administrator who wants to
49-
call the Agents API issues a key in a project like any other caller.
43+
- Paired console (`services/core-console`): the administrator signs in with the
44+
deployment's Core key, the administration credential the installer writes to
45+
`admin/core.key` under the installation directory (by default
46+
`~/.parsar/core/admin/core.key`; keeping and rotating it is described in
47+
[Core key](../../docs/getting-started/operations.md#core-key)). There are no
48+
console accounts or usernames. The browser sends the key only to sign in and
49+
keeps only the session cookie; the console server holds the Core key and forwards
50+
the Web API (`/core/v1/admin/**`) and sandbox administration
51+
(`/core/v1/sandbox/**`). The console never calls `/v1`.
52+
- The Core key is not an Agents API identity and cannot call `/v1`. An administrator
53+
who wants to call the Agents API issues a project API key like any other caller.
5054
- `/console/config` reports whether sandbox administration is available; without
5155
it the Nodes page explains that it is not configured and the fleet figures show
5256
as unavailable.
@@ -83,14 +87,17 @@ workbench.
8387
or Add node action; a sandbox's dialog adds its disk use.
8488
- **microsandbox** suspends idle sandboxes into snapshots, so its nodes show how
8589
many sleep (Core's retained minus active) on the Nodes list, a node's page, Sandbox
86-
metrics and Overview; Docker never suspends and shows none of it.
87-
- **First run**: after the administrator account is created and while no project
88-
exists, full-screen steps outside the shell create the first project (default name
89-
`Default`) and its first key, show the plaintext once with an example request,
90+
metrics and Overview; a node's allocations show how long each has been suspended and
91+
about when Core reclaims it. Docker never suspends and shows none of it.
92+
- **First run**: after signing in with the Core key while no project exists,
93+
full-screen steps outside the shell create the first project (default name
94+
`Default`) and its first project API key, show the plaintext once with an example request,
9095
then give a three-chapter tour of the console (Monitor, Resources, Platform)
9196
before opening it. Signing in uses the same stage.
9297
- Terminology: API terms stay in English in the Chinese UI (Agent, Session, Turn,
93-
Skill, Vault, Credential, API key).
98+
Skill, Vault, Credential, API key). The sign-in credential is the Core key
99+
("Core Key"); keys issued in a project for applications are project API keys
100+
("项目 API Key").
94101

95102
## Capabilities and Constraints
96103

‎apps/web/README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ described in [DESIGN.md](DESIGN.md) and its product scope in [PRODUCT.md](PRODUC
99

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

‎apps/web/e2e/access.spec.ts‎

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,48 @@
1-
import { expect, test } from "@playwright/test";
1+
import { expect, test, type Page } from "@playwright/test";
22

3-
import { expectManagementBoundary, resetFixture } from "./console";
3+
import { expectManagementBoundary, FIXTURE_CORE_KEY, resetFixture } from "./console";
44

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

7-
test("creates the administrator, keeps no credential in the browser, and signs out and back in", async ({ page, request }) => {
8-
await resetFixture(request, "setup");
7+
async function signIn(page: Page, coreKey: string) {
8+
await page.getByLabel("Core key", { exact: true }).fill(coreKey);
9+
await page.getByRole("button", { name: "Sign in" }).click();
10+
}
11+
12+
const browserStorage = (page: Page) => page.evaluate(() => JSON.stringify({ ...window.localStorage, ...window.sessionStorage }));
13+
14+
test("signs in with the Core key, keeps it out of the browser, and signs out and back in", async ({ page, request }) => {
15+
await resetFixture(request, "login");
916
await page.addInitScript(() => window.localStorage.setItem("agents-core-web.language", "en"));
1017
await page.goto("/");
1118

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

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

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

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

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

‎apps/web/e2e/console.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ import { expect, type APIRequestContext, type Page } from "@playwright/test";
33
const fixture = `http://127.0.0.1:${process.env.AGENTS_FIXTURE_PORT ?? 18092}`;
44
const web = `http://127.0.0.1:${process.env.AGENTS_WEB_PORT ?? 4174}`;
55

6-
/** Fresh fixture state: "setup" (no administrator yet), "login" or "authenticated". */
7-
export async function resetFixture(request: APIRequestContext, auth: "setup" | "login" | "authenticated" = "authenticated", options: { fresh?: boolean; sandbox?: "configured" | "none" | "e2b" } = {}) {
6+
/** The fixture deployment's Core key; the same value as CORE_KEY in fixture-console.mjs. */
7+
export const FIXTURE_CORE_KEY = "fixture-core-key-3f9a2c71";
8+
9+
/** Fresh fixture state: signed out ("login") or already signed in ("authenticated"). */
10+
export async function resetFixture(request: APIRequestContext, auth: "login" | "authenticated" = "authenticated", options: { fresh?: boolean; sandbox?: "configured" | "none" | "e2b" } = {}) {
811
await request.post(`${fixture}/__fixture/reset?auth=${auth}${options.fresh ? "&projects=none" : ""}&sandbox=${options.sandbox ?? "configured"}`);
912
}
1013

0 commit comments

Comments
 (0)