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
23 changes: 23 additions & 0 deletions .console/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,25 @@
_Chronological continuity log. Decisions, stop points, what changed and why._
_Not a task tracker — that's backlog.md. Keep entries concise and dated._

## 2026-05-19 — Phase 1 custodian fixes

- C13: added src/core_runner/process.py to c13_allowed_paths (os.environ.copy() is the env-overlay layer).
- T7: renamed tests/test_safe_run.py → tests/test_process.py to match parallel naming convention.
- X2: added contracts/** to X2 exclude_paths — PlatformManifest still has executor_runtime node key; CoreRunner→RxP edge will be wired in Phase 4 (ADR 0006).

## 2026-05-19 — ADR 0006 Phase 1: CoreRunner rename + safe_run() extraction

- Copied src/executor_runtime/ → src/core_runner/; bulk-renamed all imports, class names, error names.
- Created src/core_runner/process.py: SafeRunResult dataclass + safe_run() primitive (start_new_session, os.killpg on timeout, transient SIGTERM handler). No RxP dependency.
- Rewrote src/core_runner/runners/subprocess_runner.py to delegate to safe_run() — removes duplication.
- Updated src/core_runner/__init__.py: exports CoreRunner, safe_run, SafeRunResult.
- Updated pyproject.toml: name → core-runner, description updated, Repository URL → CoreRunner.
- Updated .custodian/config.yaml: repo_key → CoreRunner, src_root → src/core_runner, all path patterns updated.
- Bulk-updated all test imports: executor_runtime → core_runner, ExecutorRuntime → CoreRunner.
- Added tests/test_safe_run.py: 11 tests covering zero-exit, nonzero-exit, stdout/stderr capture, env overlay, cwd, timeout/kill, process-group kill, capture_output=False, dataclass shape. 76 tests pass.
- README rewritten for CoreRunner + dual surface (safe_run primitive + CoreRunner.run RxP path).
- src/executor_runtime/ left in place until GitHub repo rename (Phase 6) — old package unused by tests.

- 2026-05-19 — Removed live archon references from test fixtures. Updated
test_manual_runner.py (archon → dag_executor, archon-workflow → dag-executor).
Updated test_async_http_runner.py (archon-workflow → dag-executor). 65 tests pass.
Expand Down Expand Up @@ -81,3 +100,7 @@ truth; pre-push catches regressions before they hit GitHub.

- Added CLAUDE.md to .gitignore
- Added .custodian/tmp*.yaml to exclude custodian audit temp files

## 2026-05-19 — Remove old src/executor_runtime/ tree

Deleted legacy src/executor_runtime/ package now that all code lives in src/core_runner/. All 76 tests pass.
42 changes: 24 additions & 18 deletions .custodian/config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
repo_key: ExecutorRuntime
src_root: src/executor_runtime
repo_key: CoreRunner
src_root: src/core_runner
tests_root: tests

# ---------------------------------------------------------------------------
Expand All @@ -19,27 +19,33 @@ audit:
cross_repo:
platform_manifest_repo: ../PlatformManifest

# subprocess_runner.py constructs the env overlay for each subprocess run
# legitimately reads os.environ as the env-mutation layer.
# process.py merges the env overlay (os.environ.copy() + update)
# subprocess_runner.py delegates to it; both legitimately touch os.environ.
c13_allowed_paths:
- "src/executor_runtime/runners/subprocess_runner.py"
- "src/core_runner/process.py"
- "src/core_runner/runners/subprocess_runner.py"
exclude_paths:
T1:
- "src/executor_runtime/contracts/**"
- src/executor_runtime/errors.py
- "src/executor_runtime/io/**"
- src/executor_runtime/runners/base.py
- "src/core_runner/contracts/**"
- src/core_runner/errors.py
- "src/core_runner/io/**"
- src/core_runner/runners/base.py
T6:
- "src/executor_runtime/contracts/**"
- src/executor_runtime/errors.py
- "src/executor_runtime/io/**"
- src/executor_runtime/runners/base.py
- "src/core_runner/contracts/**"
- src/core_runner/errors.py
- "src/core_runner/io/**"
- src/core_runner/runners/base.py
T7:
- "src/executor_runtime/contracts/**"
- src/executor_runtime/errors.py
- "src/executor_runtime/io/**"
- src/executor_runtime/runners/base.py
- "src/core_runner/contracts/**"
- src/core_runner/errors.py
- "src/core_runner/io/**"
- src/core_runner/runners/base.py
X2:
# PlatformManifest node key will be updated to CoreRunner in Phase 4
# (ADR 0006). Until then the CoreRunner→RxP edge is registered under
# executor_runtime in the manifest — exclude to avoid false positives.
- "src/core_runner/contracts/**"
D11:
# http_runner / async_http_runner share _build_content_kwargs by
# design — sync/async pair of the same runner shape.
- "src/executor_runtime/runners/**"
- "src/core_runner/runners/**"
139 changes: 64 additions & 75 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
# ExecutorRuntime
# CoreRunner

`ExecutorRuntime` is the generic runtime execution layer for [RxP](https://github.com/ProtocolWarden/RxP)-shaped invocations. It dispatches by `runtime_kind` to a registered runner and returns a normalized RxP `RuntimeResult`.
`CoreRunner` is the process-group-safe subprocess library for the ProtocolWarden ecosystem. It provides two surfaces:

1. **`safe_run()` primitive** — standalone, no RxP dependency. Used by TeamExecutor, DAGExecutor, and CritiqueExecutor to replace raw `subprocess.run()` calls with full process-group safety.
2. **`CoreRunner.run(invocation)`** — full RxP invocation runner (stdout/stderr capture to files, ArtifactDescriptor production). Used by OperationsCenter's `direct_local` and `aider_local` adapters.

```text
RuntimeInvocation → ExecutorRuntime.run → RuntimeResult
├─ "subprocess" → SubprocessRunner
safe_run(cmd, ...) → SafeRunResult # lightweight primitive
CoreRunner.run(inv) → RuntimeResult # full RxP path

RuntimeInvocation → CoreRunner.run → RuntimeResult
├─ "subprocess" → SubprocessRunner → safe_run()
├─ "manual" → ManualRunner (caller-supplied dispatcher)
└─ "http" → HttpRunner (sync request/response)
```

## What this repo is

Generic runtime mechanics:

- subprocess execution with process-group safety (`start_new_session=True`, `os.killpg(SIGKILL)` on timeout, transient SIGTERM handler)
- environment overlay
- working directory control
- timeout enforcement
- stdout/stderr capture to files
- exit-code normalization
- ArtifactDescriptor collection
- dispatch-by-`runtime_kind` registry
- Process-group-safe subprocess execution (`start_new_session=True`, `os.killpg(SIGKILL)` on timeout, transient SIGTERM handler that reaps child group on supervisor death)
- Standalone `safe_run()` primitive — no RxP types, no artifact descriptors; just `SafeRunResult(returncode, stdout, stderr, timed_out)`
- Environment overlay, working directory control, timeout enforcement
- Stdout/stderr capture to files and ArtifactDescriptor collection (RxP path only)
- Dispatch-by-`runtime_kind` registry

## What this repo is not

- OperationsCenter — orchestration, planning, policy
- SwitchBoard — lane/backend selection
- SourceRegistry — source/fork/dependency tracking
- CxRP — orchestration contract
- TeamExecutor / DAGExecutor / CritiqueExecutor — AI execution backends (they consume `safe_run()`)
- a scheduler / queue system / fork manager / agent framework

## Quick start
Expand All @@ -36,39 +36,63 @@ Generic runtime mechanics:
pip install -e .
```

Dispatch an RxP invocation by `runtime_kind`:
### Primitive (no RxP dependency)

```python
from executor_runtime import ExecutorRuntime
result = ExecutorRuntime().run(invocation) # → RuntimeResult
from core_runner.process import safe_run

result = safe_run(["python", "-c", "print('hello')"], timeout_seconds=30)
print(result.returncode) # 0
print(result.stdout) # "hello\n"
print(result.timed_out) # False
```

See **Example usage** below for the full subprocess / manual / http flows.
### Full RxP path

```python
from core_runner import CoreRunner
result = CoreRunner().run(invocation) # → RuntimeResult
```

## Architecture

Single-entry dispatcher: `ExecutorRuntime.run(invocation)` reads `invocation.runtime_kind` and forwards to a registered runner. Three are bundled — `SubprocessRunner` (process-group-safe local exec), `ManualRunner` (caller-supplied callable), `HttpRunner` (kickoff + poll-until-terminal). Every runner returns a normalized RxP `RuntimeResult`. See **Runners** below for the per-kind contract.
`safe_run()` is the execution primitive — it owns all process-group logic. `SubprocessRunner` delegates to `safe_run()` and adds the file-capture / ArtifactDescriptor layer. `CoreRunner.run(invocation)` reads `invocation.runtime_kind` and forwards to a registered runner.

## Runners

| Runner | runtime_kind | What it does |
|---|---|---|
| `SubprocessRunner` | `subprocess` | Local subprocess with process-group safety. Default registered runner. |
| `ManualRunner` | `manual` | Forwards invocation to a caller-supplied dispatcher callable. For out-of-process services where ExecutorRuntime doesn't own the transport. |
| `HttpRunner` | `http` | Synchronous HTTP request/response. URL/method/body read from `RuntimeInvocation.metadata`. |
| `AsyncHttpRunner` | `http_async` | Async-shaped HTTP — kickoff (POST `→` 202 + run_id) then poll status URL until a terminal status. Sync from caller's POV. URL templates and JSON paths read from metadata. |

SSE streaming for async APIs is still deferred — track-able via the `runtime_kind` vocabulary if/when added.
| `SubprocessRunner` | `subprocess` | Local subprocess via `safe_run()`. Default registered runner. |
| `ManualRunner` | `manual` | Forwards invocation to a caller-supplied dispatcher callable. |
| `HttpRunner` | `http` | Synchronous HTTP request/response. |
| `AsyncHttpRunner` | `http_async` | 202 kickoff + poll-until-terminal. |

## Example usage

### Subprocess (default)
### safe_run() — primitive

```python
from executor_runtime import ExecutorRuntime
from executor_runtime.contracts import RuntimeInvocation
from core_runner.process import safe_run

result = safe_run(
["python", "script.py", "--arg", "value"],
cwd="/path/to/project",
env={"MY_VAR": "value"},
timeout_seconds=60,
)
if result.timed_out:
print("timed out")
elif result.returncode != 0:
print(f"failed: {result.stderr}")
```

### CoreRunner — subprocess (default)

runtime = ExecutorRuntime() # SubprocessRunner registered for "subprocess"
```python
from core_runner import CoreRunner
from core_runner.contracts import RuntimeInvocation

runtime = CoreRunner()

result = runtime.run(
RuntimeInvocation(
Expand All @@ -88,76 +112,41 @@ result = runtime.run(
print(result.status) # "succeeded"
```

### Manual (out-of-process service)
### CoreRunner — manual (out-of-process service)

```python
from executor_runtime import ExecutorRuntime
from executor_runtime.runners import ManualRunner
from core_runner import CoreRunner
from core_runner.runners import ManualRunner

def my_dispatcher(invocation):
# Your code: HTTP call, queue publish, RPC, whatever
raw = call_external_service(...)
return synthesize_runtime_result(invocation, raw)

runtime = ExecutorRuntime()
runtime = CoreRunner()
runtime.register("manual", ManualRunner(my_dispatcher))

result = runtime.run(invocation_with_kind_manual)
```

### HTTP (synchronous)

```python
from executor_runtime import ExecutorRuntime
from executor_runtime.runners import HttpRunner

runtime = ExecutorRuntime()
runtime.register("http", HttpRunner())

# Invocation metadata carries http.url + http.method + http.body
result = runtime.run(invocation_with_runtime_kind_http)
```

### HTTP (async-shaped — 202 + poll)

```python
from executor_runtime import ExecutorRuntime
from executor_runtime.runners import AsyncHttpRunner

runtime = ExecutorRuntime()
runtime.register("http_async", AsyncHttpRunner())

# Invocation metadata carries:
# http.url — kickoff URL (POST endpoint, 202 → {"run_id": "..."})
# http.poll_url_template — e.g. "https://api/runs/{run_id}"
# http.poll_run_id_path — dotted path to extract run_id from kickoff response
# http.poll_status_path — dotted path to extract status from poll response
# http.poll_terminal_states — comma-separated, e.g. "completed,failed,cancelled"
# http.poll_success_states — subset (default: "completed")
# http.poll_interval_seconds — default 2.0
result = runtime.run(invocation_with_runtime_kind_http_async)
```

## Installation

```bash
pip install executor-runtime
pip install core-runner
# or with HTTP support:
pip install "executor-runtime[http]"
pip install "core-runner[http]"
```

For development:

```bash
git clone https://github.com/ProtocolWarden/ExecutorRuntime.git
cd ExecutorRuntime
git clone https://github.com/ProtocolWarden/CoreRunner.git
cd CoreRunner
pip install -e ".[dev,http]"
pytest -q
```

## Contracts

ExecutorRuntime consumes RxP types directly — no parallel dataclasses:
CoreRunner consumes RxP types directly:

```python
from rxp.contracts import RuntimeInvocation, RuntimeResult, ArtifactDescriptor
Expand Down
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "executor-runtime"
name = "core-runner"
version = "0.1.0"
description = "Runtime execution layer for normalized RxP invocations"
description = "Process-group-safe subprocess primitives and RxP invocation runner"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"rxp @ git+https://github.com/ProtocolWarden/RxP.git",
]

[project.urls]
Repository = "https://github.com/ProtocolWarden/CoreRunner"

[project.optional-dependencies]
http = [
"httpx>=0.27",
Expand Down
6 changes: 6 additions & 0 deletions src/core_runner/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 ProtocolWarden
from core_runner.process import SafeRunResult, safe_run
from core_runner.runtime import CoreRunner

__all__ = ["CoreRunner", "safe_run", "SafeRunResult"]
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""ExecutorRuntime contract surface — canonical RxP types.
"""CoreRunner contract surface — canonical RxP types.

ExecutorRuntime delegates contract semantics to RxP:
CoreRunner delegates contract semantics to RxP:
- ``RuntimeInvocation`` — what to run
- ``RuntimeResult`` — what came back
- ``ArtifactDescriptor`` — file artifacts produced by a run
Expand All @@ -10,7 +10,7 @@
``pending | running | succeeded | failed | timed_out | cancelled |
rejected``.
"""
from executor_runtime.contracts.invocation import RuntimeInvocation
from executor_runtime.contracts.result import ArtifactDescriptor, RuntimeResult
from core_runner.contracts.invocation import RuntimeInvocation
from core_runner.contracts.result import ArtifactDescriptor, RuntimeResult

__all__ = ["RuntimeInvocation", "RuntimeResult", "ArtifactDescriptor"]
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""ExecutorRuntime consumes the canonical RxP RuntimeInvocation contract.
"""CoreRunner consumes the canonical RxP RuntimeInvocation contract.

Re-exported here so callers can ``from executor_runtime.contracts
Re-exported here so callers can ``from core_runner.contracts
import RuntimeInvocation`` without depending on the RxP package
directly.
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""ExecutorRuntime returns the canonical RxP RuntimeResult contract."""
"""CoreRunner returns the canonical RxP RuntimeResult contract."""
from rxp.contracts import ArtifactDescriptor, RuntimeResult

__all__ = ["RuntimeResult", "ArtifactDescriptor"]
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 ProtocolWarden
class ExecutorRuntimeError(Exception):
class CoreRunnerError(Exception):
"""Base exception for executor runtime package."""
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 ProtocolWarden
from executor_runtime.io.json_io import read_invocation, write_result
from core_runner.io.json_io import read_invocation, write_result

__all__ = ["read_invocation", "write_result"]
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import json
from pathlib import Path

from executor_runtime.contracts.invocation import RuntimeInvocation
from executor_runtime.contracts.result import RuntimeResult
from core_runner.contracts.invocation import RuntimeInvocation
from core_runner.contracts.result import RuntimeResult


def read_invocation(path: str) -> RuntimeInvocation:
Expand Down
Loading
Loading