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
16 changes: 9 additions & 7 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
22 changes: 14 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1741,8 +1741,14 @@ 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. 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`
remains independent of Docker's local store identity.
Expand Down Expand Up @@ -1821,15 +1827,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/<installation-id>/`; 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.
Expand Down
31 changes: 16 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -40,17 +39,18 @@ 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;
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.
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; 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.
Expand All @@ -69,13 +69,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
Expand Down
52 changes: 39 additions & 13 deletions deploy/install/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -375,26 +383,44 @@ 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.
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 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 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.")
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.")


Expand Down
5 changes: 3 additions & 2 deletions deploy/install/local_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)})
Expand Down
Loading
Loading