Skip to content
Closed
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
27 changes: 15 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 64 additions & 0 deletions docs/assessments/release_manifest_proposal.md
Original file line number Diff line number Diff line change
@@ -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.
36 changes: 36 additions & 0 deletions release_manifest.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
44 changes: 16 additions & 28 deletions scripts/deploy_light_readme.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -22,31 +25,16 @@ Required environment:
export A2A_BEARER_TOKEN='<a2a-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
Expand Down
14 changes: 14 additions & 0 deletions scripts/deploy_readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
72 changes: 72 additions & 0 deletions scripts/gen_release_manifest.py
Original file line number Diff line number Diff line change
@@ -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))
Loading