Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions deploy/install/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ This directory holds the Core/Web installer, the `oac` command and the node inst
## Configuration and apply

- Every process setting has one home: the installation's private `config.json`, described by `config.schema.json`. Keep the schema, `config_model.py`, the generator and the reference tables that `scripts/config-reference.py` renders into [configuration](../../docs/configuration.md) and [installation options](../../docs/getting-started/install-options.md) in step.
- `make generate-harness-catalog` projects the [built-in Harness catalog](../../internal/harnessconfig/builtin/catalog.json) and adapter-owned Go provider declarations into `harness_catalog.py`. `make check-harness-catalog` checks its freshness, schema enum membership and process-default membership. `acceptance.py` uses this projection to validate its explicitly selected campaign combinations and token-limit requirements; adding catalog support does not expand the live acceptance campaign. These acceptance modules run from the checkout; installed configuration and `oac.pyz` use the checked schema.
- Installation flags only seed `config.json`. A rerun of the installer over a complete installation accepts only `--install-dir` and repairs.
- `oac apply` validates `config.json`, derives `generated/` and converges on what actually runs. Each service carries the digest of its inputs (Compose label `io.oac.inputs`, native `OAC_INPUTS`), and exactly the services whose running inputs differ are recreated or restarted. Decide restarts from what runs, never from recorded bookkeeping, so the next apply finishes an interrupted one. Apply contacts running services at the last applied address before changing listeners.
- Health checks, setup, apply and generated service files derive addresses from the same `config.json`. `host` selects the gateway listener for managed ingress and the Core and Web listeners for external ingress. A non-loopback external bind requires an HTTPS public origin.
Expand Down
20 changes: 13 additions & 7 deletions deploy/install/acceptance.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@
from urllib.request import HTTPRedirectHandler, ProxyHandler, Request, build_opener
import uuid

HARNESSES = ("codex", "claude_sdk", "mcode")
from harness_catalog import PROVIDERS

# This campaign deliberately qualifies only these combinations. Catalog additions
# do not expand live model calls; supported combinations are declared in PROVIDERS.
CAMPAIGN = {"codex": "responses", "claude_sdk": "anthropic", "mcode": "anthropic"}
TERMINAL = {"completed", "cancelled", "failed"}
MAX_RESPONSE = 8 * 1024 * 1024

Expand Down Expand Up @@ -87,10 +91,12 @@ def validate_origin(raw, model=False):


def settings(args):
require(CAMPAIGN and all(harness in PROVIDERS and protocol in PROVIDERS[harness]
for harness, protocol in CAMPAIGN.items()), "unsupported_acceptance_campaign")
token = private_file(args.caller_key_file).strip()
require(token and not any(c in token for c in "\r\n\0"), "invalid_caller_key")
models = json.loads(private_file(args.model_config_file))
require(isinstance(models, dict) and set(models) == set(HARNESSES), "require_all_three_harness_configs")
require(isinstance(models, dict) and set(models) == set(CAMPAIGN), "require_all_three_harness_configs")
secrets = [token]
for harness, entry in models.items():
require(isinstance(entry, dict) and set(entry) == {"model", "model_provider"}, "invalid_model_config_fields")
Expand All @@ -101,17 +107,17 @@ def settings(args):
require(set(provider) <= allowed and {"protocol", "base_url", "api_key"} <= set(provider),
"invalid_model_provider_fields")
validate_origin(provider["base_url"], model=True)
protocol = "responses" if harness == "codex" else "anthropic"
require(provider["protocol"] == protocol, "provider_protocol_does_not_match_harness")
protocol = CAMPAIGN[harness]
require(provider["protocol"] == protocol, "provider_protocol_does_not_match_campaign")
key = provider["api_key"]
require(isinstance(key, str) and key and len(key) <= 16384
and not any(c in key for c in "\r\n\0"), "invalid_model_provider_key")
for name in ("context_window", "max_output_tokens"):
require(type(provider.get(name, 0)) is int and provider.get(name, 0) >= 0,
"invalid_model_limits")
if harness == "mcode":
if PROVIDERS[harness][protocol]["requires_token_limits"]:
require(provider.get("context_window", 0) > 0 and provider.get("max_output_tokens", 0) > 0,
"mcode_requires_positive_model_limits")
"harness_requires_positive_model_limits")
if provider.get("context_window") and provider.get("max_output_tokens"):
require(provider["max_output_tokens"] <= provider["context_window"], "invalid_model_limits")
secrets.append(key)
Expand Down Expand Up @@ -404,7 +410,7 @@ def main():
args.report_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
require(args.report_dir.stat().st_mode & 0o077 == 0, "report_directory_must_be_private")
failed = False
for harness in HARNESSES:
for harness in CAMPAIGN:
location = args.report_dir / (harness + ".json")
record = json.loads(private_file(location)) if location.exists() else {"harness": harness, "stages": {}}
require(record.get("harness") == harness, "report_harness_mismatch")
Expand Down
7 changes: 7 additions & 0 deletions deploy/install/harness_catalog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Code generated by scripts/generate-harness-catalog.py; DO NOT EDIT.
# Provider support comes from the adapter-owned Go configuration declarations.
PROVIDERS = {'claude_sdk': {'anthropic': {'requires_token_limits': False}},
'codex': {'responses': {'requires_token_limits': False}},
'mcode': {'anthropic': {'requires_token_limits': True},
'chat_completions': {'requires_token_limits': True},
'responses': {'requires_token_limits': True}}}
106 changes: 106 additions & 0 deletions deploy/install/test_acceptance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Validate acceptance inputs without contacting Core or model providers."""
import copy
import json
from types import SimpleNamespace
import unittest
from unittest import mock

import acceptance


class AcceptanceSettingsTests(unittest.TestCase):
def setUp(self):
self.models = {
harness: {"model": "fixture", "model_provider": {
"protocol": protocol, "base_url": "https://model.example/v1", "api_key": "private-key",
"context_window": 64000, "max_output_tokens": 4096,
}} for harness, protocol in acceptance.CAMPAIGN.items()
}

def settings(self, models=None):
models = self.models if models is None else models
with mock.patch.object(acceptance, "private_file", side_effect=["caller-key", json.dumps(models)]):
return acceptance.settings(SimpleNamespace(caller_key_file=None, model_config_file=None))

def test_campaign_is_explicit_and_valid(self):
self.assertEqual(acceptance.CAMPAIGN, {
"codex": "responses", "claude_sdk": "anthropic", "mcode": "anthropic"})
token, models, secrets = self.settings()
self.assertEqual(token, "caller-key")
self.assertEqual(models, self.models)
self.assertEqual(secrets, ["caller-key"] + ["private-key"] * 3)

def test_catalog_support_does_not_expand_campaign(self):
for protocol in ("responses", "chat_completions"):
self.assertIn(protocol, acceptance.PROVIDERS["mcode"])
self.models["mcode"]["model_provider"]["protocol"] = protocol
with self.subTest(protocol=protocol), self.assertRaisesRegex(
acceptance.Failure, "provider_protocol_does_not_match_campaign"):
self.settings()
expanded = {**acceptance.PROVIDERS, "additional": {"responses": {"requires_token_limits": False}}}
with mock.patch.object(acceptance, "PROVIDERS", expanded):
self.models["mcode"]["model_provider"]["protocol"] = "anthropic"
self.settings()
self.models["additional"] = copy.deepcopy(self.models["codex"])
with self.assertRaisesRegex(acceptance.Failure, "require_all_three_harness_configs"):
self.settings()

def test_unsupported_campaign_is_rejected_before_reading_secrets(self):
for campaign in ({}, {"unknown": "responses"}, {"codex": "anthropic"}):
with self.subTest(campaign=campaign), mock.patch.object(acceptance, "CAMPAIGN", campaign), \
mock.patch.object(acceptance, "private_file") as private_file:
with self.assertRaisesRegex(acceptance.Failure, "unsupported_acceptance_campaign"):
acceptance.settings(SimpleNamespace())
private_file.assert_not_called()

def test_token_limit_requirements_follow_declarations(self):
for harness in acceptance.CAMPAIGN:
models = copy.deepcopy(self.models)
del models[harness]["model_provider"]["context_window"]
del models[harness]["model_provider"]["max_output_tokens"]
if harness == "mcode":
with self.assertRaisesRegex(acceptance.Failure, "harness_requires_positive_model_limits"):
self.settings(models)
else:
self.settings(models)
providers = copy.deepcopy(acceptance.PROVIDERS)
providers["codex"]["responses"]["requires_token_limits"] = True
del self.models["codex"]["model_provider"]["context_window"]
with mock.patch.object(acceptance, "PROVIDERS", providers):
with self.assertRaisesRegex(acceptance.Failure, "harness_requires_positive_model_limits"):
self.settings()

def test_model_limit_validation(self):
for field in ("context_window", "max_output_tokens"):
for value in (False, -1, "100", 1.5, None):
models = copy.deepcopy(self.models)
models["codex"]["model_provider"][field] = value
with self.subTest(field=field, value=value), self.assertRaisesRegex(
acceptance.Failure, "invalid_model_limits"):
self.settings(models)
for field in ("context_window", "max_output_tokens"):
models = copy.deepcopy(self.models)
models["mcode"]["model_provider"][field] = 0
with self.assertRaisesRegex(acceptance.Failure, "harness_requires_positive_model_limits"):
self.settings(models)
self.models["codex"]["model_provider"]["max_output_tokens"] = 64001
with self.assertRaisesRegex(acceptance.Failure, "invalid_model_limits"):
self.settings()

def test_provider_validation_is_preserved(self):
for patch, label in (
({"unknown": "value"}, "invalid_model_provider_fields"),
({"api_key": "secret\nvalue"}, "invalid_model_provider_key"),
({"api_key": ""}, "invalid_model_provider_key"),
({"base_url": "http://model.example"}, "model_origin_requires_https"),
({"base_url": "https://api.openai.com"}, "official_openai_origin_refused"),
({"protocol": "anthropic"}, "provider_protocol_does_not_match_campaign"),
):
models = copy.deepcopy(self.models)
models["codex"]["model_provider"].update(patch)
with self.subTest(label=label), self.assertRaisesRegex(acceptance.Failure, label):
self.settings(models)


if __name__ == "__main__":
unittest.main()
35 changes: 34 additions & 1 deletion scripts/generate-harness-catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pathlib import Path
import re
import subprocess
from pprint import pformat

ROOT = Path(__file__).resolve().parent.parent
CATALOG = Path("internal/harnessconfig/builtin/catalog.json")
Expand Down Expand Up @@ -40,6 +41,25 @@ def go(source):
check=True, capture_output=True).stdout


def check_installer_schema(entries, schema):
"""Process defaults remain authored in the schema; membership is catalog-owned."""
kinds = [entry["kind"] for entry in entries]
core = schema["properties"]["core"]["properties"]
enabled, default = core["harnesses"], core["default_harness"]
if enabled["items"]["enum"] != kinds or default["enum"] != kinds:
raise ValueError("Installer Harness enums must match internal/harnessconfig/builtin/catalog.json")
if (not enabled["default"] or len(set(enabled["default"])) != len(enabled["default"])
or not set(enabled["default"]) <= set(kinds)
or default["default"] not in enabled["default"]):
raise ValueError("Installer Harness defaults must select registered, enabled Harnesses")


def render_installer(providers):
return ("# Code generated by scripts/generate-harness-catalog.py; DO NOT EDIT.\n"
"# Provider support comes from the adapter-owned Go configuration declarations.\n"
"PROVIDERS = " + pformat(providers, sort_dicts=True, width=100) + "\n")


def render(entries):
packages = sorted({entry["configuration"] for entry in entries})
imports = "\n".join(f'\t"{MODULE}/internal/harnessconfig/{package}"' for package in packages)
Expand Down Expand Up @@ -114,14 +134,27 @@ def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true")
args = parser.parse_args()
entries = load_catalog(ROOT / CATALOG)
check_installer_schema(entries, json.loads((ROOT / "deploy/install/config.schema.json").read_text()))
stale = []
for relative, content in render(load_catalog(ROOT / CATALOG)).items():

def projection(relative, content):
path = ROOT / relative
if args.check:
if not path.exists() or path.read_text() != content:
stale.append(str(relative))
else:
path.write_text(content)

for relative, content in render(entries).items():
projection(relative, content)
# Refresh the Go registration before loading the adapter declarations. In check
# mode a stale registration must fail before its declarations can be projected.
if stale:
raise SystemExit("Stale Harness catalog projections; run make generate-harness-catalog:\n" + "\n".join(stale))
providers = json.loads(subprocess.run(["go", "run", "./scripts/harness-catalog"], cwd=ROOT,
text=True, check=True, capture_output=True).stdout)
projection(Path("deploy/install/harness_catalog.py"), render_installer(providers))
if stale:
raise SystemExit("Stale Harness catalog projections; run make generate-harness-catalog:\n" + "\n".join(stale))

Expand Down
29 changes: 29 additions & 0 deletions scripts/generate-harness-catalog.test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"""Verify catalog extension and generated contract freshness without live models."""

import importlib.util
import copy
import json
from pathlib import Path
import tempfile
Expand All @@ -21,6 +22,34 @@ def module(name, file):


class HarnessCatalogTests(unittest.TestCase):
def test_installer_schema_membership_and_defaults(self):
entries = catalog.load_catalog(catalog.ROOT / catalog.CATALOG)
document = json.loads((catalog.ROOT / "deploy/install/config.schema.json").read_text())
catalog.check_installer_schema(entries, document)
for key, field, value in (
("harnesses", "items", {"enum": ["unknown"]}),
("default_harness", "enum", ["unknown"]),
("harnesses", "default", ["unknown"]),
("harnesses", "default", []),
("harnesses", "default", ["codex", "codex"]),
("harnesses", "default", ["claude_sdk"]),
("default_harness", "default", "unknown"),
):
changed = copy.deepcopy(document)
changed["properties"]["core"]["properties"][key][field] = value
with self.subTest(key=key, field=field, value=value), self.assertRaises(ValueError):
catalog.check_installer_schema(entries, changed)
extended = [*entries, {"kind": "additional"}]
with self.assertRaises(ValueError):
catalog.check_installer_schema(extended, document)

def test_installer_projection_includes_every_declaration(self):
providers = {"additional": {"responses": {"requires_token_limits": True},
"anthropic": {"requires_token_limits": False}}, "native": {}}
namespace = {}
exec(catalog.render_installer(providers), namespace)
self.assertEqual(namespace["PROVIDERS"], providers)

def test_new_registration_reaches_all_projections(self):
entries = [{"kind": "example", "label": "Example",
"configuration": "example", "profile": "exampleProfile"}]
Expand Down
31 changes: 31 additions & 0 deletions scripts/harness-catalog/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Command harness-catalog projects adapter-owned provider declarations for tooling.
package main

import (
"encoding/json"
"os"

"github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/builtin"
)

type provider struct {
RequiresTokenLimits bool `json:"requires_token_limits"`
}

func declarations() map[string]map[string]provider {
result := make(map[string]map[string]provider)
for _, kind := range builtin.Kinds() {
providers := make(map[string]provider)
for _, declaration := range builtin.Configuration(kind).Providers {
providers[declaration.Protocol] = provider{RequiresTokenLimits: declaration.RequiresTokenLimits}
}
result[kind] = providers
}
return result
}

func main() {
if err := json.NewEncoder(os.Stdout).Encode(declarations()); err != nil {
panic(err)
}
}
27 changes: 27 additions & 0 deletions scripts/harness-catalog/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package main

import (
"testing"

"github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/builtin"
)

func TestProjectionMatchesEveryAdapterDeclaration(t *testing.T) {
got := declarations()
if len(got) != len(builtin.Kinds()) {
t.Fatal("projection membership differs from catalog")
}
for _, kind := range builtin.Kinds() {
configuration := builtin.Configuration(kind)
providers, ok := got[kind]
if !ok || len(providers) != len(configuration.Providers) {
t.Fatalf("%s: missing provider declarations", kind)
}
for _, declaration := range configuration.Providers {
projected, ok := providers[declaration.Protocol]
if !ok || projected.RequiresTokenLimits != declaration.RequiresTokenLimits {
t.Fatalf("%s/%s: configuration rule differs", kind, declaration.Protocol)
}
}
}
}
Loading