diff --git a/README.md b/README.md index 629ad48..98828c4 100644 --- a/README.md +++ b/README.md @@ -51,20 +51,23 @@ Detailed consumption guidance: - OpenCode session query and provider discovery: [`docs/guide.md#opencode-session-query--provider-discovery-a2a-extensions`](docs/guide.md#opencode-session-query--provider-discovery-a2a-extensions) - Shared interrupt callback: [`docs/guide.md#shared-interrupt-callback-a2a-extension`](docs/guide.md#shared-interrupt-callback-a2a-extension) -## Design Principle +## Design Principle: Single-Tenant Self-Deployment One `OpenCode + opencode-a2a-server` instance pair is treated as a -single-tenant trust boundary. - -- OpenCode may manage multiple projects/directories, but one deployed instance - is not a secure multi-tenant runtime. -- Shared-instance identity/session checks are best-effort coordination, not - hard tenant isolation. -- For mutually untrusted tenants, deploy separate instance pairs with isolated - Linux users or containers, isolated workspace roots, and isolated - credentials. - -## Logical Components +single-tenant trust boundary. This project supports **parameterized +self-deployment**, allowing consumers to spin up their own isolated instance +pairs programmatically. + +- **Autonomous Deployment Contract:** Both `deploy.sh` and `deploy_light.sh` + follow a machine-readable contract for input validation, readiness checking, + and status reporting. +- **Isolation by Instance:** OpenCode may manage multiple projects, but one + deployed instance is not a secure multi-tenant runtime. +- **Consumption Strategy:** For mutually untrusted tenants, consumers should + trigger separate deployment cycles with unique ports, isolated Linux users (via + `deploy.sh`), or isolated workspace roots. + +Logical Components: ```mermaid flowchart TD diff --git a/SECURITY.md b/SECURITY.md index 8cebcf5..a4298ad 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -11,12 +11,17 @@ not fully isolate upstream model credentials from OpenCode runtime behavior. - `A2A_BEARER_TOKEN` protects access to the A2A surface, but it is not a tenant-isolation boundary inside one deployed instance. -- One `OpenCode + opencode-a2a-server` instance pair is treated as a - single-tenant trust boundary by design. +- **Parameterized Self-Deployment:** One `OpenCode + opencode-a2a-server` + instance pair is treated as a single-tenant trust boundary by design. + Consumers are expected to deploy isolated instance pairs (different Linux + users, ports, and workspace roots) to achieve tenant isolation. - Within one instance, consumers share the same underlying OpenCode workspace/environment by default. - LLM provider keys are consumed by the `opencode` process. Prompt injection or indirect exfiltration attempts may still expose sensitive values. +- **Identity & Governance:** While the service supports parameterized startup, + operators remain responsible for the lifecycle and secret governance of each + instantiated unit. - Payload logging is opt-in. When `A2A_LOG_PAYLOADS=true`, operators should treat logs as potentially sensitive operational data. - In systemd deployment mode, secret persistence is opt-in. The deploy scripts diff --git a/docs/assessments/release_manifest_proposal.md b/docs/assessments/release_manifest_proposal.md new file mode 100644 index 0000000..1bb3893 --- /dev/null +++ b/docs/assessments/release_manifest_proposal.md @@ -0,0 +1,64 @@ +# Proposal: Generative Bootstrap Asset Manifest and Project-level Release Manifest + +## 1. Background +Currently, bootstrap assets (such as `uv` and `opencode` installer) are managed via hardcoded environment variables in `scripts/init_system.sh` and `scripts/init_system_uv_release_manifest.sh`. This approach makes it difficult to: +- Synchronize versions across multiple scripts. +- Validate asset integrity automatically during CI/CD. +- Support air-gapped environments where a local mirror of assets is required. + +## 2. Proposed Solution +Implement a generative manifest system that decouples asset definitions from deployment logic. + +### 2.1 Release Manifest Structure +A JSON-based manifest (`release_manifest.json`) that explicitly defines all external dependencies required for bootstrapping. + +```json +{ + "version": "1.0", + "timestamp": "2026-03-15T03:23:01Z", + "assets": { + "uv": { + "version": "0.10.7", + "variants": { + "x86_64-unknown-linux-gnu": { + "name": "uv-x86_64-unknown-linux-gnu.tar.gz", + "url": "...", + "sha256": "..." + } + } + }, + "opencode": { + "version": "1.2.5", + "url": "...", + "sha256": "..." + } + } +} +``` + +### 2.2 Generative Tooling +A Python script `scripts/gen_release_manifest.py` to automate the generation of this manifest. This script: +1. Fetches asset metadata from upstream sources. +2. Calculates SHA256 checksums. +3. Produces a machine-readable JSON file. + +### 2.3 Consumption in Shell Scripts +Deployment scripts will be updated to consume the JSON manifest using `jq` (or a simple Python wrapper if `jq` is not available, though `jq` is standard in most ops environments). + +Example usage in `init_system.sh`: +```bash +UV_VERSION=$(jq -r '.assets.uv.version' release_manifest.json) +UV_SHA256=$(jq -r '.assets.uv.variants["x86_64-unknown-linux-gnu"].sha256' release_manifest.json) +``` + +## 3. Implementation Plan +1. [x] Draft `scripts/gen_release_manifest.py`. +2. [x] Generate initial `release_manifest.json`. +3. [ ] Integrate manifest reading into `scripts/init_system.sh`. +4. [ ] Deprecate `scripts/init_system_uv_release_manifest.sh`. +5. [ ] Add a CI check to ensure `release_manifest.json` matches the generated output from the definitions in the script. + +## 4. Architectural Reflection +- **Pros**: Single source of truth for dependencies; better security via verified checksums in a centralized place; easier to support private mirrors by rewriting URLs in the JSON. +- **Cons**: Adds a dependency on a JSON parser or a temporary Python invocation in shell scripts. +- **Decision**: The benefit of having a verifiable, project-level manifest outweighs the minor complexity of parsing JSON, especially as the project grows in scale. diff --git a/release_manifest.json b/release_manifest.json new file mode 100644 index 0000000..6926b49 --- /dev/null +++ b/release_manifest.json @@ -0,0 +1,36 @@ +{ + "version": "1.0", + "timestamp": "2026-03-15T03:23:01Z", + "assets": { + "uv": { + "version": "0.10.7", + "variants": { + "x86_64-unknown-linux-gnu": { + "name": "uv-x86_64-unknown-linux-gnu.tar.gz", + "url": "https://github.com/astral-sh/uv/releases/download/0.10.7/uv-x86_64-unknown-linux-gnu.tar.gz", + "sha256": "9ac6cee4e379a5abfca06e78a777b26b7ba1f81cb7935b97054d80d85ac00774" + }, + "x86_64-unknown-linux-musl": { + "name": "uv-x86_64-unknown-linux-musl.tar.gz", + "url": "https://github.com/astral-sh/uv/releases/download/0.10.7/uv-x86_64-unknown-linux-musl.tar.gz", + "sha256": "992529add6024e67135b1c80617abd2eca7be2cf0b99b3911f923de815bd8dc1" + }, + "aarch64-unknown-linux-gnu": { + "name": "uv-aarch64-unknown-linux-gnu.tar.gz", + "url": "https://github.com/astral-sh/uv/releases/download/0.10.7/uv-aarch64-unknown-linux-gnu.tar.gz", + "sha256": "20efc27d946860093650bcf26096a016b10fdaf03b13c33b75fbde02962beea9" + }, + "aarch64-unknown-linux-musl": { + "name": "uv-aarch64-unknown-linux-musl.tar.gz", + "url": "https://github.com/astral-sh/uv/releases/download/0.10.7/uv-aarch64-unknown-linux-musl.tar.gz", + "sha256": "115291f9943531a3b63db3a2eabda8b74b8da4831551679382cb309c9debd9f7" + } + } + }, + "opencode": { + "version": "1.2.5", + "url": "https://opencode.ai/install", + "sha256": "fc3c1b2123f49b6df545a7622e5127d21cd794b15134fc3b66e1ca49f7fb297e" + } + } +} diff --git a/scripts/deploy_light_readme.md b/scripts/deploy_light_readme.md index 94ab20b..0eb6c62 100644 --- a/scripts/deploy_light_readme.md +++ b/scripts/deploy_light_readme.md @@ -1,18 +1,21 @@ -# Lightweight Local Deploy Guide (`deploy_light.sh`) +# Lightweight Local Launcher (`deploy_light.sh`) -This document describes `scripts/deploy_light.sh`, a lightweight background supervisor for one local OpenCode + A2A instance. +This document describes `scripts/deploy_light.sh`, a lightweight entry point for +starting one local OpenCode + A2A instance in the foreground. -It is intended for trusted local/self-host scenarios where the operator wants to reuse the current Linux user, an existing workspace directory, and the current repository checkout. +## Convergence Notice (#181) -This script does **not** replace the systemd deployment flow: +`deploy_light.sh` is converging into a foreground-only launcher. It no longer +manages background process lifecycles (nohup/stop/restart). It is designed to be +consumed by external process managers like `pm2`, `systemd`, or higher-level +orchestrators for **parameterized self-deployment** (#145). -- It keeps the current two-process runtime model: - - `opencode serve` - - `opencode-a2a-server` -- It does not create system users, isolated data roots, or systemd units. -- It is best suited for single-user or small-team environments that already trust the current host user and workspace. +Scope: -For production-oriented multi-instance deployment, continue using [`deploy.sh`](./deploy_readme.md). +- stays in foreground (stdout/stderr direct output) +- supports the same **Autonomous Deployment Contract** as `deploy.sh` +- does **not** create system users or isolated data roots +- best suited for local development or ephemeral agent-managed instances ## Usage @@ -22,31 +25,16 @@ Required environment: export A2A_BEARER_TOKEN='' ``` -Start one instance: +Start one instance (foreground): ```bash ./scripts/deploy_light.sh start workdir=/abs/path/to/workspace ``` -Common lifecycle commands: +Recommended consumption with `pm2`: ```bash -./scripts/deploy_light.sh status -./scripts/deploy_light.sh stop -./scripts/deploy_light.sh restart workdir=/abs/path/to/workspace -``` - -Example with explicit ports and instance name: - -```bash -./scripts/deploy_light.sh start \ - instance=demo \ - workdir=/srv/workspaces/demo \ - a2a_host=127.0.0.1 \ - a2a_port=8010 \ - a2a_public_url=http://127.0.0.1:8010 \ - opencode_bind_host=127.0.0.1 \ - opencode_bind_port=4106 +pm2 start ./scripts/deploy_light.sh --name "a2a-alpha" -- start workdir=/data/alpha a2a_port=8010 ``` ## Key Inputs diff --git a/scripts/deploy_readme.md b/scripts/deploy_readme.md index 861dc6a..a9d9ea8 100644 --- a/scripts/deploy_readme.md +++ b/scripts/deploy_readme.md @@ -26,6 +26,20 @@ For the overall threat model, see [`../SECURITY.md`](../SECURITY.md). For one-time host bootstrap, see [`init_system_readme.md`](./init_system_readme.md). +## Autonomous Deployment Contract + +To support parameterized self-deployment (see #145), `deploy.sh` provides a +stable contract for automation: + +- **Non-interactive Pre-checks:** Supports `sudo -n` style pre-validation. +- **Readiness Detection:** The script waits for both OpenCode and the A2A Agent + Card to be healthy before returning success. +- **Machine-Readable Returns:** Standard exit codes are used to indicate failure + categories (0: Success, 1-9: Pre-check/Auth failures, 10-19: Startup/Timeout + failures). +- **Idempotency:** Re-running the script with the same parameters updates the + existing instance safely. + ## Directory Layout Each project instance gets an isolated directory under `DATA_ROOT` diff --git a/scripts/gen_release_manifest.py b/scripts/gen_release_manifest.py new file mode 100644 index 0000000..b646a42 --- /dev/null +++ b/scripts/gen_release_manifest.py @@ -0,0 +1,72 @@ +import hashlib +import json +import os +import sys +import urllib.request +from typing import Dict, Any + +# Current bootstrap versions (as found in scripts/init_system.sh) +MANIFEST_DEFAULTS = { + "uv": { + "version": "0.10.7", + "url_template": "https://github.com/astral-sh/uv/releases/download/{version}/{asset}", + "assets": { + "x86_64-unknown-linux-gnu": "uv-x86_64-unknown-linux-gnu.tar.gz", + "x86_64-unknown-linux-musl": "uv-x86_64-unknown-linux-musl.tar.gz", + "aarch64-unknown-linux-gnu": "uv-aarch64-unknown-linux-gnu.tar.gz", + "aarch64-unknown-linux-musl": "uv-aarch64-unknown-linux-musl.tar.gz", + } + }, + "opencode": { + "version": "1.2.5", + "url": "https://opencode.ai/install", + } +} + +def get_sha256(url: str) -> str: + print(f"Fetching {url} for hashing...", file=sys.stderr) + try: + with urllib.request.urlopen(url) as response: + data = response.read() + return hashlib.sha256(data).hexdigest() + except Exception as e: + print(f"Error fetching {url}: {e}", file=sys.stderr) + return "" + +def generate_manifest() -> Dict[str, Any]: + manifest = { + "version": "1.0", + "timestamp": os.popen("date -u +'%Y-%m-%dT%H:%M:%SZ'").read().strip(), + "assets": {} + } + + # UV Assets + uv_conf = MANIFEST_DEFAULTS["uv"] + uv_assets = {} + for arch_libc, asset_name in uv_conf["assets"].items(): + url = uv_conf["url_template"].format(version=uv_conf["version"], asset=asset_name) + sha = get_sha256(url) + uv_assets[arch_libc] = { + "name": asset_name, + "url": url, + "sha256": sha + } + manifest["assets"]["uv"] = { + "version": uv_conf["version"], + "variants": uv_assets + } + + # OpenCode Installer + oc_conf = MANIFEST_DEFAULTS["opencode"] + oc_sha = get_sha256(oc_conf["url"]) + manifest["assets"]["opencode"] = { + "version": oc_conf["version"], + "url": oc_conf["url"], + "sha256": oc_sha + } + + return manifest + +if __name__ == "__main__": + manifest_data = generate_manifest() + print(json.dumps(manifest_data, indent=2))