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
116 changes: 116 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
name: core-release

on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
ref:
description: Full source commit SHA to build
required: true
type: string
offline:
description: Also create the full offline archive
default: false
type: boolean
draft_release:
description: Upload files to a new draft release; never publish
default: false
type: boolean

permissions:
contents: read

concurrency:
group: core-release-${{ inputs.ref || github.sha }}
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-22.04
timeout-minutes: 120
outputs:
revision: ${{ steps.source.outputs.revision }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.ref || github.sha }}
persist-credentials: false
- name: Pin source identity
id: source
env:
REQUESTED_REF: ${{ inputs.ref }}
run: |
if [[ -n "$REQUESTED_REF" && ! "$REQUESTED_REF" =~ ^[0-9a-f]{40}$ ]]; then
echo 'Manual releases require a full commit SHA' >&2
exit 1
fi
revision="$(git rev-parse HEAD)"
if [[ -n "$REQUESTED_REF" && "$REQUESTED_REF" != "$revision" ]]; then exit 1; fi
echo "revision=$revision" >> "$GITHUB_OUTPUT"
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- uses: actions/setup-node@v6
with:
node-version: '22'
- name: Install build prerequisites
run: |
npm install --global pnpm@10.30.3
sudo apt-get update
sudo apt-get install -y build-essential pkg-config libssl-dev
rustup toolchain install 1.95.0 --profile minimal
- uses: actions/cache@v6
with:
path: |
~/.parsar/cache/go-build
~/.parsar/cache/go-mod
~/.parsar/cache/executor-cargo
~/.parsar/cache/executor-target
key: core-release-${{ runner.os }}-${{ hashFiles('go.sum', 'packages/codex-executor/Cargo.lock') }}
- name: Check release metadata and prepare pinned harnesses
run: |
PYTHONDONTWRITEBYTECODE=1 python3 scripts/core-distribution-manifest.test.py
bash scripts/prepare-release-runtimes.sh
- name: Build matched artifacts
env:
RELEASE_REVISION: ${{ steps.source.outputs.revision }}
RELEASE_REPOSITORY: ${{ github.repository }}
CORE_DISTRIBUTION_OFFLINE: ${{ inputs.offline && '1' || '0' }}
run: |
inputs="$HOME/.parsar/build/release-inputs/inputs.json"
export AGENTS_RUNTIME_CODEX_PACKAGE="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["codex"])' "$inputs")"
export MCODE_HARNESS_BUILD_DIR="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["mcode"])' "$inputs")"
export CORE_DISTRIBUTION_RELEASE_BASE_URL="https://github.com/$RELEASE_REPOSITORY/releases/download/$RELEASE_REVISION"
bash scripts/build-core-distribution.sh
mkdir -p "$HOME/.parsar/build/release-upload"
for asset in "$HOME/.parsar/build/core-distribution/"*; do
if [[ -f "$asset" ]]; then ln "$asset" "$HOME/.parsar/build/release-upload/"; fi
done
- uses: actions/upload-artifact@v6
with:
name: core-release-${{ steps.source.outputs.revision }}
path: ~/.parsar/build/release-upload/*
compression-level: 0
if-no-files-found: error

draft:
if: github.event_name == 'workflow_dispatch' && inputs.draft_release
needs: build
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
- uses: actions/download-artifact@v6
with:
name: core-release-${{ needs.build.outputs.revision }}
path: release-upload
- name: Create an unpublished draft
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_REVISION: ${{ needs.build.outputs.revision }}
run: |
printf 'Matched Linux amd64 artifacts from commit %s. Build output is not live execution qualification.\n' "$RELEASE_REVISION" > release-notes.md
gh release create "$RELEASE_REVISION" --draft --target "$RELEASE_REVISION" \
--title "Parsar Core $RELEASE_REVISION" --notes-file release-notes.md release-upload/*
26 changes: 23 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -1551,6 +1551,25 @@ microsandbox runtime/firmware hashes and executable native payloads. Release gen
qualification. A release must be tested from fresh extraction with real models;
no synthetic result may substitute for native execution acceptance.

The manifest is the shared download contract for Core, node and self-hosted
installers: flat versioned filenames, compressed Runtime size/hash and unpacked
size/hash, with HTTPS release URLs or the explicit offline payload. Download into
private temporary files, verify before atomic promotion, and reuse only verified
cache entries or exact image identities. Core's default image must not acquire
execution-only payloads. Python zipapps bundle the shared resolver with each
remote bootstrap; the console publishes only fixed non-secret files and declared
artifact names. Release automation builds artifacts and may create an unpublished
draft, but cannot claim real execution qualification or public availability.

Project-authenticated executor-credential extensions remain outside the upstream
API namespace and reuse the existing restricted issuer. They require the exact
creator of a live self-hosted Environment; deployment administrator authority and
shared Session read access do not grant credential issuance. The console preserves
explicit caller credentials on these routes and never substitutes its administrator
key. Self-hosted installation reuses Docker Runtime isolation, owns no sandbox
node or Core allocation, and retains user-owned native history after uncertain
launches. Report started, connected and real execution success separately.

The distribution build sets umask 022 for non-root-readable payloads; installation
credentials and state retain their explicit private permissions.

Expand All @@ -1565,8 +1584,9 @@ a provider selects microsandbox; `--provider` without enabling the option is an
error. Web-only mode cannot enable a sandbox provider. Core-only mode retains the
same opt-in rule. Missing KVM fails when microsandbox is selected without changing
that choice.
The distribution supplies native Core/helper binaries and pinned msb runtime and
firmware. For microsandbox, Core is a native systemd user service with direct
The thin distribution supplies native Core binaries. Provider helpers, the node
agent, Runtime launcher and pinned msb runtime/firmware are separate, same-revision
assets resolved only when selected. For microsandbox, Core is a native systemd user service with direct
`ExecStart` and `KillMode=process`: its restart must preserve the Provider's resident
microVM/helper processes. Never package those processes inside Core's container
PID namespace, kill their process group on Core stop, or add recovery mechanisms to
Expand All @@ -1580,7 +1600,7 @@ The basic distroless 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 distribution includes the sandbox-node binary. An enabled local node uses a
The node asset includes the sandbox-node binary. An enabled local node uses a
persistent private state directory, explicitly separate from read-only configuration.
Docker grants Core write access only to that node-state mount; native Core uses the
same installation-owned directory. Zero-node installs create no node identity
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ check-distribution:
go test ./services/core-console -count=1
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s deploy/install -p 'test_*.py'
PYTHONDONTWRITEBYTECODE=1 python3 scripts/core-distribution-manifest.test.py
bash -n deploy/install/install.sh scripts/build-core-console.sh scripts/build-core-distribution.sh
bash -n deploy/install/install.sh scripts/build-core-console.sh scripts/build-core-distribution.sh scripts/prepare-release-runtimes.sh
./scripts/build-core-console.sh

build-core-distribution:
Expand Down
2 changes: 1 addition & 1 deletion apps/web/e2e/sandbox-setup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ test("bundled console needs no extra admin key and provides one install command
await page.getByRole("button", { name: "Add node", exact: true }).click();
const command = page.getByLabel("One-time enrollment command");
await expect(command).toHaveValue(/fixture-once-token/);
await expect(command).toHaveValue(/\/node-install\/node_install.py/);
await expect(command).toHaveValue(/\/node-install\/node-install.pyz/);

await expect(page.getByRole("button", { name: "Copy node command" })).toBeVisible();
await expect(page.getByRole("dialog").getByRole("status")).toContainText("Waiting for your node");
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/features/sandbox/enrollment-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ describe("sandbox connection and enrollment", () => {
it("creates a compact verified installer command with no credential argument or redirect following", () => {
const command = nodeInstallCommand("secret'onetime", "https://core.example", "https://console.example", "docker", "installation", "a".repeat(64));
expect(command).toContain("PARSAR_NODE_ENROLLMENT_TOKEN='secret'\\''onetime' python3");
expect(command).toContain("https://console.example/node-install/node_install.py");
expect(command).toContain("https://console.example/node-install/node-install.pyz");
expect(command).toContain("sha256sum -c --status &&");
expect(command).toContain("--max-time 30 --max-filesize 1048576");
expect(command).not.toMatch(/--location| -L/);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/features/sandbox/enrollment-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const quote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;

export function nodeInstallCommand(token: string, coreUrl: string, sourceUrl: string, provider: string, installationId: string, scriptDigest: string): string {
return `(umask 077; d=$(mktemp -d) || exit; trap 'rm -rf "$d"' EXIT
curl -fsS --max-time 30 --max-filesize 1048576 ${quote(sourceUrl + "/node-install/node_install.py")} -o "$d/install.py" &&
curl -fsS --max-time 30 --max-filesize 1048576 ${quote(sourceUrl + "/node-install/node-install.pyz")} -o "$d/install.py" &&
printf '%s %s\\n' ${quote(scriptDigest)} "$d/install.py" | sha256sum -c --status &&
PARSAR_NODE_ENROLLMENT_TOKEN=${quote(token)} python3 "$d/install.py" --source-url ${quote(sourceUrl)} --core-url ${quote(coreUrl)} --provider ${quote(provider)} --installation-id ${quote(installationId)})`;
}
58 changes: 58 additions & 0 deletions contracts/agents-api/environment-executor-credentials.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Environment executor credentials

These Core extensions are outside the pinned upstream Agents API. They use the
same project caller authentication as Session creation, without `OpenAI-Beta`.
They require the exact authenticated creator of a live `self_hosted` Session;
project-shared Session read access does not grant issuance authority.

`POST /core/v1/environments/{environment_id}/executor-credentials` accepts:

```json
{"key_id":"CLIENT_GENERATED_UUID","rotate":false}
```

`key_id` is a canonical nonzero UUID chosen and retained before the request.
`rotate` is optional and defaults to false. The 201 response is the existing
daemon credential-file format:

```json
{"key_id":"UUID","environment_id":"ENVIRONMENT_UUID","executor_token":"ONE_TIME_SECRET"}
```

Responses use `Cache-Control: no-store`. Save the response directly to an owned
mode-0600 file; never place it in shell arguments, logs, a workspace or source.
Only its digest is persisted in Core. The secret authorizes daemon enrollment
and connection for this exact Environment, never public Session API calls,
sandbox-node enrollment or project resource operations. The application caller
key stays on the application machine and is never given to the Runtime.

Ordinary issuance with an existing `key_id` returns 409, even after revocation.
After an uncertain issuance response, explicitly send the same `key_id` with
`rotate:true` to replace the secret; do not automatically retry a rotation or
choose another key. Rotation preserves principal and Environment restriction,
invalidates the previous secret, and can restore an explicitly revoked key.
Credentials created without an exact Environment restriction by the operator
CLI cannot be managed through these routes.

`DELETE /core/v1/environments/{environment_id}/executor-credentials/{key_id}`
returns 204 and revokes the exact caller-owned restricted key. Repeated
revocation is safe. Other principals, targets and deleted Sessions return 404.
Revocation denies further authorized connection and dispatch; it does not stop
user-owned compute or prove that an existing process has stopped.

The distribution's `self-hosted-install.pyz` downloads the matching
`parsar-runtime` launcher, Runtime image and seccomp profile. The local launcher
uses the same Docker isolation and workspace layout as Core-managed V1, then
invokes the existing daemon `connect` with the unchanged returned Environment
ID and `remote_url`. Docker onboarding requires an externally reachable `wss`
URL; host loopback addresses are not rewritten inside the container. It creates
no sandbox node or Core-managed allocation. Workspace, credential and native
history volumes belong to the operator. Failed or uncertain launches retain
their volumes and installation receipt for inspection instead of replacing
history or retrying enrollment. Session deletion does not reclaim these volumes.
Rerunning the installer inspects a previously started container only after its
installation and Environment labels match. It reports running separately from
Session connection, or gives a command to start the same stopped container.
An uncertain launch without a success receipt gives label-filtered container
and volume inspection commands and never creates a replacement. A cached image
with the exact distribution digest and platform skips image download and import.
Loading
Loading