From 63d661b7fd749dc933624795e593f91fb5540707 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Fri, 25 Sep 2026 17:41:40 +0000 Subject: [PATCH 1/6] Print public addresses and Web next steps after install; match Web's Docker size The installer now prints the console address, the API base URL /v1 when a public URL is configured, and the loopback API labelled local-only. It points administrators to sign in to Web with the Core key file and create a Project and key on the Projects and keys page, and names the Nodes page instead of Hosted Sandbox Manager. A local Docker node now starts with Web's proposal of 2 CPUs and 2048 MiB. --- deploy/install/install.py | 27 ++++++++++++++++++++------- deploy/install/local_node.py | 5 +++-- deploy/install/test_install.py | 11 +++++++---- deploy/install/test_local_node.py | 23 +++++++++++++++-------- 4 files changed, 45 insertions(+), 21 deletions(-) diff --git a/deploy/install/install.py b/deploy/install/install.py index f8a19ca27..de2121409 100644 --- a/deploy/install/install.py +++ b/deploy/install/install.py @@ -375,26 +375,39 @@ def main(argv=None): raise InstallError("Core did not become healthy. Use --status; retained state has not been removed") if args.provider: local_node.install(root, dict(state, provider=args.provider), manifest, bundle, run) + public_url = state.get("public_url") if state["mode"] != "core-only": url = f'http://127.0.0.1:{state["web_port"]}' - host = urlsplit(state.get("public_url") or url).netloc + host = urlsplit(public_url or url).netloc if not wait_http(url + "/console/auth", {"Host": host}): raise InstallError("Web sign-in is unavailable. Use --status and inspect the Web service") core_url = state.get("core_url") or f'http://127.0.0.1:{state["core_port"]}' token = (root / "admin/core.key").read_text().strip() if not wait_http(core_url + "/core/v1/projects", {"Authorization": "Bearer " + token}): raise InstallError("Core key authentication failed. Inspect private configuration; no model was called") - print("Console: " + (state.get("public_url") or url)) - print("Sign in to Web with the Core key. Keep it private; it also authorizes the Core management API.") + # The console accepts only its configured origin, so a public URL has no loopback console. + print("Console: " + (public_url or url + " (local only)")) + if state["mode"] != "web-only": + api = f'http://127.0.0.1:{state["core_port"]}/v1' + if public_url: + print("API base URL: " + public_url + "/v1") + print("Local-only API on this host: " + api) + else: + print("API base URL: " + api + " (local only)") + core_key = root / "admin/core.key" + if state["mode"] == "core-only": + print(f"Next: create a Project and its API key through the Core management API (/core/v1) with the Core key in {core_key}.") + else: + print(f"Next: sign in to Web with the Core key in {core_key}, then create a Project and its API key on the Projects and keys page.") + print("Keep the Core key private; it also authorizes the Core management API.") if state["mode"] != "web-only": - print(f'API: http://127.0.0.1:{state["core_port"]}/v1') - print("Create a Project and issue its API key through the administrator API before calling the direct Core API.") print("Core configuration file: " + str(root / "config/core.env")) if args.provider: print("Provider: " + args.provider + ". Local node enrolled; Core provisions Sessions on demand.") + elif state["mode"] == "all": + print("No execution node was installed by this run. Choose a sandbox backend and add nodes on the Nodes page in Web.") else: - print("No execution node was installed by this run. Open Hosted Sandbox Manager to manage providers and nodes.") - print("Core key file: " + str(root / "admin/core.key")) + print("No execution node was installed by this run.") print("Services installed. No model request was made. See docs/getting-started/quickstart.md.") diff --git a/deploy/install/local_node.py b/deploy/install/local_node.py index 6809c2631..655c27390 100644 --- a/deploy/install/local_node.py +++ b/deploy/install/local_node.py @@ -43,9 +43,10 @@ def install(root, state, manifest, bundle, run): if current.get("installation_id") != state["installation_id"]: raise LocalNodeError("Core deployment identity differs; preserve its installation state") if not current.get("provider"): - resources = {"cpus": 2, "memory_mib": 4096} + # The same initial size that Web's sandbox setup proposes for this provider. + resources = {"cpus": 2, "memory_mib": 2048} if state["provider"] == "microsandbox": - resources.update(root_disk_mib=8192, environment_disk_mib=8192) + resources.update(memory_mib=4096, root_disk_mib=8192, environment_disk_mib=8192) current = request(core, admin, "POST", "deployment", { "provider": state["provider"], "core_url": state["public_url"], "resources": resources, "runtime": node_spec.release(manifest)}) diff --git a/deploy/install/test_install.py b/deploy/install/test_install.py index 0514409f2..73963e690 100644 --- a/deploy/install/test_install.py +++ b/deploy/install/test_install.py @@ -600,8 +600,10 @@ def test_local_provider_input_is_transient_and_independent_of_core_placement(sel mock.patch.object(install.native_service, "prepare"), \ mock.patch.object(install.native_service, "start"), \ mock.patch.object(install.local_node, "install") as enroll, \ - contextlib.redirect_stdout(io.StringIO()): + contextlib.redirect_stdout(io.StringIO()) as output: install.main([*flags, "--sandbox-provider", "true", "--provider", provider]) + self.assertIn("Console: https://core.example\nAPI base URL: https://core.example/v1\n" + "Local-only API on this host: http://127.0.0.1:8091/v1\n", output.getvalue()) enroll.assert_called_once() self.assertEqual(enroll.call_args.args[1]["provider"], provider) state = self.document("installation.json") @@ -665,8 +667,10 @@ def test_default_main_skips_kvm_native_service_and_runtime_import(self): self.assertIn("No execution node was installed by this run", output.getvalue()) self.assertIn("Core configuration file: " + str(self.root / "config/core.env"), output.getvalue()) self.assertNotIn((self.root / "config/database.password").read_text(), output.getvalue()) - self.assertIn("Sign in to Web with the Core key.", output.getvalue()) - self.assertIn("Core key file: " + str(self.root / "admin/core.key"), output.getvalue()) + self.assertIn("Console: http://127.0.0.1:8080 (local only)", output.getvalue()) + self.assertIn("API base URL: http://127.0.0.1:8091/v1 (local only)", output.getvalue()) + self.assertIn("sign in to Web with the Core key in " + str(self.root / "admin/core.key") + + ", then create a Project and its API key on the Projects and keys page", output.getvalue()) self.assertFalse((self.root / "config/keys.json").exists()) self.assertEqual([call.args[0] for call in health.call_args_list], [ "http://127.0.0.1:8091/healthz", "http://127.0.0.1:8080/console/auth", @@ -675,7 +679,6 @@ def test_default_main_skips_kvm_native_service_and_runtime_import(self): admin_token = (self.root / "admin/core.key").read_text() self.assertEqual(health.call_args_list[-1].args[1], {"Authorization": "Bearer " + admin_token}) self.assertNotIn(admin_token, output.getvalue()) - self.assertIn("Create a Project and issue its API key through the administrator API", output.getvalue()) def test_main_web_only_never_imports_runtime_or_leaks_administrator_token(self): source = self.administrator_file() diff --git a/deploy/install/test_local_node.py b/deploy/install/test_local_node.py index 089bce424..47ffa7330 100644 --- a/deploy/install/test_local_node.py +++ b/deploy/install/test_local_node.py @@ -28,14 +28,21 @@ def setUp(self): self.run = mock.Mock() def test_first_install_saves_spec_and_launches_ordinary_node_without_secret_arguments(self): - empty = dict(self.current, provider="") - with mock.patch.object(local_node, "request", side_effect=[empty, self.current, {"token": "one-time"}]) as request, \ - mock.patch.object(local_node.Path, "home", return_value=self.root): - local_node.install(self.root, self.state, self.manifest, self.root / "bundle", self.run) - self.assertEqual([(call.args[2], call.args[3]) for call in request.call_args_list], - [("GET", "deployment"), ("POST", "deployment"), ("POST", "enrollment-tokens")]) - setup = request.call_args_list[1].args[4] - self.assertEqual(setup["resources"], {"cpus": 2, "memory_mib": 4096, "root_disk_mib": 8192, "environment_disk_mib": 8192}) + # Web's sandbox setup proposes the same initial sizes. + for provider, resources in (("docker", {"cpus": 2, "memory_mib": 2048}), + ("microsandbox", {"cpus": 2, "memory_mib": 4096, "root_disk_mib": 8192, + "environment_disk_mib": 8192})): + with self.subTest(provider=provider): + self.run.reset_mock() + state, current = dict(self.state, provider=provider), dict(self.current, provider=provider) + empty = dict(current, provider="") + with mock.patch.object(local_node, "request", side_effect=[empty, current, {"token": "one-time"}]) as request, \ + mock.patch.object(local_node.Path, "home", return_value=self.root): + local_node.install(self.root, state, self.manifest, self.root / "bundle", self.run) + self.assertEqual([(call.args[2], call.args[3]) for call in request.call_args_list], + [("GET", "deployment"), ("POST", "deployment"), ("POST", "enrollment-tokens")]) + setup = request.call_args_list[1].args[4] + self.assertEqual(setup["resources"], resources) self.assertEqual(setup["runtime"]["image_id"], self.manifest["images"]["runtime"]) self.assertNotIn("one-time", str(self.run.call_args.args)) self.assertEqual(self.run.call_args.kwargs["env"]["PARSAR_NODE_ENROLLMENT_TOKEN"], "one-time") From 6a4bd1b0f7bcaf2b790b7c621f03227bc994e4e1 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Fri, 25 Sep 2026 17:41:40 +0000 Subject: [PATCH 2/6] Build distributions on Docker's containerd image store BuildKit's --iidfile reports the image config digest, which is Docker's image ID only in the classic store. The containerd store (Docker 29's default) names images by manifest digest and cannot resolve the config digest, and its default provenance attestations wrap each image in a two-manifest index that the archive verifier rejects. Builds now disable provenance attestations and record, from BuildKit's build metadata, the digest the local store resolves. The manifest still records the real config and OCI manifest digests from the exported archives. --- scripts/build-core-distribution.sh | 27 ++++++++++++++-------- scripts/core-distribution-manifest.py | 25 ++++++++++++++++++-- scripts/core-distribution-manifest.test.py | 23 ++++++++++++++++++ 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/scripts/build-core-distribution.sh b/scripts/build-core-distribution.sh index 3a89ab7fa..d6daabc79 100755 --- a/scripts/build-core-distribution.sh +++ b/scripts/build-core-distribution.sh @@ -44,15 +44,28 @@ case "$build_network" in default|host|none) ;; *) printf 'CORE_DISTRIBUTION_BUILD_NETWORK must be default, host, or none\n' >&2; exit 1 ;; esac +# Build one Linux amd64 image and write the ID the local image store gives it to +# $stage/NAME.id. BuildKit's --iidfile reports the config digest, which is the +# image ID only in Docker's classic store; the containerd store (Docker 29's +# default) uses the manifest digest and cannot resolve the config digest. The +# build metadata carries both, and built-image keeps the one the store resolves. +# Without provenance attestations each image is one platform manifest in both +# stores, as the archive verifier requires. build_image() { + local name="$1" + shift # Docker's predefined proxy arguments are build-only; no Dockerfile ARG or ENV # declaration persists the operator's network configuration in the images. - docker build --network "$build_network" \ + # The metadata file omits build provenance, so it does not record them either. + BUILDX_METADATA_PROVENANCE=disabled docker build --network "$build_network" \ + --platform linux/amd64 --provenance=false --metadata-file "$stage/$name.build.json" \ + --label "org.opencontainers.image.revision=$revision" \ --build-arg HTTP_PROXY --build-arg HTTPS_PROXY --build-arg ALL_PROXY --build-arg NO_PROXY \ --build-arg "http_proxy=${http_proxy:-${HTTP_PROXY:-}}" \ --build-arg "https_proxy=${https_proxy:-${HTTPS_PROXY:-}}" \ --build-arg "all_proxy=${all_proxy:-${ALL_PROXY:-}}" \ --build-arg "no_proxy=${no_proxy:-${NO_PROXY:-}}" "$@" + python3 scripts/core-distribution-manifest.py built-image "$stage/$name.build.json" > "$stage/$name.id" } require_clean_source() { @@ -127,8 +140,7 @@ tar -xzf "$stage/e2b-build/agents-api-e2b-provider-linux-amd64.tar.gz" \ --strip-components=1 -C "$stage/core/e2b" cp -R "$stage/core/e2b" "$bundle/native/e2b" cp deploy/distribution/Dockerfile "$stage/core/Dockerfile" -build_image --platform linux/amd64 --iidfile "$stage/core.id" \ - --label "org.opencontainers.image.revision=$revision" "$stage/core" +build_image core "$stage/core" core_image="$(cat "$stage/core.id")" # Fail at packaging time if the helper or runtime requires unavailable host libraries. docker run --rm --network none --entrypoint /bin/sh \ @@ -142,8 +154,7 @@ pnpm install --frozen-lockfile AGENTS_CORE_WEB_OPENAI_HOSTED_SESSIONS=1 AGENTS_CORE_WEB_ENVIRONMENT_FILES=1 pnpm build:web cp -R apps/web/dist "$stage/web/dist" cp services/core-console/Dockerfile "$stage/web/Dockerfile" -build_image --platform linux/amd64 --iidfile "$stage/web.id" \ - --label "org.opencontainers.image.revision=$revision" "$stage/web" +build_image web "$stage/web" export AGENTS_EXECUTOR_BUILD_DIR="$stage/helpers" scripts/build-agents-executor.sh @@ -167,8 +178,7 @@ else script="scripts/build-$harness-runtime.sh" if [[ "$harness" == codex ]]; then script=scripts/build-agents-runtime.sh; fi AGENTS_RUNTIME_BUILD_DIR="$stage/$harness" bash "$script" - build_image --platform linux/amd64 --iidfile "$stage/$harness.id" \ - --label "org.opencontainers.image.revision=$revision" "$stage/$harness" + build_image "$harness" "$stage/$harness" done codex_image="$(cat "$stage/codex.id")" claude_image="$(cat "$stage/claude.id")" @@ -186,8 +196,7 @@ for harness in codex claude mcode; do done mkdir "$stage/combined" cp deploy/distribution/Runtime.Dockerfile "$stage/combined/Dockerfile" -build_image --platform linux/amd64 --iidfile "$stage/runtime.id" \ - --label "org.opencontainers.image.revision=$revision" \ +build_image runtime \ --build-arg "CODEX_IMAGE=${image_tags[0]}" --build-arg "CLAUDE_IMAGE=${image_tags[1]}" \ --build-arg "MCODE_IMAGE=${image_tags[2]}" "$stage/combined" diff --git a/scripts/core-distribution-manifest.py b/scripts/core-distribution-manifest.py index 830a61fae..2002c091d 100644 --- a/scripts/core-distribution-manifest.py +++ b/scripts/core-distribution-manifest.py @@ -47,6 +47,27 @@ def verify_image(image): return details +def built_image(metadata_file): + """Print the local store ID of the image one BuildKit build just produced.""" + metadata = json.loads(pathlib.Path(metadata_file).read_text()) + # The classic store names an image by its config digest, the containerd store + # by its manifest digest; the other value never resolves to itself there. + config = metadata.get("containerimage.config.digest") + manifest = metadata.get("containerimage.digest", config) + if not all(isinstance(value, str) and DIGEST.fullmatch(value) for value in (config, manifest)): + raise ValueError("Build metadata lacks valid image digests") + resolved = [] + for candidate in dict.fromkeys((config, manifest)): + result = subprocess.run(["docker", "image", "inspect", "--format", "{{.Id}}", candidate], + stdin=subprocess.DEVNULL, capture_output=True, text=True, check=False) + if result.returncode == 0 and result.stdout.strip() == candidate: + resolved.append(candidate) + if len(resolved) != 1: + raise ValueError("The local image store does not identify the built image by exactly one of its digests") + verify_image(resolved[0]) + print(resolved[0]) + + def image_identities(archive, build_id): """Bind both Docker store identities to one exported Linux amd64 image.""" if not DIGEST.fullmatch(build_id): @@ -277,10 +298,10 @@ def archive(bundle, epoch, variant=""): if __name__ == "__main__": commands = {"extract-runtime": extract_runtime, "verify-runtime": verify_runtime, "verify-image": verify_image, - "manifest": manifest, "archive": archive, "bootstraps": bootstraps, "release-base": release_base} + "built-image": built_image, "manifest": manifest, "archive": archive, "bootstraps": bootstraps, "release-base": release_base} try: commands[sys.argv[1]](*sys.argv[2:]) except (KeyError, TypeError): - sys.exit("Usage: core-distribution-manifest.py extract-runtime|verify-runtime|verify-image|manifest|archive|bootstraps|release-base ARGS...") + sys.exit("Usage: core-distribution-manifest.py extract-runtime|verify-runtime|verify-image|built-image|manifest|archive|bootstraps|release-base ARGS...") except (OSError, ValueError, subprocess.CalledProcessError) as error: sys.exit(str(error)) diff --git a/scripts/core-distribution-manifest.test.py b/scripts/core-distribution-manifest.test.py index b083228a7..7695d6c1a 100644 --- a/scripts/core-distribution-manifest.test.py +++ b/scripts/core-distribution-manifest.test.py @@ -9,6 +9,7 @@ import tarfile import tempfile import unittest +from unittest import mock import zipfile @@ -92,6 +93,28 @@ def test_oci_manifest_identity_is_distinct_from_docker_config_identity(self): digest, name = line.split(" ", 1) self.assertEqual(digest, distribution.sha256(self.bundle / name)) + def test_built_image_records_the_id_each_docker_store_resolves(self): + config, manifest = "sha256:" + "1" * 64, "sha256:" + "2" * 64 + metadata = self.stage / "build.json" + # Classic stores report the config digest twice; containerd stores resolve only the manifest digest. + for digests, stored, expected in (((config, config), config, config), ((config, manifest), manifest, manifest), + ((config, manifest), "sha256:" + "3" * 64, None)): + with self.subTest(stored=stored): + metadata.write_text(json.dumps({"containerimage.config.digest": digests[0], + "containerimage.digest": digests[1]})) + inspect = lambda command, **_: mock.Mock(returncode=0 if command[-1] == stored else 1, + stdout=stored + "\n") + with mock.patch.object(distribution.subprocess, "run", side_effect=inspect), \ + mock.patch.object(distribution, "verify_image") as verify, \ + mock.patch("builtins.print") as output: + if expected is None: + with self.assertRaisesRegex(ValueError, "does not identify"): + distribution.built_image(metadata) + else: + distribution.built_image(metadata) + verify.assert_called_once_with(expected) + output.assert_called_once_with(expected) + def test_containerd_build_ids_still_publish_archive_config_ids(self): for name, identity in self.identities.items(): (self.stage / (name + ".id")).write_text(identity[1]) From a6ec243e8642d532320baa38e1093afd88ab6730 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Fri, 25 Sep 2026 17:41:40 +0000 Subject: [PATCH 3/6] Align install docs with the Web console and current behavior Use the Web names Nodes, Save configuration and Projects and keys; add --public-url to the local sandbox-provider commands; describe E2B CPU and memory as optional (Core adopts the template build's size); correct the standalone image base, the build- release tag, the offline install instructions, the landing page's release note, the E2B ownership summary, the legacy Vite /v1 proxy and runtime sampling comments, and record the builder's image identity rule in the contributor guide. --- .env.example | 16 +++++---- CONTRIBUTING.md | 19 ++++++----- README.md | 24 +++++++------- docs/api/web-management.md | 2 +- docs/configuration.md | 7 ++-- docs/getting-started/README.md | 5 +-- docs/getting-started/install.md | 33 ++++++++++--------- docs/getting-started/operations.md | 2 +- docs/web/README.md | 5 +-- docs/web/core-connection.md | 4 ++- services/agents-api/CONTAINER.md | 12 +++---- services/agents-api/HOSTED-SANDBOX-MANAGER.md | 26 ++++++++------- services/agents-api/README.md | 5 +-- site/index.html | 4 +-- 14 files changed, 91 insertions(+), 73 deletions(-) diff --git a/.env.example b/.env.example index 482c6add0..4186ed06f 100644 --- a/.env.example +++ b/.env.example @@ -1,20 +1,22 @@ # Server-side Vite development proxy target. This value is never exposed to -# browser JavaScript. Keep the browser-facing API base at /v1. +# browser JavaScript. AGENTS_API_PROXY_TARGET=http://127.0.0.1:8091 -# Preferred: plaintext execution-tenant bearer file read only by the local -# Vite server. If unset, Web also checks this conventional path automatically. +# Legacy development proxy only. The console never calls /v1; the Vite server +# still forwards /v1 with this Project API key for older tooling such as +# scripts/core-doctor.mjs (see docs/web/roadmap.md). Plaintext bearer file read +# only by the local Vite server; if unset, it checks this conventional path. AGENTS_API_PROXY_TOKEN_FILE=~/.parsar/agents-api/web-token # Optional server-only OTLP/HTTP Runtime history exporter configuration. # When unset, current observations and the Dashboard require no telemetry backend. # Runtime history uses the Core PostgreSQL database with 30-second periodic sampling. # Optional sampling/OTLP export overrides: services/agents-api/runtime-history/README.md # AGENTS_API_RUNTIME_HISTORY_FILE=/absolute/path/runtime-history.json -# Set sample_interval_seconds (5..300) in that server-only file to enable the -# execution-owner singleton sampler. Omit it for on-read export only. +# Set sample_interval_seconds (5..300) in that server-only file to change the +# sampling interval; omitted or zero keeps 30 seconds. -# Alternative server-side value. Never use a VITE_ prefix, and do not set this -# together with AGENTS_API_PROXY_TOKEN_FILE. +# Alternative server-side value for the legacy /v1 development proxy. Never use a +# VITE_ prefix, and do not set this together with AGENTS_API_PROXY_TOKEN_FILE. # AGENTS_API_PROXY_TOKEN= # Public, non-secret opt-in for the reviewed Codex self_hosted Session profile. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 92ea32f82..0171591d6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1678,8 +1678,8 @@ module manifests and trimmed paths. It requires no Node, Docker or product setup and the dedicated CI workflow enforce the same boundary. CI exercises the built migration command and uses the built server for official-client HTTP checks. `make docker-build-agents-api` reuses that build for Linux amd64 and sends only -its four executables and `services/agents-api/Dockerfile` to Docker. The pinned -Distroless runtime runs without root, a shell, product assets or an embedded +its executables, the E2B helper and `services/agents-api/Dockerfile` to Docker. The +digest-pinned Debian slim runtime runs without root, product assets or an embedded harness. Keep runtime credentials outside the image and migrations explicit. `make check-agents-api-container` runs the existing official-client suite against the image with a read-only root filesystem; it requires Linux Docker, a non-root @@ -1741,8 +1741,11 @@ Distribution `images` records each exported image's config digest; both from the same archive, including its referenced config and layer bytes, and require the build host's selected image ID to match one of them. Docker's classic store identifies images by config, while its containerd store uses the OCI -descriptor. Core, node and self-hosted installers share one resolver for these -required identities: confirm Linux amd64 and the returned immutable local ID, +descriptor. The builder therefore selects the digest from BuildKit's build metadata +that the local store resolves, never the `--iidfile` config digest alone, and +disables provenance attestations so each image and archive holds one platform +manifest in both stores. Core, node and self-hosted installers share one resolver +for these required identities: confirm Linux amd64 and the returned immutable local ID, then use that ID in service/provider configuration and Runtime launches. Tags do not replace identity verification. The microsandbox-qualified `runtime_ref` remains independent of Docker's local store identity. @@ -1821,15 +1824,15 @@ preserves resident microVM/helper processes across a node-service restart. User access and the Linux runtime libraries are prerequisites for microsandbox; both node providers require a systemd user session with linger. Do not add another launcher, scheduler or recovery path for local installation. -The basic distroless API image and binary builds remain independent artifacts. +The basic API image and binary builds remain independent artifacts. The standalone API release and Core distribution both include the Hosted Sandbox Manager guide at the relative path used by their packaged README. Include the guide in each artifact checksum list so extracted documentation matches its build. The node asset includes the sandbox-node binary. Local opt-in initializes an empty deployment through the same administrator API, then invokes the ordinary node -installer. Its initial default is 2 CPUs and 4096 MiB; microsandbox also requests -8192 MiB for each disk. An existing database selection is never overwritten by -installer defaults. Node configuration and identity live under +installer. Its initial size matches Web's setup proposal: 2 CPUs with 2048 MiB for +Docker, or 4096 MiB and 8192 MiB for each disk for microsandbox. An existing +database selection is never overwritten by installer defaults. Node configuration and identity live under `~/.parsar/nodes//`; microsandbox uses its separate short private Runtime home. Zero-node installs create no node identity state but retain the paired Core key for first setup. diff --git a/README.md b/README.md index d0072f6c0..816adb7a5 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,9 @@ you are ready. Core creates each required sandbox from the shared Runtime image. credentials are supplied through the existing write-only API extension. Hosted deployments select E2B cloud or one provider across their own local/remote -nodes (Docker or microsandbox). The -Hosted Sandbox Manager shows node health, capacity and Session placement. Core -places new Sessions automatically; existing Sessions retain their node across -disconnects and resume. +nodes (Docker or microsandbox). Web's **Nodes** page shows node health, capacity +and Session placement. Core places new Sessions automatically; existing Sessions +retain their node across disconnects and resume. ## Start here @@ -40,14 +39,14 @@ disconnects and resume. 2. **Sign in to Web.** Open the console address printed by the installer and 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 + automatically. Create a Project on the **Projects and keys** page, 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. -3. **Add a node.** Open **Hosted Sandbox Manager** and choose E2B cloud or - your own machines with Docker/microsandbox. E2B needs its account credentials and - qualified Runtime template, with no node installation. For your own machines, - initialize the deployment. The paired console address is used by default; +3. **Add a node.** Open **Nodes** and choose E2B cloud or your own machines with + Docker/microsandbox. E2B needs its account credentials and qualified Runtime + template, with no node installation. For your own machines, choose the sandbox + size, then select **Save configuration**. The paired console address is used by default; advanced network settings allow a different reachable HTTPS origin. Select **Add node**, then copy and run the command on a prepared Linux host. Web shows when the node is online and its provider is ready. All nodes in a deployment use the same provider. @@ -69,13 +68,14 @@ run an optional API example with your own model credentials. Run these from an extracted distribution. The plain command uses loopback for local console/API access. For node enrollment, use the reachable origin described above; the installer does not change an existing installation's public URL. -Installing a local provider is optional, and is not required for adding nodes in Web. +Installing a local provider is optional, needs that HTTPS `--public-url`, and is not +required for adding nodes in Web. ```sh ./install.sh # Core + Web + PostgreSQL, loopback access, zero nodes ./install.sh --core-only # Core + PostgreSQL, zero nodes -./install.sh --sandbox-provider true --provider microsandbox -./install.sh --sandbox-provider true --provider docker +./install.sh --sandbox-provider true --provider microsandbox --public-url https://core.example +./install.sh --sandbox-provider true --provider docker --public-url https://core.example ``` Web-only installation connects the console server to an existing Core; see the diff --git a/docs/api/web-management.md b/docs/api/web-management.md index 1fbf65858..d42bbbe83 100644 --- a/docs/api/web-management.md +++ b/docs/api/web-management.md @@ -25,7 +25,7 @@ key in constant time and never logs or returns it. Only failed attempts count toward the limit; the correct key signs in even while failures are limited. The console refuses to start with a Core key shorter than 32 characters. Sessions live only in the console's memory; a console restart or Core key rotation requires signing in again. There are no console accounts, -usernames, passwords, first-run setup or Basic authentication. +usernames, passwords, account setup or Basic authentication. Use same-origin browser requests and cookies. Mutations require the same-origin request checks; never put the Core key in JavaScript or browser storage. diff --git a/docs/configuration.md b/docs/configuration.md index 7eeead0b1..4444383f9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -106,13 +106,14 @@ request shapes, immutable identities and provider validation. | Setting | Initial Web proposal | Meaning | | --- | --- | --- | | `resources.cpus` | `2` | Integer vCPUs per sandbox, 1 through 255 | -| `resources.memory_mib` | Docker/E2B `2048`, microsandbox `4096` | MiB per sandbox, 512 through 1048576 | +| `resources.memory_mib` | Docker `2048`, microsandbox `4096` | MiB per sandbox, 512 through 1048576 | | `resources.root_disk_mib`, `environment_disk_mib` | Microsandbox `8192` each | At least 1024 MiB; omitted for Docker/E2B | | `runtime` | Matching distribution manifest | Source revision, exact image ID/manifest/ref and runtime/firmware hashes; no mutable tags | | Idle interval / snapshot retention | `300` / `86400` seconds | Database-owned microsandbox policy; no node-file override | -E2B also requires CPU/memory. Core verifies them against the ready immutable -`template-id:build-uuid`; omit `runtime`. Its account key is encrypted and write-only. +E2B resources are optional, and Web sends none: Core then adopts the CPU and memory +of the ready immutable `template-id:build-uuid`. Supplied values must match that +build. Omit `runtime`. The E2B account key is encrypted and write-only. E2B template storage remains native; Docker has no independent hard disk quota. A submitted limit must be enforceable by its provider. diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 4e6c40e42..921a77469 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -13,8 +13,9 @@ Runtime contract. The Parsar product is not required. Core and Web ship together. The default installation runs Core, Web and PostgreSQL with zero execution nodes. To prepare a local sandbox provider, install with -`--sandbox-provider true --provider microsandbox` or -`--sandbox-provider true --provider docker`. Core then creates the execution sandbox +`--sandbox-provider true --provider microsandbox --public-url https://core.example` or +`--sandbox-provider true --provider docker --public-url https://core.example`, using +your HTTPS address. Core then creates the execution sandbox when a Session needs it. Installing the service does not require a model key or run a model request. diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md index cb6e78e41..2ffd6bcc1 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -121,17 +121,18 @@ Choose English or Chinese through the System language selector. ## Add nodes after a default installation -1. Log in to the bundled Web console and open **Hosted Sandbox Manager**. +1. Log in to the bundled Web console and open **Nodes**. The paired installation needs no second key or Core connection setup. 2. Choose **E2B cloud** or **Own machines**. Own machines use Docker or microsandbox; select their per-sandbox resources and matched immutable Runtime - release. E2B uses an account key and exact ready template build whose CPU/memory - match the requested limits, with no node installation. The paired console address is used - automatically. If your network requires a different address for nodes and - guests, change it under advanced network settings during initial setup. When + release. E2B uses an account key and an exact ready template build, with no node + installation; each sandbox gets the build's CPU and memory, so setup asks for no + size. The paired console address is used automatically. If your network requires + a different address for nodes and guests, change it under advanced network + settings during initial setup. When opening the console on localhost or an HTTP address, setup requires a non-loopback HTTPS address that nodes and sandbox guests can reach. -3. Select **Initialize sandbox deployment**. It takes effect without restarting Core and remains in +3. Select **Save configuration**. It takes effect without restarting Core and remains in PostgreSQL across restarts. The provider, limits and Runtime are one saved specification; node files cannot override it. Later changes require global maintenance and completed cleanup. Microsandbox uses a five-minute idle timeout @@ -357,8 +358,8 @@ preparation is described in the [E2B guide](../../services/agents-api/deploy/e2b Start with an optional [API request](quickstart.md). The read-only example works with the default installation. The execution example requires an installation with either E2B configured through Web or a connected, ready Docker/microsandbox node. -The Hosted Sandbox Manager selects one scheme for the whole deployment. E2B uses -an account API key and a qualified immutable Runtime template; Core provisions +Sandbox setup on the **Nodes** page selects one scheme for the whole deployment. +E2B uses an account API key and a qualified immutable Runtime template; Core provisions sandboxes without a node installer. Own machines use the existing node command. Provider, per-sandbox resources and Runtime changes all require maintenance and completed resource cleanup; see @@ -416,7 +417,7 @@ gh workflow run core-release --repo MiniMax-AI/parsar-core --ref main \ ``` The workflow uploads the matched files as an Actions artifact and creates a draft -Release whose tag is that full SHA. The manifest records the same tag in every +Release tagged `build-`. The manifest records the same tag in every asset URL. Do not mix files across releases or resolve individual components through `latest`. The node command comes from its connected Core, which selects the matching release automatically. @@ -427,9 +428,11 @@ publishing the draft. A workflow build alone is not live acceptance. Retain the exact tested assets when publishing; do not rebuild or replace files under the same release identity. Publishing a Release does not change repository visibility. -For an offline installation, provide the extracted matching archive through the -existing `--offline-root` option where supported. Remote node commands use the -manifest's release URL; use the explicitly configured console-hosted offline -build described above when node hosts cannot access that URL. Download access -errors should be fixed at the distribution source, without passing repository -credentials into Runtime or changing its executor authorization. +For an offline installation, extract the matching `-offline.tar.gz` archive and run +its `install.sh`, which copies the bundled assets into the console's node payload. +`install.sh` has no `--offline-root` option; only `self-hosted-install.pyz` accepts +one, for an executor host. Remote node commands use the manifest's release URL; use +the explicitly configured console-hosted offline build described above when node +hosts cannot access that URL. Download access errors should be fixed at the +distribution source, without passing repository credentials into Runtime or +changing its executor authorization. diff --git a/docs/getting-started/operations.md b/docs/getting-started/operations.md index ee73b6964..04afbb0fc 100644 --- a/docs/getting-started/operations.md +++ b/docs/getting-started/operations.md @@ -41,7 +41,7 @@ Container liveness alone is not a healthy native harness or an available model. Use public Session, Turn, Items, Environment and Usage reads for execution. Reuse Core's Runtime observations for sandbox details when available; do not infer execution truth from Docker or invent a second lifecycle collector. Use Web's -**Hosted Sandbox Manager** for node connection, provider readiness and placement. +**Nodes** page for node connection, provider readiness and placement. ## Stop and restart diff --git a/docs/web/README.md b/docs/web/README.md index 66d6704bf..1308418ab 100644 --- a/docs/web/README.md +++ b/docs/web/README.md @@ -51,8 +51,9 @@ through `/v1`. Core has no API users, roles or memberships. Follow the [installation guide](../getting-started/install.md) for Core, Web and PostgreSQL with zero execution nodes. Installation creates no Project or application -key; an administrator creates them through the management API. The browser signs -in to the console with the Core key; only the console server sends it to Core. +key; an administrator creates them on the console's **Projects and keys** page or +through the management API. The browser signs in to the console with the Core key; +only the console server sends it to Core. - [Connection and authentication](core-connection.md) - [Architecture and ownership](architecture.md) diff --git a/docs/web/core-connection.md b/docs/web/core-connection.md index feb1445b1..5447ff724 100644 --- a/docs/web/core-connection.md +++ b/docs/web/core-connection.md @@ -62,7 +62,9 @@ and display only. Keep deployment, application, node and provider credentials ou ## Projects and application keys Installation creates no Project or application key. An administrator creates a -Project and issues named keys using the [administrator API](../../contracts/agents-api/admin-api.md). +Project and issues named keys on the console's **Projects and keys** page, which +uses the [administrator API](../../contracts/agents-api/admin-api.md), or with that +API directly. The Project owns one tenant and one principal; its keys share assets and permissions. Writes retain each key's provenance. All Projects and application keys live in PostgreSQL, independently of deployment configuration. diff --git a/services/agents-api/CONTAINER.md b/services/agents-api/CONTAINER.md index e5b387aa5..3ccd6d21e 100644 --- a/services/agents-api/CONTAINER.md +++ b/services/agents-api/CONTAINER.md @@ -16,12 +16,12 @@ AGENTS_API_IMAGE=agents-api:local make docker-build-agents-api ``` The target needs Go, Docker and access to pinned Go modules and the base image. -It reuses the isolated binary build and sends only those executables and the image -recipe to Docker. Linux amd64 is the current runtime target; other architectures -and registry publication are not included. The pinned -[Distroless static runtime](https://github.com/GoogleContainerTools/distroless) -contains CA certificates and no shell or package manager. The default user is -UID/GID 65532. No model credentials or tenant keys belong in the image. +It reuses the isolated binary build and sends only those executables, the E2B helper +and the image recipe to Docker. Linux amd64 is the current runtime target; other architectures +and registry publication are not included. The runtime base is the digest-pinned +`debian:bookworm-slim` image, with CA certificates and the glibc/libgcc runtime that +the bundled E2B helper needs. It keeps Debian's shell and package manager. The default +user is UID/GID 65532. No model credentials or tenant keys belong in the image. ## Configure and run diff --git a/services/agents-api/HOSTED-SANDBOX-MANAGER.md b/services/agents-api/HOSTED-SANDBOX-MANAGER.md index e4d9397aa..1b4caafc6 100644 --- a/services/agents-api/HOSTED-SANDBOX-MANAGER.md +++ b/services/agents-api/HOSTED-SANDBOX-MANAGER.md @@ -8,7 +8,8 @@ Docker/microsandbox nodes must match its installation, generation and specificat are distinct from user-managed `self_hosted` Environments, whose provisioning remains the user's responsibility. -The Web console's **Hosted Sandbox Manager** page uses the Core key, separate from +The Web console's **Nodes** page (shown as **Sandbox backend** when the deployment +uses E2B) uses the Core key, separate from Project API keys. After console sign-in, the console server forwards the page's `/core/v1/sandbox` requests, like every other `/core/v1` request, with the Core key it reads from its private file. The Core key never reaches the browser. There @@ -34,12 +35,14 @@ node installation payload. Project keys cannot register, edit or remove nodes. ## Start with zero nodes Default installation starts Core, Web and PostgreSQL without local compute. -Hosted Sandbox Manager first asks for **E2B cloud** or **Own machines**. Own machines -then choose Docker or microsandbox. Supply the per-sandbox resources and matched -Runtime release as part of initial setup. E2B takes an account API key, CPU/memory -limits and a qualified immutable Runtime template build (`template-id:build-uuid`); -its exact ready build must match those limits before the selection can be saved. The key is write-only, -encrypted by Core and never returned to the browser. E2B needs no node installation. +Setup on the **Nodes** page first asks for **E2B cloud** or **Own machines**. Own +machines then choose Docker or microsandbox. Supply the per-sandbox resources and +matched Runtime release as part of initial setup. E2B takes an account API key and +a qualified immutable Runtime template build (`template-id:build-uuid`) that must be +ready before the selection can be saved. E2B CPU/memory limits are optional: Web +sends none, and Core adopts the build's size; supplied limits must match the build. +The key is write-only, encrypted by Core and never returned to the browser. E2B +needs no node installation. The saved Core origin defaults to the paired console's public origin, where the reverse proxy sends `/api/v1`, including WebSocket upgrades, directly to Core; Web does not forward it. Use advanced network settings only when nodes and guests need @@ -48,8 +51,8 @@ HTTPS, setup opens the network field and requires a non-loopback HTTPS origin be The API still accepts HTTP loopback for explicit local development; a guest's loopback address cannot reach its Core host. -Saving validates and initializes the deployment without creating compute. A failed -candidate leaves the previous selection intact. Refresh after an uncertain response before +**Save configuration** validates and initializes the deployment without creating +compute. A failed candidate leaves the previous selection intact. Refresh after an uncertain response before trying again. Selection persists in PostgreSQL and activates without a restart. Removing all nodes does not reset it. E2B uses the same daemon, harness and workspace Runtime as node-backed hosting. Configuration alone does not prove provider or model @@ -107,7 +110,8 @@ the current selection, including one made after startup. ## Resources and Runtime The same deployment specification applies to every hosted sandbox. CPU count and -memory in MiB are required. Microsandbox also requires separate root and +memory in MiB are required for Docker and microsandbox; E2B may omit them and adopt +its template build's size. Microsandbox also requires separate root and `/environment` disk capacities. Docker and E2B reject nonzero independent disk limits because this contract does not enforce those hard quotas. Node active and retained reservation limits are separate controls; they do not resize a sandbox. @@ -197,7 +201,7 @@ token, then retained in PostgreSQL. Registration and node files cannot overwrite it. See [Configuration](../../docs/configuration.md#node-configuration-and-capacity) for limits, defaults and changes. -In Hosted Sandbox Manager, generate a single-use registration token. Save it in +On the **Nodes** page, generate a single-use registration token. Save it in a `0600` file on the host. The token expires after the duration shown by Core. Run the displayed command with real absolute paths, for example: diff --git a/services/agents-api/README.md b/services/agents-api/README.md index 79e8d55d5..b4f371cfc 100644 --- a/services/agents-api/README.md +++ b/services/agents-api/README.md @@ -8,8 +8,9 @@ and a daemon execution worker. Public execution supports qualified Codex, Claude (`claude_sdk`) and MiniMax Code (`mcode`) profiles through the shared Runtime contract. The three-harness Linux amd64 Docker V1 MVP has accepted evidence. V1 user-managed Runtime enrollment has [recorded real acceptance](../../contracts/agents-api/user-managed-runtime-v1.md) -with explicit deployment coverage. [E2B deployment](deploy/e2b/README.md) is user-managed; -its earlier Core-managed qualification remains historical evidence. +with explicit deployment coverage. [E2B](deploy/e2b/README.md) can back Core-managed +hosted sandboxes, selected in Web's sandbox setup, or application-managed +`self_hosted` Environments. It builds and runs with its own PostgreSQL database and credentials; Parsar's product service, frontend and database are not required. diff --git a/site/index.html b/site/index.html index 21d112c34..2889cfc6e 100644 --- a/site/index.html +++ b/site/index.html @@ -78,14 +78,14 @@

One Core.
Many agents.

Install. Sign in.
Add a node.

Install Core and Web together. Add execution nodes when you need them.

Read the installation guide -
BEFORE YOU START

Use a verified Linux amd64 bundle. Choose an HTTPS address reachable from your nodes and sandboxes, and configure DNS/TLS before the first install.

Public release bundles are not published yet. Obtain one from your deployment administrator or build from source.

+
BEFORE YOU START

Use a verified Linux amd64 bundle. Choose an HTTPS address reachable from your nodes and sandboxes, and configure DNS/TLS before the first install.

Download a matching bundle from GitHub Releases, signing in if GitHub asks. For unreleased changes, build from source.

  1. Install Core + Web

    ZERO NODES

    Run from your extracted bundle. Core, Web and PostgreSQL start together. No model key required.

    ./install.sh \
         --public-url https://core.example
  2. Sign in to your console

    Open the address printed by the installer. Sign in with the Core key in this private file:

    ~/.parsar/core/admin/core.key

    The paired console is already connected to Core.

  3. -
  4. Bring a node online

    Open Hosted Sandbox Manager, choose Docker or microsandbox, and initialize the deployment. Select Add node, then copy and run the command on a prepared Linux host.

    Dockermicrosandbox

    Web shows when the node is online and its provider is ready. All nodes use the same provider.

  5. +
  6. Bring a node online

    Open Nodes, choose Docker or microsandbox, then Save configuration. Select Add node, then copy and run the command on a prepared Linux host.

    Dockermicrosandbox

    Web shows when the node is online and its provider is ready. All nodes use the same provider.

Installation and enrollment do not call a model. Other installation options

From 917038947f1d371384684a7b3bd2d7f8f5d263d2 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Fri, 25 Sep 2026 17:43:28 +0000 Subject: [PATCH 4/6] Describe Core's container image accurately in the microsandbox guides Core's images use Debian slim, not distroless/static. Only Web's console image is distroless. What matters is that Core's container never runs the microsandbox helper; the standalone node does. --- services/agents-api/deploy/microsandbox/README.md | 2 +- services/agents-api/tools/microsandbox-provider/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/services/agents-api/deploy/microsandbox/README.md b/services/agents-api/deploy/microsandbox/README.md index d4fc5d2ea..6d8120f1f 100644 --- a/services/agents-api/deploy/microsandbox/README.md +++ b/services/agents-api/deploy/microsandbox/README.md @@ -51,7 +51,7 @@ removing an environment variable does not migrate its configuration ownership. Use a dedicated service account with access to `/dev/kvm`, a C compiler and the repository's Go version for source builds. The node's helper requires glibc and the standard Linux dynamic libraries. Core remains a CGO-disabled build. Its -`distroless/static` image does not run the helper; the standalone native node does. +Debian slim container image does not run the helper; the standalone native node does. Use the matched distribution's ordinary node installer for an operator installation. Build from the repository root: diff --git a/services/agents-api/tools/microsandbox-provider/README.md b/services/agents-api/tools/microsandbox-provider/README.md index 61ec5d586..e4b48761d 100644 --- a/services/agents-api/tools/microsandbox-provider/README.md +++ b/services/agents-api/tools/microsandbox-provider/README.md @@ -17,7 +17,7 @@ are checked. This does not claim a new direct root-capacity measurement on resto The ordinary standalone sandbox node runs this helper natively on Linux amd64, under a dedicated service user with KVM access. Core owns lifecycle intent through the node protocol and can run in a container or on another host. Core's -`distroless/static` image does not execute this glibc helper. The installer's local +container image does not execute this helper; only the node does. The installer's local microsandbox opt-in still uses native Core packaging; that is an installation choice, not an architectural requirement. From 1db8de90e5c9aa1fa9ab8e985f49fe0476e8abce Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Fri, 25 Sep 2026 18:18:23 +0000 Subject: [PATCH 5/6] Pin the distribution database image by its linux/amd64 manifest On Docker's containerd store a pulled multi-platform tag keeps its whole index, so the exported database archive held every platform and the identity check stopped after a full build. The default PostgreSQL input is now postgres:16-alpine pinned by its linux/amd64 platform manifest digest, which stays one image in both stores. The built-image tests also cover metadata without a config digest and a digest that resolves to another image. --- CONTRIBUTING.md | 5 +++- scripts/build-core-distribution.sh | 5 +++- scripts/core-distribution-manifest.test.py | 31 +++++++++++++--------- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0171591d6..2ff4c0452 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1744,7 +1744,10 @@ store identifies images by config, while its containerd store uses the OCI descriptor. The builder therefore selects the digest from BuildKit's build metadata that the local store resolves, never the `--iidfile` config digest alone, and disables provenance attestations so each image and archive holds one platform -manifest in both stores. Core, node and self-hosted installers share one resolver +manifest in both stores. For the same reason the default PostgreSQL input is pinned +by its linux/amd64 platform manifest digest: a pulled multi-platform tag keeps its +whole index in the containerd store, and that export holds every platform. Core, +node and self-hosted installers share one resolver for these required identities: confirm Linux amd64 and the returned immutable local ID, then use that ID in service/provider configuration and Runtime launches. Tags do not replace identity verification. The microsandbox-qualified `runtime_ref` diff --git a/scripts/build-core-distribution.sh b/scripts/build-core-distribution.sh index d6daabc79..505dcd59f 100755 --- a/scripts/build-core-distribution.sh +++ b/scripts/build-core-distribution.sh @@ -200,7 +200,10 @@ build_image runtime \ --build-arg "CODEX_IMAGE=${image_tags[0]}" --build-arg "CLAUDE_IMAGE=${image_tags[1]}" \ --build-arg "MCODE_IMAGE=${image_tags[2]}" "$stage/combined" -database_image="${CORE_DISTRIBUTION_DATABASE_IMAGE:-postgres:16-alpine}" +# Pin the linux/amd64 platform manifest, not the multi-platform tag: the +# containerd store keeps a pulled tag's whole index, whose export holds every +# platform. A platform manifest stays one image in both stores. +database_image="${CORE_DISTRIBUTION_DATABASE_IMAGE:-postgres:16-alpine@sha256:1a66d744c1b459e13b05a8fca341da84cb63383e99ce262210efee5a319d4551}" if [[ ! "$database_image" =~ ^sha256:[0-9a-f]{64}$ ]]; then docker pull --platform linux/amd64 "$database_image" fi diff --git a/scripts/core-distribution-manifest.test.py b/scripts/core-distribution-manifest.test.py index 7695d6c1a..32658c6e0 100644 --- a/scripts/core-distribution-manifest.test.py +++ b/scripts/core-distribution-manifest.test.py @@ -94,26 +94,31 @@ def test_oci_manifest_identity_is_distinct_from_docker_config_identity(self): self.assertEqual(digest, distribution.sha256(self.bundle / name)) def test_built_image_records_the_id_each_docker_store_resolves(self): - config, manifest = "sha256:" + "1" * 64, "sha256:" + "2" * 64 + config, manifest, other = ("sha256:" + digit * 64 for digit in "123") metadata = self.stage / "build.json" - # Classic stores report the config digest twice; containerd stores resolve only the manifest digest. - for digests, stored, expected in (((config, config), config, config), ((config, manifest), manifest, manifest), - ((config, manifest), "sha256:" + "3" * 64, None)): - with self.subTest(stored=stored): - metadata.write_text(json.dumps({"containerimage.config.digest": digests[0], - "containerimage.digest": digests[1]})) - inspect = lambda command, **_: mock.Mock(returncode=0 if command[-1] == stored else 1, - stdout=stored + "\n") + both = {"containerimage.config.digest": config, "containerimage.digest": manifest} + # Classic stores resolve the config digest, containerd stores the manifest digest. A digest + # resolving to another image, or metadata without a config digest, identifies nothing. + cases = ((dict(both, **{"containerimage.digest": config}), {config: config}, config), + (both, {manifest: manifest}, manifest), + (both, {config: other}, "does not identify"), + ({"containerimage.digest": manifest}, {manifest: manifest}, "lacks valid image digests")) + for build, store, expected in cases: + with self.subTest(build=build, store=store): + metadata.write_text(json.dumps(build)) + inspect = lambda command, **_: mock.Mock(returncode=0 if command[-1] in store else 1, + stdout=store.get(command[-1], "") + "\n") with mock.patch.object(distribution.subprocess, "run", side_effect=inspect), \ mock.patch.object(distribution, "verify_image") as verify, \ mock.patch("builtins.print") as output: - if expected is None: - with self.assertRaisesRegex(ValueError, "does not identify"): - distribution.built_image(metadata) - else: + if expected.startswith("sha256:"): distribution.built_image(metadata) verify.assert_called_once_with(expected) output.assert_called_once_with(expected) + else: + with self.assertRaisesRegex(ValueError, expected): + distribution.built_image(metadata) + verify.assert_not_called() def test_containerd_build_ids_still_publish_archive_config_ids(self): for name, identity in self.identities.items(): From 08064743b89df26664144100b32ebc2b7caa874a Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Fri, 25 Sep 2026 18:18:23 +0000 Subject: [PATCH 6/6] Label loopback install addresses and name every setup's final step A loopback --public-url is marked local only. When it is Web's port, which answers 404 on /v1, the installer prints Core's loopback API instead. The core-only next step names the local /core/v1 address. README step 3 says both E2B and own machines finish with Save configuration. --- README.md | 11 ++++++----- deploy/install/install.py | 31 ++++++++++++++++++++++--------- deploy/install/test_install.py | 29 ++++++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 816adb7a5..a1dcf3099 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,12 @@ retain their node across disconnects and resume. same Project and revoking the old one. 3. **Add a node.** Open **Nodes** and choose E2B cloud or your own machines with Docker/microsandbox. E2B needs its account credentials and qualified Runtime - template, with no node installation. For your own machines, choose the sandbox - size, then select **Save configuration**. The paired console address is used by default; - advanced network settings allow a different reachable HTTPS origin. Select - **Add node**, then copy and run the command on a prepared Linux host. Web shows when the node is online - and its provider is ready. All nodes in a deployment use the same provider. + template, with no node installation; your own machines also need a sandbox size. + Either way, finish setup with **Save configuration**. The paired console address + is used by default; advanced network settings allow a different reachable HTTPS + origin. For your own machines, then select **Add node**, and copy and run the + command on a prepared Linux host. Web shows when the node is online and its + provider is ready. All nodes in a deployment use the same provider. Installation and node enrollment do not call a model. Once a node is ready, run an optional API example with your own model credentials. diff --git a/deploy/install/install.py b/deploy/install/install.py index de2121409..2b2b28f19 100644 --- a/deploy/install/install.py +++ b/deploy/install/install.py @@ -108,6 +108,19 @@ def core_target(value): return value.rstrip("/") +def loopback_origin(value): + hostname = urlsplit(value or "").hostname + try: + return ipaddress.ip_address(hostname).is_loopback + except ValueError: + return hostname == "localhost" + + +def origin_port(value): + parsed = urlsplit(value) + return parsed.port or (443 if parsed.scheme == "https" else 80) + + def arguments(argv=None): parser = argparse.ArgumentParser(description=__doc__) modes = parser.add_mutually_exclusive_group() @@ -139,12 +152,7 @@ def arguments(argv=None): parser.error("--web-only cannot install a sandbox provider") args.provider = (args.provider or "microsandbox") if args.sandbox_provider else None if args.provider and not (args.status or args.stop): - endpoint = urlsplit(args.public_url or "") - try: - loopback = ipaddress.ip_address(endpoint.hostname).is_loopback - except ValueError: - loopback = endpoint.hostname == "localhost" - if endpoint.scheme != "https" or loopback: + if urlsplit(args.public_url or "").scheme != "https" or loopback_origin(args.public_url): parser.error("Local sandbox installation requires --public-url with HTTPS reachable from sandbox guests; loopback origins cannot be used") if args.status and args.stop: parser.error("Choose status or stop") @@ -386,17 +394,22 @@ def main(argv=None): if not wait_http(core_url + "/core/v1/projects", {"Authorization": "Bearer " + token}): raise InstallError("Core key authentication failed. Inspect private configuration; no model was called") # The console accepts only its configured origin, so a public URL has no loopback console. - print("Console: " + (public_url or url + " (local only)")) + console = public_url or url + print("Console: " + console + (" (local only)" if loopback_origin(console) else "")) if state["mode"] != "web-only": api = f'http://127.0.0.1:{state["core_port"]}/v1' - if public_url: + if public_url and not loopback_origin(public_url): print("API base URL: " + public_url + "/v1") print("Local-only API on this host: " + api) + elif public_url and origin_port(public_url) != state["web_port"]: + print("API base URL: " + public_url + "/v1 (local only)") else: + # Web answers 404 on /v1, so only Core's own port serves the API locally. print("API base URL: " + api + " (local only)") core_key = root / "admin/core.key" if state["mode"] == "core-only": - print(f"Next: create a Project and its API key through the Core management API (/core/v1) with the Core key in {core_key}.") + print(f'Next: create a Project and its API key through the Core management API at ' + f'http://127.0.0.1:{state["core_port"]}/core/v1 (local only) with the Core key in {core_key}.') else: print(f"Next: sign in to Web with the Core key in {core_key}, then create a Project and its API key on the Projects and keys page.") print("Keep the Core key private; it also authorizes the Core management API.") diff --git a/deploy/install/test_install.py b/deploy/install/test_install.py index 73963e690..070a781cc 100644 --- a/deploy/install/test_install.py +++ b/deploy/install/test_install.py @@ -664,7 +664,8 @@ def test_default_main_skips_kvm_native_service_and_runtime_import(self): [str(bundle / ("images/" + name + ".tar")) for name in ("core", "database", "web")]) self.assertFalse((self.root / "native").exists()) self.assertFalse((self.root / "config/seccomp.json").exists()) - self.assertIn("No execution node was installed by this run", output.getvalue()) + self.assertIn("No execution node was installed by this run. Choose a sandbox backend and add nodes on the Nodes page in Web.", + output.getvalue()) self.assertIn("Core configuration file: " + str(self.root / "config/core.env"), output.getvalue()) self.assertNotIn((self.root / "config/database.password").read_text(), output.getvalue()) self.assertIn("Console: http://127.0.0.1:8080 (local only)", output.getvalue()) @@ -680,6 +681,29 @@ def test_default_main_skips_kvm_native_service_and_runtime_import(self): self.assertEqual(health.call_args_list[-1].args[1], {"Authorization": "Bearer " + admin_token}) self.assertNotIn(admin_token, output.getvalue()) + def test_main_output_labels_local_addresses(self): + cases = {"core-only": (["--core-only"], [ + "API base URL: http://127.0.0.1:8091/v1 (local only)\n", + "Next: create a Project and its API key through the Core management API at " + "http://127.0.0.1:8091/core/v1 (local only) with the Core key in {key}."]), + # Web answers 404 on /v1, so a loopback public URL on its port points to Core's API. + "loopback": (["--public-url", "http://localhost:8080"], [ + "Console: http://localhost:8080 (local only)\n", + "API base URL: http://127.0.0.1:8091/v1 (local only)\n"])} + for name, (flags, expected) in cases.items(): + with self.subTest(name=name): + self.root = self.work / name + bundle, output = self.bundle(), io.StringIO() + with mock.patch.object(install, "__file__", str(bundle / "install.py")), \ + mock.patch.object(install.platform, "system", return_value="Linux"), \ + mock.patch.object(install.platform, "machine", return_value="x86_64"), \ + mock.patch.object(install, "run"), mock.patch.object(install, "wait_http", return_value=True), \ + contextlib.redirect_stdout(output): + install.main(["--install-dir", str(self.root), *flags]) + for line in expected: + self.assertIn(line.format(key=self.root / "admin/core.key"), output.getvalue()) + shutil.rmtree(bundle) + def test_main_web_only_never_imports_runtime_or_leaks_administrator_token(self): source = self.administrator_file() bundle = self.bundle() @@ -705,6 +729,9 @@ def external_command(args, **_kwargs): self.assertEqual([call.args[0] for call in health.call_args_list], ["http://127.0.0.1:8080/console/auth", "http://127.0.0.1:9091/core/v1/projects"]) self.assertEqual(health.call_args_list[-1].args[1], {"Authorization": "Bearer " + source.read_text()}) self.assertNotIn(source.read_text(), output.getvalue()) + self.assertIn("Console: http://127.0.0.1:8080 (local only)\nNext: sign in to Web with the Core key in " + + str(self.root / "admin/core.key") + ", then create a Project", output.getvalue()) + self.assertNotIn("API base URL", output.getvalue()) services = self.document("compose.json")["services"] self.assertEqual(set(services), {"web"}) web = services["web"]