diff --git a/.gitignore b/.gitignore
index 1d7998b..ba6777e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,7 +27,7 @@ Thumbs.db
._*
# Canyon artifacts. `.car` is generated from the application source by the
-# porting skill and `ventis build`; it is never committed.
+# porting skill and `canyonos build`; it is never committed.
.car/
# Generated stubs
@@ -48,6 +48,6 @@ uv.lock
Agent Artifacts
docs/
-# testing-porting-to-ventis working tree: clones, artifacts, results db
-.ventis-tests/
+# testing-porting-to-canyonos working tree: clones, artifacts, results db
+.canyonos-tests/
.harness/
diff --git a/README.md b/README.md
index 4cb7ed0..fe306d1 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,11 @@
-
+
-Ventis is a bottom-up control plane and agent serving framework that enables developers to build, deploy and control agentic workflow serving with ease. Ventis derives it's name from the latin word 'ventus' meaning wind. True to its name, Ventis is like the wind, invisible but always present.
+CanyonOS is a bottom-up control plane and agent serving framework that enables developers to build, deploy and control agentic workflow serving with ease. CanyonOS derives it's name from the latin word 'ventus' meaning wind. True to its name, CanyonOS is like the wind, invisible but always present.
## Core Features
-- **Easy development and deployment**: Developers write agents in python as if writing single node local code. Ventis takes care of deployment, management and orchestration of agents and workflows. Deployment engineers running this workflow can specify authorization and other serving policies, Ventis will enforce them.
+- **Easy development and deployment**: Developers write agents in python as if writing single node local code. CanyonOS takes care of deployment, management and orchestration of agents and workflows. Deployment engineers running this workflow can specify authorization and other serving policies, CanyonOS will enforce them.
- **Distributed Futures**: Asynchronous execution without any user workflow modification.
- **Pluggable Policy Engine**: Supports multiple policies for orchestration, authorization and other serving policies.
@@ -16,17 +16,17 @@ Ventis is a bottom-up control plane and agent serving framework that enables dev
### 1. Installation
```bash
-git clone https://github.com/your-repo/ventis.git
-cd ventis
+git clone https://github.com/your-repo/canyonos.git
+cd canyonos
pip install -e .
```
-Note: Installation of ventis only needs to be done on the machine where you are running the deploy command. It does not need to be installed on the remote hosts where the agents are deployed. Ventis runs the built container images on the target hosts; for remote EC2 deployments, make sure the image is already available on the host.
+Note: Installation of canyonos only needs to be done on the machine where you are running the deploy command. It does not need to be installed on the remote hosts where the agents are deployed. CanyonOS runs the built container images on the target hosts; for remote EC2 deployments, make sure the image is already available on the host.
### 2. Prerequisites
- **Python 3.10+**
- **Docker** — Used to manage agents.
-- **Docker Buildx** (optional) — If available, `ventis build` builds all agent/workflow images in a single parallel `docker buildx bake` pass; otherwise it falls back to building them sequentially.
+- **Docker Buildx** (optional) — If available, `canyonos build` builds all agent/workflow images in a single parallel `docker buildx bake` pass; otherwise it falls back to building them sequentially.
---
@@ -34,7 +34,7 @@ Note: Installation of ventis only needs to be done on the machine where you are
#### Step 1: Create a Project
```bash
-ventis new-project my-app
+canyonos new-project my-app
cd my-app
```
This command creates a new directory `my-app` with the following structure:
@@ -73,7 +73,7 @@ Edit `.car/config/global_controller.yaml` in your project directory to list the
#### Step 1.1: Passing secrets to agents (optional)
-Agents that need API keys read them from environment variables. Point `env_file` at a `.env` file to have Ventis inject it into every agent container:
+Agents that need API keys read them from environment variables. Point `env_file` at a `.env` file to have CanyonOS inject it into every agent container:
```yaml
# .car/config/global_controller.yaml
@@ -91,7 +91,7 @@ env_file: .env
#### Step 2: Build the project
```bash
-ventis build
+canyonos build
```
#### Step 2.1 (Only if performing distributed deployment):
If you are deploying agents and tools to multiple hosts, make sure the hosts are reachable from the machine where you are running the deploy command and that SSH key-based access is already configured. A guide to set that up can be found [here](https://www.redhat.com/en/blog/passwordless-ssh).
@@ -99,12 +99,12 @@ If you are deploying agents and tools to multiple hosts, make sure the hosts are
#### Step 3: Deploy the project
```bash
-ventis deploy
+canyonos deploy
```
#### Step 4: Sending requests to the workflow
-Upon running the deploy command, ventis automatically generates a REST API endpoint for the workflow.
+Upon running the deploy command, canyonos automatically generates a REST API endpoint for the workflow.
Users can send requests to this endpoint to trigger the workflow. For this example, workflow to send a request -
```bash
@@ -124,17 +124,17 @@ curl http://localhost:8080/status/
Remove all generated stub and gRPC files:
```bash
-ventis clean
+canyonos clean
```
-### Harnessing the power of Ventis
-Beyond an easy programming model and end-to-end deployment. Ventis, enables developers to write custom policies to perform fine-grained control over their agents, workflows.
+### Harnessing the power of CanyonOS
+Beyond an easy programming model and end-to-end deployment. CanyonOS, enables developers to write custom policies to perform fine-grained control over their agents, workflows.
Currently, we support two types of policies, with plans to add more in the future.
* **Authorization Policies**: Define rules based on the fields in the request to restrict agent access. For example, `examples/config/policy.yaml` defines rules to restrict access to the `FinanceAgent` to only authorized callers like 'CEO' or 'Analyst'. A developer can specify rules based on the fields in the request to restrict agent access.
-* **Load Balancing & Efficiency**: Ventis has built-in policies to perform load-balancing across multiple instances of the same agent. Request migrations ease head-of-line blocking, and our experiments show that Ventis's performance control can reduce tail latencies and enable efficient GPU utilization. Here is an example of the results.
+* **Load Balancing & Efficiency**: CanyonOS has built-in policies to perform load-balancing across multiple instances of the same agent. Request migrations ease head-of-line blocking, and our experiments show that CanyonOS's performance control can reduce tail latencies and enable efficient GPU utilization. Here is an example of the results.

@@ -150,7 +150,7 @@ For more details, please refer to our paper - [Nalar: An agent serving framework
### Citation
-If you find Ventis (Nalar) useful for your research, please cite our paper:
+If you find CanyonOS (Nalar) useful for your research, please cite our paper:
```bibtex
@misc{laju2026nalar,
title={Nalar: An agent serving framework},
diff --git a/VENTIS_TO_CANYONOS_RENAME.md b/VENTIS_TO_CANYONOS_RENAME.md
new file mode 100644
index 0000000..43c2d4e
--- /dev/null
+++ b/VENTIS_TO_CANYONOS_RENAME.md
@@ -0,0 +1,691 @@
+# Ventis → CanyonOS Rename: Verified Migration Plan
+
+> **Audit status (2026-09-05):** re-checked against the complete tracked tree,
+> hidden files, ignored/generated state, current tests, and Canyon Code company
+> memory. Seven independent Luna agents audited runtime, packaging, infrastructure,
+> tests, documentation, edge cases, and inventory. This file is analysis and an
+> execution plan only; no runtime code has been renamed yet.
+
+## Executive verdict
+
+Do **not** implement this as a global search-and-replace. The current tree has one
+blocking architecture decision and several versioned contracts that require an
+additive compatibility phase.
+
+1. **Blocking package-name collision.** The root distribution/package is currently
+ `ventis` (`pyproject.toml:2,24,31`), while `cli/` already owns the distribution,
+ import package, and executable name `canyonos` (`cli/pyproject.toml:2,14,21`).
+ Renaming the root package and distribution to `canyonos` would make two editable
+ projects provide the same distribution and top-level package. A temporary
+ reproduction fails `uv lock --offline` with conflicting URLs for `canyonos`.
+2. **The repository deliberately documents old names as compatibility protocol.**
+ `.claude/skills/porting-to-canyonos-core/SKILL.md:4-12` and
+ `references/runtime-contract.md:1-5` currently require the `ventis` Python/CLI,
+ `VENTIS_*` variables, and `ventis-*` Docker names. That policy must be replaced
+ or deprecated; simply changing code makes the skill teach users the wrong API.
+3. **Generated artifacts are part of the runtime contract.** The build generator
+ creates a nested `ventis.llm_proxy` package, copies `ventis_context.py`, writes
+ `VENTIS_AGENT_*`, and the generated controller launches `python -m
+ ventis.llm_proxy`. Updating only the source package will produce images that
+ build but fail at runtime.
+4. **The host CLI participates in the old protocol.** Contrary to the previous
+ draft, `cli/canyonos/init.py:156` writes `VENTIS_REDIS_HOST` into the Global
+ Controller container. It also consumes old Docker prefixes and the validator's
+ `capabilities.ventis` JSON field.
+5. **The test baseline is not green.** A clean `uv run pytest -q` currently stops
+ with nine collection errors because `local_controler_pb2` is not generated.
+ With both protos generated into a temporary `PYTHONPATH`, the baseline is
+ **212 passed, 11 failed, 3 subtests passed**. Those 11 failures are pre-existing
+ behavior/test drift, not rename regressions.
+
+The safe route is: decide package ownership, introduce CanyonOS names alongside
+legacy readers/aliases, switch every producer and consumer together, validate
+fresh and upgrade deployments, then remove compatibility names in a later major
+release. A literal zero-match tree is the **end state**, not a safe first commit.
+
+> **NOTE:** The operator has since chosen a **hard cutover with no compatibility
+> shims** (see "Locked decisions" below). That decision **supersedes** every
+> "additive/compat/fallback/dual-read/shim" recommendation in this document. The
+> *contract coupling* (which producers and consumers must change together) still
+> fully applies — only the transitional fallbacks are dropped. Where a section
+> below proposes old+new fallbacks, read it as "change producer and consumer to the
+> new name in the same commit; delete the old name outright."
+
+## Locked decisions (2026-09-05, operator-approved)
+
+Hard cutover. **No** legacy env-var dual-read, **no** legacy import alias, **no**
+old-header fallback, **no** compatibility release window. Existing running
+deployments must be torn down and rebuilt; this is an accepted breaking change.
+
+| Surface | Decision |
+|---|---|
+| Core import package / dir | `canyonos_core` (dir `ventis/` → `canyonos_core/`); all imports `from canyonos_core…` |
+| Core distribution name | `canyonos-core` |
+| `ventis_context` module + alias | `canyonos_context.py` / alias `canyonos_context` |
+| Env vars | `VENTIS_*` → `CANYONOS_*` (⚠ collision note below) |
+| Resource prefix (network/image/container/redis/tags/ids) | `canyonos-*` (`canyonos-local`, `canyonos-`, `canyonos-redis-*`, `canyonos-ec2-*`) |
+| Future-ID HTTP header | `X-Canyonos-Future-ID` (`Canyonos` cased, `ID` all-caps) — injector **and** reader standardized to this exact spelling |
+| Published GC image | **unchanged** — stays `saakeths/canyonos:latest` |
+| Core executable | **removed** — core is import-only; the standalone `canyonos` CLI is the sole console script |
+| Root `pyproject.toml` | **NOT deleted — rewritten import-only** (deleting breaks the container build; see below) |
+| `uv.lock` | regenerate **after** the rename + pyproject rewrite land; never hand-edit |
+| SSH key default | **unchanged** — stays `~/.ssh/ventis_ec2` for now |
+| Logo asset + README clone URL | **unchanged** for now (`images/ventis-logo.png`, git URL) |
+
+**Root `pyproject.toml` is critical — do not delete.** `ventis/Dockerfile:5-6`
+runs `COPY . /ventis` + `RUN pip install /ventis`, which requires the root
+`pyproject.toml`. It also supplies the entire runtime dependency set (boto3,
+grpcio(-tools), redis, sqlalchemy, psycopg, flask, opentelemetry-\*) and the
+`[tool.setuptools.package-data]` that ships `controller/proto/*.proto` and
+`controller/utils/aws_pricing_chart.db` into the installed package. Deleting it
+makes the container build fail and the runtime lose its bundled data. **Rewrite it
+instead:**
+- `name = "canyonos-core"`, `version` kept;
+- **drop** `[project.scripts]` entirely (import-only);
+- **drop** `[dependency-groups] dev` `canyonos` entry **and** `[tool.uv.sources]
+ canyonos = { path = "cli", editable = true }` — this self-dependency is exactly
+ what caused the `uv lock` collision noted in company memory; removing it is what
+ makes the two distributions coexist;
+- `[tool.setuptools.packages.find] include = ["canyonos_core*"]`;
+- `[tool.setuptools.package-data]` key `ventis` → `canyonos_core` (and drop the
+ stale `templates/**/*` entry — that dir no longer exists);
+- Ty `[tool.ty.*]` include/exclude/allowed-unresolved-import paths
+ (`ventis` → `canyonos_core`, `ventis_context` → `canyonos_context`).
+
+**⚠ `CANYONOS_*` collision watch.** `VENTIS_REDIS_HOST`/`VENTIS_REDIS_PORT` become
+`CANYONOS_REDIS_HOST`/`CANYONOS_REDIS_PORT`, which are **also** the names the
+user-side dashboard stack already writes (`cli/canyonos/dashboard_stack.py:227-228`).
+They live in different process/compose scopes (core GC/agent containers vs. the
+dashboard compose), so there is no runtime clash today — but the names are now
+semantically overloaded. Verify no single process reads both; if that ever changes,
+the core vars would need a `CANYONOS_CORE_*` namespace.
+
+## Verified inventory
+
+The canonical count uses the tracked `HEAD` tree (before this currently untracked
+analysis file is added) and case-insensitive token matches. All future recounts
+must exclude this document so it does not count its own inventory:
+
+| Scope | Matching files | Occurrences | Matching lines |
+|---|---:|---:|---:|
+| Source tree, excluding this document and `uv.lock` | 93 | 628 | 578 |
+| `uv.lock` | 1 | 1 | 1 |
+| Source tree including `uv.lock`, excluding this document | 94 | 629 | 579 |
+
+Exact-case totals excluding `uv.lock` are 458 `ventis`, 62 `Ventis`, and 108
+`VENTIS`. There are 173 tracked files total. The 94 matching files break down as:
+
+| Area | Files with content matches |
+|---|---:|
+| Root runtime directory | 36 |
+| Tests | 25 |
+| Examples | 15 |
+| Vendored porting skill | 7 |
+| Standalone CLI | 7 |
+| Root metadata/docs | 4 |
+
+There are 55 tracked paths containing the old name: all 53 files under `ventis/`,
+plus `images/ventis-logo.png` and `tests/test_ventis_context.py`. The migration
+document's own filename/content must be excluded while work is in progress and
+renamed or archived at final cleanup.
+
+Reproduce the audit with:
+
+```bash
+git grep -I -i -l ventis -- . ':!uv.lock' ':!VENTIS_TO_CANYONOS_RENAME.md' | wc -l
+git grep -I -i -o ventis -- . ':!uv.lock' ':!VENTIS_TO_CANYONOS_RENAME.md' | wc -l
+git grep -I -i -n ventis -- . ':!VENTIS_TO_CANYONOS_RENAME.md'
+git ls-files | rg -i 'ventis'
+find . -path './.git' -prune -o -iname '*ventis*' -print
+rg --hidden --no-ignore -i ventis -g '!.git/**'
+```
+
+`git grep` is the tracked-source authority; the final `rg` catches stale virtual
+environments, editable-install metadata, generated `.car/` output, and other
+ignored files that can mask a bad migration.
+
+## Decision gate 1: package and executable ownership
+
+This must be resolved before moving `ventis/`. **RESOLVED** — see Locked decisions.
+
+### Topology (locked)
+
+| Surface | Owner/name |
+|---|---|
+| User-facing distribution | existing `cli/` distribution: `canyonos` |
+| User-facing import package | existing `cli/canyonos/` |
+| User-facing executable | existing `canyonos` console script (the **only** one) |
+| Core runtime distribution | `canyonos-core` |
+| Core runtime import package | `canyonos_core` |
+| In-container entrypoints | `python -m canyonos_core.server`, `.cli`, `.llm_proxy` |
+| Legacy package/executable | **none** — hard cutover, no shim; core console script removed |
+
+This keeps the already-thin host CLI independent and avoids two wheels overwriting
+one `canyonos/` directory. It also lets the root runtime be versioned independently
+inside `saakeths/canyonos:`.
+
+The valid alternative is to merge the root runtime into the existing CLI
+distribution under one intentionally owned `canyonos` tree. That is a larger
+packaging refactor and must include dependency, image, and release ownership.
+
+**Invalid topology:** changing root `name`, package include, directory, and console
+script to `canyonos` while leaving `cli/pyproject.toml` unchanged. It breaks lock
+resolution, editable installs, module resolution from the repo root, and script
+ownership.
+
+Also decide whether the old root command remains temporarily available. The old
+runtime command exposes `new-project`, `deploy`, and `clean`; the standalone
+`canyonos` CLI exposes `new-app`, agent-driven `build`, HTTP-driven `deploy`, and
+other commands. `tests/run_tests.sh` therefore cannot be fixed by replacing the
+word in place—the desired command semantics must be mapped explicitly.
+
+## Decision gate 2: compatibility policy
+
+**RESOLVED — hard cutover, declared breaking.** No compatibility window, no
+dual-read, no fallback. A full teardown/rebuild is required; existing deployments
+do **not** survive the switch. Every producer and its consumer(s) change to the new
+name in the same commit, and the old name is deleted outright. The per-item
+"legacy fallback" bullets below are **void** and retained only to enumerate the
+producer/consumer pairs that must move together:
+
+- Env variable: producer + consumer switch to `CANYONOS_*` together; no `VENTIS_*` reader remains.
+- HTTP header: injector + reader switch to `X-Canyonos-Future-ID` together.
+- Validator capability key: emitter (`validate.py`) + consumer (`cli/canyonos/verify.py`) + tests switch together.
+- SSH default: **unchanged** (`~/.ssh/ventis_ec2`) per Locked decisions.
+- Docker/resource prefixes: generators + `cli/canyonos/verify.py` switch to `canyonos-*` together; no old-prefix recognition.
+
+## Contract map: changes that must move together
+
+### 1. Distribution, Python imports, and process entrypoints — critical
+
+Current package metadata in `pyproject.toml` contains:
+
+- distribution `name = "ventis"`;
+- console script `ventis = "ventis.cli:main"`;
+- package discovery `include = ["ventis*"]`;
+- package-data ownership under `ventis`;
+- Ty include/exclude paths rooted at `ventis`;
+- Ty's allowed flat import `ventis_context`.
+
+After choosing the topology, update all of those together and regenerate
+`uv.lock`; do not hand-edit the lock. `uv.lock:57` already contains the CLI
+`canyonos` package and `uv.lock:1225` contains the root `ventis` package, which is
+direct evidence of the collision.
+
+Runtime imports span `server.py`, `cli.py`, `stub_generator.py`, all controller
+modules/providers/utilities, `OTLP_Exporter`, and `llm_proxy`. The easy-to-miss
+process boundaries are:
+
+- `ventis/Dockerfile:5-17`: `/ventis` build root, protoc paths, and
+ `python -m ventis.server`;
+- `ventis/server.py:9-10,56`: imports runtime helpers and spawns
+ `python -m ventis.cli deploy`;
+- `ventis/controller/local_controller.py:143-147`: passes runtime env and spawns
+ `python -m ventis.llm_proxy`;
+- `ventis/controller/instance_manager.py:14,232`: imports both provider runtimes;
+- `.claude/skills/porting-to-canyonos-core/validate.py:122-141`: imports the runtime
+ and probes its env-file module paths.
+
+The bare fallbacks (`import ventis_context`, `import deploy`, generated gRPC
+modules, etc.) exist because source files are copied flat into generated images.
+Do not mechanically convert those to package-qualified imports without testing
+both installed-source and generated-flat layouts.
+
+### 2. Generated agent/workflow build contexts — critical
+
+`ventis/stub_generator.py` is effectively a template engine even though it does
+not use template files:
+
+- lines 310-324 copy `llm_proxy` to `/ventis/llm_proxy` and copy the
+ package `__init__.py`;
+- lines 395-414 and 521 copy `controller/ventis_context.py` as the flat file
+ `ventis_context.py`;
+- lines 443-461 emit `ENV VENTIS_AGENT_NAME` and `VENTIS_AGENT_FILE`;
+- copied `local_controller.py` launches `python -m ventis.llm_proxy`;
+- the collision list in `validate.py:41-46` explicitly reserves
+ `ventis_context.py`.
+
+Source, generated destination, fallback import names, validator collision rules,
+and generated Dockerfile env names must change in one slice. During a compatibility
+release, generated contexts may carry a small legacy import shim and both env-name
+read paths. Tests must inspect the generated files and boot them; a successful
+host-side import is insufficient.
+
+### 3. Host CLI ↔ Global Controller contracts — critical
+
+The host CLI communicates with the image over stable HTTP endpoints (`/deploy`,
+`/clean`, `/status`, `/endpoints`); those endpoint paths contain no old brand and
+should not be renamed.
+
+Brand-bearing coupling that does require coordination:
+
+- `cli/canyonos/init.py:156` injects `VENTIS_REDIS_HOST`; `ventis/server.py:85-95`
+ and the controller read it. During a mixed-image transition the CLI should pass
+ both names, and the new runtime should dual-read with `CANYONOS_*` precedence.
+- `cli/canyonos/verify.py:43,260-262` looks for `ventis-local-*` containers and
+ `ventis-*` images. It must recognize both during upgrade and switch its emitted
+ guidance to CanyonOS.
+- Validator JSON is a wire-like contract: `validate.py:120-126` emits
+ `capabilities["ventis"]`; `cli/canyonos/verify.py:88-109` consumes it; tests in
+ `tests/test_canyonos_test.py:44-50,150-173` encode it. Prefer a new neutral or
+ runtime-specific key while accepting the old key for one compatibility release.
+- CLI docstrings/help in `cli/cli.py`, `cli/canyonos/deploy.py`, `gc.py`,
+ `verify.py`, `dashboard.compose.yml`, and `cli/ARCHITECTURE.md` still describe
+ the old runtime and must follow the functional cutover.
+
+The deploy progress parser matches message substrings rather than logger prefixes,
+so renaming `logging.getLogger("ventis")` should not break its current parser.
+However, `tests/test_deploy_progress.py` hardcodes many complete
+`INFO:ventis...` lines and must be updated.
+
+### 4. Environment variables — critical external API
+
+Distinct current variables:
+
+```text
+VENTIS_AGENT_FILE
+VENTIS_AGENT_HOST
+VENTIS_AGENT_NAME
+VENTIS_AGENT_PORT
+VENTIS_DATABASE_URL
+VENTIS_DEMO_SERVER_COST_MULTIPLIER
+VENTIS_DEMO_TOKEN_COST_MULTIPLIER
+VENTIS_DOCKER_PLATFORM
+VENTIS_LC_HOST
+VENTIS_LC_PORT
+VENTIS_MAX_AGENT_INSTANCES
+VENTIS_OTEL_DESTINATIONS
+VENTIS_POLL_INTERVAL
+VENTIS_PROJECT_ID
+VENTIS_REDIS_HOST
+VENTIS_REDIS_PORT
+```
+
+Producers include both provider runtimes, `stub_generator.py`, and the standalone
+CLI's `init.py`. Consumers include deploy/future/global/local controllers,
+controller frontend, server, session/telemetry logging, LLM proxy config, and root
+CLI. Tests heavily patch only the legacy names today.
+
+For a no-break transition:
+
+1. Add a single helper for `CANYONOS_*` first / `VENTIS_*` fallback and warn once.
+2. Update producers to emit new names; where old images may consume them, emit
+ both temporarily.
+3. Add precedence, fallback, and warning tests for every externally configurable
+ variable class—not just a blind test-string rename.
+4. Update docs/skill only after the new readers are released.
+5. Remove the legacy branch only at the announced compatibility boundary.
+
+`VENTIS_OTEL_DESTINATIONS` appears in docs/tests but current runtime configuration
+has moved to the Redis key `otel:destinations`; confirm whether the env name is
+already obsolete before adding a new alias.
+
+### 5. Future-ID HTTP header — telemetry correctness
+
+`ventis/llm_proxy/proxy.py:30-46` injects `X-Ventis-Future-ID`; the proxy reads it
+at `ventis/llm_proxy/hooks.py:94` using different casing. HTTP header names are
+case-insensitive, so the casing difference itself is safe.
+
+**Locked:** switch injector **and** reader to the exact spelling
+`X-Canyonos-Future-ID` in the same commit (no legacy header accepted). Fix the
+existing casing inconsistency at the same time so both sides use `X-Canyonos-Future-ID`.
+Add the currently missing producer→consumer regression test; otherwise attribution
+can silently disappear while requests still succeed.
+
+Do not rename the existing `gen_ai.*`, `project_id`, or `canyon.project.id` OTEL
+attributes merely for branding. Those are separate telemetry schemas and no
+`ventis`-prefixed OTEL attribute exists.
+
+### 6. Docker, EC2, Redis, and filesystem resource names — upgrade risk
+
+Name generation is distributed, not confined to `global_controller.py`:
+
+- Local provider (`Local/_runtime.py:19,42-56`): network `ventis-local`, Redis
+ host/container, runtime IDs, image names;
+- EC2 provider (`EC2/_runtime.py:86-108,211,241-242`): AWS `Name` tags,
+ `ventis-ec2-*` runtime IDs, Redis containers, images, and containers;
+- Global controller (`global_controller.py:153-179,396`): stale-resource cleanup
+ and Redis containers;
+- root build (`ventis/cli.py:400`): image tags;
+- CLI verification (`cli/canyonos/verify.py:43,260-262`): expected image/container
+ names;
+- Redis probe (`controller/utils/redis_utils.py:10`): exact key
+ `__ventis_redis_healthcheck__`;
+- remote secret copy (`controller/utils/env_file.py:61`):
+ `/tmp/ventis-env-`;
+- Flask/logger identifiers (`server.py:12`, `controller/deploy.py:103`,
+ `ventis/cli.py:22`) and user-facing controller description
+ (`global_controller.py:922`).
+
+There is also a pre-existing cleanup mismatch: global cleanup expects
+`ventis--` while the Local provider launches
+`ventis-local--`. Fix or explicitly account for that before using
+cleanup behavior as proof of a successful rename.
+
+A compatible upgrade must:
+
+- stop the active deployment before switching image versions;
+- discover and remove both old and new container prefixes during the transition;
+- account for both network names and avoid orphaning the old network;
+- make verification recognize old resources but label them as legacy;
+- rebuild every agent/workflow image so generated code and the GC agree;
+- update EC2 tag expectations and any operational filters;
+- clean both old and new remote env-file patterns best-effort;
+- test rollback using a pinned previous GC image, not mutable `latest` alone.
+
+Runtime routing Redis keys such as `routing_table:*`, `agent:*`, `future:*`, and
+`request:*` are brand-neutral and should remain unchanged. Runtime IDs stored in
+those records do contain old Docker names, so an in-place Redis deployment must
+not straddle versions; prefer a controlled teardown and fresh deploy.
+
+### 7. Files, defaults, and persistent data
+
+- SSH defaults exist in `EC2/_runtime.py:33` and
+ `global_controller.py:783` as `~/.ssh/ventis_ec2`. **Locked: leave unchanged for
+ now** — both stay `~/.ssh/ventis_ec2`. (These two lines are an intentional
+ exception to the zero-`ventis` end state until a later pass.)
+- `examples/helloworld/config/global_controller.yaml:42` uses
+ `sqlite:///ventis_runtime.db`. Updating the sample does not migrate user-owned
+ databases. Existing config paths should remain valid; document an optional
+ user-controlled file move.
+- `ventis/OTLP_Exporter/otel_queue.db` is tracked beneath the package directory.
+ Preserve it across the directory move and verify whether packaging/runtime
+ writes beside installed code are intentional before changing its location.
+- `images/ventis-logo.png` and its `README.md` reference (plus the README clone URL)
+ are **locked as unchanged for now** — deferred to a later branding pass.
+- `.gitignore:30,51-52` includes old comments and `.ventis-tests/`.
+- `controller/utils/env_file.py` remote temp names can leave old files after a
+ crash; cleanup should understand both patterns, without broad `/tmp` deletion.
+
+### 8. Porting skill and remote delivery
+
+The entire vendored `.claude/skills/porting-to-canyonos-core/` tree teaches the
+legacy compatibility contract. Functional changes are required in `validate.py`,
+not just prose:
+
+- import/module probes at lines 120-141;
+- flat-name collision list at line 45;
+- dependency-name exception at line 1002;
+- capability JSON/report handling at lines 1171-1172;
+- messages and command examples throughout.
+
+The standalone CLI does not necessarily use this working-tree copy.
+`cli/canyonos/build.py` downloads a skill from `SKILL_REF` and `SKILL_PATH` in the
+GitHub repository. Update/publish that referenced branch/path first (or repoint it
+to the merged source), then test a fresh cache. Existing local/global skill caches
+can otherwise continue generating legacy scaffolding after this repo appears clean.
+
+### 9. Tests, examples, docs, and assets
+
+Functional test updates cover 24 test source/script files plus `tests/README.md`:
+
+- package imports/patch targets/loggers: `test_cli.py`, `test_deploy.py`,
+ `test_error_propagation.py`, `test_future.py`, controller tests, exporter tests,
+ Redis/runtime tests, session/telemetry tests, and `test_ventis_context.py`;
+- Docker/resource contracts: `test_canyonos_test.py`,
+ `test_instance_manager_runtime.py`, `test_global_controller_redis_reuse.py`,
+ and `test_runtime_ec2.py`;
+- log fixtures: `test_deploy_progress.py`;
+- path injection: `test_future.py`, `test_error_propagation.py`,
+ `test_local_controller_metrics.py`, and `test_otel_exporter_fanout.py`;
+- integration command semantics and temp/project paths: `tests/run_tests.sh`.
+
+All four example projects contain old prose, commands, source comments, or config
+defaults. Documentation cleanup includes root `README.md`, `ventis/README.md`,
+`FUTURE_SCHEMA.md` by directory move, exporter/proxy/EC2 docs,
+`examples/helloworld/README.md`, `tests/README.md`, CLI docs, and the complete
+porting-skill tree.
+
+Do docs/comments last. Several apparent prose strings are actually executable
+examples or validator guidance and should be covered by command/import checks.
+
+## Pre-existing blockers to establish before rename work
+
+Record or fix these on a baseline commit so the migration has trustworthy gates:
+
+1. `tests/run_tests.sh` invokes pytest before generating protobuf modules. Three
+ tests also insert the absent `ventis/templates/grpc_stubs` path. Generate stubs
+ into a deterministic test location or isolate imports with fixtures.
+2. After temporary proto generation, the current suite reports 212 passed and 11
+ failed. Capture the exact expected baseline or fix those failures separately.
+3. Root `ventis new-project` expects a `ventis/templates` directory that no longer
+ exists, while the standalone CLI uses the different `new-app` workflow.
+4. Root `pyproject.toml` still has stale `templates/**/*` package-data and Ty
+ exclude entries. Confirm removal versus restoration instead of carrying them
+ through mechanically.
+5. CI runs Ruff and Ty but not pytest, wheel-install tests, generated-context
+ tests, or image builds. Passing CI currently does not prove rename safety.
+6. Ignored `.venv/`, `ventis.egg-info/`, `.pytest_cache/`, generated `.car/`, and
+ Docker state can preserve old entrypoints/imports. Verification must start from
+ clean generated state.
+
+## Ordered implementation plan
+
+### Phase 0 — freeze and baseline
+
+- Choose the package topology and compatibility window.
+- Pin the current GC image by digest/tag for rollback.
+- Fix or record baseline tests and deterministic proto generation.
+- Add contract tests for env fallback/precedence, header fallback, validator JSON,
+ generated contexts, and old/new resource discovery.
+
+**Gate:** reproducible baseline in a clean environment, with known failures
+explicitly separated from rename work.
+
+### Phase 1 — introduce the new runtime identity ✅ DONE (2026-09-05)
+
+**Executed (hard cutover, package identity only):**
+- `git mv ventis/ → canyonos_core/`; `controller/ventis_context.py → canyonos_context.py`;
+ `tests/test_ventis_context.py → test_canyonos_context.py`.
+- All `from ventis…/import ventis…` and module-path strings (test mocks, `-m` spawns,
+ logger names) → `canyonos_core`; `ventis_context` alias → `canyonos_context`.
+- Generated flat-copy identity in `stub_generator.py` (`ventis/llm_proxy` →
+ `canyonos_core/llm_proxy`, flat `canyonos_context.py`) + fallback imports in
+ `local_controller.py`/`proxy.py` so agent containers import `canyonos_core`.
+- Package logger `getLogger("ventis")` → `"canyonos_core"` (+ `test_deploy_progress`,
+ `test_cli` expectations); argparse `prog` → `canyonos_core`.
+- `Dockerfile`: `COPY . /src`, `pip install /src`, protoc `-I/src/canyonos_core/...`,
+ `ENTRYPOINT python -m canyonos_core.server`. Published image name kept `saakeths/canyonos`.
+- Root `pyproject.toml` rewritten import-only: `name = canyonos-core`, no
+ `[project.scripts]`, `find.include = [canyonos_core*]`, package-data key + Ty paths
+ updated, stale `templates/**` dropped. **Kept** the `canyonos` (cli) editable dev-dep
+ + `[tool.uv.sources]` — no longer collides now that root is `canyonos-core`, and the
+ root suite imports the CLI. `uv.lock` regenerated (`ventis` gone, `canyonos-core` in).
+- **Verification:** `py_compile` all tracked `.py` OK; `canyonos_core` + entrypoints
+ import OK; **`uv run pytest -q` = 223 passed, 3 subtests passed, 0 failed.**
+
+### Environment-variable phase ✅ DONE (2026-09-05)
+
+**Executed (hard cutover, `VENTIS_*` → `CANYONOS_*`, producers + consumers together):**
+- All core env reads/writes renamed across `canyonos_core/**` (controllers, both
+ provider `_runtime.py`, `future.py`, `server.py`, `deploy.py`, `llm_proxy/config.py`,
+ session/telemetry logging incl. `CANYONOS_DEMO_*` multipliers) and the generated
+ agent Dockerfile `ENV` in `stub_generator.py` (`CANYONOS_AGENT_NAME/FILE`).
+- **Cross-boundary producer:** `cli/canyonos/init.py` now injects `CANYONOS_REDIS_HOST`
+ into the GC container, matching the core reader.
+- Test expectations updated (`test_deploy`, `test_instance_manager_runtime`,
+ `test_global_controller_identity`, `test_session/telemetry_logging`, etc.).
+- `VENTIS_OTEL_DESTINATIONS` confirmed **dead in code** (replaced by Redis key
+ `otel:destinations`) — no code rename needed; only stale in docs.
+- **Collision watch confirmed benign:** `CANYONOS_REDIS_HOST/PORT` is also written by
+ `cli/canyonos/dashboard_stack.py`, but that targets the dashboard compose while
+ `init.py` targets the GC container — different processes, no single reader of both.
+- **Verification:** `uv run pytest -q` = **223 passed, 3 subtests passed, 0 failed.**
+- **Still `VENTIS_*` on purpose:** only the porting-skill docs (`SKILL.md`,
+ `runtime-contract.md`, `validate.py` message strings) and `OTLP_Exporter/DESIGN.md`
+ — deferred to the skill/docs phase.
+
+### Resource-name + header + validator-key + cosmetic phases ✅ DONE (2026-09-05)
+
+**Executed (hard cutover; every producer + consumer moved together):**
+- **Resource prefixes `ventis-*` → `canyonos-*`:** core generators (both provider
+ `_runtime.py`, `global_controller.py` network/redis/container names, `cli.py` image
+ tags, `deploy.py`/`server.py` Flask app names, `env_file.py` `/tmp/canyonos-env-`,
+ `redis_utils.py` `__canyonos_redis_healthcheck__`) **and** the user-CLI consumer
+ `cli/canyonos/verify.py` (`RUNTIME_PREFIX`, image name) + all resource-name tests.
+- **Future-ID header:** injector (`proxy.py`) and reader (`hooks.py`) both standardized
+ to exactly `X-Canyonos-Future-ID` (fixed the old `-ID`/`-Id` casing split), plus
+ `_inject_canyonos_headers`.
+- **Validator capability key + framework-import check:** `caps["canyonos_core"]` /
+ `capabilities.canyonos_core` / `name == "canyonos_core"` aligned across
+ `validate.py` (emitter), `cli/canyonos/verify.py` (consumer), and
+ `test_canyonos_test.py`.
+- **Docs/prose/cosmetic:** brand sweep `Ventis`→`CanyonOS`, `ventis`→`canyonos` across
+ READMEs, porting-skill docs (incl. remaining `VENTIS_*`→`CANYONOS_*`), `DESIGN.md`,
+ example configs/comments, `run_tests.sh` (`canyonos_test`), `.gitignore`
+ (`.canyonos-tests/`), and code comments/log strings; stale `ventis_context.py` doc
+ ref → `canyonos_context.py`; `VentisContextTests` → `CanyonosContextTests`.
+- **Verification:** `py_compile` all tracked `.py` OK; header injector/reader agree;
+ `verify.py` prefix agrees with core generators; capability key aligned; `uv.lock`
+ has zero `ventis`; **`uv run pytest -q` = 223 passed, 3 subtests passed, 0 failed.**
+
+**Intentionally still `ventis` (operator decision):** only the EC2 SSH key default
+`~/.ssh/ventis_ec2` (2 code lines + EC2 README + example config). Logo/URL were
+changed by the operator directly. **No other `ventis` token remains anywhere in the
+tracked tree.**
+
+#### Porting-skill semantic caveat
+The token sweep updated the skill's identifiers, but `SKILL.md` /
+`runtime-contract.md` still *describe* the old names as a "compatibility protocol that
+remains" — which is no longer true under the hard cutover. A follow-up semantic pass
+should rewrite that framing (out of scope for a pure rename).
+
+---
+
+#### Original Phase 1 intent (for reference)
+
+- Create the chosen distinct runtime distribution/import package.
+- Update packaging, Ty paths, internal imports, process module paths, and root
+ Dockerfile; regenerate `uv.lock`.
+- If backward compatibility is promised, ship a minimal old import/command shim
+ that delegates to the new runtime and warns.
+- Do not let root and `cli/` both own `canyonos`.
+
+**Gate:** isolated wheel installs prove the CLI and runtime packages can coexist;
+the `canyonos` executable resolves to the standalone CLI; both new and promised
+legacy imports behave as specified.
+
+### Phase 2 — migrate generated runtime artifacts
+
+- Update generator source/destination paths, flat context module, copied proxy
+ package, local-controller process invocation, Dockerfile env, and validator
+ collision rules as one unit.
+- Rebuild all generated contexts from scratch; never reuse old output.
+
+**Gate:** generated agent and workflow contexts contain the intended package/env
+names, import their controller/proxy, load an example agent, and boot in Docker.
+
+### Phase 3 — migrate protocol identifiers compatibly
+
+- Add new-first/old-fallback env reads and header reads.
+- Change producers, including CLI `init.py` and generated Dockerfiles.
+- Version the validator capability JSON transition and update CLI verification.
+- Publish the updated remote porting skill and test an empty cache.
+
+**Gate:** old CLI/new image and new CLI/old image combinations either work within
+the declared matrix or fail early with a precise version error; telemetry
+attribution remains intact.
+
+### Phase 4 — migrate operational resource names
+
+- Change Local/EC2 image, container, network, runtime ID, Redis container, AWS tag,
+ healthcheck, and remote env-file names.
+- Update GC cleanup and CLI verification together, recognizing both generations
+ for the compatibility release.
+- Resolve the existing Local stale-cleanup mismatch.
+
+**Gate:** fresh local deploy, EC2 mocked/probe tests, upgrade teardown, verify,
+clean, and rollback all leave no unexpected containers/networks/temp files.
+
+### Phase 5 — publish and switch
+
+- Build and inspect both wheels/sdists in clean environments.
+- Build the GC image from the renamed runtime, pin a versioned tag/digest, smoke
+ `/status`, then update `GC_IMAGE`/release metadata.
+- Run a representative end-to-end workflow through build, deploy, request, status,
+ telemetry, verify, stop, and quit.
+
+**Gate:** the published artifacts—not editable source installs—pass the full
+matrix on a clean machine or clean VM.
+
+### Phase 6 — cosmetic cleanup and later compatibility removal
+
+- Update prose, help, examples, comments, ASCII/logo assets, and test names.
+- After the announced compatibility period, remove shims/fallbacks and legacy
+ resource discovery in a major release.
+- Rename/archive this migration document, recreate all generated state, and run
+ the final forbidden-token scan.
+
+## Acceptance matrix
+
+| Layer | Required proof |
+|---|---|
+| Static tree | No old token/path outside an explicit temporary compatibility allowlist |
+| Lock/metadata | `uv lock` succeeds; wheel metadata has distinct owners/names |
+| Clean installs | CLI and runtime wheels coexist; import paths and script owner are exact |
+| Type/lint | Ruff and Ty pass with renamed include/exclude/unresolved-import paths |
+| Unit tests | Protos generated deterministically; rename does not add failures |
+| Generated output | Context contains new proxy/context/env names and no accidental stale package |
+| Header telemetry | New header attributes correctly; legacy fallback works during transition |
+| Env contract | New-wins precedence and every promised legacy fallback are tested |
+| Local runtime | Image/network/Redis/container names agree with `canyonos verify` |
+| EC2 runtime | Tags, image/container names, SSH fallback, and remote temp cleanup agree |
+| Fresh deploy | Build → deploy → request → poll → telemetry → verify → teardown succeeds |
+| Upgrade deploy | Old resources are detected/removed; no mixed-version silent failure |
+| Rollback | Previous pinned image can be restored without deleting user DBs/keys/config |
+| Remote skill | Fresh download/cache teaches and validates the new contract |
+| Published image | New Docker entrypoint imports and `/status` answers from the released tag |
+
+Suggested final scans (the migration file and explicitly approved compatibility
+shim are the only temporary exceptions):
+
+```bash
+git grep -I -i -n ventis -- . ':!VENTIS_TO_CANYONOS_RENAME.md'
+git ls-files | rg -i 'ventis'
+rg --hidden --no-ignore -i ventis \
+ -g '!.git/**' -g '!VENTIS_TO_CANYONOS_RENAME.md'
+find . -path './.git' -prune -o -iname '*ventis*' -print
+```
+
+## Rollback rules
+
+- Never make `latest` the only rollback reference; retain the previous image
+ digest and compatibility matrix.
+- Stop the deploy before changing resource prefixes. Do not run old and new GCs
+ against one Redis state concurrently.
+- Preserve user config, `.env`, SSH keys, SQLite databases, and OTEL data. Rename
+ or copy user-owned files only on explicit user action.
+- Keep cleanup exact and prefix-scoped; never broadly delete Docker or `/tmp`
+ state.
+- If the new image fails, tear down only resources created by that attempt,
+ restore the previous pinned image, and use legacy env/header/resource support
+ until the failure is fixed.
+
+## Complete matching-file coverage
+
+The scan includes all old-name matches in these groups:
+
+- **Runtime (36):** the package Dockerfile; package README; exporter design/source;
+ package/controller initializers; root runtime CLI/server/stub generator; Local and
+ EC2 runtime/readme; deploy/future/global/instance/local controllers; env,
+ process-supervisor, Redis, session, and telemetry utilities; the LLM proxy README,
+ entrypoint, app/config/core/hooks/proxy, and all provider modules.
+- **Tests (25):** `tests/README.md`, `run_tests.sh`, `test_canyonos_test.py`,
+ `test_cli.py`, `test_deploy.py`, `test_deploy_progress.py`,
+ `test_error_propagation.py`, `test_future.py`, every `test_global_controller_*`,
+ `test_gpu_metrics.py`, `test_instance_manager_runtime.py`, both
+ `test_local_controller_*`, both exporter tests, Redis/EC2/session/stub/telemetry
+ tests, and `test_ventis_context.py`.
+- **Examples (15):** `examples/helloworld/README.md`; finance agent/config/workflow;
+ helloworld config/workflow; portfolio advisor/intent/metrics agents plus
+ config/workflow; text2sql generator/vLLM agents plus config/workflow.
+- **Porting skill (7):** `SKILL.md`, `validate.py`, and the EC2, LLM proxy,
+ packaging, runtime-contract, and troubleshooting references.
+- **Standalone CLI (7):** `cli/ARCHITECTURE.md`, `cli/cli.py`, and CanyonOS
+ dashboard compose, deploy, GC, init, and verify modules.
+- **Root (4):** `.gitignore`, `README.md`, `pyproject.toml`, and `uv.lock`.
+
+No additional `setup.py`, `setup.cfg`, package manifest, Dockerfile, or compose
+file contains the old token. `requirements.txt` has no project-name match. Binary
+inspection found no embedded old token in the tracked SQLite/JPEG/PNG assets; the
+PNG still requires a filename/reference rename because its basename is branded.
diff --git a/ventis/Dockerfile b/canyonos_core/Dockerfile
similarity index 57%
rename from ventis/Dockerfile
rename to canyonos_core/Dockerfile
index 1814548..0edcbe1 100644
--- a/ventis/Dockerfile
+++ b/canyonos_core/Dockerfile
@@ -2,19 +2,19 @@ FROM python:3.11-slim
RUN apt-get update && apt-get install -y docker.io && rm -rf /var/lib/apt/lists/*
-COPY . /ventis
-RUN pip install /ventis
+COPY . /src
+RUN pip install /src
# global_controller.py bare-imports these; pip install only ships the .proto source.
RUN python -m grpc_tools.protoc \
- -I/ventis/ventis/controller/proto \
+ -I/src/canyonos_core/controller/proto \
--python_out=/usr/local/lib/python3.11/site-packages \
--grpc_python_out=/usr/local/lib/python3.11/site-packages \
- /ventis/ventis/controller/proto/local_controler.proto
+ /src/canyonos_core/controller/proto/local_controler.proto
EXPOSE 8000
-ENTRYPOINT ["python", "-m", "ventis.server"]
+ENTRYPOINT ["python", "-m", "canyonos_core.server"]
-# to run: docker build -f ventis/Dockerfile -t saakeths/canyonos:latest .
+# to run: docker build -f canyonos_core/Dockerfile -t saakeths/canyonos:latest .
diff --git a/ventis/FUTURE_SCHEMA.md b/canyonos_core/FUTURE_SCHEMA.md
similarity index 100%
rename from ventis/FUTURE_SCHEMA.md
rename to canyonos_core/FUTURE_SCHEMA.md
diff --git a/ventis/OTLP_Exporter/DESIGN.md b/canyonos_core/OTLP_Exporter/DESIGN.md
similarity index 92%
rename from ventis/OTLP_Exporter/DESIGN.md
rename to canyonos_core/OTLP_Exporter/DESIGN.md
index ec2957d..15f0163 100644
--- a/ventis/OTLP_Exporter/DESIGN.md
+++ b/canyonos_core/OTLP_Exporter/DESIGN.md
@@ -1,4 +1,4 @@
-# OTLP Exporter for Ventis GlobalController — Design
+# OTLP Exporter for CanyonOS GlobalController — Design
Status: **implemented (single-table design; multi-destination fan-out in progress)**. `GlobalController` writes futures into a
`waiting` table (SQLite); a GC-supervised, GC-restarted OTel Exporter process reads
@@ -6,11 +6,11 @@ finished/unsent rows, converts each to an OTel span, and hands it to a real
`BatchSpanProcessor`/`OTLPSpanExporter`. Batching, serialization, and sending are all
OTel SDK code — the only custom pieces are the row→span conversion and durable
sent-tracking. This doc is a design/rationale reference; the actual files
-(`otel_exporter.py`, `db.py`, `convert.py`, `ventis/controller/utils/process_supervisor.py`)
+(`otel_exporter.py`, `db.py`, `convert.py`, `canyonos/controller/utils/process_supervisor.py`)
are the source of truth for current behavior.
## Context
-Ventis futures need to reach an external OTLP-compatible tracing backend. Design: a
+CanyonOS futures need to reach an external OTLP-compatible tracing backend. Design: a
separate OTLP Exporter process, spawned and supervised by GlobalController, that reads
unsent finished future rows from a local SQLite DB, converts them into OTel spans, and
hands them to the OTel SDK's own batching/export machinery, which ships them to an
@@ -20,7 +20,7 @@ service).
Decisions (final status):
- **Process model**: a true separate OS process, spawned and supervised by
GlobalController (not an in-process thread) — via `ProcessSupervisor`
- (`ventis/controller/utils/process_supervisor.py`, built): `register`/`start_all` to
+ (`canyonos/controller/utils/process_supervisor.py`, built): `register`/`start_all` to
spawn, `check_and_respawn` (called from GC's existing poll tick, guarded on
`self.running` to avoid a shutdown race) to restart it if it ever dies unexpectedly,
`terminate_all` (called from GC's `stop()`) to shut it down cleanly. Rationale: fault
@@ -29,7 +29,7 @@ Decisions (final status):
- **Config**: implemented via a new `otel:` section in `global_controller.yaml`
holding a `destinations` list, *not* by making `otel_exporter.py` itself
config-aware. `GlobalController` serializes that list to JSON and passes it to the
- exporter subprocess as a single `VENTIS_OTEL_DESTINATIONS` env var via
+ exporter subprocess as a single `CANYONOS_OTEL_DESTINATIONS` env var via
`ProcessSupervisor.register(..., env=...)`. The exporter builds one independent
exporter/`BatchSpanProcessor` pair per destination, picking the gRPC vs HTTP
exporter class from each destination's `protocol` field. gRPC and HTTP destinations
@@ -44,7 +44,7 @@ Decisions (final status):
subprocess entirely. Configuration is read at exporter startup; changing it requires
a GlobalController/exporter restart.
- **Data source**: NOT `runtime_information` — a dedicated `waiting` table in its own
- SQLite file (`ventis/OTLP_Exporter/otel_queue.db`, see `db.py`), written by GC's existing
+ SQLite file (`canyonos/OTLP_Exporter/otel_queue.db`, see `db.py`), written by GC's existing
`_poll_controllers` *alongside* (not instead of) the existing
`send_runtime_information` write. Keeps this pipeline's schema/state fully decoupled
from the dashboard/cost table.
@@ -77,7 +77,7 @@ otel:
Authorization: Basic ${LANGFUSE_OTLP_HEADERS} # deployer pre-encodes public:secret
```
`GlobalController._otel_exporter_env()` translates the `destinations` list into
-`VENTIS_OTEL_DESTINATIONS` and hands it to `ProcessSupervisor.register(
+`CANYONOS_OTEL_DESTINATIONS` and hands it to `ProcessSupervisor.register(
"otel_exporter", ..., env=...)`, which supports an `env` param (merged on top of the
parent process's own environment, not a replacement). If `otel.destinations` is
absent, `_otel_exporter_env()` returns `None` and `GlobalController.__init__` skips
@@ -85,7 +85,7 @@ registering the exporter subprocess entirely, logging that no OTel metrics
collection will happen. No shape
validation is duplicated on the GlobalController side (deliberately: keep this side
simple, `otel_exporter.py` itself validates destination shape at subprocess startup,
-and raises if invoked directly without `VENTIS_OTEL_DESTINATIONS` set).
+and raises if invoked directly without `CANYONOS_OTEL_DESTINATIONS` set).
`otel_exporter.py` parses the destination configuration at startup and constructs the
appropriate OTLP exporter for each entry (gRPC or HTTP), passing that destination's
@@ -94,7 +94,7 @@ endpoint and headers to the SDK. `BatchSpanProcessor(..., schedule_delay_millis=
`max_export_batch_size` is left at the SDK default (512), which already approximates the
original "500 spans" batching ask without any override needed.
-### 2. `ventis/OTLP_Exporter/otel_exporter.py`
+### 2. `canyonos/OTLP_Exporter/otel_exporter.py`
A plain loop, polling every `POLL_INTERVAL_SECONDS` (5s, checked every 1s so SIGTERM
stays responsive), calling `_send_pending()` each tick. At startup it constructs one
independent OTLP exporter and `BatchSpanProcessor` for each configured destination;
@@ -110,7 +110,7 @@ each pair may use a different protocol, endpoint, and headers:
since spans are hand-built and handed straight to the processors via `on_end()`.
- Every processor is shut down on exit, flushing its pending batch independently.
-### 3. Future row → OTel span conversion (`ventis/OTLP_Exporter/convert.py`)
+### 3. Future row → OTel span conversion (`canyonos/OTLP_Exporter/convert.py`)
`future_id` maps to OTel `span_id`, not `trace_id` — `session_id` (== `request_id`) is
the one that maps to `trace_id`. Both are `uuid4().hex` (32 hex chars / 16 bytes); OTel
`trace_id` is 128-bit (16 bytes, fits directly) and `span_id` is 64-bit (8 bytes, needs
@@ -135,13 +135,13 @@ exported under Langfuse's documented `langfuse.observation.input`/
`langfuse.observation.output` attributes. The span name is the stable logical
`service.method`, not the executing instance's UUID. `cpu`/`gpu`/
`execution_time_ms`/`queue_time_ms`/`token_count` keep plain names deliberately: none of
-them have an OTel GenAI equivalent (cpu/gpu/queue-time are Ventis infra concepts, and
+them have an OTel GenAI equivalent (cpu/gpu/queue-time are CanyonOS infra concepts, and
`token_count`, an input+output sum, isn't part of the spec at all — inventing a
`gen_ai.*`-shaped name for any of these would fabricate a standard rather than follow
one. `cached_tokens`/`cache_hit_ratio` exist on the `waiting` row but aren't exported to
attributes at all yet — a separate, pre-existing gap, not touched here.
-### 4. Process supervisor — `ventis/controller/utils/process_supervisor.py` (built)
+### 4. Process supervisor — `canyonos/controller/utils/process_supervisor.py` (built)
`ProcessSupervisor`: `register(name, argv, env=None)` declares a process spec (`env`,
when given, is merged on top of — not a replacement for — the parent's own environment);
`start_all()` spawns everything registered; `check_and_respawn()` restarts anything that
@@ -152,7 +152,7 @@ process (all `.terminate()` calls first, then `.wait()` on each, falling back to
`.kill()`), called from GC's `stop()`. Adding a future second daemon is one more
`register()` call — no new spawn/monitor/terminate code needed.
-### 5. Poll/cleanup race fix (`ventis/controller/global_controller.py`)
+### 5. Poll/cleanup race fix (`canyonos/controller/global_controller.py`)
GC's cleanup thread used to run on its own `cleanup_interval` timer (default 10s),
fully independent of the poll loop's `poll_interval` (default 5s) that writes futures
into `waiting`. On a fast-completing request, cleanup could delete a session's Redis
@@ -185,7 +185,7 @@ config work, since `protocol: http` now needs that package importable).
is added.
- `waiting` grows unboundedly: sent rows are never pruned, and futures that never finish
(`finished_at` never arrives) also stay forever, invisible and un-expiring.
-- `error_name` is always `NULL` — Ventis's own Redis writer never records a distinct
+- `error_name` is always `NULL` — CanyonOS's own Redis writer never records a distinct
exception-type field, only a message string.
- Test coverage is still limited; the waiting-field migration/normalization/conversion
path is covered, but the exporter process and live OTLP delivery are not.
diff --git a/ventis/OTLP_Exporter/SCHEMA.md b/canyonos_core/OTLP_Exporter/SCHEMA.md
similarity index 100%
rename from ventis/OTLP_Exporter/SCHEMA.md
rename to canyonos_core/OTLP_Exporter/SCHEMA.md
diff --git a/ventis/OTLP_Exporter/__init__.py b/canyonos_core/OTLP_Exporter/__init__.py
similarity index 100%
rename from ventis/OTLP_Exporter/__init__.py
rename to canyonos_core/OTLP_Exporter/__init__.py
diff --git a/ventis/OTLP_Exporter/convert.py b/canyonos_core/OTLP_Exporter/convert.py
similarity index 95%
rename from ventis/OTLP_Exporter/convert.py
rename to canyonos_core/OTLP_Exporter/convert.py
index 72e2342..fc749c8 100644
--- a/ventis/OTLP_Exporter/convert.py
+++ b/canyonos_core/OTLP_Exporter/convert.py
@@ -30,7 +30,7 @@ def waiting_row_to_span(row):
trace_id = int(row["session_id"], 16)
# future_id/parent_id are already 64-bit (Future.id is generated at that
- # width directly -- see ventis/controller/future.py), matching OTel's
+ # width directly -- see canyonos/controller/future.py), matching OTel's
# span_id, so no truncation is needed here.
span_id = int(row["future_id"], 16)
parent_id = row.get("parent_id")
@@ -64,7 +64,7 @@ def waiting_row_to_span(row):
# Model, token, agent, and cache-read usage use OTel GenAI semantic-convention
# names (see https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/).
- # total_cost uses gen_ai.usage.cost. The remaining Ventis-specific values (project_id, server/token cost
+ # total_cost uses gen_ai.usage.cost. The remaining CanyonOS-specific values (project_id, server/token cost
# breakdown, cache_hit_ratio) have no GenAI or Langfuse equivalent, so they keep
# plain names.
attributes = {
diff --git a/ventis/OTLP_Exporter/db.py b/canyonos_core/OTLP_Exporter/db.py
similarity index 99%
rename from ventis/OTLP_Exporter/db.py
rename to canyonos_core/OTLP_Exporter/db.py
index f1438f7..16c02ae 100644
--- a/ventis/OTLP_Exporter/db.py
+++ b/canyonos_core/OTLP_Exporter/db.py
@@ -12,7 +12,7 @@
import os
import sqlite3
-from ventis.controller.utils import pricing
+from canyonos_core.controller.utils import pricing
# Will need to eventually delete dependency on this and move to OTLP
# It is currently stored here for backcompat with the old telemetry collecting
diff --git a/ventis/OTLP_Exporter/otel_exporter.py b/canyonos_core/OTLP_Exporter/otel_exporter.py
similarity index 99%
rename from ventis/OTLP_Exporter/otel_exporter.py
rename to canyonos_core/OTLP_Exporter/otel_exporter.py
index 1ed210a..2d1cab1 100644
--- a/ventis/OTLP_Exporter/otel_exporter.py
+++ b/canyonos_core/OTLP_Exporter/otel_exporter.py
@@ -20,7 +20,7 @@
import time
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-from ventis.controller.utils.redis_client import RedisClient
+from canyonos_core.controller.utils.redis_client import RedisClient
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
OTLPSpanExporter as GrpcOTLPSpanExporter,
diff --git a/ventis/OTLP_Exporter/otel_queue.db b/canyonos_core/OTLP_Exporter/otel_queue.db
similarity index 100%
rename from ventis/OTLP_Exporter/otel_queue.db
rename to canyonos_core/OTLP_Exporter/otel_queue.db
diff --git a/ventis/README.md b/canyonos_core/README.md
similarity index 100%
rename from ventis/README.md
rename to canyonos_core/README.md
diff --git a/canyonos_core/__init__.py b/canyonos_core/__init__.py
new file mode 100644
index 0000000..186efdf
--- /dev/null
+++ b/canyonos_core/__init__.py
@@ -0,0 +1,2 @@
+# CanyonOS - Distributed Agent Framework
+__version__ = "0.1.0"
diff --git a/ventis/cli.py b/canyonos_core/cli.py
similarity index 92%
rename from ventis/cli.py
rename to canyonos_core/cli.py
index c66a106..2064c36 100644
--- a/ventis/cli.py
+++ b/canyonos_core/cli.py
@@ -1,9 +1,9 @@
"""
-Ventis CLI
+CanyonOS CLI
-Entry point for the `ventis` command. Provides these subcommands:
- ventis new-project — Scaffold a new Ventis project
- ventis deploy — Build (stubs + Docker images) then launch
+Entry point for the `canyonos` command. Provides these subcommands:
+ canyonos new-project — Scaffold a new CanyonOS project
+ canyonos deploy — Build (stubs + Docker images) then launch
agents via the Global Controller
"""
@@ -16,10 +16,10 @@
import subprocess
import sys
-from ventis.controller.utils.env_file import resolve_env_file
+from canyonos_core.controller.utils.env_file import resolve_env_file
logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger("ventis")
+logger = logging.getLogger("canyonos_core")
DEFAULT_DOCKER_PLATFORM = "linux/amd64"
ARTIFACT_DIR_NAME = ".car"
SOURCE_DIR_NAME = "app"
@@ -42,7 +42,7 @@ def _get_templates_dir():
def _get_package_dir():
- """Return the absolute path to the ventis package directory."""
+ """Return the absolute path to the canyonos package directory."""
return os.path.dirname(os.path.abspath(__file__))
@@ -74,7 +74,7 @@ def _normalize_requirements(agent_cfg):
def _docker_platform():
"""Return the target Docker platform for portable runtime images."""
- return os.environ.get("VENTIS_DOCKER_PLATFORM", DEFAULT_DOCKER_PLATFORM)
+ return os.environ.get("CANYONOS_DOCKER_PLATFORM", DEFAULT_DOCKER_PLATFORM)
def _docker_build_cmd(*args):
@@ -115,7 +115,7 @@ def _write_bake_file(bake_targets, bake_file_path, platform):
"platforms": [platform],
"output": ["type=docker"],
# type=docker could be changed to tarring it up, which would be
- # faster but skipped because that change would alter ventis deploy
+ # faster but skipped because that change would alter canyonos deploy
}
for target in bake_targets
}
@@ -129,7 +129,7 @@ def _require_docker_for_ec2(command_name):
if _docker_available():
return
raise RuntimeError(
- f"EC2-backed `ventis {command_name}` requires local Docker, but Docker is unavailable "
+ f"EC2-backed `canyonos {command_name}` requires local Docker, but Docker is unavailable "
"or unreachable."
)
@@ -145,7 +145,7 @@ def _ensure_grpc_stubs_importable(project_dir):
except ImportError as exc:
raise RuntimeError(
"Deploy failed: generated grpc_stubs are missing or not importable. "
- "Run `ventis build` on this host first."
+ "Run `canyonos build` on this host first."
) from exc
@@ -162,12 +162,12 @@ def _preflight_ec2_deploy(config, project_dir):
# ------------------------------------------------------------------ #
-# ventis new-project #
+# canyonos new-project #
# ------------------------------------------------------------------ #
def cmd_new_project(args):
- """Scaffold a new Ventis project."""
+ """Scaffold a new CanyonOS project."""
project_name = args.name
project_dir = os.path.abspath(project_name)
@@ -206,14 +206,14 @@ def cmd_new_project(args):
os.makedirs(os.path.join(artifact_root, "stubs"), exist_ok=True)
os.makedirs(os.path.join(artifact_root, "grpc_stubs"), exist_ok=True)
- logger.info("Created new Ventis project: %s", project_dir)
+ logger.info("Created new CanyonOS project: %s", project_dir)
logger.info("")
logger.info(" cd %s", project_name)
- logger.info(" ventis deploy")
+ logger.info(" canyonos deploy")
# ------------------------------------------------------------------ #
-# ventis build #
+# canyonos build #
# ------------------------------------------------------------------ #
@@ -223,7 +223,7 @@ def _run_build(config_path):
and build Docker images.
Must be run from the project root (where config/ lives). Invoked as the
- first phase of `ventis deploy`.
+ first phase of `canyonos deploy`.
"""
if not os.path.isfile(config_path):
logger.error("Config file not found: %s", config_path)
@@ -244,7 +244,7 @@ def _run_build(config_path):
stubs_dir = os.path.join(artifact_root, "stubs")
os.makedirs(stubs_dir, exist_ok=True)
- from ventis.stub_generator import (
+ from canyonos_core.stub_generator import (
generate_stub,
generate_docker,
generate_workflow_docker,
@@ -397,7 +397,7 @@ def _run_build(config_path):
{
"name": agent_name.lower(),
"context": docker_context,
- "image_name": f"ventis-{agent_name.lower()}",
+ "image_name": f"canyonos-{agent_name.lower()}",
}
)
@@ -436,7 +436,7 @@ def _run_build(config_path):
# ------------------------------------------------------------------ #
-# ventis deploy #
+# canyonos deploy #
# ------------------------------------------------------------------ #
@@ -454,7 +454,7 @@ def cmd_deploy(args):
sys.exit(1)
# Build first (stubs, protos, Docker contexts, images), then deploy them.
- # `ventis build` was merged into `ventis deploy`.
+ # `canyonos build` was merged into `canyonos deploy`.
_run_build(config_path)
config = _load_config(config_path)
@@ -479,7 +479,7 @@ def cmd_deploy(args):
):
_preflight_ec2_deploy(config, artifact_root)
- from ventis.controller.global_controller import GlobalController
+ from canyonos_core.controller.global_controller import GlobalController
controller = GlobalController(config_path)
@@ -510,7 +510,7 @@ def _reload_handler(sig, frame):
# ------------------------------------------------------------------ #
-# ventis clean #
+# canyonos clean #
# ------------------------------------------------------------------ #
@@ -551,20 +551,20 @@ def main():
_artifact_prefix(os.getcwd()), "config", "global_controller.yaml"
)
parser = argparse.ArgumentParser(
- prog="ventis",
- description="Ventis — Distributed Agent Orchestration Framework",
+ prog="canyonos_core",
+ description="CanyonOS — Distributed Agent Orchestration Framework",
)
subparsers = parser.add_subparsers(dest="command", help="Available commands")
- # ventis new-project
+ # canyonos new-project
new_proj = subparsers.add_parser(
"new-project",
- help="Scaffold a new Ventis project",
+ help="Scaffold a new CanyonOS project",
)
new_proj.add_argument("name", help="Name of the project directory to create")
new_proj.set_defaults(func=cmd_new_project)
- # ventis deploy
+ # canyonos deploy
deploy = subparsers.add_parser(
"deploy",
help="Build stubs/images, then launch agents via the Global Controller",
@@ -577,7 +577,7 @@ def main():
)
deploy.set_defaults(func=cmd_deploy)
- # ventis clean
+ # canyonos clean
clean = subparsers.add_parser(
"clean",
help="Remove generated stubs, compiled protos, and Docker contexts",
diff --git a/canyonos_core/controller/__init__.py b/canyonos_core/controller/__init__.py
new file mode 100644
index 0000000..948e344
--- /dev/null
+++ b/canyonos_core/controller/__init__.py
@@ -0,0 +1 @@
+# CanyonOS Controller Sub-Package
diff --git a/ventis/controller/ventis_context.py b/canyonos_core/controller/canyonos_context.py
similarity index 100%
rename from ventis/controller/ventis_context.py
rename to canyonos_core/controller/canyonos_context.py
diff --git a/ventis/controller/cloud_provider_logic/EC2/README.md b/canyonos_core/controller/cloud_provider_logic/EC2/README.md
similarity index 91%
rename from ventis/controller/cloud_provider_logic/EC2/README.md
rename to canyonos_core/controller/cloud_provider_logic/EC2/README.md
index fdec0ba..ea9ee2b 100644
--- a/ventis/controller/cloud_provider_logic/EC2/README.md
+++ b/canyonos_core/controller/cloud_provider_logic/EC2/README.md
@@ -9,7 +9,7 @@ For global controller
- Instance with all of the following installed:
- Both things in local controller
- Python 3.10+
- - Ventis folder
+ - CanyonOS folder
- pip requirements installed in env
- pip install -e . --break-system-packages
- Private key labeled as ventis_ec2 inside ~/.ssh
@@ -35,9 +35,9 @@ Steps:
3. Change the agents/configs/workflow folders to suit your needs
-4. Run ventis build + ventis deploy
+4. Run canyonos build + canyonos deploy
-For cleanup, use ventis clean to clean stubs/containers
+For cleanup, use canyonos clean to clean stubs/containers
If encountering permission errors with the keys, run this to give key more permissions if blocked
chmod 700 ~/.ssh
diff --git a/ventis/controller/cloud_provider_logic/EC2/_runtime.py b/canyonos_core/controller/cloud_provider_logic/EC2/_runtime.py
similarity index 89%
rename from ventis/controller/cloud_provider_logic/EC2/_runtime.py
rename to canyonos_core/controller/cloud_provider_logic/EC2/_runtime.py
index ce68744..6d59151 100644
--- a/ventis/controller/cloud_provider_logic/EC2/_runtime.py
+++ b/canyonos_core/controller/cloud_provider_logic/EC2/_runtime.py
@@ -1,5 +1,5 @@
"""
-EC2 runtime helpers for Ventis.
+EC2 runtime helpers for CanyonOS.
This module is the EC2-specific backend for `provider: EC2` agents.
It does four things:
@@ -23,9 +23,9 @@
import boto3
-from ventis.controller.utils.env_file import env_file_args
-from ventis.controller.utils.redis_utils import _wait_for_redis
-from ventis.controller.utils.redis_client import RedisClient
+from canyonos_core.controller.utils.env_file import env_file_args
+from canyonos_core.controller.utils.redis_utils import _wait_for_redis
+from canyonos_core.controller.utils.redis_client import RedisClient
logger = logging.getLogger(__name__)
@@ -87,7 +87,7 @@ def provision_instance(spec, replica_index, next_host_port=None):
{
"ResourceType": "instance",
"Tags": [
- {"Key": "Name", "Value": f"ventis-{agent_name}-{replica_index}"},
+ {"Key": "Name", "Value": f"canyonos-{agent_name}-{replica_index}"},
{"Key": "CreatedBy", "Value": "EC2 Fast Launch"},
],
},
@@ -105,7 +105,7 @@ def provision_instance(spec, replica_index, next_host_port=None):
response = client.run_instances(**request)
instance_id = response["Instances"][0]["InstanceId"]
- runtime_id = f"ventis-ec2-{agent_name.lower()}-{replica_index}--{instance_id}"
+ runtime_id = f"canyonos-ec2-{agent_name.lower()}-{replica_index}--{instance_id}"
client.get_waiter("instance_running").wait(InstanceIds=[instance_id])
deadline = time.time() + cfg.get("public_ip_timeout", 120)
@@ -208,7 +208,7 @@ def _bootstrap_instance(host, spec, replica_index, cfg, redis_host, redis_port,
else:
raise TimeoutError(f"SSH never became ready on {host}")
- redis_container = f"ventis-redis-{host.replace('.', '-')}"
+ redis_container = f"canyonos-redis-{host.replace('.', '-')}"
result = _controller._run_cmd(
[
"docker",
@@ -238,8 +238,8 @@ def _bootstrap_instance(host, spec, replica_index, cfg, redis_host, redis_port,
node_redis.set(f"agent:{agent_id}:instance_type", spec["instance_type"])
agent_name = spec["name"]
- image = f"ventis-{agent_name.lower()}"
- container_name = f"ventis-ec2-{agent_name.lower()}-{replica_index}"
+ image = f"canyonos-{agent_name.lower()}"
+ container_name = f"canyonos-ec2-{agent_name.lower()}-{replica_index}"
key = _ssh_key_path(cfg)
port_args = ["-p", f"{CONTAINER_PORT}:{CONTAINER_PORT}"]
if spec.get("type") == "workflow":
@@ -279,27 +279,32 @@ def _bootstrap_instance(host, spec, replica_index, cfg, redis_host, redis_port,
container_name,
*port_args,
"-e",
- f"VENTIS_REDIS_HOST={redis_host}",
+ f"CANYONOS_REDIS_HOST={redis_host}",
"-e",
- f"VENTIS_REDIS_PORT={redis_port}",
+ f"CANYONOS_REDIS_PORT={redis_port}",
"-e",
- f"VENTIS_AGENT_HOST={host}",
+ f"CANYONOS_AGENT_HOST={host}",
"-e",
- f"VENTIS_AGENT_PORT={CONTAINER_PORT}",
+ f"CANYONOS_AGENT_PORT={CONTAINER_PORT}",
"-e",
- f"VENTIS_POLL_INTERVAL={_controller.config.get('poll_interval', 5)}",
+ f"CANYONOS_POLL_INTERVAL={_controller.config.get('poll_interval', 5)}",
# Route the agent's boto3 Bedrock calls through the in-container LLM
# proxy (started by LocalController) so token/cost telemetry is captured.
"-e",
"AWS_ENDPOINT_URL_BEDROCK_RUNTIME=http://127.0.0.1:8081/bedrock",
+ # The LLM stub is a local-only `canyonos test` control; it must never be
+ # active on EC2. Pin it empty explicitly so a user's --env-file cannot
+ # turn it on (docker: -e beats --env-file).
+ "-e",
+ "CANYONOS_LLM_STUB_TEXT=",
]
if spec.get("type") == "workflow":
db_url = _controller.config.get("database", {}).get("url")
project_id = _controller.config.get("project_id")
if db_url:
- cmd.extend(["-e", f"VENTIS_DATABASE_URL={db_url}"])
+ cmd.extend(["-e", f"CANYONOS_DATABASE_URL={db_url}"])
if project_id:
- cmd.extend(["-e", f"VENTIS_PROJECT_ID={project_id}"])
+ cmd.extend(["-e", f"CANYONOS_PROJECT_ID={project_id}"])
# User secrets from `env_file`. Explicit -e flags above still win over
# anything in the file.
diff --git a/ventis/controller/cloud_provider_logic/Local/_runtime.py b/canyonos_core/controller/cloud_provider_logic/Local/_runtime.py
similarity index 78%
rename from ventis/controller/cloud_provider_logic/Local/_runtime.py
rename to canyonos_core/controller/cloud_provider_logic/Local/_runtime.py
index 6329311..56e41f3 100644
--- a/ventis/controller/cloud_provider_logic/Local/_runtime.py
+++ b/canyonos_core/controller/cloud_provider_logic/Local/_runtime.py
@@ -1,5 +1,5 @@
"""
-Local runtime helpers for Ventis.
+Local runtime helpers for CanyonOS.
This module is the local-provider backend for `provider: local` agents.
It keeps the existing Docker launch/teardown behavior while letting
@@ -7,8 +7,9 @@
"""
import logging
+import os
-from ventis.controller.utils.env_file import env_file_args
+from canyonos_core.controller.utils.env_file import env_file_args
logger = logging.getLogger(__name__)
@@ -16,7 +17,7 @@
CONTAINER_PORT = 50051
PROVIDER = "local"
MAX_PORT_ATTEMPTS = 50
-NETWORK = "ventis-local"
+NETWORK = "canyonos-local"
_controller = None
@@ -43,8 +44,8 @@ def provision_instance(spec, replica_index, next_host_port):
"provider": PROVIDER,
"host": host,
"host_port": host_port,
- "redis_host": f"ventis-redis-{host.replace('.', '-')}",
- "runtime_id": f"ventis-{PROVIDER}-{agent_name.lower()}-{replica_index}",
+ "redis_host": f"canyonos-redis-{host.replace('.', '-')}",
+ "runtime_id": f"canyonos-{PROVIDER}-{agent_name.lower()}-{replica_index}",
"user": spec.get("user"),
}
@@ -53,7 +54,7 @@ def bootstrap_instance(provisioned, spec, replica_index, agent_id):
agent_name = spec["name"]
resources = spec.get("resources", {})
ctrl_type = spec.get("type", "agent")
- image = f"ventis-{agent_name.lower()}"
+ image = f"canyonos-{agent_name.lower()}"
host = provisioned["host"]
host_port = provisioned["host_port"]
user = provisioned.get("user")
@@ -84,29 +85,42 @@ def bootstrap_instance(provisioned, spec, replica_index, agent_id):
"-p",
f"{host_port}:{CONTAINER_PORT}",
"-e",
- f"VENTIS_AGENT_PORT={CONTAINER_PORT}",
+ f"CANYONOS_AGENT_PORT={CONTAINER_PORT}",
"-e",
- f"VENTIS_AGENT_HOST={runtime_id}",
+ f"CANYONOS_AGENT_HOST={runtime_id}",
"-e",
- f"VENTIS_REDIS_HOST={redis_host}",
+ f"CANYONOS_REDIS_HOST={redis_host}",
"-e",
- f"VENTIS_REDIS_PORT={spec.get('redis_port', 6379)}",
+ f"CANYONOS_REDIS_PORT={spec.get('redis_port', 6379)}",
"-e",
- f"VENTIS_POLL_INTERVAL={_require_controller().config.get('poll_interval', 5)}",
+ f"CANYONOS_POLL_INTERVAL={_require_controller().config.get('poll_interval', 5)}",
# Route the agent's boto3 Bedrock calls through the in-container LLM
# proxy (started by LocalController) so token/cost telemetry is captured.
"-e",
"AWS_ENDPOINT_URL_BEDROCK_RUNTIME=http://127.0.0.1:8081/bedrock",
]
+
+ # LLM stub is a `canyonos test`-only control. `canyonos test` injects
+ # CANYONOS_LLM_STUB_TEXT into THIS controller's (GC container) env; a
+ # normal `canyonos deploy` never does (run_container only sets it from
+ # canyonos test's extra_env). Set it explicitly on every agent -- to that
+ # value, or empty -- so it ALWAYS wins over --env-file (docker: -e beats
+ # --env-file). A user's .env can therefore neither enable the stub nor
+ # change it; it is reachable only through `canyonos test`.
+ cmd.extend([
+ "-e",
+ f"CANYONOS_LLM_STUB_TEXT={os.environ.get('CANYONOS_LLM_STUB_TEXT', '')}",
+ ])
+
if ctrl_type == "workflow":
cmd.extend(["-p", f"{spec.get('api_port', 8080)}:8080"])
config = _require_controller().config
db_url = config.get("database", {}).get("url")
project_id = config.get("project_id")
if db_url:
- cmd.extend(["-e", f"VENTIS_DATABASE_URL={db_url}"])
+ cmd.extend(["-e", f"CANYONOS_DATABASE_URL={db_url}"])
if project_id:
- cmd.extend(["-e", f"VENTIS_PROJECT_ID={project_id}"])
+ cmd.extend(["-e", f"CANYONOS_PROJECT_ID={project_id}"])
if resources.get("cpu"):
cmd.extend(["--cpus", str(resources["cpu"])])
if resources.get("memory"):
@@ -115,7 +129,7 @@ def bootstrap_instance(provisioned, spec, replica_index, agent_id):
cmd.extend(["--gpus", str(resources["gpu"])])
# User secrets from `env_file`. Explicit -e flags above still win, so a
- # stray VENTIS_* line in someone's .env cannot break agent wiring.
+ # stray CANYONOS_* line in someone's .env cannot break agent wiring.
with env_file_args(
_require_controller(), host, user, runtime_id, _is_local_host(host)
) as env_args:
diff --git a/ventis/controller/deploy.py b/canyonos_core/controller/deploy.py
similarity index 93%
rename from ventis/controller/deploy.py
rename to canyonos_core/controller/deploy.py
index 47de634..f480dc4 100644
--- a/ventis/controller/deploy.py
+++ b/canyonos_core/controller/deploy.py
@@ -1,25 +1,25 @@
"""
-Ventis Deploy Module
+CanyonOS Deploy Module
Provides `deploy()` to expose a workflow function as an async REST API endpoint.
Requests are assigned a unique ID and processed asynchronously. Results are
stored in Redis and can be polled via GET /status/.
Usage:
- import ventis
+ import canyonos_core
def my_workflow(query: str):
finance = FinanceAgent()
price = finance.get_stock_price(ticker=query)
return {"price": price.value()}
- ventis.deploy(my_workflow, port=8080)
+ canyonos_core.deploy(my_workflow, port=8080)
"""
try:
- import ventis.controller.ventis_context as ventis_context
+ import canyonos_core.controller.canyonos_context as canyonos_context
except ImportError:
- import ventis_context
+ import canyonos_context
import json
import logging
import os
@@ -33,12 +33,12 @@ def my_workflow(query: str):
# Try to import from absolute package (local install) or fallback to flat file (Docker container)
try:
- from ventis.controller.utils.redis_client import RedisClient
+ from canyonos_core.controller.utils.redis_client import RedisClient
except ImportError:
from redis_client import RedisClient
try:
- from ventis.controller.utils.session_logging import get_session, upsert_session
+ from canyonos_core.controller.utils.session_logging import get_session, upsert_session
except ImportError:
from session_logging import get_session, upsert_session
@@ -90,17 +90,17 @@ def deploy(workflow_fn, port=8080, host="0.0.0.0", redis_host=None, redis_port=N
redis_host: Redis host (default: from env or localhost).
redis_port: Redis port (default: from env or 6379).
"""
- redis_host = redis_host or os.environ.get("VENTIS_REDIS_HOST", "localhost")
- redis_port = redis_port or int(os.environ.get("VENTIS_REDIS_PORT", 6379))
+ redis_host = redis_host or os.environ.get("CANYONOS_REDIS_HOST", "localhost")
+ redis_port = redis_port or int(os.environ.get("CANYONOS_REDIS_PORT", 6379))
redis_client = RedisClient(host=redis_host, port=redis_port)
# These are fallbacks only. _current_identity() reads the controller's
# current Redis value for every session transition and status fallback.
- env_db_url = os.environ.get("VENTIS_DATABASE_URL")
- env_project_id = os.environ.get("VENTIS_PROJECT_ID")
+ env_db_url = os.environ.get("CANYONOS_DATABASE_URL")
+ env_project_id = os.environ.get("CANYONOS_PROJECT_ID")
fn_name = workflow_fn.__name__
- app = Flask(f"ventis-{fn_name}")
+ app = Flask(f"canyonos-{fn_name}")
def _expire_request_keys(request_id):
"""Let a finished request's Redis keys age out instead of living forever."""
@@ -151,7 +151,7 @@ def _execute_workflow(request_id, kwargs, context=None):
redis_client.set(context_key, json.dumps(context))
# Set thread-local request ID so Futures spawned here carry it
- ventis_context.set_request_id(request_id)
+ canyonos_context.set_request_id(request_id)
result = workflow_fn(**kwargs)
diff --git a/ventis/controller/future.py b/canyonos_core/controller/future.py
similarity index 93%
rename from ventis/controller/future.py
rename to canyonos_core/controller/future.py
index 04b050a..dbc5868 100644
--- a/ventis/controller/future.py
+++ b/canyonos_core/controller/future.py
@@ -8,12 +8,12 @@
import grpc
try:
- import ventis.controller.ventis_context as ventis_context
+ import canyonos_core.controller.canyonos_context as canyonos_context
except ImportError:
- import ventis_context
+ import canyonos_context
try:
- from ventis.controller.utils.grpc_options import GRPC_CHANNEL_OPTIONS
+ from canyonos_core.controller.utils.grpc_options import GRPC_CHANNEL_OPTIONS
except ImportError:
from grpc_options import GRPC_CHANNEL_OPTIONS
@@ -23,7 +23,7 @@
sys.path.insert(0, os.path.abspath("grpc_stubs"))
try:
- from ventis.controller.utils.redis_client import RedisClient
+ from canyonos_core.controller.utils.redis_client import RedisClient
except ImportError:
from redis_client import RedisClient
import local_controler_pb2
@@ -35,13 +35,13 @@
# defines the future object which will be returned by each function call
class Future(object):
redis = RedisClient(
- host=os.environ.get("VENTIS_REDIS_HOST", "localhost"),
- port=int(os.environ.get("VENTIS_REDIS_PORT", 6379)),
+ host=os.environ.get("CANYONOS_REDIS_HOST", "localhost"),
+ port=int(os.environ.get("CANYONOS_REDIS_PORT", 6379)),
)
# Single local controller connection, shared across all futures
- _lc_host = os.environ.get("VENTIS_LC_HOST", "localhost")
- _lc_port = os.environ.get("VENTIS_LC_PORT", "50051")
+ _lc_host = os.environ.get("CANYONOS_LC_HOST", "localhost")
+ _lc_port = os.environ.get("CANYONOS_LC_PORT", "50051")
_channel = None
_stub = None
@@ -70,13 +70,13 @@ def __init__(self, parent, service, method, args=None):
self.id = secrets.token_hex(8)
# Grab the request_id from the thread-local context (set by deploy)
- self.request_id = ventis_context.get_request_id()
+ self.request_id = canyonos_context.get_request_id()
# this provides the funtionality we need to execute
self.funtionality = None
self.executor = None
self.result = None
- self.parent = ventis_context.get_current_future_id()
+ self.parent = canyonos_context.get_current_future_id()
self.service = service
self.method = method
self.args = args or {}
diff --git a/ventis/controller/global_controller.py b/canyonos_core/controller/global_controller.py
similarity index 95%
rename from ventis/controller/global_controller.py
rename to canyonos_core/controller/global_controller.py
index 4bf109a..5a330a1 100644
--- a/ventis/controller/global_controller.py
+++ b/canyonos_core/controller/global_controller.py
@@ -17,20 +17,20 @@
from concurrent.futures import ThreadPoolExecutor
import yaml
-from ventis.OTLP_Exporter import db as otel_db
-from ventis.controller.instance_manager import InstanceManager
-from ventis.controller.utils.agent_specs import write_agent_specs
-from ventis.controller.utils.env_file import resolve_env_file
-from ventis.controller.utils.process_supervisor import ProcessSupervisor
-from ventis.controller.utils.redis_utils import _wait_for_redis
-from ventis.controller.utils.telemetry_logging import (
+from canyonos_core.OTLP_Exporter import db as otel_db
+from canyonos_core.controller.instance_manager import InstanceManager
+from canyonos_core.controller.utils.agent_specs import write_agent_specs
+from canyonos_core.controller.utils.env_file import resolve_env_file
+from canyonos_core.controller.utils.process_supervisor import ProcessSupervisor
+from canyonos_core.controller.utils.redis_utils import _wait_for_redis
+from canyonos_core.controller.utils.telemetry_logging import (
assign_project_id,
pull_runtime_information,
send_runtime_information,
send_agent_information,
)
-from ventis.controller.utils.redis_client import RedisClient
-from ventis.controller.utils.grpc_options import GRPC_CHANNEL_OPTIONS
+from canyonos_core.controller.utils.redis_client import RedisClient
+from canyonos_core.controller.utils.grpc_options import GRPC_CHANNEL_OPTIONS
# Add generated grpc_stubs from the local project to the path. Projects using
# the .car artifact layout keep grpc_stubs under .car/; older/plain layouts
@@ -44,7 +44,14 @@
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
-LOCAL_NETWORK = "ventis-local"
+LOCAL_NETWORK = "canyonos-local"
+
+# Internal runtime controls that must never be settable from a user's `.env`.
+# The .env is for the user's own secrets (API keys, etc.); these keys steer
+# framework behavior, so honoring them from user data would be a control-plane
+# injection. CANYONOS_LLM_STUB_TEXT (the `canyonos test` LLM stub) is reachable
+# only via `canyonos test`, never a deploy's env_file.
+_RESERVED_ENV_KEYS = frozenset({"CANYONOS_LLM_STUB_TEXT"})
def _is_local_host(host):
@@ -116,7 +123,7 @@ def __init__(self, config_path):
self._cleanup_thread = threading.Thread(target=self._cleanup_loop, daemon=True)
self._cleanup_thread.start()
- # Spawn the OTLP exporter as a separate process (see ventis/OTLP_Exporter/DESIGN.md),
+ # Spawn the OTLP exporter as a separate process (see canyonos/OTLP_Exporter/DESIGN.md),
# supervised so it gets restarted if it ever exits unexpectedly.
otel_exporter_dir = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
@@ -162,8 +169,8 @@ def _cleanup_stale_containers(self):
for i, (host, port) in enumerate(placements):
if host not in host_containers:
host_containers[host] = (user, set())
- host_containers[host][1].add(f"ventis-redis-{host.replace('.', '-')}")
- host_containers[host][1].add(f"ventis-{name.lower()}-{i}")
+ host_containers[host][1].add(f"canyonos-redis-{host.replace('.', '-')}")
+ host_containers[host][1].add(f"canyonos-{name.lower()}-{i}")
# Try to remove each one on its respective host
for host, (user, container_names) in host_containers.items():
@@ -206,7 +213,7 @@ def _load_config(config_path):
@staticmethod
def _assign_new_project_id(config_path):
"""Generate a project_id and append it to the config file so it stays stable across reloads/restarts."""
- project_id = str(uuid.uuid4())
+ project_id = uuid.uuid4().hex
with open(config_path, "a") as f:
f.write(f'project_id: "{project_id}"\n')
return project_id
@@ -226,6 +233,9 @@ def _load_dotenv(path):
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
value = value[1:-1]
+ if key in _RESERVED_ENV_KEYS:
+ # Reserved internal control -- never honor it from user .env.
+ continue
if key and key not in os.environ:
os.environ[key] = value
@@ -393,10 +403,10 @@ def _launch_redis_containers(self):
for host, node_cfg in nodes.items():
redis_port = node_cfg["redis_port"]
user = node_cfg["user"]
- container_name = f"ventis-redis-{host.replace('.', '-')}"
- # VENTIS_REDIS_HOST overrides the localhost case for a containerized GC; host/remote paths unchanged.
+ container_name = f"canyonos-redis-{host.replace('.', '-')}"
+ # CANYONOS_REDIS_HOST overrides the localhost case for a containerized GC; host/remote paths unchanged.
if host in ("localhost", "127.0.0.1"):
- connect_host = os.environ.get("VENTIS_REDIS_HOST", "localhost")
+ connect_host = os.environ.get("CANYONOS_REDIS_HOST", "localhost")
else:
connect_host = host
@@ -562,7 +572,7 @@ def _poll_controllers(self):
self.process_supervisor.check_and_respawn()
# Polled in parallel, one instance's slow Redis/Postgres round-trip no longer
- # gates every other instance's poll -- see ventis/OTLP_Exporter/DESIGN.md.
+ # gates every other instance's poll -- see canyonos/OTLP_Exporter/DESIGN.md.
instances = self.instance_manager.list_instances()
if instances:
with ThreadPoolExecutor(max_workers=len(instances)) as executor:
@@ -919,7 +929,7 @@ def stop(self):
import argparse
- parser = argparse.ArgumentParser(description="Ventis Global Controller daemon.")
+ parser = argparse.ArgumentParser(description="CanyonOS Global Controller daemon.")
parser.add_argument(
"-c",
"--config",
diff --git a/ventis/controller/instance_manager.py b/canyonos_core/controller/instance_manager.py
similarity index 97%
rename from ventis/controller/instance_manager.py
rename to canyonos_core/controller/instance_manager.py
index 4117fd1..fa74b77 100644
--- a/ventis/controller/instance_manager.py
+++ b/canyonos_core/controller/instance_manager.py
@@ -3,7 +3,7 @@
This file decides whether each agent replica should run locally or on EC2,
starts missing instances, records their runtime metadata in Redis, and
-publishes the routing data other parts of Ventis use to reach those agents.
+publishes the routing data other parts of CanyonOS use to reach those agents.
"""
import json
@@ -11,7 +11,7 @@
import uuid
from concurrent.futures import ThreadPoolExecutor, as_completed
-from ventis.controller.cloud_provider_logic.Local import _runtime as local_runtime
+from canyonos_core.controller.cloud_provider_logic.Local import _runtime as local_runtime
DEFAULT_HOST_PORT_START = 8000
@@ -229,7 +229,7 @@ def _instance_id_from_record(self, instance):
def _provider_runtime(self, provider):
if provider.upper() == "EC2":
- from ventis.controller.cloud_provider_logic.EC2 import _runtime as runtime
+ from canyonos_core.controller.cloud_provider_logic.EC2 import _runtime as runtime
else:
runtime = local_runtime
runtime._controller = self.controller
diff --git a/ventis/controller/local_controller.py b/canyonos_core/controller/local_controller.py
similarity index 94%
rename from ventis/controller/local_controller.py
rename to canyonos_core/controller/local_controller.py
index 1cc879f..a3406c2 100644
--- a/ventis/controller/local_controller.py
+++ b/canyonos_core/controller/local_controller.py
@@ -16,10 +16,10 @@
import psutil
try:
- from ventis.controller.local_controller_frontend import start_server
- from ventis.controller.utils.gpu_metrics import read_gpu_percent
- from ventis.controller.utils.redis_client import RedisClient
- from ventis.controller.utils.grpc_options import GRPC_CHANNEL_OPTIONS
+ from canyonos_core.controller.local_controller_frontend import start_server
+ from canyonos_core.controller.utils.gpu_metrics import read_gpu_percent
+ from canyonos_core.controller.utils.redis_client import RedisClient
+ from canyonos_core.controller.utils.grpc_options import GRPC_CHANNEL_OPTIONS
except ImportError:
from gpu_metrics import read_gpu_percent
from local_controller_frontend import start_server
@@ -32,15 +32,15 @@
sys.path.insert(0, os.path.abspath("grpc_stubs"))
try:
- import ventis.controller.ventis_context as ventis_context
+ import canyonos_core.controller.canyonos_context as canyonos_context
except ImportError:
- import ventis_context
+ import canyonos_context
-# Auto-inject X-Ventis-Future-ID into all boto3 Bedrock calls so the LLM proxy
+# Auto-inject X-Canyonos-Future-ID into all boto3 Bedrock calls so the LLM proxy
# can attribute token/cost telemetry to the executing future. Import for its
# global boto3 event-hook side effect; safe no-op if the proxy isn't present.
try:
- from ventis.llm_proxy import proxy as _llm_proxy_autoinject # noqa: F401
+ from canyonos_core.llm_proxy import proxy as _llm_proxy_autoinject # noqa: F401
except ImportError:
try:
from llm_proxy import proxy as _llm_proxy_autoinject # noqa: F401
@@ -63,13 +63,13 @@ class LocalController(object):
def __init__(self, port=50051):
self.port = port
- self.agent_host = os.environ.get("VENTIS_AGENT_HOST", "localhost")
- self.agent_name = os.environ.get("VENTIS_AGENT_NAME")
- self.agent_file = os.environ.get("VENTIS_AGENT_FILE")
+ self.agent_host = os.environ.get("CANYONOS_AGENT_HOST", "localhost")
+ self.agent_name = os.environ.get("CANYONOS_AGENT_NAME")
+ self.agent_file = os.environ.get("CANYONOS_AGENT_FILE")
# Public port is how the routing table and other nodes know us;
# internally the gRPC server binds to `port` (50051 inside Docker).
- self.public_port = os.environ.get("VENTIS_AGENT_PORT", str(port))
+ self.public_port = os.environ.get("CANYONOS_AGENT_PORT", str(port))
self._my_endpoint = f"{self.agent_host}:{self.public_port}"
@@ -80,8 +80,8 @@ def __init__(self, port=50051):
self.servicer.on_result = self._fan_out_to_consumers
# Connect to Redis and report healthy status
- redis_host = os.environ.get("VENTIS_REDIS_HOST", "localhost")
- redis_port = int(os.environ.get("VENTIS_REDIS_PORT", 6379))
+ redis_host = os.environ.get("CANYONOS_REDIS_HOST", "localhost")
+ redis_port = int(os.environ.get("CANYONOS_REDIS_PORT", 6379))
self.redis = RedisClient(host=redis_host, port=redis_port)
self._status_key = f"controller:{self.agent_host}:{self.public_port}:status"
self.redis.set(self._status_key, "healthy")
@@ -93,9 +93,9 @@ def __init__(self, port=50051):
)
# Periodically publish instance metrics, on the same cadence
- # GlobalController polls with (via VENTIS_POLL_INTERVAL).
+ # GlobalController polls with (via CANYONOS_POLL_INTERVAL).
self._metrics_key = f"controller:{self.agent_host}:{self.public_port}:metrics"
- self._metrics_interval = float(os.environ.get("VENTIS_POLL_INTERVAL", 5))
+ self._metrics_interval = float(os.environ.get("CANYONOS_POLL_INTERVAL", 5))
psutil.cpu_percent(interval=None) # prime so the first real reading isn't 0.0
self._metrics_stop_event = threading.Event()
self._metrics_thread = threading.Thread(target=self._metrics_loop, daemon=True)
@@ -111,7 +111,7 @@ def __init__(self, port=50051):
# Thread pool for executing agent methods concurrently.
# This prevents deadlocks when an agent method creates nested Futures
# that need to be routed through the same controller's request queue.
- max_instances = int(os.environ.get("VENTIS_MAX_AGENT_INSTANCES", 8))
+ max_instances = int(os.environ.get("CANYONOS_MAX_AGENT_INSTANCES", 8))
self._executor = ThreadPoolExecutor(max_workers=max_instances)
# Start the LLM proxy alongside the agent in this container. Bedrock
@@ -140,11 +140,11 @@ def _start_llm_proxy(self, redis_host, redis_port):
proxy_env.update({
"PROXY_HOST": "127.0.0.1",
"PROXY_PORT": "8081",
- "VENTIS_REDIS_HOST": redis_host,
- "VENTIS_REDIS_PORT": str(redis_port),
+ "CANYONOS_REDIS_HOST": redis_host,
+ "CANYONOS_REDIS_PORT": str(redis_port),
})
proxy_process = subprocess.Popen(
- [sys.executable, "-m", "ventis.llm_proxy"],
+ [sys.executable, "-m", "canyonos_core.llm_proxy"],
env=proxy_env,
)
logger.info(
@@ -190,7 +190,7 @@ def _load_agent(self):
"""Dynamically load and instantiate the agent class."""
if not self.agent_name or not self.agent_file:
logger.warning(
- "VENTIS_AGENT_NAME or VENTIS_AGENT_FILE not set. Running without an agent."
+ "CANYONOS_AGENT_NAME or CANYONOS_AGENT_FILE not set. Running without an agent."
)
return None
@@ -603,9 +603,9 @@ def _execute_locally(
self.redis.hset_multiple(f"future:{future_id}", initial_fields)
if request_id:
self.redis.sadd(f"request:{request_id}:futures", future_id)
- ventis_context.set_request_id(request_id)
- ventis_context.set_current_future_id(future_id)
- ventis_context.set_current_metrics_key(self._metrics_key)
+ canyonos_context.set_request_id(request_id)
+ canyonos_context.set_current_future_id(future_id)
+ canyonos_context.set_current_metrics_key(self._metrics_key)
if self.agent is None:
logger.error("No agent loaded, cannot execute %s.%s", service, function)
self._mark_future_failed(future_id, "No agent loaded", origin)
@@ -695,7 +695,7 @@ def _execute_locally(
origin, future_id, failed=1, error_message=error_message or ""
)
- ventis_context.set_current_future_id(parent or "")
+ canyonos_context.set_current_future_id(parent or "")
# ------------------------------------------------------------------ #
# Request forwarding #
diff --git a/ventis/controller/local_controller_frontend.py b/canyonos_core/controller/local_controller_frontend.py
similarity index 95%
rename from ventis/controller/local_controller_frontend.py
rename to canyonos_core/controller/local_controller_frontend.py
index 722bd16..ec1da4e 100644
--- a/ventis/controller/local_controller_frontend.py
+++ b/canyonos_core/controller/local_controller_frontend.py
@@ -29,10 +29,10 @@ def __init__(self, my_endpoint="unknown"):
self.request_queue = queue.Queue()
self.my_endpoint = my_endpoint
# Redis client for writing results back to local Redis
- redis_host = os.environ.get("VENTIS_REDIS_HOST", "localhost")
- redis_port = int(os.environ.get("VENTIS_REDIS_PORT", 6379))
+ redis_host = os.environ.get("CANYONOS_REDIS_HOST", "localhost")
+ redis_port = int(os.environ.get("CANYONOS_REDIS_PORT", 6379))
try:
- from ventis.controller.utils.redis_client import RedisClient
+ from canyonos_core.controller.utils.redis_client import RedisClient
except ImportError:
from redis_client import RedisClient
self.redis = RedisClient(host=redis_host, port=redis_port)
@@ -152,7 +152,7 @@ def _cleanup_request(self, request_id):
def start_server(port=50051, my_endpoint="unknown"):
"""Start the gRPC server."""
try:
- from ventis.controller.utils.grpc_options import GRPC_SERVER_OPTIONS
+ from canyonos_core.controller.utils.grpc_options import GRPC_SERVER_OPTIONS
except ImportError:
from grpc_options import GRPC_SERVER_OPTIONS
diff --git a/ventis/controller/proto/global_controller.proto b/canyonos_core/controller/proto/global_controller.proto
similarity index 100%
rename from ventis/controller/proto/global_controller.proto
rename to canyonos_core/controller/proto/global_controller.proto
diff --git a/ventis/controller/proto/local_controler.proto b/canyonos_core/controller/proto/local_controler.proto
similarity index 100%
rename from ventis/controller/proto/local_controler.proto
rename to canyonos_core/controller/proto/local_controler.proto
diff --git a/canyonos_core/controller/utils/__init__.py b/canyonos_core/controller/utils/__init__.py
new file mode 100644
index 0000000..d597e67
--- /dev/null
+++ b/canyonos_core/controller/utils/__init__.py
@@ -0,0 +1 @@
+# CanyonOS Controller Utility helpers
diff --git a/ventis/controller/utils/agent_specs.py b/canyonos_core/controller/utils/agent_specs.py
similarity index 100%
rename from ventis/controller/utils/agent_specs.py
rename to canyonos_core/controller/utils/agent_specs.py
diff --git a/ventis/controller/utils/aws_pricing_chart.db b/canyonos_core/controller/utils/aws_pricing_chart.db
similarity index 100%
rename from ventis/controller/utils/aws_pricing_chart.db
rename to canyonos_core/controller/utils/aws_pricing_chart.db
diff --git a/ventis/controller/utils/env_file.py b/canyonos_core/controller/utils/env_file.py
similarity index 99%
rename from ventis/controller/utils/env_file.py
rename to canyonos_core/controller/utils/env_file.py
index b1f9381..5a738b4 100644
--- a/ventis/controller/utils/env_file.py
+++ b/canyonos_core/controller/utils/env_file.py
@@ -101,7 +101,7 @@ def remote_env_path(container_name):
while the secrets stayed on the host, with nothing in the log to say so.
"""
safe_name = _UNSAFE_PATH_CHARS.sub("-", container_name)
- return f"{REMOTE_ENV_DIR}/ventis-env-{safe_name}"
+ return f"{REMOTE_ENV_DIR}/canyonos-env-{safe_name}"
@contextmanager
diff --git a/ventis/controller/utils/gpu_metrics.py b/canyonos_core/controller/utils/gpu_metrics.py
similarity index 100%
rename from ventis/controller/utils/gpu_metrics.py
rename to canyonos_core/controller/utils/gpu_metrics.py
diff --git a/ventis/controller/utils/grpc_options.py b/canyonos_core/controller/utils/grpc_options.py
similarity index 100%
rename from ventis/controller/utils/grpc_options.py
rename to canyonos_core/controller/utils/grpc_options.py
diff --git a/ventis/controller/utils/pricing.py b/canyonos_core/controller/utils/pricing.py
similarity index 100%
rename from ventis/controller/utils/pricing.py
rename to canyonos_core/controller/utils/pricing.py
diff --git a/ventis/controller/utils/process_supervisor.py b/canyonos_core/controller/utils/process_supervisor.py
similarity index 95%
rename from ventis/controller/utils/process_supervisor.py
rename to canyonos_core/controller/utils/process_supervisor.py
index f5336e6..00f611b 100644
--- a/ventis/controller/utils/process_supervisor.py
+++ b/canyonos_core/controller/utils/process_supervisor.py
@@ -3,7 +3,7 @@
register() + start_all() spawn processes; check_and_respawn() (call from GC's existing
poll tick) restarts any that exit unexpectedly; terminate_all() (call from GC's shutdown
path) stops them all cleanly. Deliberately GC-agnostic -- callers are responsible for not
-calling check_and_respawn() during their own shutdown (see ventis/OTLP_Exporter/DESIGN.md's
+calling check_and_respawn() during their own shutdown (see canyonos/OTLP_Exporter/DESIGN.md's
shutdown-race note).
"""
diff --git a/ventis/controller/utils/redis_client.py b/canyonos_core/controller/utils/redis_client.py
similarity index 100%
rename from ventis/controller/utils/redis_client.py
rename to canyonos_core/controller/utils/redis_client.py
diff --git a/ventis/controller/utils/redis_utils.py b/canyonos_core/controller/utils/redis_utils.py
similarity index 90%
rename from ventis/controller/utils/redis_utils.py
rename to canyonos_core/controller/utils/redis_utils.py
index 4bdc265..54b6dad 100644
--- a/ventis/controller/utils/redis_utils.py
+++ b/canyonos_core/controller/utils/redis_utils.py
@@ -7,7 +7,7 @@ def _wait_for_redis(redis_client, host, port, timeout=30, interval=1):
last_error = None
while time.time() < deadline:
try:
- redis_client.set("__ventis_redis_healthcheck__", "ok")
+ redis_client.set("__canyonos_redis_healthcheck__", "ok")
return
except Exception as exc:
last_error = exc
diff --git a/ventis/controller/utils/session_logging.py b/canyonos_core/controller/utils/session_logging.py
similarity index 98%
rename from ventis/controller/utils/session_logging.py
rename to canyonos_core/controller/utils/session_logging.py
index df8e4dc..0817c8a 100644
--- a/ventis/controller/utils/session_logging.py
+++ b/canyonos_core/controller/utils/session_logging.py
@@ -45,7 +45,7 @@
def _get_engine(database_url):
"""Return a cached Engine for `database_url`, building one on first use per resolved URL."""
global _engines
- url = os.environ.get("VENTIS_DATABASE_URL", str(database_url))
+ url = os.environ.get("CANYONOS_DATABASE_URL", str(database_url))
if url.startswith("postgresql://"):
url = "postgresql+psycopg://" + url[len("postgresql://"):]
engine = _engines.get(url)
diff --git a/ventis/controller/utils/telemetry_logging.py b/canyonos_core/controller/utils/telemetry_logging.py
similarity index 92%
rename from ventis/controller/utils/telemetry_logging.py
rename to canyonos_core/controller/utils/telemetry_logging.py
index 503f3e1..dbb15f5 100644
--- a/ventis/controller/utils/telemetry_logging.py
+++ b/canyonos_core/controller/utils/telemetry_logging.py
@@ -6,8 +6,8 @@
from datetime import datetime, timezone
from sqlalchemy import create_engine, text
-from ventis.controller.utils import pricing
-from ventis.controller.utils.redis_client import RedisClient
+from canyonos_core.controller.utils import pricing
+from canyonos_core.controller.utils.redis_client import RedisClient
logger = logging.getLogger(__name__)
@@ -90,7 +90,7 @@ def assign_project_id(project_id) -> None:
def _get_engine(database_url):
global _engine
if _engine is None:
- url = os.environ.get("VENTIS_DATABASE_URL", str(database_url))
+ url = os.environ.get("CANYONOS_DATABASE_URL", str(database_url))
if url.startswith("postgresql://"):
url = "postgresql+psycopg://" + url[len("postgresql://"):]
_engine = create_engine(url)
@@ -112,6 +112,15 @@ def pull_runtime_information(redis_client):
return rows
+def _demo_cost_multiplier(env_var):
+ """Off (1x) unless the env var opts in; logs a warning since it inflates recorded costs."""
+ raw = os.environ.get(env_var)
+ if raw is None:
+ return 1
+ logger.warning("%s=%s is set -- displayed costs are scaled and do not reflect real recorded costs.", env_var, raw)
+ return float(raw)
+
+
def send_runtime_information(
rows,
redis_client: RedisClient | None = None,
@@ -122,8 +131,8 @@ def send_runtime_information(
return
# Demo-only multipliers for scaling displayed costs; not real recorded costs.
- token_cost_multiplier = 10000
- server_cost_multiplier = 100000
+ token_cost_multiplier = _demo_cost_multiplier("CANYONOS_DEMO_TOKEN_COST_MULTIPLIER")
+ server_cost_multiplier = _demo_cost_multiplier("CANYONOS_DEMO_SERVER_COST_MULTIPLIER")
with _get_engine(database_url).begin() as conn:
for raw in rows:
diff --git a/canyonos_core/llm_proxy/README.md b/canyonos_core/llm_proxy/README.md
new file mode 100644
index 0000000..8de3d8e
--- /dev/null
+++ b/canyonos_core/llm_proxy/README.md
@@ -0,0 +1,112 @@
+# llm_proxy
+
+A local, single-machine pass-through proxy for **OpenAI**, **Anthropic**, and
+**Bedrock**. Callers keep their exact SDK calling convention — the only change is
+one base-URL env var per provider. Every call flows through one function
+(`core.proxy_request`) where token/metrics hooks fire.
+
+**Scope:** request/response ("call and return") only. Streaming is intentionally
+not implemented yet.
+
+## How it works
+
+```
+your app (unchanged) localhost:8080 real upstream
+ openai SDK ─/openai/... ─┐
+ anthropic SDK ─/anthropic/ ─┼─▶ proxy_request(ctx) ─▶ provider ─▶ api.openai.com
+ boto3 bedrock ─/bedrock/... ┘ (metrics hooks) adapter api.anthropic.com
+ bedrock-runtime..amazonaws.com
+```
+
+- **OpenAI / Anthropic** — straight HTTP reverse-proxy: rewrite host, swap in the
+ real key, forward with `requests`, return the response.
+- **Bedrock** — re-issued through the proxy's own `boto3` client (handles SigV4
+ signing + URL-encoding correctly). Only `invoke` is wired up.
+
+## Run
+
+```bash
+pip install -r llm_proxy/requirements.txt
+
+# real upstream credentials live here; callers can use dummy keys
+export OPENAI_API_KEY=sk-...
+export ANTHROPIC_API_KEY=sk-ant-...
+export AWS_REGION=us-east-1 # + normal AWS creds (env / ~/.aws / role)
+
+python -m llm_proxy # listens on 127.0.0.1:8080
+```
+
+## Point your SDKs at it
+
+No code changes — just env vars:
+
+```bash
+export OPENAI_BASE_URL=http://localhost:8080/openai/v1
+export ANTHROPIC_BASE_URL=http://localhost:8080/anthropic
+export AWS_ENDPOINT_URL_BEDROCK_RUNTIME=http://localhost:8080/bedrock
+```
+
+Then your existing code works unchanged:
+
+```python
+from openai import OpenAI
+OpenAI().chat.completions.create(model="gpt-4o-mini",
+ messages=[{"role": "user", "content": "hi"}])
+
+from anthropic import Anthropic
+Anthropic().messages.create(model="claude-3-5-sonnet-20241022", max_tokens=64,
+ messages=[{"role": "user", "content": "hi"}])
+
+import boto3, json
+boto3.client("bedrock-runtime").invoke_model(
+ modelId="anthropic.claude-3-5-sonnet-20240620-v1:0",
+ body=json.dumps({"anthropic_version": "bedrock-2023-05-31",
+ "max_tokens": 64,
+ "messages": [{"role": "user", "content": "hi"}]}))
+```
+
+## Configuration (env vars)
+
+| Var | Default | Purpose |
+|---|---|---|
+| `PROXY_HOST` / `PROXY_PORT` | `127.0.0.1` / `8080` | where the proxy listens |
+| `PROXY_CONNECT_TIMEOUT` / `PROXY_READ_TIMEOUT` | `10` / `600` | upstream timeouts (s) |
+| `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` | — | real upstream keys the proxy injects |
+| `OPENAI_UPSTREAM_BASE` / `ANTHROPIC_UPSTREAM_BASE` | official APIs | override upstream (e.g. Azure/gateway) |
+| `BEDROCK_REGION` (or `AWS_REGION`) | `us-east-1` | Bedrock region |
+| `BEDROCK_UPSTREAM_HOST` | `bedrock-runtime..amazonaws.com` | override Bedrock host |
+
+## Telemetry & Metrics
+
+**Automatic telemetry is currently Bedrock-only.** The proxy captures:
+- Model ID
+- Input/output/total token counts
+- Cache tokens (read & write)
+- Error status
+
+Telemetry is automatically written to Redis under `future:` keys.
+
+### How it works (Bedrock only)
+
+1. **Auto-injection:** boto3 hook (`proxy.py`) injects `X-Canyonos-Future-ID` header from thread-local context
+2. **Token extraction:** `hooks.py` parses response `usage` field
+3. **Redis write:** All metrics written to `future:` hash
+
+### Why Bedrock-only?
+
+OpenAI and Anthropic use their own Python SDKs (`openai`, `anthropic`), not boto3.
+The boto3 event hook doesn't fire for non-AWS SDKs. To add telemetry for those:
+- Would need separate hooks in each SDK's HTTP client
+- Or callers would need to use proxy directly (not through SDKs)
+
+The proxy *forwards* OpenAI/Anthropic requests and *can* extract tokens, but doesn't
+automatically inject headers or write telemetry.
+
+## Limitations
+
+- **No streaming.** `stream=True` / `invoke-with-response-stream` are not handled.
+- **Bedrock error bodies are reconstructed**, not passed through byte-for-byte
+ (boto3 raises on 4xx/5xx; we rebuild a JSON body with the real status +
+ message). OpenAI/Anthropic errors pass through unchanged.
+- **Dev server.** Runs on Flask's built-in server — fine for a local proxy, not
+ meant for production traffic.
diff --git a/canyonos_core/llm_proxy/__init__.py b/canyonos_core/llm_proxy/__init__.py
new file mode 100644
index 0000000..827377e
--- /dev/null
+++ b/canyonos_core/llm_proxy/__init__.py
@@ -0,0 +1,14 @@
+"""Local LLM proxy.
+
+A transparent, single-machine pass-through for OpenAI, Anthropic, and Bedrock.
+Point each provider's SDK at this service via its base-URL env var and calls flow
+through one choke point (``llm_proxy.core.proxy_request``) where request/response
+metrics hooks fire.
+
+Scope: request/response ("call and return") only. Streaming is intentionally
+not implemented yet.
+"""
+
+__all__ = ["__version__"]
+
+__version__ = "0.1.0"
diff --git a/canyonos_core/llm_proxy/__main__.py b/canyonos_core/llm_proxy/__main__.py
new file mode 100644
index 0000000..f5fea0c
--- /dev/null
+++ b/canyonos_core/llm_proxy/__main__.py
@@ -0,0 +1,29 @@
+"""Entry point: ``python -m llm_proxy``."""
+
+from __future__ import annotations
+
+import logging
+
+from canyonos_core.llm_proxy.app import create_app
+from canyonos_core.llm_proxy.config import Config
+
+
+def main() -> None:
+ logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s %(levelname)s %(name)s: %(message)s",
+ )
+ cfg = Config.from_env()
+ app = create_app(cfg)
+ logging.getLogger("llm_proxy").info(
+ "llm_proxy on http://%s:%d (openai=%s, anthropic=%s, bedrock=%s [%s])",
+ cfg.host, cfg.port, cfg.openai.upstream_base, cfg.anthropic.upstream_base,
+ cfg.bedrock_upstream_host, cfg.bedrock_region,
+ )
+ # threaded so concurrent callers don't serialize; dev server is fine for a
+ # local proxy.
+ app.run(host=cfg.host, port=cfg.port, threaded=True)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/canyonos_core/llm_proxy/app.py b/canyonos_core/llm_proxy/app.py
new file mode 100644
index 0000000..d9b53df
--- /dev/null
+++ b/canyonos_core/llm_proxy/app.py
@@ -0,0 +1,46 @@
+"""Flask app: one catch-all route per provider prefix, all funneled through
+``proxy_request``."""
+
+from __future__ import annotations
+
+import logging
+
+from flask import Flask, jsonify, request
+
+from canyonos_core.llm_proxy.config import Config
+from canyonos_core.llm_proxy.core import proxy_request
+from canyonos_core.llm_proxy.providers import build_registry
+
+log = logging.getLogger("llm_proxy")
+
+ALL_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"]
+
+
+def create_app(cfg: Config = None) -> Flask:
+ cfg = cfg or Config.from_env()
+ app = Flask(__name__)
+ registry = build_registry(cfg)
+
+ # Initialize hooks with config for Redis
+ from canyonos_core.llm_proxy import hooks as hooks_module
+ hooks_module.hooks = hooks_module.Hooks(cfg)
+
+ @app.route("/healthz", methods=["GET"])
+ def healthz():
+ return jsonify(status="ok", providers=sorted(registry.keys()))
+
+ @app.route("//", methods=ALL_METHODS)
+ def dispatch(provider, subpath):
+ prov = registry.get(provider)
+ if prov is None:
+ return (
+ jsonify(error=f"unknown provider '{provider}'", known=sorted(registry.keys())),
+ 404,
+ )
+ try:
+ return proxy_request(prov, subpath, request)
+ except Exception as exc: # surface upstream/adapter errors as 502
+ log.exception("proxy error for %s/%s", provider, subpath)
+ return jsonify(error="proxy_error", detail=str(exc)), 502
+
+ return app
diff --git a/canyonos_core/llm_proxy/config.py b/canyonos_core/llm_proxy/config.py
new file mode 100644
index 0000000..df87694
--- /dev/null
+++ b/canyonos_core/llm_proxy/config.py
@@ -0,0 +1,66 @@
+"""Configuration, read once from the environment at startup.
+
+The proxy holds the *real* upstream credentials; callers can send dummy keys.
+"""
+
+from __future__ import annotations
+
+import os
+from dataclasses import dataclass
+from typing import Optional
+
+
+@dataclass
+class ProviderConfig:
+ upstream_base: str
+ api_key: Optional[str] = None
+
+
+@dataclass
+class Config:
+ host: str
+ port: int
+ connect_timeout: float
+ read_timeout: float
+
+ openai: ProviderConfig
+ anthropic: ProviderConfig
+
+ bedrock_region: str
+ bedrock_upstream_host: str
+
+ redis_host: str
+ redis_port: int
+
+ @classmethod
+ def from_env(cls) -> "Config":
+ region = (
+ os.getenv("BEDROCK_REGION")
+ or os.getenv("AWS_REGION")
+ or os.getenv("AWS_DEFAULT_REGION")
+ or "us-east-1"
+ )
+ return cls(
+ host=os.getenv("PROXY_HOST", "127.0.0.1"),
+ port=int(os.getenv("PROXY_PORT", "8080")),
+ connect_timeout=float(os.getenv("PROXY_CONNECT_TIMEOUT", "10")),
+ read_timeout=float(os.getenv("PROXY_READ_TIMEOUT", "600")),
+ openai=ProviderConfig(
+ upstream_base=os.getenv(
+ "OPENAI_UPSTREAM_BASE", "https://api.openai.com"
+ ).rstrip("/"),
+ api_key=os.getenv("OPENAI_API_KEY"),
+ ),
+ anthropic=ProviderConfig(
+ upstream_base=os.getenv(
+ "ANTHROPIC_UPSTREAM_BASE", "https://api.anthropic.com"
+ ).rstrip("/"),
+ api_key=os.getenv("ANTHROPIC_API_KEY"),
+ ),
+ bedrock_region=region,
+ bedrock_upstream_host=os.getenv(
+ "BEDROCK_UPSTREAM_HOST", f"bedrock-runtime.{region}.amazonaws.com"
+ ),
+ redis_host=os.getenv("CANYONOS_REDIS_HOST", "localhost"),
+ redis_port=int(os.getenv("CANYONOS_REDIS_PORT", "6379")),
+ )
diff --git a/canyonos_core/llm_proxy/core.py b/canyonos_core/llm_proxy/core.py
new file mode 100644
index 0000000..38558af
--- /dev/null
+++ b/canyonos_core/llm_proxy/core.py
@@ -0,0 +1,57 @@
+"""The single choke point every proxied call flows through."""
+
+from __future__ import annotations
+
+import json
+import time
+from typing import Optional
+
+from flask import Response
+
+from canyonos_core.llm_proxy.hooks import Ctx
+
+
+def _guess_model(body: bytes) -> Optional[str]:
+ """Best-effort model name from the JSON body, for logging/metrics.
+
+ Never raises. Returns None for requests whose model isn't in the body
+ (e.g. Bedrock, where it's in the path and already shown via the subpath).
+ """
+ try:
+ model = json.loads(body).get("model")
+ return model if isinstance(model, str) else None
+ except Exception:
+ return None
+
+
+def proxy_request(provider, subpath, flask_request):
+ # Import hooks here to get the instance created by create_app
+ from canyonos_core.llm_proxy.hooks import hooks
+
+ body = flask_request.get_data()
+ ctx = Ctx(
+ provider=provider.name,
+ method=flask_request.method,
+ subpath=subpath,
+ body=body,
+ headers=dict(flask_request.headers),
+ t0=time.monotonic(),
+ model=_guess_model(body),
+ )
+ hooks.on_request(ctx)
+
+ # Test mode: if CANYONOS_LLM_STUB_TEXT is set, return canned text instead of
+ # calling the real upstream. Telemetry hooks still fire so the whole pipeline
+ # is exercised end-to-end without cloud credentials.
+ from canyonos_core.llm_proxy.stub import build_stub, stub_text
+
+ # Empty string (the runtime's explicit "disabled" value) is falsy, so only a
+ # non-empty stub text -- which only `canyonos test` sets -- enables stubbing.
+ _stub = stub_text()
+ if _stub:
+ pr = build_stub(provider.name, subpath, _stub)
+ else:
+ pr = provider.forward(flask_request, subpath, body)
+
+ hooks.on_response(ctx, pr)
+ return Response(pr.content, status=pr.status, headers=pr.headers)
diff --git a/canyonos_core/llm_proxy/hooks.py b/canyonos_core/llm_proxy/hooks.py
new file mode 100644
index 0000000..89182a5
--- /dev/null
+++ b/canyonos_core/llm_proxy/hooks.py
@@ -0,0 +1,159 @@
+"""The metrics seam.
+
+Every proxied call passes through ``on_request`` / ``on_response``. Today these
+only log. Token accounting lands here later: because the whole response is
+buffered, usage extraction is a one-liner, e.g. ``resp.json().get("usage")`` for
+OpenAI/Anthropic (Bedrock's usage lives in its per-model response body).
+"""
+
+from __future__ import annotations
+
+import json
+import logging
+import time
+from dataclasses import dataclass
+from typing import Any, Dict, Optional
+
+log = logging.getLogger("llm_proxy")
+
+
+@dataclass
+class TokenUsage:
+ """Token usage extracted from LLM responses."""
+ input_tokens: int = 0
+ output_tokens: int = 0
+ total_tokens: int = 0
+ input_cache_tokens: int = 0
+ input_cache_write_tokens: int = 0
+
+ def __repr__(self):
+ parts = [f"in={self.input_tokens}", f"out={self.output_tokens}"]
+ if self.input_cache_tokens:
+ parts.append(f"cache_read={self.input_cache_tokens}")
+ if self.input_cache_write_tokens:
+ parts.append(f"cache_write={self.input_cache_write_tokens}")
+ return f"TokenUsage({', '.join(parts)})"
+
+
+@dataclass
+class Ctx:
+ provider: str
+ method: str
+ subpath: str
+ body: bytes
+ headers: Dict[str, str]
+ t0: float
+ model: Optional[str] = None
+
+ def elapsed_ms(self) -> float:
+ return (time.monotonic() - self.t0) * 1000.0
+
+
+class Hooks:
+ def __init__(self, config=None):
+ self.config = config
+ self._redis = None
+
+ if config:
+ try:
+ try:
+ from canyonos_core.controller.utils.redis_client import RedisClient
+ except ImportError:
+ # In-container the framework files are copied flat to /app.
+ from redis_client import RedisClient
+ self._redis = RedisClient(
+ host=config.redis_host,
+ port=config.redis_port,
+ )
+ log.info("Redis telemetry enabled: %s:%s", config.redis_host, config.redis_port)
+ except Exception as e:
+ log.warning("Redis not available: %s", e)
+
+ def on_request(self, ctx: Ctx) -> None:
+ log.info(
+ "→ %s %s /%s model=%s (%d bytes)",
+ ctx.provider, ctx.method, ctx.subpath, ctx.model, len(ctx.body),
+ )
+
+ def on_response(self, ctx: Ctx, resp: Any) -> None:
+ # Extract tokens for Bedrock
+ usage = None
+ if ctx.provider == "bedrock":
+ usage = self._extract_bedrock_tokens(resp)
+
+ log.info(
+ "← %s %s /%s -> %s in %.0fms | %s",
+ ctx.provider, ctx.method, ctx.subpath,
+ getattr(resp, "status", "?"), ctx.elapsed_ms(),
+ usage or "no usage"
+ )
+
+ # Write to Redis if we have context
+ log.info("Checking telemetry write: redis=%s", "yes" if self._redis else "no")
+ if self._redis:
+ future_id = ctx.headers.get("X-Canyonos-Future-ID")
+ log.info("Future ID from headers: %s", future_id)
+ if future_id:
+ try:
+ # Extract model ID
+ model_id = self._extract_model_id(ctx)
+
+ is_error = resp.status >= 400
+
+ # Build telemetry data
+ data = {
+ "model": model_id,
+ "errors": "1" if is_error else "0",
+ }
+
+ # Add token data if available
+ if usage:
+ data.update({
+ "input_token_count": str(usage.input_tokens),
+ "output_token_count": str(usage.output_tokens),
+ "token_count": str(usage.total_tokens),
+ "input_cache_tokens": str(usage.input_cache_tokens),
+ "input_cache_write_tokens": str(usage.input_cache_write_tokens),
+ })
+
+ self._redis.hset_multiple(f"future:{future_id}", data)
+ log.info("Wrote telemetry to future:%s with data: %s", future_id, data)
+ except Exception as e:
+ log.error("Failed to write telemetry: %s", e)
+
+ def _extract_model_id(self, ctx: Ctx) -> str:
+ """Extract model ID from context or subpath."""
+ if ctx.model:
+ return ctx.model
+
+ # For Bedrock: subpath is "model//operation"
+ # Use rpartition to peel operation off the right (same as provider logic)
+ if ctx.provider == "bedrock" and ctx.subpath.startswith("model/"):
+ model_id, sep, op = ctx.subpath[len("model/"):].rpartition("/")
+ if sep: # Found a separator
+ return model_id
+
+ return "unknown"
+
+ def _extract_bedrock_tokens(self, resp: Any) -> Optional[TokenUsage]:
+ """Extract token usage from Bedrock response. It requires diff logic from OpenAI/Anthropic"""
+ if resp.status != 200:
+ return None
+
+ try:
+ data = json.loads(resp.content.decode("utf-8"))
+ usage = data.get("usage", {})
+ if usage:
+ return TokenUsage(
+ input_tokens=usage.get("inputTokens", 0),
+ output_tokens=usage.get("outputTokens", 0),
+ total_tokens=usage.get("totalTokens", 0),
+ input_cache_tokens=usage.get("cacheReadInputTokens", 0),
+ input_cache_write_tokens=usage.get("cacheCreationInputTokens", 0),
+ )
+ except:
+ pass
+ return None
+
+
+hooks = Hooks()
diff --git a/canyonos_core/llm_proxy/providers/__init__.py b/canyonos_core/llm_proxy/providers/__init__.py
new file mode 100644
index 0000000..1ca3bd1
--- /dev/null
+++ b/canyonos_core/llm_proxy/providers/__init__.py
@@ -0,0 +1,14 @@
+from __future__ import annotations
+
+from canyonos_core.llm_proxy.providers.anthropic import AnthropicProvider
+from canyonos_core.llm_proxy.providers.bedrock import BedrockProvider
+from canyonos_core.llm_proxy.providers.openai import OpenAIProvider
+
+
+def build_registry(cfg):
+ """Map the URL prefix -> provider instance."""
+ return {
+ "openai": OpenAIProvider(cfg),
+ "anthropic": AnthropicProvider(cfg),
+ "bedrock": BedrockProvider(cfg),
+ }
diff --git a/canyonos_core/llm_proxy/providers/anthropic.py b/canyonos_core/llm_proxy/providers/anthropic.py
new file mode 100644
index 0000000..87a101e
--- /dev/null
+++ b/canyonos_core/llm_proxy/providers/anthropic.py
@@ -0,0 +1,19 @@
+from __future__ import annotations
+
+from canyonos_core.llm_proxy.providers.base import HttpProvider, UpstreamRequest, client_headers
+
+
+class AnthropicProvider(HttpProvider):
+ name = "anthropic"
+
+ def target(self, req, subpath, body):
+ headers = client_headers(req, drop=["x-api-key", "authorization"])
+ if self.cfg.anthropic.api_key:
+ headers["x-api-key"] = self.cfg.anthropic.api_key
+ # `anthropic-version` is supplied by the SDK and passes through untouched.
+ return UpstreamRequest(
+ method=req.method,
+ url=f"{self.cfg.anthropic.upstream_base}/{subpath}",
+ headers=headers,
+ params=req.args.to_dict(flat=True),
+ )
diff --git a/canyonos_core/llm_proxy/providers/base.py b/canyonos_core/llm_proxy/providers/base.py
new file mode 100644
index 0000000..ef5db41
--- /dev/null
+++ b/canyonos_core/llm_proxy/providers/base.py
@@ -0,0 +1,96 @@
+"""Provider abstraction and shared HTTP plumbing.
+
+A provider's only job is to take the incoming request and produce a
+``ProxyResponse``. Straight HTTP reverse-proxy providers (OpenAI, Anthropic)
+subclass ``HttpProvider`` and just describe the upstream target; Bedrock owns
+its own ``forward`` because it re-issues through boto3.
+"""
+
+from __future__ import annotations
+
+import json
+from dataclasses import dataclass, field
+from typing import Dict, Iterable, List, Tuple
+
+import requests
+
+# Request headers we never forward: hop-by-hop (RFC 7230), ones we rewrite, and
+# accept-encoding (we let the HTTP client negotiate + decode, then re-frame the
+# response ourselves).
+DROP_REQUEST_HEADERS = {
+ "connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
+ "te", "trailers", "transfer-encoding", "upgrade",
+ "host", "content-length", "accept-encoding",
+}
+
+# Response headers we drop: we return already-decoded content and let the WSGI
+# layer recompute framing headers.
+DROP_RESPONSE_HEADERS = {
+ "content-encoding", "content-length", "transfer-encoding",
+ "connection", "keep-alive",
+}
+
+
+@dataclass
+class UpstreamRequest:
+ method: str
+ url: str
+ headers: Dict[str, str]
+ params: Dict[str, str] = field(default_factory=dict)
+
+
+@dataclass
+class ProxyResponse:
+ status: int
+ headers: List[Tuple[str, str]]
+ content: bytes
+
+ def json(self):
+ return json.loads(self.content.decode("utf-8"))
+
+
+def client_headers(incoming, drop: Iterable[str] = ()) -> Dict[str, str]:
+ """Copy the caller's headers minus the ones we must not forward."""
+ extra = {d.lower() for d in drop}
+ return {
+ k: v
+ for k, v in incoming.headers.items()
+ if k.lower() not in DROP_REQUEST_HEADERS and k.lower() not in extra
+ }
+
+
+def filter_response_headers(headers) -> List[Tuple[str, str]]:
+ return [(k, v) for k, v in headers.items() if k.lower() not in DROP_RESPONSE_HEADERS]
+
+
+class Provider:
+ name = "base"
+
+ def __init__(self, cfg):
+ self.cfg = cfg
+
+ def forward(self, req, subpath: str, body: bytes) -> ProxyResponse:
+ raise NotImplementedError
+
+
+class HttpProvider(Provider):
+ """Providers that are a straight HTTP reverse-proxy (OpenAI, Anthropic)."""
+
+ def target(self, req, subpath: str, body: bytes) -> UpstreamRequest:
+ raise NotImplementedError
+
+ def forward(self, req, subpath, body):
+ up = self.target(req, subpath, body)
+ resp = requests.request(
+ up.method,
+ up.url,
+ headers=up.headers,
+ params=up.params,
+ data=body,
+ timeout=(self.cfg.connect_timeout, self.cfg.read_timeout),
+ )
+ return ProxyResponse(
+ status=resp.status_code,
+ headers=filter_response_headers(resp.headers),
+ content=resp.content,
+ )
diff --git a/canyonos_core/llm_proxy/providers/bedrock.py b/canyonos_core/llm_proxy/providers/bedrock.py
new file mode 100644
index 0000000..a3f3fb6
--- /dev/null
+++ b/canyonos_core/llm_proxy/providers/bedrock.py
@@ -0,0 +1,119 @@
+"""Bedrock adapter.
+
+Rather than re-sign the caller's SigV4 request (fiddly once model IDs contain
+``:`` and ``/``), we re-issue the call through the proxy's own boto3 client,
+which handles signing and URL-encoding correctly by construction. This is clean
+for request/response; streaming (``invoke-with-response-stream``) is out of scope
+for now.
+"""
+
+from __future__ import annotations
+
+import json
+
+import boto3
+from botocore.exceptions import ClientError
+
+from canyonos_core.llm_proxy.providers.base import Provider, ProxyResponse
+
+# bedrock-runtime operations that can appear as the last path segment; only the
+# non-streaming "invoke" is wired up for now.
+_SUPPORTED_OPS = {"invoke", "invoke-with-response-stream", "converse", "converse-stream"}
+
+
+class BedrockProvider(Provider):
+ name = "bedrock"
+
+ def __init__(self, cfg):
+ super().__init__(cfg)
+ # Explicitly set endpoint_url to bypass AWS_ENDPOINT_URL_BEDROCK_RUNTIME
+ # environment variable that points to this proxy (would create infinite loop)
+ self._client = boto3.client(
+ "bedrock-runtime",
+ region_name=cfg.bedrock_region,
+ endpoint_url=f"https://{cfg.bedrock_upstream_host}"
+ )
+
+ def forward(self, req, subpath, body):
+ model_id, op = self._parse(subpath)
+
+ try:
+ if op == "invoke":
+ resp = self._client.invoke_model(
+ modelId=model_id,
+ body=body,
+ contentType=req.headers.get("Content-Type", "application/json"),
+ accept=req.headers.get("Accept", "application/json"),
+ )
+ # For invoke, return raw response body
+ payload = resp["body"].read()
+ status = resp.get("ResponseMetadata", {}).get("HTTPStatusCode", 200)
+ headers = [("Content-Type", resp.get("contentType", "application/json"))]
+ return ProxyResponse(status=status, headers=headers, content=payload)
+
+ elif op == "converse":
+ params = json.loads(body)
+ params["modelId"] = model_id
+ resp = self._client.converse(**params)
+
+ # Return response as JSON
+ response_data = {
+ "output": resp.get("output", {}),
+ "stopReason": resp.get("stopReason"),
+ "usage": resp.get("usage", {}),
+ }
+ # Include optional fields if present
+ for field in ["metrics", "trace", "additionalModelResponseFields"]:
+ if field in resp:
+ response_data[field] = resp[field]
+
+ payload = json.dumps(response_data).encode("utf-8")
+ status = resp.get("ResponseMetadata", {}).get("HTTPStatusCode", 200)
+ return ProxyResponse(
+ status=status,
+ headers=[("Content-Type", "application/json")],
+ content=payload
+ )
+ else:
+ raise NotImplementedError(
+ f"bedrock op '{op}' not supported (only invoke and converse)"
+ )
+
+ except ClientError as exc:
+ return self._error_response(exc)
+ except (json.JSONDecodeError, KeyError) as exc:
+ return ProxyResponse(
+ status=400,
+ headers=[("Content-Type", "application/json")],
+ content=json.dumps({"message": f"Invalid request: {exc}"}).encode(),
+ )
+
+
+
+ @staticmethod
+ def _parse(subpath):
+ # subpath looks like "model//"; the modelId may itself
+ # contain "/" (inference-profile ARNs), so peel the op off the right.
+ if not subpath.startswith("model/"):
+ raise ValueError(f"unrecognized bedrock path: /{subpath}")
+ model_id, sep, op = subpath[len("model/"):].rpartition("/")
+ if not sep or op not in _SUPPORTED_OPS:
+ raise ValueError(f"unrecognized bedrock path: /{subpath}")
+ return model_id, op
+
+ @staticmethod
+ def _error_response(exc: ClientError) -> ProxyResponse:
+ # boto3 raises on 4xx/5xx; reconstruct a JSON error body carrying the
+ # real status + message. (Byte-for-byte error passthrough is a property
+ # only the HTTP providers have; this is the cost of re-issuing via boto3.)
+ meta = exc.response.get("ResponseMetadata", {})
+ err = exc.response.get("Error", {})
+ status = meta.get("HTTPStatusCode", 500)
+ body = json.dumps(
+ {"message": err.get("Message", str(exc)), "code": err.get("Code")}
+ ).encode("utf-8")
+ return ProxyResponse(
+ status=status,
+ headers=[("Content-Type", "application/json")],
+ content=body,
+ )
diff --git a/canyonos_core/llm_proxy/providers/openai.py b/canyonos_core/llm_proxy/providers/openai.py
new file mode 100644
index 0000000..501698b
--- /dev/null
+++ b/canyonos_core/llm_proxy/providers/openai.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+from canyonos_core.llm_proxy.providers.base import HttpProvider, UpstreamRequest, client_headers
+
+
+class OpenAIProvider(HttpProvider):
+ name = "openai"
+
+ def target(self, req, subpath, body):
+ headers = client_headers(req, drop=["authorization"])
+ if self.cfg.openai.api_key:
+ headers["Authorization"] = f"Bearer {self.cfg.openai.api_key}"
+ return UpstreamRequest(
+ method=req.method,
+ url=f"{self.cfg.openai.upstream_base}/{subpath}",
+ headers=headers,
+ params=req.args.to_dict(flat=True),
+ )
diff --git a/canyonos_core/llm_proxy/proxy.py b/canyonos_core/llm_proxy/proxy.py
new file mode 100644
index 0000000..50260e7
--- /dev/null
+++ b/canyonos_core/llm_proxy/proxy.py
@@ -0,0 +1,74 @@
+"""Auto-inject CanyonOS headers into ALL boto3 Bedrock calls.
+
+Import this module once and all subsequent boto3.client("bedrock-runtime") calls
+will automatically include the X-Canyonos-Future-ID header.
+
+Usage:
+ import canyonos_core.llm_proxy_auto # Just import once
+ import boto3
+
+ # Now this automatically includes the header!
+ client = boto3.client("bedrock-runtime")
+ response = client.converse(...)
+"""
+
+import os
+
+import boto3
+import logging
+
+# Test/stub mode: when CANYONOS_LLM_STUB_TEXT is set, the LLM proxy returns
+# canned text and NEVER calls AWS. boto3 still needs *some* credentials to
+# compute a local SigV4 signature for the request it sends to the local proxy
+# endpoint (AWS_ENDPOINT_URL_BEDROCK_RUNTIME -> 127.0.0.1:8081), so supply
+# throwaway ones here. The signed request goes only to the local proxy; these
+# credentials are never transmitted to AWS. This makes a stubbed e2e run need
+# no real AWS credentials at all.
+if os.getenv("CANYONOS_LLM_STUB_TEXT"):
+ os.environ.setdefault("AWS_ACCESS_KEY_ID", "stub")
+ os.environ.setdefault("AWS_SECRET_ACCESS_KEY", "stub")
+ os.environ.setdefault(
+ "AWS_DEFAULT_REGION", os.getenv("AWS_REGION", "us-east-1")
+ )
+
+try:
+ import canyonos_core.controller.canyonos_context as canyonos_context
+except ImportError:
+ # In-container the framework files are copied flat to /app.
+ try:
+ import canyonos_context
+ except ImportError:
+ canyonos_context = None
+
+log = logging.getLogger(__name__)
+
+
+def _inject_canyonos_headers(params=None, **kwargs):
+ """Inject X-Canyonos-Future-ID into the outgoing Bedrock HTTP request.
+
+ Registered on boto3's ``before-call.bedrock-runtime`` event, whose handlers
+ receive the prepared-request ``params`` dict (with a mutable ``headers``).
+ The ``request`` object only exists on the later ``before-send`` event, so
+ reading it here would always be None and silently drop the header.
+ """
+ if not canyonos_context or params is None:
+ return
+
+ # Get current future_id from thread-local context
+ try:
+ future_id = canyonos_context.get_current_future_id()
+ if future_id:
+ params.setdefault("headers", {})["X-Canyonos-Future-ID"] = future_id
+ log.debug("Injected X-Canyonos-Future-ID: %s", future_id)
+ except Exception as e:
+ log.debug("Could not inject future_id: %s", e)
+
+
+# Register the hook globally on the default session
+_session = boto3.Session()
+_session.events.register_first('before-call.bedrock-runtime', _inject_canyonos_headers)
+
+# Also patch the default session used by boto3.client()
+boto3.DEFAULT_SESSION = _session
+
+log.info("CanyonOS boto3 hook registered - all Bedrock calls will include future_id header")
diff --git a/canyonos_core/llm_proxy/requirements.txt b/canyonos_core/llm_proxy/requirements.txt
new file mode 100644
index 0000000..2f7091c
--- /dev/null
+++ b/canyonos_core/llm_proxy/requirements.txt
@@ -0,0 +1,3 @@
+flask>=2.0
+requests>=2.28
+boto3>=1.28
diff --git a/canyonos_core/llm_proxy/stub.py b/canyonos_core/llm_proxy/stub.py
new file mode 100644
index 0000000..c919e1e
--- /dev/null
+++ b/canyonos_core/llm_proxy/stub.py
@@ -0,0 +1,73 @@
+"""Test-mode LLM stub.
+
+When ``CANYONOS_LLM_STUB_TEXT`` is set in the environment, every proxied LLM
+call short-circuits and returns that text as the model output instead of hitting
+a real upstream (Bedrock/OpenAI/Anthropic). This lets a full workflow be
+exercised end-to-end with no cloud credentials and zero token cost -- the whole
+deploy/route/proxy/telemetry path still runs, only the upstream call is replaced.
+
+Enable it per-deploy via an ``env_file`` entry (injected into every agent
+container, and inherited by the in-container proxy subprocess):
+
+ # .env
+ CANYONOS_LLM_STUB_TEXT=testing
+"""
+
+from __future__ import annotations
+
+import json
+import os
+
+from canyonos_core.llm_proxy.providers.base import ProxyResponse
+
+STUB_ENV = "CANYONOS_LLM_STUB_TEXT"
+
+
+def stub_text():
+ """Return the configured stub text, or None when stubbing is disabled."""
+ return os.getenv(STUB_ENV)
+
+
+def _json_response(obj, status=200):
+ return ProxyResponse(
+ status=status,
+ headers=[("Content-Type", "application/json")],
+ content=json.dumps(obj).encode("utf-8"),
+ )
+
+
+def build_stub(provider_name, subpath, text):
+ """Build a provider-appropriate canned response carrying ``text``."""
+ if provider_name == "bedrock":
+ op = subpath.rsplit("/", 1)[-1] if subpath else ""
+ if op in ("converse", "converse-stream"):
+ return _json_response({
+ "output": {"message": {"role": "assistant",
+ "content": [{"text": text}]}},
+ "stopReason": "end_turn",
+ "usage": {"inputTokens": 1, "outputTokens": 1, "totalTokens": 2},
+ })
+ # invoke / other ops: a minimal body that common model families read.
+ return _json_response({
+ "outputText": text,
+ "results": [{"outputText": text}],
+ "generation": text,
+ })
+
+ if provider_name == "openai":
+ return _json_response({
+ "id": "stub-cmpl", "object": "chat.completion", "model": "stub",
+ "choices": [{"index": 0, "finish_reason": "stop",
+ "message": {"role": "assistant", "content": text}}],
+ "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
+ })
+
+ if provider_name == "anthropic":
+ return _json_response({
+ "id": "stub-msg", "type": "message", "role": "assistant", "model": "stub",
+ "content": [{"type": "text", "text": text}],
+ "stop_reason": "end_turn",
+ "usage": {"input_tokens": 1, "output_tokens": 1},
+ })
+
+ return _json_response({"text": text})
diff --git a/ventis/server.py b/canyonos_core/server.py
similarity index 91%
rename from ventis/server.py
rename to canyonos_core/server.py
index e9e9561..c1e4b48 100644
--- a/ventis/server.py
+++ b/canyonos_core/server.py
@@ -6,10 +6,10 @@
import yaml
from flask import Flask, jsonify, request
-from ventis.cli import _artifact_prefix
-from ventis.controller.utils.redis_client import RedisClient
+from canyonos_core.cli import _artifact_prefix
+from canyonos_core.controller.utils.redis_client import RedisClient
-app = Flask("ventis-server")
+app = Flask("canyonos-server")
# The project files are copied here (into a named volume) by `canyonos sync` /
# `canyonos deploy`. Deploy builds and launches against this path.
@@ -38,7 +38,7 @@ def deploy():
return jsonify({"error": "already running"}), 409
data = request.get_json(force=True, silent=True) or {}
- # Resolved with ventis' own artifact-layout rule rather than a second copy
+ # Resolved with canyonos' own artifact-layout rule rather than a second copy
# of it, so a `.car` project works when the client sends no config_path.
config_path = data.get("config_path") or os.path.join(
_artifact_prefix(WORKSPACE_DIR), "config", "global_controller.yaml"
@@ -52,12 +52,12 @@ def deploy():
if not os.path.isfile(full_path):
return jsonify({"error": f"config file not found: {full_path}"}), 400
- # `ventis deploy` builds (stubs/protos/images) then launches the Global
+ # `canyonos deploy` builds (stubs/protos/images) then launches the Global
# Controller. cwd is the workspace so build outputs land alongside the
# project files and the controller finds them. Build+deploy output streams
# to the container logs, which `canyonos deploy` tails.
_gc_process = subprocess.Popen(
- [sys.executable, "-m", "ventis.cli", "deploy", "-c", config_path],
+ [sys.executable, "-m", "canyonos_core.cli", "deploy", "-c", config_path],
cwd=WORKSPACE_DIR,
)
_config_path = full_path
@@ -92,7 +92,7 @@ def _primary_redis(config):
port = agent.get("redis_port", port)
break
if host in ("localhost", "127.0.0.1"):
- host = os.environ.get("VENTIS_REDIS_HOST", host)
+ host = os.environ.get("CANYONOS_REDIS_HOST", host)
return RedisClient(host=host, port=int(port))
diff --git a/ventis/stub_generator.py b/canyonos_core/stub_generator.py
similarity index 96%
rename from ventis/stub_generator.py
rename to canyonos_core/stub_generator.py
index 3edc970..a830735 100644
--- a/ventis/stub_generator.py
+++ b/canyonos_core/stub_generator.py
@@ -1,5 +1,5 @@
"""
-Stub generator for Ventis agents.
+Stub generator for CanyonOS agents.
Reads a YAML agent definition and generates an importable Python stub file
where each function returns a Future object. Similar in spirit to how gRPC
@@ -269,7 +269,7 @@ def _format_source(source):
return "\n".join(formatted) + "\n"
-# Directories ventis build itself generates inside a project -- never swept.
+# Directories canyonos build itself generates inside a project -- never swept.
_GENERATED_DIRS = {"docker_container", "stubs", "grpc_stubs"}
@@ -306,19 +306,19 @@ def _stub_destination(stub_file, stub_entrypoints):
def _copy_llm_proxy(output_dir, script_dir):
- """Copy the ventis.llm_proxy package into the build context as an importable
- `ventis` package so the in-container proxy can run via `python -m ventis.llm_proxy`.
- Its cross-package imports (redis_client, ventis_context) fall back to the flat
+ """Copy the canyonos_core.llm_proxy package into the build context as an importable
+ `canyonos_core` package so the in-container proxy can run via `python -m canyonos_core.llm_proxy`.
+ Its cross-package imports (redis_client, canyonos_context) fall back to the flat
copies already placed at the context root."""
shutil.copytree(
os.path.join(script_dir, "llm_proxy"),
- os.path.join(output_dir, "ventis", "llm_proxy"),
+ os.path.join(output_dir, "canyonos_core", "llm_proxy"),
dirs_exist_ok=True,
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
)
shutil.copy2(
os.path.join(script_dir, "__init__.py"),
- os.path.join(output_dir, "ventis", "__init__.py"),
+ os.path.join(output_dir, "canyonos_core", "__init__.py"),
)
@@ -394,7 +394,7 @@ def generate_docker(
files_to_copy += [
# (source_path, destination_filename)
(os.path.join(script_dir, "controller", "future.py"), "future.py"),
- (os.path.join(script_dir, "controller", "ventis_context.py"), "ventis_context.py"),
+ (os.path.join(script_dir, "controller", "canyonos_context.py"), "canyonos_context.py"),
(
os.path.join(script_dir, "controller", "local_controller.py"),
"local_controller.py",
@@ -457,8 +457,8 @@ def generate_docker(
COPY . .
-ENV VENTIS_AGENT_NAME={agent_name}
-ENV VENTIS_AGENT_FILE={agent_basename}
+ENV CANYONOS_AGENT_NAME={agent_name}
+ENV CANYONOS_AGENT_FILE={agent_basename}
EXPOSE 50051
@@ -522,7 +522,7 @@ def generate_workflow_docker(
files_to_copy += [
(os.path.join(script_dir, "controller", "future.py"), "future.py"),
- (os.path.join(script_dir, "controller", "ventis_context.py"), "ventis_context.py"),
+ (os.path.join(script_dir, "controller", "canyonos_context.py"), "canyonos_context.py"),
(os.path.join(script_dir, "controller", "deploy.py"), "deploy.py"),
(
os.path.join(script_dir, "controller", "local_controller.py"),
diff --git a/cli/ARCHITECTURE.md b/cli/ARCHITECTURE.md
index 07619da..e719ba2 100644
--- a/cli/ARCHITECTURE.md
+++ b/cli/ARCHITECTURE.md
@@ -20,7 +20,7 @@ If you remember only one picture, remember this:
│ │ copy project in (docker cp) │
│ ├───────────────────────────────▶│ /workspace
│ │ POST /deploy │
- │ ├───────────────────────────────▶│ ventis build + launch
+ │ ├───────────────────────────────▶│ canyonos build + launch
│ │◀── log stream (docker logs) ───┤ │
│◀── readable progress ─┤ │ ▼
│ │ spawns Redis + agents
@@ -39,7 +39,7 @@ If you remember only one picture, remember this:
│ │ CLI │ /status /endpoints │ container │ │
│ │ │ ───── docker cp ─────────▶│ ├─ /workspace (a copy │ │
│ │ │ ───── docker logs -f ────▶│ │ of your project) │ │
-│ └─────┬─────┘ │ └─ runs `ventis` │ │
+│ └─────┬─────┘ │ └─ runs `canyonos` │ │
│ │ └───────────┬──────────────┘ │
│ │ docker compose │ docker.sock │
│ ▼ ▼ (spawns siblings)│
@@ -98,7 +98,7 @@ container is involved yet.**
│
├─ 2. ship code → docker cp your project into /workspace
│
- ├─ 3. trigger → POST /deploy (container runs `ventis`:
+ ├─ 3. trigger → POST /deploy (container runs `canyonos`:
│ build stubs/images + launch the workflow)
│
└─ 4. narrate → tail container logs, boil them down to phases,
diff --git a/cli/canyonos/dashboard.compose.yml b/cli/canyonos/dashboard.compose.yml
index 00416f4..0effbac 100644
--- a/cli/canyonos/dashboard.compose.yml
+++ b/cli/canyonos/dashboard.compose.yml
@@ -5,7 +5,7 @@ services:
# actually reach this via host.docker.internal -- Docker's host-gateway
# route doesn't reach ports bound to loopback only, so a 127.0.0.1 bind
# here silently black-holed every OTLP span export. That route also
- # renames ventis' `project_id` attribute to the `canyon.project.id` every
+ # renames canyonos' `project_id` attribute to the `canyon.project.id` every
# dashboard query filters on.
ports:
- "3000:3000"
diff --git a/cli/canyonos/deploy.py b/cli/canyonos/deploy.py
index 6412cfc..8cddd81 100644
--- a/cli/canyonos/deploy.py
+++ b/cli/canyonos/deploy.py
@@ -1,7 +1,7 @@
"""
Logic for `canyonos deploy`: copy the project into the container's /workspace
volume (via `canyonos sync`), then tell the Global Controller container to
-build and deploy it. The container's `ventis deploy` handles both the build
+build and deploy it. The container's `canyonos deploy` handles both the build
(stubs, protos, Docker images) and the launch -- the CLI just ships files,
triggers it, and watches the logs.
@@ -134,7 +134,7 @@ def agents_ready_message(self):
def run_deploy(config_path=None, serve=True, verbose=False):
- # Left as None when unset: ventis resolves the artifact layout itself.
+ # Left as None when unset: canyonos resolves the artifact layout itself.
if config_path is not None:
config_path = workspace_relative(config_path)
if config_path is None:
@@ -149,7 +149,7 @@ def run_deploy(config_path=None, serve=True, verbose=False):
state = load_state()
- # Read for display only -- ventis resolves the path it actually deploys.
+ # Read for display only -- canyonos resolves the path it actually deploys.
api_port = workflow_api_port(config_path or default_config_path())
try:
@@ -272,7 +272,7 @@ def _tail_verbose(stream, state, api_port, serve):
def _tail_quiet(lines, state, api_port, serve):
"""Only the phase transitions, until the workflow is up or something fails.
- Nothing is echoed raw: the buildx transcript, ventis' bare prints and grpc's
+ Nothing is echoed raw: the buildx transcript, canyonos' bare prints and grpc's
stderr have no common prefix to filter on, so anything unrecognized is
dropped rather than allow-listed. `-v` and `canyonos logs` still have it all.
"""
diff --git a/cli/canyonos/gc.py b/cli/canyonos/gc.py
index 594c08b..1f7bc54 100644
--- a/cli/canyonos/gc.py
+++ b/cli/canyonos/gc.py
@@ -54,7 +54,7 @@ def require_state():
def post_deploy(port, config_path=None):
"""Start a deploy inside the container. Raises GCError on failure.
- Omitting config_path lets ventis resolve it against the synced workspace.
+ Omitting config_path lets canyonos resolve it against the synced workspace.
"""
body = json.dumps({"config_path": config_path} if config_path else {}).encode()
try:
@@ -64,7 +64,7 @@ def post_deploy(port, config_path=None):
def post_clean(port):
- """Tear down the running deploy: SIGTERMs the in-container `ventis deploy`
+ """Tear down the running deploy: SIGTERMs the in-container `canyonos deploy`
process, whose handler calls GlobalController.stop() and blocks until it
returns. This is what actually removes the local controller and Redis
containers a deploy spawned via docker-outside-of-docker.
diff --git a/cli/canyonos/init.py b/cli/canyonos/init.py
index 69d4eba..eac2edf 100644
--- a/cli/canyonos/init.py
+++ b/cli/canyonos/init.py
@@ -127,31 +127,32 @@ def _port_reachable(port, attempts=10, delay=0.5):
return False
-def run_container(image=GC_IMAGE, max_attempts=50):
+def run_container(image=GC_IMAGE, max_attempts=50, extra_env=None):
port = GC_CONTAINER_PORT
for _ in range(max_attempts):
- result = subprocess.run(
- [
- "docker",
- "run",
- "-d",
- "-p",
- f"127.0.0.1:{port}:{GC_CONTAINER_PORT}",
- # Docker-outside-of-Docker: GC shells out to `docker` to launch
- # Redis/agent containers, so it needs the host's real daemon,
- # not a nested one.
- "-v",
- "/var/run/docker.sock:/var/run/docker.sock",
- "-v",
- f"{GC_WORKSPACE_VOLUME}:{GC_WORKSPACE_PATH}",
- "--add-host=host.docker.internal:host-gateway",
- "-e",
- "VENTIS_REDIS_HOST=host.docker.internal",
- image,
- ],
- capture_output=True,
- text=True,
- )
+ cmd = [
+ "docker",
+ "run",
+ "-d",
+ "-p",
+ f"127.0.0.1:{port}:{GC_CONTAINER_PORT}",
+ # Docker-outside-of-Docker: GC shells out to `docker` to launch
+ # Redis/agent containers, so it needs the host's real daemon,
+ # not a nested one.
+ "-v",
+ "/var/run/docker.sock:/var/run/docker.sock",
+ "-v",
+ f"{GC_WORKSPACE_VOLUME}:{GC_WORKSPACE_PATH}",
+ "--add-host=host.docker.internal:host-gateway",
+ "-e",
+ "CANYONOS_REDIS_HOST=host.docker.internal",
+ ]
+ # Extra env for the GC container. The local runtime forwards select keys
+ # (e.g. CANYONOS_LLM_STUB_TEXT) from here into each agent container.
+ for _k, _v in (extra_env or {}).items():
+ cmd.extend(["-e", f"{_k}={_v}"])
+ cmd.append(image) # image must come after all flags
+ result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
container_id = result.stdout.strip()
if _port_reachable(port):
@@ -194,7 +195,7 @@ def quit_existing():
run_quit()
-def run_init(banner=True):
+def run_init(banner=True, extra_env=None):
if banner:
ui.gradient(figlet_format("CANYON OS", font="ansi_shadow", width=200))
@@ -205,6 +206,6 @@ def run_init(banner=True):
with ui.status("Pulling Global Controller image..."):
pull_image()
with ui.status("Starting Global Controller container..."):
- container_id, port = run_container()
+ container_id, port = run_container(extra_env=extra_env)
save_state(container_id, port)
ui.ok(f"Global Controller running in container {container_id[:12]} on port {port}")
diff --git a/cli/canyonos/test.py b/cli/canyonos/test.py
index 23f08f7..895e8e0 100644
--- a/cli/canyonos/test.py
+++ b/cli/canyonos/test.py
@@ -40,6 +40,10 @@
from canyonos.verify import ARTIFACT_DIR, verify_build_artifact, verify_runtime
DEFAULT_QUERY = "hello"
+# `canyonos test` stubs the in-container LLM proxy by default so a smoke test
+# never calls a real LLM (no credentials, no token cost). Every model call
+# returns this text; pass --real-llm to use the actual provider instead.
+DEFAULT_LLM_STUB = "test"
READY_TIMEOUT = 60
REQUEST_TIMEOUT = 60
SUBMIT_TIMEOUT = 30
@@ -186,9 +190,15 @@ def _verify_build(run, config_path):
run.done(f"{run.validation['warnings']} warning(s), {stale} stale source(s)")
-def _deploy_locally(run, config_path, api_port):
+def _deploy_locally(run, config_path, api_port, llm_stub=DEFAULT_LLM_STUB):
run.begin("deploy", 2, "Deploy locally")
- run_init(banner=False)
+ # When stubbing, hand the flag to the GC container; the local runtime
+ # forwards it into every agent so their LLM calls are replaced with canned
+ # text (see canyonos_core/llm_proxy/stub.py).
+ extra_env = {"CANYONOS_LLM_STUB_TEXT": llm_stub} if llm_stub else None
+ if llm_stub:
+ ui.say(f"LLM stub on: every model call returns {llm_stub!r} (no real LLM). Pass --real-llm to disable.")
+ run_init(banner=False, extra_env=extra_env)
if not run_sync():
raise RuntimeError("Could not sync the project into the container.")
@@ -246,7 +256,7 @@ def _query(run, gc_port, api_port):
run.done(f"answered in {run.elapsed()}s")
-def _run_test(run):
+def _run_test(run, llm_stub=DEFAULT_LLM_STUB):
"""Walk the four phases, restoring the config whatever happens."""
config_path = workspace_relative(default_config_path())
if config_path is None:
@@ -262,7 +272,7 @@ def _run_test(run):
original_config = _force_local_providers(config_path)
try:
- state = _deploy_locally(run, config_path, api_port)
+ state = _deploy_locally(run, config_path, api_port, llm_stub=llm_stub)
_verify_runtime(run, config_path, state["port"])
_query(run, state["port"], api_port)
finally:
@@ -339,14 +349,14 @@ def _payload(run):
}
-def run_test(prompt=None, as_json=False):
+def run_test(prompt=None, as_json=False, llm_stub=DEFAULT_LLM_STUB):
run = _Run(prompt or DEFAULT_QUERY)
ui.set_quiet(as_json)
try:
container_live = False
try:
- _run_test(run)
+ _run_test(run, llm_stub=llm_stub)
except KeyboardInterrupt:
run.error = "cancelled by user"
except RuntimeError as e:
diff --git a/cli/canyonos/verify.py b/cli/canyonos/verify.py
index 95fd36a..4f9a46a 100644
--- a/cli/canyonos/verify.py
+++ b/cli/canyonos/verify.py
@@ -36,13 +36,13 @@
VALIDATOR_NAME = "validate.py"
SKILL_CACHE_DIR = os.path.join(STATE_DIR, "skill")
-# These two rules decide their verdict by importing `ventis` and probing it for
+# These two rules decide their verdict by importing `canyonos` and probing it for
# env-file injection and editable-install support. The runtime lives in the
# Global Controller image, not on the host running this CLI, so the probe always
# comes back empty here and the rules report a failure that isn't one.
CAPABILITY_GATED_CHECKS = frozenset({"V030", "V031"})
-RUNTIME_PREFIX = "ventis-local-"
+RUNTIME_PREFIX = "canyonos-local-"
# ------------------------------------------------------------------ #
@@ -84,12 +84,12 @@ def _run_validator(validator, artifact_dir):
def _drop_unprobeable(report):
- """Remove the rules that can only be judged with `ventis` importable.
+ """Remove the rules that can only be judged with `canyonos` importable.
Their verdict without it is not merely uncertain, it is wrong: V030 reports
that the runtime never reads `env_file` when the container's runtime does.
"""
- if report.get("capabilities", {}).get("ventis"):
+ if report.get("capabilities", {}).get("canyonos_core"):
return 0
kept = []
@@ -181,7 +181,7 @@ def verify_build_artifact(project_root="."):
(ui.fail if summary["errors"] else ui.ok)(f"porting validator: {counts}")
_report_findings(summary["findings"])
if skipped:
- ui.hint(f" {skipped} rule(s) need the ventis runtime to judge and were skipped")
+ ui.hint(f" {skipped} rule(s) need the canyonos runtime to judge and were skipped")
summary["stale"] = _stale_sources(project_root, artifact_dir)
for relative in summary["stale"]:
@@ -255,7 +255,7 @@ def verify_runtime(config_path, gc_port):
if not name:
continue
# Image and container names the local provider derives from the agent name.
- image = f"ventis-{name.lower()}"
+ image = f"canyonos-{name.lower()}"
expected = int(agent.get("replicas", 1) or 1)
running = sum(1 for c in containers if c.startswith(f"{RUNTIME_PREFIX}{name.lower()}-"))
image_built = image in images
diff --git a/cli/cli.py b/cli/cli.py
index c20e3d8..2d38c61 100644
--- a/cli/cli.py
+++ b/cli/cli.py
@@ -19,7 +19,7 @@
from canyonos.serve import run_serve
from canyonos.status import run_status
from canyonos.stop import run_stop
-from canyonos.test import DEFAULT_QUERY, run_test
+from canyonos.test import DEFAULT_LLM_STUB, DEFAULT_QUERY, run_test
from utils.help_screen import DESCRIPTIONS, print_custom_help
def _parse_bool(value):
@@ -57,7 +57,7 @@ def add(name, run):
deploy.add_argument(
"-c",
"--config",
- help="Path to global controller config (default: resolved by ventis inside the container)",
+ help="Path to global controller config (default: resolved by canyonos inside the container)",
)
deploy.add_argument(
"--serve",
@@ -83,8 +83,12 @@ def add(name, run):
add("serve", lambda args: sys.exit(run_serve()))
add("status", lambda args: run_status())
- # Test has 2 args: prompt, --json.
- test = add("test", lambda args: sys.exit(run_test(args.prompt, as_json=args.json)))
+ # Test has args: prompt, --json, --real-llm, --stub-text.
+ test = add("test", lambda args: sys.exit(run_test(
+ args.prompt,
+ as_json=args.json,
+ llm_stub=(None if args.real_llm else args.stub_text),
+ )))
test.add_argument(
"prompt",
nargs="?",
@@ -96,6 +100,18 @@ def add(name, run):
action="store_true",
help="Print a single JSON result object and nothing else (for CI)",
)
+ test.add_argument(
+ "--stub-text",
+ default=DEFAULT_LLM_STUB,
+ metavar="TEXT",
+ help=("Text the in-container LLM proxy returns for every model call so "
+ f"tests never hit a real LLM (default: {DEFAULT_LLM_STUB!r})."),
+ )
+ test.add_argument(
+ "--real-llm",
+ action="store_true",
+ help="Use the real LLM provider instead of the stub (requires credentials).",
+ )
args = parser.parse_args()
if not getattr(args, "command", None):
diff --git a/examples/finance/agents/finance_agent.py b/examples/finance/agents/finance_agent.py
index db70b01..4b6d9bb 100644
--- a/examples/finance/agents/finance_agent.py
+++ b/examples/finance/agents/finance_agent.py
@@ -1,6 +1,6 @@
# `agents.vllm_agent` is where the generated VllmAgent stub actually lands
# inside this agent's own Docker container (stubs are copied to their source
-# agent's own entrypoint-mirrored path -- see ventis/stub_generator.py). The
+# agent's own entrypoint-mirrored path -- see canyonos/stub_generator.py). The
# bare `vllm_agent` fallback covers running outside that layout.
try:
from agents.vllm_agent import VllmAgent
diff --git a/examples/finance/config/global_controller.yaml b/examples/finance/config/global_controller.yaml
index 7199d6d..b4b3cec 100644
--- a/examples/finance/config/global_controller.yaml
+++ b/examples/finance/config/global_controller.yaml
@@ -58,7 +58,7 @@ redis:
# Docker image registry (legacy; no longer used).
-# Remote nodes are expected to already have the image before `ventis deploy`.
+# Remote nodes are expected to already have the image before `canyonos deploy`.
#
# registry:
# url: myregistry.example.com:5000
diff --git a/examples/finance/workflow/example_workflow.py b/examples/finance/workflow/example_workflow.py
index 644e483..b0540fd 100644
--- a/examples/finance/workflow/example_workflow.py
+++ b/examples/finance/workflow/example_workflow.py
@@ -10,7 +10,7 @@
import sys
import os
-# Add src directory so `import ventis` works
+# Add src directory so `import canyonos_core` works
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
# Add stubs directory to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "stubs"))
diff --git a/examples/helloworld/README.md b/examples/helloworld/README.md
index 38483be..0d265b8 100644
--- a/examples/helloworld/README.md
+++ b/examples/helloworld/README.md
@@ -1,15 +1,15 @@
-# My Ventis Project
+# My CanyonOS Project
-A distributed agent orchestration project built with [Ventis](https://github.com/ventis).
+A distributed agent orchestration project built with [CanyonOS](https://github.com/canyonos).
## Quick Start
```bash
# Build stubs and Docker images
-ventis build
+canyonos build
# Launch all agents
-ventis deploy
+canyonos deploy
# Test with curl
curl -X POST http://:8080/main \
@@ -41,8 +41,8 @@ curl http://:8080/status/
1. Create `agents/my_agent.yaml` with the agent interface definition
2. Create `agents/my_agent.py` with the implementation class
3. Add the agent entry to `config/global_controller.yaml`
-4. Run `ventis build` to regenerate stubs and Docker images
-5. Run `ventis deploy` to launch
+4. Run `canyonos build` to regenerate stubs and Docker images
+5. Run `canyonos deploy` to launch
## Policy Rules
diff --git a/examples/helloworld/config/global_controller.yaml b/examples/helloworld/config/global_controller.yaml
index c7d99cc..8b24a49 100644
--- a/examples/helloworld/config/global_controller.yaml
+++ b/examples/helloworld/config/global_controller.yaml
@@ -1,5 +1,5 @@
# Global Controller Configuration
-# Lists all agents and workflows that Ventis manages.
+# Lists all agents and workflows that CanyonOS manages.
agents:
- name: ExampleAgent
@@ -40,7 +40,7 @@ redis:
db: 0
database:
- url: sqlite:///ventis_runtime.db
+ url: sqlite:///canyonos_runtime.db
# EC2 defaults for `provider: EC2` replicas.
# Keep them here so `config/global_controller.yaml` stays the only source of truth.
diff --git a/examples/helloworld/workflow/example_workflow.py b/examples/helloworld/workflow/example_workflow.py
index 693590e..624cd38 100644
--- a/examples/helloworld/workflow/example_workflow.py
+++ b/examples/helloworld/workflow/example_workflow.py
@@ -1,7 +1,7 @@
# Example Workflow
# This file demonstrates how to call agent stubs and deploy as a REST API.
#
-# After running `ventis build` and `ventis deploy`:
+# After running `canyonos build` and `canyonos deploy`:
# curl -X POST http://localhost:8080/main -H 'Content-Type: application/json' -d '{"query": "World"}'
# curl http://localhost:8080/status/
diff --git a/examples/portfolio/agents/advisor_agent.py b/examples/portfolio/agents/advisor_agent.py
index 33ac8fc..82c9f6e 100644
--- a/examples/portfolio/agents/advisor_agent.py
+++ b/examples/portfolio/agents/advisor_agent.py
@@ -3,7 +3,7 @@
# Final stage. Turns the computed portfolio metrics and risk figures into a
# short, plain-English briefing using a small, cheap model on AWS Bedrock
# (Converse API), called directly via boto3. Token/cost telemetry is recorded
-# onto this execution's future: hash transparently by the Ventis LLM
+# onto this execution's future: hash transparently by the CanyonOS LLM
# proxy each agent container's boto3 calls are routed through. Configure
# with env vars:
# BEDROCK_MODEL_ID (default: meta.llama3-8b-instruct-v1:0)
diff --git a/examples/portfolio/agents/intent_agent.py b/examples/portfolio/agents/intent_agent.py
index 4bb915c..13847fd 100644
--- a/examples/portfolio/agents/intent_agent.py
+++ b/examples/portfolio/agents/intent_agent.py
@@ -9,7 +9,7 @@
#
# Calls AWS Bedrock (Converse API) directly via boto3 -- same pattern as
# AdvisorAgent. Token/cost telemetry is recorded onto this execution's
-# future: hash transparently by the Ventis LLM proxy, which each
+# future: hash transparently by the CanyonOS LLM proxy, which each
# agent container's boto3 calls are routed through (AWS_ENDPOINT_URL_BEDROCK_RUNTIME).
# Configure with env vars:
# BEDROCK_MODEL_ID (default: meta.llama3-8b-instruct-v1:0)
diff --git a/examples/portfolio/agents/metrics_agent.py b/examples/portfolio/agents/metrics_agent.py
index 253a2d2..3a280fe 100644
--- a/examples/portfolio/agents/metrics_agent.py
+++ b/examples/portfolio/agents/metrics_agent.py
@@ -13,9 +13,9 @@
# `agents.price_agent` is where the generated PriceAgent stub actually lands
# inside this agent's own Docker container (stubs are copied to their source
-# agent's own entrypoint-mirrored path -- see ventis/stub_generator.py). The
+# agent's own entrypoint-mirrored path -- see canyonos/stub_generator.py). The
# bare `price_agent` fallback covers running outside that layout (e.g. local
-# dev, where `ventis build` only emits a flat stubs/ directory).
+# dev, where `canyonos build` only emits a flat stubs/ directory).
try:
from agents.price_agent import PriceAgent
except ImportError:
@@ -32,7 +32,7 @@ def __init__(self):
def compute(self, ticker: str, lookback_days: int = 365) -> dict:
"""Compute return/volatility/Sharpe/drawdown metrics for one ticker."""
# get_history() returns a dict, but a Future's .value() only ever gives back
- # the raw string ventis stored in Redis -- it never auto-deserializes
+ # the raw string canyonos stored in Redis -- it never auto-deserializes
# non-str return types, so the JSON has to be parsed back out here.
history = json.loads(
self.price.get_history(ticker=ticker, lookback_days=lookback_days).value()
diff --git a/examples/portfolio/config/global_controller.yaml b/examples/portfolio/config/global_controller.yaml
index 60abb9e..8e6ae1a 100644
--- a/examples/portfolio/config/global_controller.yaml
+++ b/examples/portfolio/config/global_controller.yaml
@@ -1,5 +1,5 @@
# Global Controller Configuration — portfolio-analysis fan-out pipeline
-# Lists all agents and the workflow that Ventis manages.
+# Lists all agents and the workflow that CanyonOS manages.
#
# The pipeline fans out per-ticker metrics computation, then aggregates into
# portfolio-level risk, then generates an LLM briefing. Resource classes below
@@ -7,7 +7,7 @@
agents:
# Stage 0: parse the free-text request into structured holdings + lookback
- # window (calls Bedrock via boto3, routed through the Ventis LLM proxy). Cheap CPU, one
+ # window (calls Bedrock via boto3, routed through the CanyonOS LLM proxy). Cheap CPU, one
# call per request, on the critical path before the fan-out.
- name: IntentAgent
redis_port: 6379
diff --git a/examples/portfolio/workflow/portfolio_workflow.py b/examples/portfolio/workflow/portfolio_workflow.py
index 619c4bd..772f935 100644
--- a/examples/portfolio/workflow/portfolio_workflow.py
+++ b/examples/portfolio/workflow/portfolio_workflow.py
@@ -13,7 +13,7 @@
# lookback window before the fan-out begins. The whole JSON body is splatted
# into main() as kwargs by deploy().
#
-# Start agents first: python -m ventis.controller.global_controller
+# Start agents first: python -m canyonos_core.controller.global_controller
# Test:
# curl -X POST http://localhost:8080/main \
# -H 'Content-Type: application/json' \
@@ -60,7 +60,7 @@ def main(
for t in tickers
}
# compute() returns a dict, but a Future's .value() only ever gives back the
- # raw string ventis stored in Redis -- it never auto-deserializes non-str
+ # raw string canyonos stored in Redis -- it never auto-deserializes non-str
# return types, so the JSON has to be parsed back out here.
per_ticker = {t: json.loads(f.value()) for t, f in metric_futures.items()}
diff --git a/examples/text2sql/agents/sql_generator_agent.py b/examples/text2sql/agents/sql_generator_agent.py
index 4964ce6..3422b7d 100644
--- a/examples/text2sql/agents/sql_generator_agent.py
+++ b/examples/text2sql/agents/sql_generator_agent.py
@@ -11,7 +11,7 @@
# `agents.vllm_agent` is where the generated VllmAgent stub actually lands
# inside this agent's own Docker container (stubs are copied to their source
-# agent's own entrypoint-mirrored path -- see ventis/stub_generator.py). The
+# agent's own entrypoint-mirrored path -- see canyonos/stub_generator.py). The
# bare `vllm_agent` fallback covers running outside that layout.
try:
from agents.vllm_agent import VllmAgent
diff --git a/examples/text2sql/agents/vllm_agent.py b/examples/text2sql/agents/vllm_agent.py
index 1d8141f..020bc91 100644
--- a/examples/text2sql/agents/vllm_agent.py
+++ b/examples/text2sql/agents/vllm_agent.py
@@ -3,7 +3,7 @@
# LLM backend for SQL candidate generation, called remotely by
# SQLGeneratorAgent. Calls AWS Bedrock (Converse API) directly via boto3.
# Token/cost telemetry is recorded onto this execution's future:
-# hash transparently by the Ventis LLM proxy each agent container's boto3 calls
+# hash transparently by the CanyonOS LLM proxy each agent container's boto3 calls
# are routed through — same pattern as
# examples/portfolio/agents/advisor_agent.py.
# Configure with env vars:
diff --git a/examples/text2sql/config/global_controller.yaml b/examples/text2sql/config/global_controller.yaml
index 0df046d..51edd8d 100644
--- a/examples/text2sql/config/global_controller.yaml
+++ b/examples/text2sql/config/global_controller.yaml
@@ -1,5 +1,5 @@
# Global Controller Configuration — NL-to-SQL staged-validation pipeline
-# Lists all agents and the workflow that Ventis manages.
+# Lists all agents and the workflow that CanyonOS manages.
#
# Each agent declares a distinct resource class so the scheduler has real
# placement / batching / admission decisions to make (see the `resources`,
diff --git a/examples/text2sql/workflow/text2sql_workflow.py b/examples/text2sql/workflow/text2sql_workflow.py
index b8ce45b..a65891a 100644
--- a/examples/text2sql/workflow/text2sql_workflow.py
+++ b/examples/text2sql/workflow/text2sql_workflow.py
@@ -7,7 +7,7 @@
# 4. SandboxExecutorAgent - run survivors on a small sample, vote on best
# 5. ProductionExecutorAgent- run the winner on the big warehouse, cost-gated
#
-# Start agents first: python -m ventis.controller.global_controller
+# Start agents first: python -m canyonos_core.controller.global_controller
# Test:
# curl -X POST http://localhost:8080/main \
# -H 'Content-Type: application/json' \
diff --git a/images/canyonos-banner.gif b/images/canyonos-banner.gif
new file mode 100644
index 0000000..0a11087
Binary files /dev/null and b/images/canyonos-banner.gif differ
diff --git a/pyproject.toml b/pyproject.toml
index a94a9b0..32fa745 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
[project]
-name = "ventis"
+name = "canyonos-core"
version = "0.1.0"
-description = "Distributed agent orchestration framework"
+description = "Distributed agent orchestration framework (CanyonOS core runtime)"
requires-python = ">=3.10"
dependencies = [
"boto3",
@@ -20,33 +20,30 @@ dependencies = [
"opentelemetry-exporter-otlp-proto-http>=1.44.0",
]
-[project.scripts]
-ventis = "ventis.cli:main"
+# Import-only: the core runtime ships no console script. The user-facing `canyonos`
+# executable lives in the separate `cli/` distribution. In-container entrypoints are
+# invoked as `python -m canyonos_core.server` / `.cli` / `.llm_proxy`.
[build-system]
requires = ["setuptools>=64"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
-include = ["ventis*"]
+include = ["canyonos_core*"]
[tool.setuptools.package-data]
-ventis = [
- "templates/**/*",
+canyonos_core = [
"controller/proto/*.proto",
"controller/utils/aws_pricing_chart.db",
]
-
-
[tool.ty.environment]
python = ".venv"
[tool.ty.src]
-include = ["ventis"]
+include = ["canyonos_core"]
exclude = [
- "ventis/templates/**",
- "ventis/stub_generator.py",
+ "canyonos_core/stub_generator.py",
]
[tool.ty.analysis]
@@ -55,7 +52,7 @@ allowed-unresolved-imports = [
"local_controler_pb2_grpc",
"local_controller_frontend",
"redis_client",
- "ventis_context",
+ "canyonos_context",
"deploy",
"*_stub",
"*_agent_stub",
@@ -64,6 +61,9 @@ allowed-unresolved-imports = [
[dependency-groups]
dev = [
"pytest>=9.1.1",
+ # The standalone user CLI (`canyonos`) lives in cli/. It is a DISTINCT
+ # distribution from this `canyonos-core` runtime, so there is no name
+ # collision; the root test suite imports it to cover CLI behavior.
"canyonos",
]
diff --git a/tests/README.md b/tests/README.md
index 09394de..90b902a 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -1,13 +1,13 @@
-# Ventis Testing & Load Analysis Tools
+# CanyonOS Testing & Load Analysis Tools
-This directory contains an automated end-to-end testing suite for Ventis. It is designed to verify both functional correctness and concurrent performance of the distributed agent architecture.
+This directory contains an automated end-to-end testing suite for CanyonOS. It is designed to verify both functional correctness and concurrent performance of the distributed agent architecture.
## 1. Automated Test Runner (`run_tests.sh`)
-This script automates the entire testing lifecycle by interacting with the `ventis` CLI:
+This script automates the entire testing lifecycle by interacting with the `canyonos` CLI:
0. Runs a small pytest suite from this `tests/` directory.
-1. Scaffolds a new temporary project using `ventis new-project`.
-2. Compiles the project using `ventis build`.
-3. Launches the project using `ventis deploy` in the background.
+1. Scaffolds a new temporary project using `canyonos new-project`.
+2. Compiles the project using `canyonos build`.
+3. Launches the project using `canyonos deploy` in the background.
4. Waits for the deployed workflow endpoint to become reachable, then gives the agents a few extra seconds to register.
5. Runs the Python integration and performance scripts.
6. **Cleanup:** Automatically terminates the deployment and cleans up the temporary directory upon success or failure.
@@ -18,22 +18,22 @@ To run the complete suite:
```
## 2. Functional Integration Validation (`test_integration.py`)
-Verifies that Ventis correctly passes data and dependencies between chained agents.
+Verifies that CanyonOS correctly passes data and dependencies between chained agents.
- Dispatches a single query to the deployed `/main` endpoint.
- Polls the `/status/` endpoint until completion.
- Validates the output payload structure and ensures that data successfully flowed through `FinanceAgent`, `MarketResearchAgent`, and `VllmAgent`.
-To run manually against an already-deployed Ventis instance:
+To run manually against an already-deployed CanyonOS instance:
```bash
python test_integration.py
```
## 3. High-Concurrency Stress Test (`test_performance.py`)
-Evaluates the robustness and scalability of the Ventis Redis routing and Docker architecture under load. Using `concurrent.futures`, this script models N concurrent users actively polling Ventis simultaneously.
+Evaluates the robustness and scalability of the CanyonOS Redis routing and Docker architecture under load. Using `concurrent.futures`, this script models N concurrent users actively polling CanyonOS simultaneously.
It produces an analytical report summarizing throughput, dropped requests, and latency percentiles.
-To run manually against an already-deployed Ventis instance (e.g. 50 requests across 10 concurrent virtual users):
+To run manually against an already-deployed CanyonOS instance (e.g. 50 requests across 10 concurrent virtual users):
```bash
python test_performance.py --concurrent 10 --total 50
```
diff --git a/tests/run_tests.sh b/tests/run_tests.sh
index c5556ec..4eec785 100755
--- a/tests/run_tests.sh
+++ b/tests/run_tests.sh
@@ -2,7 +2,7 @@
set -e
echo "==========================================="
-echo " Ventis Integration & Performance Tests"
+echo " CanyonOS Integration & Performance Tests"
echo "==========================================="
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
@@ -10,8 +10,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
echo ">> 0. Running small pytest suite..."
python3 -m pytest "$SCRIPT_DIR"
-TEST_DIR="/tmp/ventis_test_env_$$"
-PROJECT_NAME="ventis_test"
+TEST_DIR="/tmp/canyonos_test_env_$$"
+PROJECT_NAME="canyonos_test"
# Cleanup function ensures we kill the deployed Flask/GlobalController on exit
function cleanup {
@@ -28,13 +28,13 @@ mkdir -p "$TEST_DIR"
cd "$TEST_DIR"
echo ">> 1. Generating new project..."
-ventis new-project $PROJECT_NAME
+canyonos new-project $PROJECT_NAME
cd $PROJECT_NAME
grep -v 'gpu:' .car/config/global_controller.yaml > .car/config/global_controller.yaml.tmp
mv .car/config/global_controller.yaml.tmp .car/config/global_controller.yaml
-echo ">> 2. Building and deploying workflow (ventis deploy)..."
-ventis deploy &
+echo ">> 2. Building and deploying workflow (canyonos deploy)..."
+canyonos deploy &
DEPLOY_PID=$!
# Wait for the workflow flask app to become reachable
diff --git a/tests/test_canyonos_context.py b/tests/test_canyonos_context.py
new file mode 100644
index 0000000..43fc0d2
--- /dev/null
+++ b/tests/test_canyonos_context.py
@@ -0,0 +1,49 @@
+import os
+import sys
+import unittest
+
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
+
+import canyonos_core.controller.canyonos_context as canyonos_context
+
+
+class CanyonosContextTests(unittest.TestCase):
+ def setUp(self):
+ canyonos_context._local = canyonos_context.threading.local()
+
+ def tearDown(self):
+ canyonos_context._local = canyonos_context.threading.local()
+
+ def test_request_id_defaults_to_empty_string(self):
+ self.assertEqual(canyonos_context.get_request_id(), "")
+
+ def test_request_id_round_trips(self):
+ canyonos_context.set_request_id("req-123")
+ self.assertEqual(canyonos_context.get_request_id(), "req-123")
+
+ def test_current_future_id_defaults_to_empty_string(self):
+ self.assertEqual(canyonos_context.get_current_future_id(), "")
+
+ def test_current_future_id_round_trips(self):
+ canyonos_context.set_current_future_id("future-abc")
+ self.assertEqual(canyonos_context.get_current_future_id(), "future-abc")
+
+ def test_request_id_and_future_id_are_independent(self):
+ canyonos_context.set_request_id("req-123")
+ canyonos_context.set_current_future_id("future-abc")
+ self.assertEqual(canyonos_context.get_request_id(), "req-123")
+ self.assertEqual(canyonos_context.get_current_future_id(), "future-abc")
+
+ def test_current_metrics_key_defaults_to_empty_string(self):
+ self.assertEqual(canyonos_context.get_current_metrics_key(), "")
+
+ def test_current_metrics_key_round_trips(self):
+ canyonos_context.set_current_metrics_key("controller:localhost:50051:metrics")
+ self.assertEqual(
+ canyonos_context.get_current_metrics_key(),
+ "controller:localhost:50051:metrics",
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_canyonos_test.py b/tests/test_canyonos_test.py
index 13f0b68..21e0918 100644
--- a/tests/test_canyonos_test.py
+++ b/tests/test_canyonos_test.py
@@ -41,9 +41,9 @@ def project(monkeypatch, tmp_path):
return tmp_path
-def report(errors=0, warnings=0, findings=(), ventis=False):
+def report(errors=0, warnings=0, findings=(), canyonos=False):
return {
- "capabilities": {"ventis": ventis},
+ "capabilities": {"canyonos_core": canyonos},
"errors": errors,
"warnings": warnings,
"findings": list(findings),
@@ -147,7 +147,7 @@ def test_validator_warnings_pass(monkeypatch, project):
assert (summary["errors"], summary["warnings"]) == (0, 1)
-def test_rules_needing_ventis_are_dropped_when_it_is_not_importable(monkeypatch, project):
+def test_rules_needing_canyonos_are_dropped_when_it_is_not_importable(monkeypatch, project):
monkeypatch.setattr(verify, "_find_validator", lambda _root: "/validate.py")
monkeypatch.setattr(
verify,
@@ -161,12 +161,12 @@ def test_rules_needing_ventis_are_dropped_when_it_is_not_importable(monkeypatch,
assert summary["findings"] == []
-def test_rules_needing_ventis_are_kept_when_it_is_importable(monkeypatch, project):
+def test_rules_needing_canyonos_are_kept_when_it_is_importable(monkeypatch, project):
monkeypatch.setattr(verify, "_find_validator", lambda _root: "/validate.py")
monkeypatch.setattr(
verify,
"_run_validator",
- lambda *_: report(errors=1, findings=[finding("V030")], ventis=True),
+ lambda *_: report(errors=1, findings=[finding("V030")], canyonos=True),
)
with pytest.raises(RuntimeError):
@@ -242,14 +242,14 @@ def install(images, containers):
ALL_UP = [
- "ventis-local-echoagent-0",
- "ventis-local-echoagent-1",
- "ventis-local-workflow-0",
+ "canyonos-local-echoagent-0",
+ "canyonos-local-echoagent-1",
+ "canyonos-local-workflow-0",
]
def test_a_complete_deploy_passes(project, runtime):
- runtime({"ventis-echoagent", "ventis-workflow"}, ALL_UP)
+ runtime({"canyonos-echoagent", "canyonos-workflow"}, ALL_UP)
result = verify.verify_runtime(str(project / ".car" / "config" / "global_controller.yaml"), 8000)
@@ -260,21 +260,21 @@ def test_a_complete_deploy_passes(project, runtime):
def test_a_short_replica_count_fails(project, runtime):
- runtime({"ventis-echoagent", "ventis-workflow"}, ALL_UP[1:])
+ runtime({"canyonos-echoagent", "canyonos-workflow"}, ALL_UP[1:])
with pytest.raises(RuntimeError, match="1 of 2 replicas"):
verify.verify_runtime(str(project / ".car" / "config" / "global_controller.yaml"), 8000)
def test_an_image_that_was_never_built_fails(project, runtime):
- runtime({"ventis-workflow"}, ["ventis-local-workflow-0"])
+ runtime({"canyonos-workflow"}, ["canyonos-local-workflow-0"])
- with pytest.raises(RuntimeError, match="ventis-echoagent was never built"):
+ with pytest.raises(RuntimeError, match="canyonos-echoagent was never built"):
verify.verify_runtime(str(project / ".car" / "config" / "global_controller.yaml"), 8000)
def test_the_workflow_endpoint_falls_back_to_the_configured_port(project, runtime):
- runtime({"ventis-echoagent", "ventis-workflow"}, ALL_UP)
+ runtime({"canyonos-echoagent", "canyonos-workflow"}, ALL_UP)
result = verify.verify_runtime(str(project / ".car" / "config" / "global_controller.yaml"), 8000)
@@ -293,7 +293,7 @@ def deployable(monkeypatch, project):
calls = {"post_deploy": 0, "quit": 0}
monkeypatch.setattr(test_cmd, "verify_build_artifact", lambda *a: {"warnings": 0, "stale": []})
- monkeypatch.setattr(test_cmd, "run_init", lambda banner=True: None)
+ monkeypatch.setattr(test_cmd, "run_init", lambda banner=True, extra_env=None: None)
monkeypatch.setattr(test_cmd, "run_sync", lambda: True)
monkeypatch.setattr(test_cmd, "load_state", lambda: {"container_id": "abc", "port": 8000})
monkeypatch.setattr(test_cmd, "_port_in_use", lambda _port: False)
@@ -320,6 +320,27 @@ def test_a_passing_run_tears_everything_down(deployable):
assert deployable["quit"] == 1
+def test_llm_is_stubbed_by_default(monkeypatch, deployable):
+ """`canyonos test` hands the stub flag to the GC container so no real LLM is hit."""
+ seen = {}
+ monkeypatch.setattr(
+ test_cmd, "run_init",
+ lambda banner=True, extra_env=None: seen.update(extra_env=extra_env),
+ )
+ assert test_cmd.run_test("hi") == 0
+ assert seen["extra_env"] == {"CANYONOS_LLM_STUB_TEXT": "test"}
+
+
+def test_real_llm_flag_disables_the_stub(monkeypatch, deployable):
+ seen = {}
+ monkeypatch.setattr(
+ test_cmd, "run_init",
+ lambda banner=True, extra_env=None: seen.update(extra_env=extra_env),
+ )
+ assert test_cmd.run_test("hi", llm_stub=None) == 0
+ assert seen["extra_env"] is None
+
+
def test_the_provider_is_restored_after_the_run(project, deployable):
config = project / ".car" / "config" / "global_controller.yaml"
@@ -418,9 +439,9 @@ def test_running_containers_are_filtered_to_the_local_provider(monkeypatch):
def fake_run(argv, **_):
seen.append(argv)
- return subprocess.CompletedProcess(argv, 0, "ventis-local-echoagent-0\n", "")
+ return subprocess.CompletedProcess(argv, 0, "canyonos-local-echoagent-0\n", "")
monkeypatch.setattr(verify.subprocess, "run", fake_run)
- assert verify._running_containers() == ["ventis-local-echoagent-0"]
- assert "name=ventis-local-" in seen[0]
+ assert verify._running_containers() == ["canyonos-local-echoagent-0"]
+ assert "name=canyonos-local-" in seen[0]
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 44b9270..f07a39d 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -12,20 +12,20 @@
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
-from ventis import cli
+from canyonos_core import cli
class CliDeployTests(unittest.TestCase):
def _fake_controller_module(self, controller):
- module = types.ModuleType("ventis.controller.global_controller")
+ module = types.ModuleType("canyonos_core.controller.global_controller")
module.GlobalController = lambda _config_path: controller
return module
@patch("atexit.register")
@patch("signal.signal")
- @patch("ventis.cli._run_build")
- @patch("ventis.cli._ensure_grpc_stubs_importable")
- @patch("ventis.cli._preflight_ec2_deploy")
+ @patch("canyonos_core.cli._run_build")
+ @patch("canyonos_core.cli._ensure_grpc_stubs_importable")
+ @patch("canyonos_core.cli._preflight_ec2_deploy")
def test_deploy_skips_ec2_preflight_for_local_config(
self,
preflight,
@@ -40,10 +40,10 @@ def test_deploy_skips_ec2_preflight_for_local_config(
config = {"agents": [{"name": "LocalAgent", "provider": "local"}]}
with (
- patch("ventis.cli.os.path.isfile", return_value=True),
- patch("ventis.cli._load_config", return_value=config),
+ patch("canyonos_core.cli.os.path.isfile", return_value=True),
+ patch("canyonos_core.cli._load_config", return_value=config),
patch.dict(
- sys.modules, {"ventis.controller.global_controller": controller_module}
+ sys.modules, {"canyonos_core.controller.global_controller": controller_module}
),
):
cli.cmd_deploy(args)
@@ -56,9 +56,9 @@ def test_deploy_skips_ec2_preflight_for_local_config(
@patch("atexit.register")
@patch("signal.signal")
- @patch("ventis.cli._run_build")
- @patch("ventis.cli._ensure_grpc_stubs_importable")
- @patch("ventis.cli._preflight_ec2_deploy")
+ @patch("canyonos_core.cli._run_build")
+ @patch("canyonos_core.cli._ensure_grpc_stubs_importable")
+ @patch("canyonos_core.cli._preflight_ec2_deploy")
def test_deploy_runs_ec2_preflight_for_ec2_config(
self,
preflight,
@@ -73,10 +73,10 @@ def test_deploy_runs_ec2_preflight_for_ec2_config(
config = {"agents": [{"name": "Ec2Agent", "provider": "EC2"}]}
with (
- patch("ventis.cli.os.path.isfile", return_value=True),
- patch("ventis.cli._load_config", return_value=config),
+ patch("canyonos_core.cli.os.path.isfile", return_value=True),
+ patch("canyonos_core.cli._load_config", return_value=config),
patch.dict(
- sys.modules, {"ventis.controller.global_controller": controller_module}
+ sys.modules, {"canyonos_core.controller.global_controller": controller_module}
),
):
cli.cmd_deploy(args)
@@ -87,9 +87,9 @@ def test_deploy_runs_ec2_preflight_for_ec2_config(
@patch("atexit.register")
@patch("signal.signal")
- @patch("ventis.cli._run_build")
- @patch("ventis.cli._ensure_grpc_stubs_importable")
- @patch("ventis.cli._preflight_ec2_deploy")
+ @patch("canyonos_core.cli._run_build")
+ @patch("canyonos_core.cli._ensure_grpc_stubs_importable")
+ @patch("canyonos_core.cli._preflight_ec2_deploy")
def test_deploy_uses_car_when_present(
self, preflight, ensure_grpc, _run_build, _signal_patch, _atexit_patch
):
@@ -98,11 +98,11 @@ def test_deploy_uses_car_when_present(
args = SimpleNamespace(config=".car/config/global_controller.yaml")
with tempfile.TemporaryDirectory() as tmpdir, patch(
- "ventis.cli.os.path.isfile", return_value=True
+ "canyonos_core.cli.os.path.isfile", return_value=True
), patch(
- "ventis.cli._load_config", return_value={"agents": []}
+ "canyonos_core.cli._load_config", return_value={"agents": []}
), patch.dict(
- sys.modules, {"ventis.controller.global_controller": controller_module}
+ sys.modules, {"canyonos_core.controller.global_controller": controller_module}
):
Path(tmpdir, ".car").mkdir()
cwd = os.getcwd()
@@ -115,8 +115,8 @@ def test_deploy_uses_car_when_present(
ensure_grpc.assert_called_once_with(os.path.join(os.path.realpath(tmpdir), ".car"))
preflight.assert_not_called()
- @patch("ventis.cli._ensure_grpc_stubs_importable")
- @patch("ventis.cli._require_docker_for_ec2")
+ @patch("canyonos_core.cli._ensure_grpc_stubs_importable")
+ @patch("canyonos_core.cli._require_docker_for_ec2")
def test_preflight_does_not_require_ssh_fields(self, require_docker, ensure_grpc):
config = {
"ec2": {
@@ -167,20 +167,20 @@ def fake_generate_stub(yaml_path, _output_path):
with (
patch(
- "ventis.cli._get_package_dir",
+ "canyonos_core.cli._get_package_dir",
return_value=str(project_dir / "package"),
),
- patch("ventis.cli.glob.glob", side_effect=fake_glob),
+ patch("canyonos_core.cli.glob.glob", side_effect=fake_glob),
patch(
- "ventis.stub_generator.generate_stub", side_effect=fake_generate_stub
+ "canyonos_core.stub_generator.generate_stub", side_effect=fake_generate_stub
),
- patch("ventis.stub_generator.generate_docker") as generate_docker,
+ patch("canyonos_core.stub_generator.generate_docker") as generate_docker,
patch(
- "ventis.stub_generator.generate_workflow_docker"
+ "canyonos_core.stub_generator.generate_workflow_docker"
) as generate_workflow_docker,
- patch("ventis.cli.subprocess.run", side_effect=fake_run),
- patch("ventis.cli._docker_available", return_value=buildx_available),
- patch("ventis.cli._docker_platform", return_value=platform),
+ patch("canyonos_core.cli.subprocess.run", side_effect=fake_run),
+ patch("canyonos_core.cli._docker_available", return_value=buildx_available),
+ patch("canyonos_core.cli._docker_platform", return_value=platform),
):
cwd = os.getcwd()
os.chdir(project_dir)
@@ -280,14 +280,14 @@ def test_build_uses_buildx_bake_when_available(self):
os.path.realpath(project_dir / "docker_container" / "ExampleAgent"),
)
self.assertTrue(os.path.isabs(targets["exampleagent"]["context"]))
- self.assertEqual(targets["exampleagent"]["tags"], ["ventis-exampleagent"])
+ self.assertEqual(targets["exampleagent"]["tags"], ["canyonos-exampleagent"])
self.assertEqual(targets["exampleagent"]["platforms"], ["linux/amd64"])
self.assertEqual(targets["exampleagent"]["output"], ["type=docker"])
self.assertEqual(
os.path.realpath(targets["workflow"]["context"]),
os.path.realpath(project_dir / "docker_container" / "Workflow"),
)
- self.assertEqual(targets["workflow"]["tags"], ["ventis-workflow"])
+ self.assertEqual(targets["workflow"]["tags"], ["canyonos-workflow"])
def test_build_uses_car_when_present(self):
with tempfile.TemporaryDirectory() as tmpdir:
@@ -438,7 +438,7 @@ def test_build_ignores_non_list_requirements(self):
)
)
- with self.assertLogs("ventis", level="WARNING") as log:
+ with self.assertLogs("canyonos_core", level="WARNING") as log:
_, generate_docker, _ = self._run_build(
project_dir, [str(example_yaml)], buildx_available=True
)
diff --git a/tests/test_deploy.py b/tests/test_deploy.py
index 3c02008..a16578d 100644
--- a/tests/test_deploy.py
+++ b/tests/test_deploy.py
@@ -6,7 +6,7 @@
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
-import ventis.controller.deploy as deploy_module
+import canyonos_core.controller.deploy as deploy_module
class _FakeRedis:
@@ -94,16 +94,16 @@ def fake_run(self, *args, **kwargs):
class DeployHandleWorkflowTests(unittest.TestCase):
def setUp(self):
- os.environ.pop("VENTIS_DATABASE_URL", None)
- os.environ.pop("VENTIS_PROJECT_ID", None)
+ os.environ.pop("CANYONOS_DATABASE_URL", None)
+ os.environ.pop("CANYONOS_PROJECT_ID", None)
def tearDown(self):
- os.environ.pop("VENTIS_DATABASE_URL", None)
- os.environ.pop("VENTIS_PROJECT_ID", None)
+ os.environ.pop("CANYONOS_DATABASE_URL", None)
+ os.environ.pop("CANYONOS_PROJECT_ID", None)
def test_records_working_status_before_dispatch_when_configured(self):
- os.environ["VENTIS_DATABASE_URL"] = "postgresql://example/db"
- os.environ["VENTIS_PROJECT_ID"] = "11111111-1111-1111-1111-111111111111"
+ os.environ["CANYONOS_DATABASE_URL"] = "postgresql://example/db"
+ os.environ["CANYONOS_PROJECT_ID"] = "11111111-1111-1111-1111-111111111111"
with patch.object(deploy_module, "upsert_session") as mock_upsert, \
_deployed_app() as app:
@@ -130,8 +130,8 @@ def test_skips_session_upsert_when_not_configured(self):
mock_upsert.assert_not_called()
def test_session_upsert_failure_does_not_fail_the_request(self):
- os.environ["VENTIS_DATABASE_URL"] = "postgresql://example/db"
- os.environ["VENTIS_PROJECT_ID"] = "11111111-1111-1111-1111-111111111111"
+ os.environ["CANYONOS_DATABASE_URL"] = "postgresql://example/db"
+ os.environ["CANYONOS_PROJECT_ID"] = "11111111-1111-1111-1111-111111111111"
with patch.object(
deploy_module, "upsert_session", side_effect=RuntimeError("db down")
@@ -143,8 +143,8 @@ def test_session_upsert_failure_does_not_fail_the_request(self):
self.assertIn("request_id", resp.get_json())
def test_marks_session_success_when_workflow_completes(self):
- os.environ["VENTIS_DATABASE_URL"] = "postgresql://example/db"
- os.environ["VENTIS_PROJECT_ID"] = "11111111-1111-1111-1111-111111111111"
+ os.environ["CANYONOS_DATABASE_URL"] = "postgresql://example/db"
+ os.environ["CANYONOS_PROJECT_ID"] = "11111111-1111-1111-1111-111111111111"
with patch.object(deploy_module, "upsert_session") as mock_upsert, \
_deployed_app() as app:
@@ -158,8 +158,8 @@ def test_marks_session_success_when_workflow_completes(self):
self.assertEqual(success_call_kwargs["output_payload"], {"x": 2})
def test_marks_session_failed_when_workflow_raises(self):
- os.environ["VENTIS_DATABASE_URL"] = "postgresql://example/db"
- os.environ["VENTIS_PROJECT_ID"] = "11111111-1111-1111-1111-111111111111"
+ os.environ["CANYONOS_DATABASE_URL"] = "postgresql://example/db"
+ os.environ["CANYONOS_PROJECT_ID"] = "11111111-1111-1111-1111-111111111111"
with patch.object(deploy_module, "upsert_session") as mock_upsert, \
_deployed_app(workflow_fn=_failing_workflow) as app:
@@ -177,7 +177,7 @@ def test_marks_session_failed_when_workflow_raises(self):
def test_skips_session_upsert_when_project_id_is_missing(self):
# project_id is NOT NULL in the session table, so a URL without a project
# id can only produce failing writes -- don't attempt them at all.
- os.environ["VENTIS_DATABASE_URL"] = "postgresql://example/db"
+ os.environ["CANYONOS_DATABASE_URL"] = "postgresql://example/db"
with patch.object(deploy_module, "upsert_session") as mock_upsert, \
_deployed_app() as app:
@@ -202,8 +202,8 @@ class DeployRequestKeyExpiryTests(unittest.TestCase):
accumulate for the lifetime of the Redis instance and eventually OOM it."""
def setUp(self):
- os.environ.pop("VENTIS_DATABASE_URL", None)
- os.environ.pop("VENTIS_PROJECT_ID", None)
+ os.environ.pop("CANYONOS_DATABASE_URL", None)
+ os.environ.pop("CANYONOS_PROJECT_ID", None)
def test_expires_status_and_result_on_success(self):
with _deployed_app() as app:
@@ -252,12 +252,12 @@ class DeployStatusFallbackTests(unittest.TestCase):
session row instead of 404-ing."""
def setUp(self):
- os.environ["VENTIS_DATABASE_URL"] = "postgresql://example/db"
- os.environ["VENTIS_PROJECT_ID"] = "11111111-1111-1111-1111-111111111111"
+ os.environ["CANYONOS_DATABASE_URL"] = "postgresql://example/db"
+ os.environ["CANYONOS_PROJECT_ID"] = "11111111-1111-1111-1111-111111111111"
def tearDown(self):
- os.environ.pop("VENTIS_DATABASE_URL", None)
- os.environ.pop("VENTIS_PROJECT_ID", None)
+ os.environ.pop("CANYONOS_DATABASE_URL", None)
+ os.environ.pop("CANYONOS_PROJECT_ID", None)
def test_maps_completed_session_to_done_with_result(self):
row = {"status": "completed", "output": {"x": 2}}
@@ -334,8 +334,8 @@ def test_does_not_touch_postgres_while_redis_still_has_the_request(self):
mock_get.assert_not_called()
def test_skips_the_fallback_when_the_database_is_not_configured(self):
- os.environ.pop("VENTIS_DATABASE_URL", None)
- os.environ.pop("VENTIS_PROJECT_ID", None)
+ os.environ.pop("CANYONOS_DATABASE_URL", None)
+ os.environ.pop("CANYONOS_PROJECT_ID", None)
with patch.object(deploy_module, "get_session") as mock_get, \
_deployed_app() as app:
@@ -346,19 +346,19 @@ def test_skips_the_fallback_when_the_database_is_not_configured(self):
class DeployLiveIdentityTests(unittest.TestCase):
- """Bug E: VENTIS_PROJECT_ID/VENTIS_DATABASE_URL are Docker env vars frozen at container
+ """Bug E: CANYONOS_PROJECT_ID/CANYONOS_DATABASE_URL are Docker env vars frozen at container
launch. A GlobalController reload (SIGHUP) publishes the current project/database identity
to Redis (controller:identity); this container must read that fresh on every request
instead of trusting the env vars it booted with, or a project switch leaves it creating
session rows under the *old* project indefinitely."""
def setUp(self):
- os.environ["VENTIS_DATABASE_URL"] = "postgresql://example/old-db"
- os.environ["VENTIS_PROJECT_ID"] = "11111111-1111-1111-1111-111111111111"
+ os.environ["CANYONOS_DATABASE_URL"] = "postgresql://example/old-db"
+ os.environ["CANYONOS_PROJECT_ID"] = "11111111-1111-1111-1111-111111111111"
def tearDown(self):
- os.environ.pop("VENTIS_DATABASE_URL", None)
- os.environ.pop("VENTIS_PROJECT_ID", None)
+ os.environ.pop("CANYONOS_DATABASE_URL", None)
+ os.environ.pop("CANYONOS_PROJECT_ID", None)
def test_a_value_already_in_redis_at_boot_overrides_the_env_var(self):
with patch.object(deploy_module, "upsert_session") as mock_upsert, \
diff --git a/tests/test_deploy_progress.py b/tests/test_deploy_progress.py
index d1c8cf6..db0bb5a 100644
--- a/tests/test_deploy_progress.py
+++ b/tests/test_deploy_progress.py
@@ -23,16 +23,16 @@ def drive(lines):
def test_a_full_run_reports_each_phase_once():
_, spinners, done, errored = drive(
[
- "INFO:ventis:Generating stub: a.yaml -> a_stub.py\n",
- "INFO:ventis:Compiling gRPC proto: a.proto\n",
- "INFO:ventis:Building 3 Docker image(s) via `docker buildx bake`.\n",
+ "INFO:canyonos_core:Generating stub: a.yaml -> a_stub.py\n",
+ "INFO:canyonos_core:Compiling gRPC proto: a.proto\n",
+ "INFO:canyonos_core:Building 3 Docker image(s) via `docker buildx bake`.\n",
"#5 [4/7] RUN pip install -r requirements.txt\n",
- "INFO:ventis:Build complete.\n",
- "INFO:ventis:Deploying from config: config.yaml\n",
- "INFO:ventis.controller.global_controller:Redis launched on 1 node(s).\n",
- "INFO:ventis.controller.global_controller:Waiting for 2 replica(s) to become healthy (timeout=300s)...\n",
- "INFO:ventis.controller.global_controller:Controller Intent (127.0.0.1:50051) is ready.\n",
- "INFO:ventis.controller.global_controller:Controller Metrics (127.0.0.1:50052) is ready.\n",
+ "INFO:canyonos_core:Build complete.\n",
+ "INFO:canyonos_core:Deploying from config: config.yaml\n",
+ "INFO:canyonos_core.controller.global_controller:Redis launched on 1 node(s).\n",
+ "INFO:canyonos_core.controller.global_controller:Waiting for 2 replica(s) to become healthy (timeout=300s)...\n",
+ "INFO:canyonos_core.controller.global_controller:Controller Intent (127.0.0.1:50051) is ready.\n",
+ "INFO:canyonos_core.controller.global_controller:Controller Metrics (127.0.0.1:50052) is ready.\n",
]
)
assert not errored
@@ -48,9 +48,9 @@ def test_phases_are_matched_in_the_order_the_container_emits_them():
"""
_, spinners, done, _ = drive(
[
- "INFO:ventis.controller.global_controller:Checking for stale containers from previous runs...\n",
- "INFO:ventis.controller.global_controller:Redis launched on 1 node(s).\n",
- "INFO:ventis:Deploying from config: config.yaml\n",
+ "INFO:canyonos_core.controller.global_controller:Checking for stale containers from previous runs...\n",
+ "INFO:canyonos_core.controller.global_controller:Redis launched on 1 node(s).\n",
+ "INFO:canyonos_core:Deploying from config: config.yaml\n",
]
)
assert done == ["Redis ready"]
@@ -60,9 +60,9 @@ def test_phases_are_matched_in_the_order_the_container_emits_them():
def test_repeated_build_lines_collapse_to_one_spinner_update():
_, spinners, _, _ = drive(
[
- "INFO:ventis:Generating stub: a.yaml -> a_stub.py\n",
- "INFO:ventis:Generating stub: b.yaml -> b_stub.py\n",
- "INFO:ventis:Generating Docker context for 'b'\n",
+ "INFO:canyonos_core:Generating stub: a.yaml -> a_stub.py\n",
+ "INFO:canyonos_core:Generating stub: b.yaml -> b_stub.py\n",
+ "INFO:canyonos_core:Generating Docker context for 'b'\n",
]
)
assert spinners == ["Generating stubs and Docker contexts..."]
@@ -71,8 +71,8 @@ def test_repeated_build_lines_collapse_to_one_spinner_update():
def test_a_run_with_nothing_to_build_still_reports_the_phase():
_, _, done, _ = drive(
[
- "INFO:ventis:No Docker images to build.\n",
- "INFO:ventis:Build complete.\n",
+ "INFO:canyonos_core:No Docker images to build.\n",
+ "INFO:canyonos_core:Build complete.\n",
]
)
assert done == ["No images to build", "Build complete"]
@@ -81,9 +81,9 @@ def test_a_run_with_nothing_to_build_still_reports_the_phase():
def test_agent_progress_counts_up_against_the_announced_total():
tracker, spinners, _, _ = drive(
[
- "INFO:ventis.controller.global_controller:Waiting for 3 replica(s) to become healthy (timeout=300s)...\n",
- "INFO:ventis.controller.global_controller:Controller A (127.0.0.1:1) is ready.\n",
- "INFO:ventis.controller.global_controller:Controller B (127.0.0.1:2) is ready.\n",
+ "INFO:canyonos_core.controller.global_controller:Waiting for 3 replica(s) to become healthy (timeout=300s)...\n",
+ "INFO:canyonos_core.controller.global_controller:Controller A (127.0.0.1:1) is ready.\n",
+ "INFO:canyonos_core.controller.global_controller:Controller B (127.0.0.1:2) is ready.\n",
]
)
assert spinners[-1] == "Starting agents (2/3 ready)..."
@@ -96,9 +96,9 @@ def test_replicas_of_one_agent_are_counted_separately():
"""
tracker, spinners, _, _ = drive(
[
- "INFO:ventis.controller.global_controller:Waiting for 2 replica(s) to become healthy (timeout=300s)...\n",
- "INFO:ventis.controller.global_controller:Controller Echo (127.0.0.1:50051) is ready.\n",
- "INFO:ventis.controller.global_controller:Controller Echo (127.0.0.1:50052) is ready.\n",
+ "INFO:canyonos_core.controller.global_controller:Waiting for 2 replica(s) to become healthy (timeout=300s)...\n",
+ "INFO:canyonos_core.controller.global_controller:Controller Echo (127.0.0.1:50051) is ready.\n",
+ "INFO:canyonos_core.controller.global_controller:Controller Echo (127.0.0.1:50052) is ready.\n",
]
)
assert spinners[-1] == "Starting agents (2/2 ready)..."
@@ -108,9 +108,9 @@ def test_replicas_of_one_agent_are_counted_separately():
def test_a_re_read_ready_line_does_not_double_count():
tracker, _, _, _ = drive(
[
- "INFO:ventis.controller.global_controller:Waiting for 2 replica(s) to become healthy (timeout=300s)...\n",
- "INFO:ventis.controller.global_controller:Controller Echo (127.0.0.1:50051) is ready.\n",
- "INFO:ventis.controller.global_controller:Controller Echo (127.0.0.1:50051) is ready.\n",
+ "INFO:canyonos_core.controller.global_controller:Waiting for 2 replica(s) to become healthy (timeout=300s)...\n",
+ "INFO:canyonos_core.controller.global_controller:Controller Echo (127.0.0.1:50051) is ready.\n",
+ "INFO:canyonos_core.controller.global_controller:Controller Echo (127.0.0.1:50051) is ready.\n",
]
)
assert tracker.agents_ready_message() == (
@@ -125,8 +125,8 @@ def test_coming_up_short_of_the_announced_replicas_is_not_reported_as_success():
"""
tracker, _, _, _ = drive(
[
- "INFO:ventis.controller.global_controller:Waiting for 3 replica(s) to become healthy (timeout=300s)...\n",
- "INFO:ventis.controller.global_controller:Controller A (127.0.0.1:1) is ready.\n",
+ "INFO:canyonos_core.controller.global_controller:Waiting for 3 replica(s) to become healthy (timeout=300s)...\n",
+ "INFO:canyonos_core.controller.global_controller:Controller A (127.0.0.1:1) is ready.\n",
]
)
message, all_ready = tracker.agents_ready_message()
@@ -135,13 +135,13 @@ def test_coming_up_short_of_the_announced_replicas_is_not_reported_as_success():
def test_a_run_that_never_announced_replicas_still_reports_ready():
- tracker, _, _, _ = drive(["INFO:ventis:Build complete.\n"])
+ tracker, _, _, _ = drive(["INFO:canyonos_core:Build complete.\n"])
assert tracker.agents_ready_message() == ("Workflow ready", True)
def test_replicas_ready_without_an_announced_total_still_reports_progress():
_, spinners, _, _ = drive(
- ["INFO:ventis.controller.global_controller:Controller A (127.0.0.1:1) is ready.\n"]
+ ["INFO:canyonos_core.controller.global_controller:Controller A (127.0.0.1:1) is ready.\n"]
)
assert spinners == ["Starting agents..."]
@@ -149,7 +149,7 @@ def test_replicas_ready_without_an_announced_total_still_reports_progress():
@pytest.mark.parametrize(
"line",
[
- "ERROR:ventis:Config file not found: missing.yaml\n",
+ "ERROR:canyonos_core:Config file not found: missing.yaml\n",
"Traceback (most recent call last):\n",
"ERROR: failed to solve: process \"/bin/sh -c pip install\" did not complete successfully\n",
],
@@ -162,7 +162,7 @@ def test_fatal_lines_are_flagged(line):
@pytest.mark.parametrize(
"line",
[
- "WARNING:ventis.controller.global_controller:otel.destinations not configured -- no OTel metrics collection will happen.\n",
+ "WARNING:canyonos_core.controller.global_controller:otel.destinations not configured -- no OTel metrics collection will happen.\n",
" Warning: no entrypoint mapping for 'agent'\n",
],
)
@@ -206,8 +206,8 @@ def test_the_clis_own_status_requests_are_not_shown_or_buffered(monkeypatch):
iter(
[
'172.17.0.1 - - [04/Sep/2026 21:00:00] "GET /status HTTP/1.1" 200 -\n',
- "INFO:ventis:Build complete.\n",
- "INFO:ventis.controller.global_controller:Global controller started, polling every 5s...\n",
+ "INFO:canyonos_core:Build complete.\n",
+ "INFO:canyonos_core.controller.global_controller:Global controller started, polling every 5s...\n",
]
)
)
@@ -225,7 +225,7 @@ def test_a_build_that_dies_silently_does_not_hang(monkeypatch, capsys):
monkeypatch.setattr(deploy_cmd, "_REVEAL_GRACE_SECONDS", 0.5)
monkeypatch.setattr(deploy_cmd, "deploy_status", lambda _p: {"running": False})
- lines = deploy_cmd._queued_lines(iter(["INFO:ventis:Building 2 Docker image(s) via `x`.\n"]))
+ lines = deploy_cmd._queued_lines(iter(["INFO:canyonos_core:Building 2 Docker image(s) via `x`.\n"]))
# The queue never yields None: the stream stays open, as it does in reality.
lines.put = lambda *a, **k: None
diff --git a/tests/test_env_file_reserved_keys.py b/tests/test_env_file_reserved_keys.py
new file mode 100644
index 0000000..19dcbd1
--- /dev/null
+++ b/tests/test_env_file_reserved_keys.py
@@ -0,0 +1,48 @@
+"""The LLM stub is a `canyonos test`-only control: a user's project `.env` must
+never be able to inject CANYONOS_LLM_STUB_TEXT into the controller environment
+(which would silently stub real LLM calls in a normal deploy)."""
+
+import os
+import sys
+import tempfile
+import unittest
+
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
+
+from canyonos_core.controller.global_controller import GlobalController
+
+
+class LoadDotenvReservedKeysTests(unittest.TestCase):
+ def _write_env(self, text):
+ f = tempfile.NamedTemporaryFile("w", suffix=".env", delete=False)
+ f.write(text)
+ f.close()
+ self.addCleanup(os.unlink, f.name)
+ return f.name
+
+ def test_reserved_stub_key_is_not_loaded_from_user_env(self):
+ os.environ.pop("CANYONOS_LLM_STUB_TEXT", None)
+ os.environ.pop("MY_API_KEY", None)
+ self.addCleanup(os.environ.pop, "MY_API_KEY", None)
+
+ path = self._write_env("CANYONOS_LLM_STUB_TEXT=sneaky\nMY_API_KEY=real-secret\n")
+ GlobalController._load_dotenv(path)
+
+ # The reserved control key is ignored...
+ self.assertNotIn("CANYONOS_LLM_STUB_TEXT", os.environ)
+ # ...while ordinary user secrets still load as before.
+ self.assertEqual(os.environ.get("MY_API_KEY"), "real-secret")
+
+ def test_a_stub_value_already_set_is_left_untouched(self):
+ # `canyonos test` sets it on the GC container; _load_dotenv must not clear it.
+ os.environ["CANYONOS_LLM_STUB_TEXT"] = "test"
+ self.addCleanup(os.environ.pop, "CANYONOS_LLM_STUB_TEXT", None)
+
+ path = self._write_env("CANYONOS_LLM_STUB_TEXT=sneaky\n")
+ GlobalController._load_dotenv(path)
+
+ self.assertEqual(os.environ["CANYONOS_LLM_STUB_TEXT"], "test")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_error_propagation.py b/tests/test_error_propagation.py
index 59c6eba..78ad996 100644
--- a/tests/test_error_propagation.py
+++ b/tests/test_error_propagation.py
@@ -10,14 +10,14 @@
0,
os.path.abspath(
os.path.join(
- os.path.dirname(__file__), "..", "ventis", "templates", "grpc_stubs"
+ os.path.dirname(__file__), "..", "canyonos_core", "templates", "grpc_stubs"
)
),
)
-from ventis.controller.local_controller import LocalController
-from ventis.controller.local_controller_frontend import LocalControllerServicer
-from ventis.controller.future import Future
+from canyonos_core.controller.local_controller import LocalController
+from canyonos_core.controller.local_controller_frontend import LocalControllerServicer
+from canyonos_core.controller.future import Future
import local_controler_pb2
@@ -42,11 +42,19 @@ def hincrby(self, name, field, amount=1):
bucket[field] = int(bucket.get(field, 0)) + amount
return bucket[field]
+ def smembers(self, key):
+ return set()
+
def _bind_failure_marker(controller):
controller._mark_future_failed = lambda future_id, error, origin=None: (
LocalController._mark_future_failed(controller, future_id, error, origin)
)
+ controller._fan_out_to_consumers = (
+ lambda future_id, result=None, failed=0, error_message="": LocalController._fan_out_to_consumers(
+ controller, future_id, result, failed, error_message
+ )
+ )
return controller
@@ -222,6 +230,9 @@ def capture_write_result(request):
executor._send_result_callback = lambda *a, **k: (
LocalController._send_result_callback(executor, *a, **k)
)
+ executor._fan_out_to_consumers = lambda *a, **k: (
+ LocalController._fan_out_to_consumers(executor, *a, **k)
+ )
LocalController._execute_locally(
executor, "Greeter", "greet", {}, "future-1", origin="origin:50051"
diff --git a/tests/test_future.py b/tests/test_future.py
index 4914b29..d26e6e4 100644
--- a/tests/test_future.py
+++ b/tests/test_future.py
@@ -8,13 +8,13 @@
0,
os.path.abspath(
os.path.join(
- os.path.dirname(__file__), "..", "ventis", "templates", "grpc_stubs"
+ os.path.dirname(__file__), "..", "canyonos_core", "templates", "grpc_stubs"
)
),
)
-import ventis.controller.future as future_module
-import ventis.controller.ventis_context as ventis_context
+import canyonos_core.controller.future as future_module
+import canyonos_core.controller.canyonos_context as canyonos_context
class _FakeRedis:
@@ -45,12 +45,12 @@ def setUp(self):
self._orig_stub = future_module.Future._stub
future_module.Future.redis = self.fake_redis
future_module.Future._stub = MagicMock()
- ventis_context.set_current_future_id("")
+ canyonos_context.set_current_future_id("")
def tearDown(self):
future_module.Future.redis = self._orig_redis
future_module.Future._stub = self._orig_stub
- ventis_context.set_current_future_id("")
+ canyonos_context.set_current_future_id("")
def test_parent_defaults_to_empty_when_no_future_executing(self):
f = future_module.Future(
@@ -60,7 +60,7 @@ def test_parent_defaults_to_empty_when_no_future_executing(self):
self.assertEqual(self.fake_redis.hashes[f"future:{f.id}"]["parent"], "")
def test_parent_is_the_currently_executing_future_id(self):
- ventis_context.set_current_future_id("caller-future-id")
+ canyonos_context.set_current_future_id("caller-future-id")
f = future_module.Future(
parent="ignored/file.py", service="Svc", method="do_thing"
diff --git a/tests/test_global_controller_cleanup.py b/tests/test_global_controller_cleanup.py
index 5d2c264..581a966 100644
--- a/tests/test_global_controller_cleanup.py
+++ b/tests/test_global_controller_cleanup.py
@@ -6,7 +6,7 @@
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "grpc_stubs")))
-from ventis.controller.global_controller import GlobalController
+from canyonos_core.controller.global_controller import GlobalController
import local_controler_pb2
diff --git a/tests/test_global_controller_identity.py b/tests/test_global_controller_identity.py
index 76a6689..46eb330 100644
--- a/tests/test_global_controller_identity.py
+++ b/tests/test_global_controller_identity.py
@@ -1,4 +1,4 @@
-"""Bug E: a Workflow container's VENTIS_PROJECT_ID/VENTIS_DATABASE_URL env vars are frozen at
+"""Bug E: a Workflow container's CANYONOS_PROJECT_ID/CANYONOS_DATABASE_URL env vars are frozen at
launch. _write_identity() publishes the controller's current project/database identity to
every node's Redis (mirroring the existing policy:rules/routing_table:* pattern) so deploy.py's
_current_identity() can read it live instead of trusting a boot-time env var. reload_config()
@@ -13,7 +13,7 @@
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
-from ventis.controller.global_controller import GlobalController
+from canyonos_core.controller.global_controller import GlobalController
class _FakeRedis:
diff --git a/tests/test_global_controller_project_id.py b/tests/test_global_controller_project_id.py
index 1feebc8..cf5f43a 100644
--- a/tests/test_global_controller_project_id.py
+++ b/tests/test_global_controller_project_id.py
@@ -13,7 +13,7 @@
import yaml
-from ventis.controller.global_controller import GlobalController
+from canyonos_core.controller.global_controller import GlobalController
UUID_HEX_RE = re.compile(r"^[0-9a-f]{32}$")
diff --git a/tests/test_global_controller_redis_reuse.py b/tests/test_global_controller_redis_reuse.py
index 1c1682d..2f3e74a 100644
--- a/tests/test_global_controller_redis_reuse.py
+++ b/tests/test_global_controller_redis_reuse.py
@@ -1,6 +1,6 @@
"""Fix C: a restart must not unconditionally wipe and recreate each node's Redis container.
-_launch_redis_containers() used to `docker run` a fresh ventis-redis- container on every
+_launch_redis_containers() used to `docker run` a fresh canyonos-redis- container on every
__init__, unconditionally -- wiping every `agent_instance:*` record InstanceManager needs to
recognize already-running EC2 replicas as reusable. ensure_instances()'s dedup logic was already
correct; it was just fed an empty Redis on every restart, so it reprovisioned everything from
@@ -16,7 +16,7 @@
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
-from ventis.controller.global_controller import GlobalController
+from canyonos_core.controller.global_controller import GlobalController
def _bare_controller(controllers):
@@ -40,8 +40,8 @@ def fake_run_cmd(cmd, host, user=None):
controller._run_cmd = fake_run_cmd
- with patch("ventis.controller.global_controller.RedisClient") as fake_redis_cls, patch(
- "ventis.controller.global_controller._wait_for_redis"
+ with patch("canyonos_core.controller.global_controller.RedisClient") as fake_redis_cls, patch(
+ "canyonos_core.controller.global_controller._wait_for_redis"
):
fake_redis_cls.return_value = MagicMock()
controller._launch_redis_containers()
@@ -88,14 +88,14 @@ def fake_run_cmd(cmd, host, user=None):
controller._run_cmd = fake_run_cmd
- with patch("ventis.controller.global_controller.RedisClient") as fake_redis_cls, patch(
- "ventis.controller.global_controller._wait_for_redis"
+ with patch("canyonos_core.controller.global_controller.RedisClient") as fake_redis_cls, patch(
+ "canyonos_core.controller.global_controller._wait_for_redis"
):
fake_redis_cls.return_value = MagicMock()
controller._launch_redis_containers()
self.assertEqual(len(inspect_calls), 1)
- self.assertIn("ventis-redis-10-0-0-5", inspect_calls[0])
+ self.assertIn("canyonos-redis-10-0-0-5", inspect_calls[0])
if __name__ == "__main__":
diff --git a/tests/test_global_controller_reload.py b/tests/test_global_controller_reload.py
index 26d0662..fd4b40c 100644
--- a/tests/test_global_controller_reload.py
+++ b/tests/test_global_controller_reload.py
@@ -16,8 +16,8 @@
import yaml
-import ventis.controller.utils.telemetry_logging as sqlmod
-from ventis.controller.global_controller import GlobalController
+import canyonos_core.controller.utils.telemetry_logging as sqlmod
+from canyonos_core.controller.global_controller import GlobalController
class _FakeInstanceManager:
diff --git a/tests/test_gpu_metrics.py b/tests/test_gpu_metrics.py
index 9eac921..a4c6df4 100644
--- a/tests/test_gpu_metrics.py
+++ b/tests/test_gpu_metrics.py
@@ -6,13 +6,13 @@
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
-from ventis.controller.utils.gpu_metrics import read_gpu_percent
+from canyonos_core.controller.utils.gpu_metrics import read_gpu_percent
class ReadGpuPercentTests(unittest.TestCase):
def test_falls_back_to_zero_when_nvidia_smi_missing(self):
with patch(
- "ventis.controller.utils.gpu_metrics.subprocess.run",
+ "canyonos_core.controller.utils.gpu_metrics.subprocess.run",
side_effect=FileNotFoundError(),
):
self.assertEqual(read_gpu_percent(), 0.0)
@@ -20,7 +20,7 @@ def test_falls_back_to_zero_when_nvidia_smi_missing(self):
def test_parses_nvidia_smi_output(self):
fake_result = SimpleNamespace(returncode=0, stdout="42\n")
with patch(
- "ventis.controller.utils.gpu_metrics.subprocess.run",
+ "canyonos_core.controller.utils.gpu_metrics.subprocess.run",
return_value=fake_result,
):
self.assertEqual(read_gpu_percent(), 42.0)
@@ -28,7 +28,7 @@ def test_parses_nvidia_smi_output(self):
def test_falls_back_on_nonzero_returncode(self):
fake_result = SimpleNamespace(returncode=1, stdout="")
with patch(
- "ventis.controller.utils.gpu_metrics.subprocess.run",
+ "canyonos_core.controller.utils.gpu_metrics.subprocess.run",
return_value=fake_result,
):
self.assertEqual(read_gpu_percent(), 0.0)
diff --git a/tests/test_instance_manager_runtime.py b/tests/test_instance_manager_runtime.py
index 2c481c3..41bfb50 100644
--- a/tests/test_instance_manager_runtime.py
+++ b/tests/test_instance_manager_runtime.py
@@ -6,8 +6,8 @@
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
-from ventis.controller.cloud_provider_logic.Local import _runtime as local_runtime
-from ventis.controller.instance_manager import InstanceManager
+from canyonos_core.controller.cloud_provider_logic.Local import _runtime as local_runtime
+from canyonos_core.controller.instance_manager import InstanceManager
class _FakeRedis:
@@ -138,9 +138,9 @@ def test_local_instances_keep_default_host_and_increment_host_ports(self):
"host_port": "8000",
"container_port": "50051",
"endpoint": "localhost:8000",
- "redis_host": "ventis-redis-localhost",
+ "redis_host": "canyonos-redis-localhost",
"redis_port": "6379",
- "runtime_id": "ventis-local-alpha-0",
+ "runtime_id": "canyonos-local-alpha-0",
},
)
self.assertEqual(beta["host"], "localhost")
@@ -155,24 +155,26 @@ def test_local_instances_keep_default_host_and_increment_host_ports(self):
"-d",
"-it",
"--network",
- "ventis-local",
+ "canyonos-local",
"--name",
- "ventis-local-alpha-0",
+ "canyonos-local-alpha-0",
"-p",
"8000:50051",
"-e",
- "VENTIS_AGENT_PORT=50051",
+ "CANYONOS_AGENT_PORT=50051",
"-e",
- "VENTIS_AGENT_HOST=ventis-local-alpha-0",
+ "CANYONOS_AGENT_HOST=canyonos-local-alpha-0",
"-e",
- "VENTIS_REDIS_HOST=ventis-redis-localhost",
+ "CANYONOS_REDIS_HOST=canyonos-redis-localhost",
"-e",
- "VENTIS_REDIS_PORT=6379",
+ "CANYONOS_REDIS_PORT=6379",
"-e",
- "VENTIS_POLL_INTERVAL=5",
+ "CANYONOS_POLL_INTERVAL=5",
"-e",
"AWS_ENDPOINT_URL_BEDROCK_RUNTIME=http://127.0.0.1:8081/bedrock",
- "ventis-alpha",
+ "-e",
+ "CANYONOS_LLM_STUB_TEXT=",
+ "canyonos-alpha",
],
"localhost",
None,
@@ -187,7 +189,31 @@ def test_bootstrap_instance_passes_poll_interval_env_var(self):
manager.ensure_instances([{"name": "Alpha", "provider": "local"}])
cmd = controller._run_cmd.call_args.args[0]
- self.assertIn("VENTIS_POLL_INTERVAL=7", cmd)
+ self.assertIn("CANYONOS_POLL_INTERVAL=7", cmd)
+
+ def test_llm_stub_env_is_forwarded_to_the_agent_when_set(self):
+ controller = _fake_controller()
+ manager = InstanceManager(controller, controller.redis)
+
+ with patch.dict(os.environ, {"CANYONOS_LLM_STUB_TEXT": "test"}):
+ manager.ensure_instances([{"name": "Alpha", "provider": "local"}])
+
+ cmd = controller._run_cmd.call_args_list[1].args[0]
+ self.assertIn("CANYONOS_LLM_STUB_TEXT=test", cmd)
+
+ def test_llm_stub_is_explicitly_disabled_by_default(self):
+ """Without `canyonos test`, the stub is pinned empty (off) and immune to --env-file."""
+ controller = _fake_controller()
+ manager = InstanceManager(controller, controller.redis)
+
+ os.environ.pop("CANYONOS_LLM_STUB_TEXT", None)
+ manager.ensure_instances([{"name": "Alpha", "provider": "local"}])
+
+ cmd = controller._run_cmd.call_args_list[1].args[0]
+ # Always present, explicitly empty -> stub off, and a user's .env value
+ # for this key is overridden (docker: -e beats --env-file).
+ self.assertIn("CANYONOS_LLM_STUB_TEXT=", cmd)
+ self.assertNotIn("CANYONOS_LLM_STUB_TEXT=test", cmd)
def test_local_workflow_and_resource_flags_stay_the_same(self):
controller = _fake_controller()
@@ -213,23 +239,25 @@ def test_local_workflow_and_resource_flags_stay_the_same(self):
"-d",
"-it",
"--network",
- "ventis-local",
+ "canyonos-local",
"--name",
- "ventis-local-workflow-0",
+ "canyonos-local-workflow-0",
"-p",
"8000:50051",
"-e",
- "VENTIS_AGENT_PORT=50051",
+ "CANYONOS_AGENT_PORT=50051",
"-e",
- "VENTIS_AGENT_HOST=ventis-local-workflow-0",
+ "CANYONOS_AGENT_HOST=canyonos-local-workflow-0",
"-e",
- "VENTIS_REDIS_HOST=ventis-redis-localhost",
+ "CANYONOS_REDIS_HOST=canyonos-redis-localhost",
"-e",
- "VENTIS_REDIS_PORT=6379",
+ "CANYONOS_REDIS_PORT=6379",
"-e",
- "VENTIS_POLL_INTERVAL=5",
+ "CANYONOS_POLL_INTERVAL=5",
"-e",
"AWS_ENDPOINT_URL_BEDROCK_RUNTIME=http://127.0.0.1:8081/bedrock",
+ "-e",
+ "CANYONOS_LLM_STUB_TEXT=",
"-p",
"8080:8080",
"--cpus",
@@ -238,7 +266,7 @@ def test_local_workflow_and_resource_flags_stay_the_same(self):
"1024m",
"--gpus",
"1",
- "ventis-workflow",
+ "canyonos-workflow",
],
"localhost",
None,
@@ -261,7 +289,7 @@ def test_agent_id_is_published_under_the_controller_endpoint_key(self):
alpha = manager.ensure_instances([{"name": "Alpha", "provider": "local"}])[0]
self.assertEqual(
- controller.redis.get("controller:ventis-local-alpha-0:50051:agent_id"),
+ controller.redis.get("controller:canyonos-local-alpha-0:50051:agent_id"),
alpha["agent_id"],
)
@@ -275,7 +303,7 @@ def test_local_remove_instance_still_removes_the_same_container(self):
self.assertEqual(
controller._run_cmd.call_args.args,
- (["docker", "rm", "-f", "ventis-local-alpha-0"], "localhost", None),
+ (["docker", "rm", "-f", "canyonos-local-alpha-0"], "localhost", None),
)
self.assertEqual(controller.redis.hgetall("agent_instance:local:Alpha:0"), {})
self.assertEqual(controller.containers["Alpha"], [])
@@ -286,7 +314,7 @@ def test_manager_keeps_ec2_runtime_boundary_behavior(self):
provisioned = {
"host": "10.0.0.30",
- "runtime_id": "ventis-ec2-remote-0--i-test1",
+ "runtime_id": "canyonos-ec2-remote-0--i-test1",
"redis_port": 6390,
}
instance = {
@@ -299,7 +327,7 @@ def test_manager_keeps_ec2_runtime_boundary_behavior(self):
"endpoint": "10.0.0.30:50051",
"redis_host": "10.0.0.30",
"redis_port": "6390",
- "runtime_id": "ventis-ec2-remote-0--i-test1",
+ "runtime_id": "canyonos-ec2-remote-0--i-test1",
}
runtime = _fake_runtime(
@@ -368,7 +396,7 @@ def test_manager_uses_same_runtime_contract_for_local_and_ec2(self):
"endpoint": "localhost:8000",
"redis_host": "host.docker.internal",
"redis_port": "6379",
- "runtime_id": "ventis-local-local-0",
+ "runtime_id": "canyonos-local-local-0",
}
ec2_instance = {
"agent_name": "Remote",
@@ -380,7 +408,7 @@ def test_manager_uses_same_runtime_contract_for_local_and_ec2(self):
"endpoint": "10.0.0.30:50051",
"redis_host": "10.0.0.30",
"redis_port": "6379",
- "runtime_id": "ventis-ec2-remote-0--i-test1",
+ "runtime_id": "canyonos-ec2-remote-0--i-test1",
}
local_runtime = _fake_runtime(
diff --git a/tests/test_local_controller_cleanup.py b/tests/test_local_controller_cleanup.py
index 0466bb7..aa0c7ff 100644
--- a/tests/test_local_controller_cleanup.py
+++ b/tests/test_local_controller_cleanup.py
@@ -8,7 +8,7 @@
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "grpc_stubs")))
-from ventis.controller.local_controller_frontend import LocalControllerServicer
+from canyonos_core.controller.local_controller_frontend import LocalControllerServicer
import local_controler_pb2
@@ -36,7 +36,7 @@ def test_batched_request_ids_dispatches_cleanup_for_each(self):
resonse=json.dumps({"request_ids": ["req1", "req2", "req3"]})
)
- with patch("ventis.controller.local_controller_frontend.Thread", _SyncThread):
+ with patch("canyonos_core.controller.local_controller_frontend.Thread", _SyncThread):
LocalControllerServicer.Cleanup(servicer, request, context=None)
self.assertEqual(cleaned, ["req1", "req2", "req3"])
@@ -46,7 +46,7 @@ def test_missing_ids_does_not_dispatch(self):
servicer = SimpleNamespace(_cleanup_request=lambda rid: cleaned.append(rid))
request = local_controler_pb2.JsonResponse(resonse=json.dumps({}))
- with patch("ventis.controller.local_controller_frontend.Thread", _SyncThread):
+ with patch("canyonos_core.controller.local_controller_frontend.Thread", _SyncThread):
LocalControllerServicer.Cleanup(servicer, request, context=None)
self.assertEqual(cleaned, [])
@@ -61,7 +61,7 @@ def test_old_single_request_id_payload_is_no_longer_supported(self):
resonse=json.dumps({"request_id": "req-legacy"})
)
- with patch("ventis.controller.local_controller_frontend.Thread", _SyncThread):
+ with patch("canyonos_core.controller.local_controller_frontend.Thread", _SyncThread):
LocalControllerServicer.Cleanup(servicer, request, context=None)
self.assertEqual(cleaned, [])
diff --git a/tests/test_local_controller_metrics.py b/tests/test_local_controller_metrics.py
index 6dbe332..2498999 100644
--- a/tests/test_local_controller_metrics.py
+++ b/tests/test_local_controller_metrics.py
@@ -13,12 +13,12 @@
0,
os.path.abspath(
os.path.join(
- os.path.dirname(__file__), "..", "ventis", "templates", "grpc_stubs"
+ os.path.dirname(__file__), "..", "canyonos_core", "templates", "grpc_stubs"
)
),
)
-from ventis.controller.local_controller import LocalController
+from canyonos_core.controller.local_controller import LocalController
def _bind_failure_marker(controller):
@@ -30,6 +30,11 @@ def _bind_failure_marker(controller):
controller, origin, future_id, result, failed, error_message
)
)
+ controller._fan_out_to_consumers = (
+ lambda future_id, result=None, failed=0, error_message="": LocalController._fan_out_to_consumers(
+ controller, future_id, result, failed, error_message
+ )
+ )
return controller
@@ -71,6 +76,9 @@ def set(self, key, value):
def get(self, key):
return self.strings.get(key)
+ def smembers(self, key):
+ return set()
+
class LocalControllerMetricsTests(unittest.TestCase):
def test_collect_metrics_returns_expected_keys(self):
@@ -79,7 +87,7 @@ def test_collect_metrics_returns_expected_keys(self):
_metrics_interval=5,
)
with patch(
- "ventis.controller.local_controller.read_gpu_percent", return_value=0.0
+ "canyonos_core.controller.local_controller.read_gpu_percent", return_value=0.0
):
metrics = LocalController._collect_metrics(controller)
self.assertEqual(metrics["status"], "healthy")
@@ -148,7 +156,7 @@ def test_execute_locally_writes_gpu_resource_to_future_hash(self):
))
with patch(
- "ventis.controller.local_controller.read_gpu_percent", return_value=17.5
+ "canyonos_core.controller.local_controller.read_gpu_percent", return_value=17.5
):
LocalController._execute_locally(
controller, "Greeter", "greet", {"name": "world"}, "future-1"
@@ -183,7 +191,7 @@ def boom(name):
))
with patch(
- "ventis.controller.local_controller.read_gpu_percent", return_value=0.0
+ "canyonos_core.controller.local_controller.read_gpu_percent", return_value=0.0
):
LocalController._execute_locally(
controller, "Greeter", "greet", {"name": "world"}, "future-2"
@@ -214,7 +222,7 @@ def test_execute_locally_marks_missing_agent_as_failed(self):
))
with patch(
- "ventis.controller.local_controller.read_gpu_percent", return_value=0.0
+ "canyonos_core.controller.local_controller.read_gpu_percent", return_value=0.0
):
LocalController._execute_locally(
controller, "MissingAgent", "greet", {}, "future-3"
@@ -245,7 +253,7 @@ def boom():
))
with patch(
- "ventis.controller.local_controller.read_gpu_percent", return_value=0.0
+ "canyonos_core.controller.local_controller.read_gpu_percent", return_value=0.0
):
LocalController._execute_locally(
controller,
@@ -290,7 +298,7 @@ def spy_send_result_callback(origin, future_id, result=None, failed=0, error_mes
)
with patch(
- "ventis.controller.local_controller.read_gpu_percent", return_value=0.0
+ "canyonos_core.controller.local_controller.read_gpu_percent", return_value=0.0
):
LocalController._execute_locally(
controller,
diff --git a/tests/test_otel_exporter_fanout.py b/tests/test_otel_exporter_fanout.py
index 59d4345..38d2cd4 100644
--- a/tests/test_otel_exporter_fanout.py
+++ b/tests/test_otel_exporter_fanout.py
@@ -1,4 +1,4 @@
-"""Focused tests for the Ventis OTel exporter fan-out configuration."""
+"""Focused tests for the CanyonOS OTel exporter fan-out configuration."""
import json
import os
@@ -13,7 +13,7 @@
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
# ``otel_exporter.py`` is also executed as a script from its own directory and
# therefore imports ``convert`` and ``db`` as top-level modules.
-sys.path.insert(0, os.path.join(ROOT, "ventis", "OTLP_Exporter"))
+sys.path.insert(0, os.path.join(ROOT, "canyonos_core", "OTLP_Exporter"))
import db # noqa: E402
import otel_exporter # noqa: E402
@@ -137,9 +137,9 @@ def test_configured_destinations_rejects_malformed_empty_and_duplicate_values(se
def test_controller_expands_env_in_destinations(self):
# NOTE: the pre-existing Basic-auth-header-injection expectation this test
# once carried was already unimplemented/failing before the Redis-backed
- # reload change (VENTIS_OTEL_DESTINATIONS -> otel:destinations); out of
+ # reload change (CANYONOS_OTEL_DESTINATIONS -> otel:destinations); out of
# scope here, so this only covers ${ENV_VAR} expansion, which does work.
- from ventis.controller.global_controller import GlobalController
+ from canyonos_core.controller.global_controller import GlobalController
with patch.dict(
os.environ,
@@ -164,7 +164,7 @@ def test_controller_expands_env_in_destinations(self):
)
def test_controller_destinations_is_none_when_otel_not_configured(self):
- from ventis.controller.global_controller import GlobalController
+ from canyonos_core.controller.global_controller import GlobalController
self.assertIsNone(GlobalController._otel_destinations({}))
diff --git a/tests/test_otel_exporter_fields.py b/tests/test_otel_exporter_fields.py
index f6c2074..7ca3a02 100644
--- a/tests/test_otel_exporter_fields.py
+++ b/tests/test_otel_exporter_fields.py
@@ -5,7 +5,7 @@
import unittest
from unittest.mock import patch
-from ventis.OTLP_Exporter import convert, db
+from canyonos_core.OTLP_Exporter import convert, db
class OTelExporterFieldTests(unittest.TestCase):
diff --git a/tests/test_redis_utils.py b/tests/test_redis_utils.py
index 789b04d..4e1ff56 100644
--- a/tests/test_redis_utils.py
+++ b/tests/test_redis_utils.py
@@ -5,12 +5,12 @@
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
-from ventis.controller.utils.redis_utils import _wait_for_redis
+from canyonos_core.controller.utils.redis_utils import _wait_for_redis
class WaitForRedisTests(unittest.TestCase):
- @patch("ventis.controller.utils.redis_utils.time.sleep")
- @patch("ventis.controller.utils.redis_utils.time.time", return_value=0)
+ @patch("canyonos_core.controller.utils.redis_utils.time.sleep")
+ @patch("canyonos_core.controller.utils.redis_utils.time.time", return_value=0)
def test_timeout_message_stays_the_same(self, mock_time, mock_sleep):
redis_client = MagicMock()
diff --git a/tests/test_runtime_ec2.py b/tests/test_runtime_ec2.py
index c9853db..ebeda8a 100644
--- a/tests/test_runtime_ec2.py
+++ b/tests/test_runtime_ec2.py
@@ -7,7 +7,7 @@
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
-from ventis.controller.cloud_provider_logic.EC2 import _runtime as ec2_runtime
+from canyonos_core.controller.cloud_provider_logic.EC2 import _runtime as ec2_runtime
class _FakeWaiter:
@@ -111,7 +111,7 @@ def test_aws_clients_fails_when_required_fields_are_missing(self):
def test_aws_clients_rejects_missing_ssh_private_key(self):
self.controller.config["ec2"]["ssh_private_key_path"] = (
- "/tmp/missing-ventis-key"
+ "/tmp/missing-canyonos-key"
)
with self.assertRaisesRegex(ValueError, "does not exist"):
@@ -139,7 +139,7 @@ def test_provision_uses_ec2_client(self):
self.assertNotIn("UserData", request)
self.assertEqual(
request["TagSpecifications"][0]["Tags"][0],
- {"Key": "Name", "Value": "ventis-Tagged-2"},
+ {"Key": "Name", "Value": "canyonos-Tagged-2"},
)
self.assertEqual(self.fake_client.waiter.calls, [["i-test1"]])
self.assertEqual(provisioned["host"], "10.0.0.30")
diff --git a/tests/test_session_logging.py b/tests/test_session_logging.py
index 55ccccc..5cec43d 100644
--- a/tests/test_session_logging.py
+++ b/tests/test_session_logging.py
@@ -9,7 +9,7 @@
from sqlalchemy import text
-import ventis.controller.utils.session_logging as session_logging
+import canyonos_core.controller.utils.session_logging as session_logging
def _stored_epoch(stored):
@@ -21,7 +21,7 @@ class SessionStoreTests(unittest.TestCase):
def setUp(self):
self.db = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
self.db.close()
- os.environ["VENTIS_DATABASE_URL"] = f"sqlite:///{self.db.name}"
+ os.environ["CANYONOS_DATABASE_URL"] = f"sqlite:///{self.db.name}"
session_logging._engine = None
# session_logging no longer bootstraps the schema itself (that's expected
# to already exist on the real database) -- tests create it directly.
diff --git a/tests/test_stub_generator.py b/tests/test_stub_generator.py
index 916bf80..d3af7f2 100644
--- a/tests/test_stub_generator.py
+++ b/tests/test_stub_generator.py
@@ -8,7 +8,7 @@
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
-from ventis.stub_generator import (
+from canyonos_core.stub_generator import (
BASE_AGENT_REQUIREMENTS,
BASE_WORKFLOW_REQUIREMENTS,
_stub_destination,
diff --git a/tests/test_telemetry_logging.py b/tests/test_telemetry_logging.py
index 89e753f..b321965 100644
--- a/tests/test_telemetry_logging.py
+++ b/tests/test_telemetry_logging.py
@@ -9,7 +9,7 @@
from sqlalchemy import create_engine, text
-import ventis.controller.utils.telemetry_logging as sqlmod
+import canyonos_core.controller.utils.telemetry_logging as sqlmod
def _parse_shifted(stored):
@@ -99,11 +99,11 @@ class RuntimeSqlalchemyTests(unittest.TestCase):
def setUp(self):
self.db = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
self.db.close()
- os.environ["VENTIS_DATABASE_URL"] = f"sqlite:///{self.db.name}"
+ os.environ["CANYONOS_DATABASE_URL"] = f"sqlite:///{self.db.name}"
# sqlmod no longer creates the schema itself (a separate service owns
# that in real deployments) -- create it here so tests still get a
# ready-to-use database, matching what that service is assumed to do.
- engine = create_engine(os.environ["VENTIS_DATABASE_URL"])
+ engine = create_engine(os.environ["CANYONOS_DATABASE_URL"])
with engine.begin() as conn:
conn.execute(_RUNTIME_CREATE_TABLE)
conn.execute(_AGENT_CREATE_TABLE)
@@ -410,20 +410,20 @@ def test_demo_cost_multipliers_scale_costs_independently_and_warn(self):
)
rows = sqlmod.pull_runtime_information(redis)
- os.environ["VENTIS_DEMO_TOKEN_COST_MULTIPLIER"] = "2"
- os.environ["VENTIS_DEMO_SERVER_COST_MULTIPLIER"] = "3"
+ os.environ["CANYONOS_DEMO_TOKEN_COST_MULTIPLIER"] = "2"
+ os.environ["CANYONOS_DEMO_SERVER_COST_MULTIPLIER"] = "3"
try:
- with self.assertLogs("ventis.controller.utils.telemetry_logging", level="WARNING") as cm:
+ with self.assertLogs("canyonos_core.controller.utils.telemetry_logging", level="WARNING") as cm:
sqlmod.send_runtime_information(rows, redis)
self.assertTrue(
- any("VENTIS_DEMO_TOKEN_COST_MULTIPLIER" in msg for msg in cm.output)
+ any("CANYONOS_DEMO_TOKEN_COST_MULTIPLIER" in msg for msg in cm.output)
)
self.assertTrue(
- any("VENTIS_DEMO_SERVER_COST_MULTIPLIER" in msg for msg in cm.output)
+ any("CANYONOS_DEMO_SERVER_COST_MULTIPLIER" in msg for msg in cm.output)
)
finally:
- del os.environ["VENTIS_DEMO_TOKEN_COST_MULTIPLIER"]
- del os.environ["VENTIS_DEMO_SERVER_COST_MULTIPLIER"]
+ del os.environ["CANYONOS_DEMO_TOKEN_COST_MULTIPLIER"]
+ del os.environ["CANYONOS_DEMO_SERVER_COST_MULTIPLIER"]
with sqlmod._get_engine("").connect() as conn:
row = conn.execute(
diff --git a/tests/test_ventis_context.py b/tests/test_ventis_context.py
deleted file mode 100644
index f860d1f..0000000
--- a/tests/test_ventis_context.py
+++ /dev/null
@@ -1,49 +0,0 @@
-import os
-import sys
-import unittest
-
-sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
-
-import ventis.controller.ventis_context as ventis_context
-
-
-class VentisContextTests(unittest.TestCase):
- def setUp(self):
- ventis_context._local = ventis_context.threading.local()
-
- def tearDown(self):
- ventis_context._local = ventis_context.threading.local()
-
- def test_request_id_defaults_to_empty_string(self):
- self.assertEqual(ventis_context.get_request_id(), "")
-
- def test_request_id_round_trips(self):
- ventis_context.set_request_id("req-123")
- self.assertEqual(ventis_context.get_request_id(), "req-123")
-
- def test_current_future_id_defaults_to_empty_string(self):
- self.assertEqual(ventis_context.get_current_future_id(), "")
-
- def test_current_future_id_round_trips(self):
- ventis_context.set_current_future_id("future-abc")
- self.assertEqual(ventis_context.get_current_future_id(), "future-abc")
-
- def test_request_id_and_future_id_are_independent(self):
- ventis_context.set_request_id("req-123")
- ventis_context.set_current_future_id("future-abc")
- self.assertEqual(ventis_context.get_request_id(), "req-123")
- self.assertEqual(ventis_context.get_current_future_id(), "future-abc")
-
- def test_current_metrics_key_defaults_to_empty_string(self):
- self.assertEqual(ventis_context.get_current_metrics_key(), "")
-
- def test_current_metrics_key_round_trips(self):
- ventis_context.set_current_metrics_key("controller:localhost:50051:metrics")
- self.assertEqual(
- ventis_context.get_current_metrics_key(),
- "controller:localhost:50051:metrics",
- )
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/uv.lock b/uv.lock
index 017bf9e..be54781 100644
--- a/uv.lock
+++ b/uv.lock
@@ -72,6 +72,57 @@ requires-dist = [
{ name = "ruamel-yaml" },
]
+[[package]]
+name = "canyonos-core"
+version = "0.1.0"
+source = { editable = "." }
+dependencies = [
+ { name = "boto3" },
+ { name = "flask" },
+ { name = "grpcio" },
+ { name = "grpcio-tools" },
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-exporter-otlp-proto-grpc" },
+ { name = "opentelemetry-exporter-otlp-proto-http" },
+ { name = "opentelemetry-sdk" },
+ { name = "psutil" },
+ { name = "psycopg", extra = ["binary"] },
+ { name = "pyyaml" },
+ { name = "redis" },
+ { name = "requests" },
+ { name = "sqlalchemy" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "canyonos" },
+ { name = "pytest" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "boto3" },
+ { name = "flask" },
+ { name = "grpcio" },
+ { name = "grpcio-tools" },
+ { name = "opentelemetry-api", specifier = ">=1.44.0" },
+ { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.44.0" },
+ { name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.44.0" },
+ { name = "opentelemetry-sdk", specifier = ">=1.44.0" },
+ { name = "psutil" },
+ { name = "psycopg", extras = ["binary"] },
+ { name = "pyyaml" },
+ { name = "redis" },
+ { name = "requests" },
+ { name = "sqlalchemy" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "canyonos", editable = "cli" },
+ { name = "pytest", specifier = ">=9.1.1" },
+]
+
[[package]]
name = "certifi"
version = "2026.7.22"
@@ -1212,57 +1263,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
]
-[[package]]
-name = "ventis"
-version = "0.1.0"
-source = { editable = "." }
-dependencies = [
- { name = "boto3" },
- { name = "flask" },
- { name = "grpcio" },
- { name = "grpcio-tools" },
- { name = "opentelemetry-api" },
- { name = "opentelemetry-exporter-otlp-proto-grpc" },
- { name = "opentelemetry-exporter-otlp-proto-http" },
- { name = "opentelemetry-sdk" },
- { name = "psutil" },
- { name = "psycopg", extra = ["binary"] },
- { name = "pyyaml" },
- { name = "redis" },
- { name = "requests" },
- { name = "sqlalchemy" },
-]
-
-[package.dev-dependencies]
-dev = [
- { name = "canyonos" },
- { name = "pytest" },
-]
-
-[package.metadata]
-requires-dist = [
- { name = "boto3" },
- { name = "flask" },
- { name = "grpcio" },
- { name = "grpcio-tools" },
- { name = "opentelemetry-api", specifier = ">=1.44.0" },
- { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.44.0" },
- { name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.44.0" },
- { name = "opentelemetry-sdk", specifier = ">=1.44.0" },
- { name = "psutil" },
- { name = "psycopg", extras = ["binary"] },
- { name = "pyyaml" },
- { name = "redis" },
- { name = "requests" },
- { name = "sqlalchemy" },
-]
-
-[package.metadata.requires-dev]
-dev = [
- { name = "canyonos", editable = "cli" },
- { name = "pytest", specifier = ">=9.1.1" },
-]
-
[[package]]
name = "werkzeug"
version = "3.1.8"
diff --git a/ventis/__init__.py b/ventis/__init__.py
deleted file mode 100644
index d33ecaa..0000000
--- a/ventis/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-# Ventis - Distributed Agent Framework
-__version__ = "0.1.0"
diff --git a/ventis/controller/__init__.py b/ventis/controller/__init__.py
deleted file mode 100644
index a124ecf..0000000
--- a/ventis/controller/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# Ventis Controller Sub-Package
diff --git a/ventis/controller/utils/__init__.py b/ventis/controller/utils/__init__.py
deleted file mode 100644
index 3db8269..0000000
--- a/ventis/controller/utils/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# Ventis Controller Utility helpers