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: 0 additions & 1 deletion deploy/distribution/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ COPY e2b/ /opt/oac/e2b/
COPY native-installers/ /opt/oac/native-installers/

ENV OAC_ADDR=:8091
ENV OAC_E2B_PROVIDER_BIN=/opt/oac/e2b/oac-e2b-provider
EXPOSE 8091
USER 65532:65532
CMD ["/usr/local/bin/oac-core"]
5 changes: 2 additions & 3 deletions deploy/install/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,14 +207,13 @@ def core_environment(root, config, state):
"OAC_CORE_KEY_DIGESTS_FILE": generated + "/core-key-digests.json",
"OAC_INSTALLATION_ID": state["installation_id"],
"OAC_SETTINGS_FILE": generated + "/settings.json",
"OAC_E2B_STATE_DIR": str(root / "state/e2b") if native else "/state/e2b",
"OAC_PROVIDER_ROOT": str(root / "native") if native else "/opt/oac",
"OAC_PROVIDER_STATE_ROOT": str(root / "state") if native else "/state",
"OAC_DEFAULT_HARNESS": core["default_harness"],
"OAC_HARNESSES": ",".join(core["harnesses"]),
"OAC_EXECUTION_CONCURRENCY": str(core["execution_concurrency"]),
"OAC_WRITE_AUDIT_RETENTION": core["write_audit_retention"],
}
if native:
result["OAC_E2B_PROVIDER_BIN"] = str(root / "native/e2b/oac-e2b-provider")
if (root / "native-installers/catalog.json").is_file():
result["OAC_NATIVE_INSTALLER_DIR"] = str(root / "native-installers") if native else "/opt/oac/native-installers"
if core["oauth_trusted_origins"]:
Expand Down
8 changes: 4 additions & 4 deletions deploy/install/node_generations.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def helper_archive(args, installer):
with tempfile.TemporaryDirectory() as directory:
package = Path(directory)
source_dir = Path(installer.__file__).parent
for name in ("node_install.py", "node_spec.py", "distribution.py", "node_generations.py", "install_display.py", "node_output.py"):
for name in ("node_install.py", "node_spec.py", "distribution.py", "node_generations.py", "install_display.py", "node_output.py", "provider_assets.py"):
shutil.copyfile(source_dir / name, package / ("__main__.py" if name == "node_install.py" else name))
archive = io.BytesIO()
zipapp.create_archive(package, archive, compressed=True)
Expand Down Expand Up @@ -124,8 +124,8 @@ def marker_identity(args):

def record_root_runtime(root, args, manifest, sums, installer):
checksums = {"runtime/seccomp.json": sums["runtime/seccomp.json"]}
if args.provider == "microsandbox":
checksums.update({name: installer.distribution.artifact(manifest, name)["sha256"] for name in installer.MICRO})
checksums.update({name: installer.distribution.artifact(manifest, name)["sha256"]
for name in installer.provider_assets.artifacts(args.provider, ("runtime",))})
archive = installer.distribution.artifact(manifest, "images/runtime.tar.gz")
checksums["images/runtime.tar.gz"] = archive["sha256"]
checksums["images/runtime.tar"] = archive["unpacked_sha256"]
Expand Down Expand Up @@ -330,7 +330,7 @@ def runtime_files(root, value, args, manifest, sums, installer):
raise installer.InstallError("Retained Runtime artifacts are outside this installation")
installer.no_links(release)
installer.safe_directory(release)
names = ("runtime/seccomp.json",) + (installer.MICRO if args.provider == "microsandbox" else ())
names = installer.provider_assets.artifacts(args.provider, ("policy", "runtime"))
saved = installer.private_json(release / "manifest.json")
if (release / "manifest.json").exists():
if saved is None:
Expand Down
15 changes: 7 additions & 8 deletions deploy/install/node_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import uuid

import distribution
import provider_assets
import node_spec
import node_generations
import install_display
Expand All @@ -47,9 +48,7 @@ class RuntimeDownloadError(InstallError):
"""A fixed private helper exit category for transfer/provenance failures."""


COMMON = ("native/bin/oac-node", "runtime/seccomp.json")
MICRO = ("native/bin/oac-microsandbox-provider", "native/microsandbox/msb",
"native/microsandbox/libkrunfw.so.5.6.1")
MICRO = provider_assets.artifacts("microsandbox", ("runtime",))
DOCKER = ("docker", "--host", "unix:///var/run/docker.sock")
DOCKER_SOCKET = Path("/var/run/docker.sock")
KVM = Path("/dev/kvm")
Expand Down Expand Up @@ -211,7 +210,7 @@ def read(name):
or not re.fullmatch(r"sha256:[0-9a-f]{64}", manifest.get("images", {}).get("runtime", ""))):
raise RuntimeDownloadError("Unsupported node distribution")
distribution.image_identities(manifest, "runtime")
for name in (COMMON[0], "images/runtime.tar.gz") + MICRO:
for name in dict.fromkeys(item["path"] for items in provider_assets.CATALOG.values() for item in items if item["role"] != "policy"):
distribution.artifact(manifest, name)
# Metadata stays on the console; artifact requests may redirect to its pinned release.
manifest["artifact_base_url"] = source + "/node-install/releases/" + manifest["source_commit"] + "/artifacts" if source else ""
Expand Down Expand Up @@ -392,7 +391,7 @@ def register_node(root, args, token, helper_archive=None):
manifest, sums = metadata(args.source_url, prefix="releases/" + selected["source_commit"] + "/")
node_spec.verify_release(args.configuration, manifest)
runtime_prefix = "releases/" + manifest["source_commit"] + "/"
names = COMMON + (MICRO if args.provider == "microsandbox" else ())
names = provider_assets.artifacts(args.provider, ("node", "runtime", "policy"))
if "runtime/seccomp.json" not in sums:
raise InstallError("The distribution is missing required node checksums")
state = {"installation_id": args.installation_id, "provider": args.provider, "core_url": args.core_url,
Expand All @@ -414,7 +413,7 @@ def register_node(root, args, token, helper_archive=None):
target = root / name
safe_directory(target.parent)
existing_file(target)
distribution.obtain_artifact(program_manifest if name == COMMON[0] else manifest, name, target, getattr(args, "bundle", None))
distribution.obtain_artifact(program_manifest if name in provider_assets.artifacts(args.provider, ("node",)) else manifest, name, target, getattr(args, "bundle", None))
os.chmod(target, 0o700)
node_generations.install_helper(root, args, sys.modules[__name__], helper_archive)
safe_directory(root / "state/node")
Expand All @@ -439,7 +438,7 @@ def register_node(root, args, token, helper_archive=None):
secret.write(token)
install_display.step("Registering this node with Core")
try:
checked([str(root / COMMON[0]), "register", "--config", str(root / "provider.json"), "--state-dir", str(root / "state/node"),
checked([str(root / provider_assets.artifacts(args.provider, ("node",))[0]), "register", "--config", str(root / "provider.json"), "--state-dir", str(root / "state/node"),
"--core-url", args.core_url, "--name", socket.gethostname(),
"--enrollment-token-file", secret_path], REGISTRATION_UNCONFIRMED, explain=registration_failure)
except AddressChanged:
Expand Down Expand Up @@ -948,7 +947,7 @@ def quote(value):
# service runs with the account's own primary group.
return ("[Unit]\nDescription=OpenAgentCore sandbox node " + root.name + "\nWants=network-online.target\nAfter=" + after
+ "\nStartLimitIntervalSec=0\n\n[Service]\nType=exec\nUser=" + SERVICE_USER
+ "\nExecStart=:" + quote(root / COMMON[0]) + " run --config " + quote(root / "provider.json")
+ "\nExecStart=:" + quote(root / provider_assets.artifacts(provider, ("node",))[0]) + " run --config " + quote(root / "provider.json")
+ " --state-dir " + quote(root / "state/node") + "\nWorkingDirectory=" + str(root).replace("%", "%%")
+ "\nRestart=on-failure\nRestartSec=5s\nRestartPreventExitStatus=78\nKillMode=process\nUMask=0077"
+ "\n\n[Install]\nWantedBy=multi-user.target\n")
Expand Down
7 changes: 7 additions & 0 deletions deploy/install/provider_assets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Code generated by go run ./services/core/cmd/provider-artifacts -write; DO NOT EDIT.
import json

CATALOG = json.loads("{\n \"docker\": [\n {\n \"path\": \"native/bin/oac-node\",\n \"suffix\": \"sandbox-node\",\n \"role\": \"node\"\n },\n {\n \"path\": \"images/runtime.tar.gz\",\n \"suffix\": \"runtime.tar.gz\",\n \"role\": \"image\"\n },\n {\n \"path\": \"runtime/seccomp.json\",\n \"suffix\": \"seccomp.json\",\n \"role\": \"policy\"\n }\n ],\n \"microsandbox\": [\n {\n \"path\": \"native/bin/oac-node\",\n \"suffix\": \"sandbox-node\",\n \"role\": \"node\"\n },\n {\n \"path\": \"images/runtime.tar.gz\",\n \"suffix\": \"runtime.tar.gz\",\n \"role\": \"image\"\n },\n {\n \"path\": \"runtime/seccomp.json\",\n \"suffix\": \"seccomp.json\",\n \"role\": \"policy\"\n },\n {\n \"path\": \"native/bin/oac-microsandbox-provider\",\n \"suffix\": \"microsandbox-provider\",\n \"role\": \"runtime\"\n },\n {\n \"path\": \"native/microsandbox/msb\",\n \"suffix\": \"msb\",\n \"role\": \"runtime\"\n },\n {\n \"path\": \"native/microsandbox/libkrunfw.so.5.6.1\",\n \"suffix\": \"libkrunfw.so.5.6.1\",\n \"role\": \"runtime\"\n }\n ]\n}\n")

def artifacts(provider, roles=None):
return tuple(item["path"] for item in CATALOG[provider] if roles is None or item["role"] in roles)
11 changes: 11 additions & 0 deletions deploy/install/test_config_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ def schemas(node):


class ConfigModelTests(unittest.TestCase):
def test_provider_roots_follow_the_installed_layout(self):
root = Path("/installation")
for native, artifacts, state in ((False, "/opt/oac", "/state"),
(True, "/installation/native", "/installation/state")):
with self.subTest(native=native):
config = config_model.initial("all", native, **({"ports.database": 15432} if native else {}))
environment = configuration.core_environment(root, config, {"installation_id": "fixture"})
self.assertEqual(environment["OAC_PROVIDER_ROOT"], artifacts)
self.assertEqual(environment["OAC_PROVIDER_STATE_ROOT"], state)


def test_schema_uses_only_the_supported_keyword_subset(self):
for node in schemas(config_model.SCHEMA):
self.assertLessEqual(set(node), config_model.KEYWORDS)
Expand Down
2 changes: 1 addition & 1 deletion deploy/install/test_node_helper_transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def test_captured_archive_survives_removal_of_private_source(self):
self.assertEqual(helper.read_bytes(), captured)
self.assertEqual(stat.S_IMODE(helper.stat().st_mode), 0o600)
with zipfile.ZipFile(helper) as archive:
self.assertEqual(set(archive.namelist()), {"__main__.py", "node_spec.py", "distribution.py", "node_generations.py", "install_display.py", "node_output.py"})
self.assertEqual(set(archive.namelist()), {"__main__.py", "node_spec.py", "distribution.py", "node_generations.py", "install_display.py", "node_output.py", "provider_assets.py"})
self.assertEqual(json.loads((root / "preparation.json").read_text()), {"source_url": "https://core.example"})

@unittest.skipUnless(hasattr(os, "fork") and os.geteuid() == 0, "requires a disposable Linux root test environment")
Expand Down
22 changes: 11 additions & 11 deletions deploy/install/test_node_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def setUp(self):
"image_manifest_digests": {"runtime": "sha256:" + "c" * 64},
"runtime_ref": "oac-runtime@sha256:" + "c" * 64,
"microsandbox": {"runtime_sha256": "d" * 64, "firmware_sha256": "e" * 64}}
self.payloads = {name: b"fixture-payload-" + name.encode() for name in installer.COMMON + installer.MICRO}
self.payloads = {name: b"fixture-payload-" + name.encode() for name in installer.provider_assets.artifacts("microsandbox", ("node", "policy", "runtime"))}
self.payloads["images/runtime.tar.gz"] = gzip.compress(b"runtime archive")
self.refresh_manifest()
self.containerd = False
Expand Down Expand Up @@ -169,7 +169,7 @@ def install_current_program_with_retained_runtime(self, provider):
program = copy.deepcopy(self.manifest)
program["source_commit"] = new_source
payloads = dict(self.payloads)
payloads[installer.COMMON[0]] = b"new protocol program from current release"
payloads[installer.provider_assets.artifacts("docker", ("node",))[0]] = b"new protocol program from current release"
for name, artifact in program["artifacts"].items():
artifact["filename"] = artifact["filename"].replace(old_source, new_source)
artifact["size"] = len(payloads[name])
Expand All @@ -191,12 +191,12 @@ def artifact_response(request, **_kwargs):
for name, item in manifest["artifacts"].items():
if url == prefix + item["filename"]:
# Only the node executable comes from the host release.
self.assertEqual(manifest["source_commit"], new_source if name == installer.COMMON[0] else old_source)
self.assertEqual(manifest["source_commit"], new_source if name == installer.provider_assets.artifacts("docker", ("node",))[0] else old_source)
return Response(files[name])
raise AssertionError("Unexpected immutable artifact URL " + url)
with mock.patch.object(installer, "fetch", side_effect=fetch), mock.patch.object(installer.distribution.urllib.request, "build_opener", return_value=mock.Mock(open=artifact_response)):
self.install()
self.assertEqual((self.root / installer.COMMON[0]).read_bytes(), payloads[installer.COMMON[0]])
self.assertEqual((self.root / installer.provider_assets.artifacts("docker", ("node",))[0]).read_bytes(), payloads[installer.provider_assets.artifacts("docker", ("node",))[0]])
config = json.loads((self.root / "provider.json").read_text())
self.assertEqual(config["specification"]["runtime"], node_spec.release(self.manifest))
self.assertEqual(json.loads((self.root / "registered.json").read_text())["source_commit"], old_source)
Expand All @@ -220,7 +220,7 @@ def test_bundle_without_selected_runtime_refuses_before_payload_or_registration(
with self.assertRaisesRegex(installer.InstallError, "does not contain Core's selected Runtime"):
self.install()
self.assertFalse((self.root / "installation.json").exists())
self.assertFalse((self.root / installer.COMMON[0]).exists())
self.assertFalse((self.root / installer.provider_assets.artifacts("docker", ("node",))[0]).exists())
self.assertFalse(any("register" in command or "load" in command or "enable" in command for command, _ in self.calls))

def test_missing_retained_runtime_never_falls_back_to_current_runtime(self):
Expand Down Expand Up @@ -253,7 +253,7 @@ def test_original_runtime_gc_interruption_preserves_restart_identity_and_success
self.args.provider = "microsandbox"
self.install()
successor = self.prepare_successor_runtime()
keep = (installer.COMMON[0], "generation-preparer.pyz", "provider.json", "registered.json",
keep = (installer.provider_assets.artifacts("docker", ("node",))[0], "generation-preparer.pyz", "provider.json", "registered.json",
"installation.json", "preparation.json", "runtime-artifacts.json", "state/node/identity.json")
before = {name: (self.root / name).read_bytes() for name in keep}
original = installer.private_json(self.root / "provider.json")
Expand Down Expand Up @@ -337,7 +337,7 @@ def inventory(command, *_args, **_kwargs):
installer.node_generations.collect(self.args, installer)
self.assertFalse((self.root / installer.MICRO[2]).exists())
self.assertTrue((self.root / "provider.json").exists())
self.assertTrue((self.root / installer.COMMON[0]).exists())
self.assertTrue((self.root / installer.provider_assets.artifacts("docker", ("node",))[0]).exists())

def test_original_runtime_gc_preserves_exact_shared_native_file_references(self):
self.args.provider = "microsandbox"
Expand Down Expand Up @@ -528,7 +528,7 @@ def test_changed_public_url_clears_unregistered_state_for_a_new_command(self):
self.install()
for name in ("installation.json", "provider.json", "state/node/identity.json", "registered.json"):
self.assertFalse((self.root / name).exists(), name)
self.assertTrue((self.root / installer.COMMON[0]).is_file())
self.assertTrue((self.root / installer.provider_assets.artifacts("docker", ("node",))[0]).is_file())
self.register_stderr = None
self.args.core_url = "https://core-new.example"
self.install()
Expand Down Expand Up @@ -566,15 +566,15 @@ def test_corrupt_manifest_and_payload_fail_before_provider_use(self):
with self.assertRaisesRegex(installer.InstallError, "manifest checksum"):
self.install()
self.refresh_manifest()
self.payloads[installer.COMMON[0]] += b"corrupt"
self.payloads[installer.provider_assets.artifacts("docker", ("node",))[0]] += b"corrupt"
with self.assertRaisesRegex(installer.distribution.DistributionError, "published size|checksum"):
self.install()
self.assertFalse(self.calls)
self.assertFalse((self.root / installer.COMMON[0]).exists())
self.assertFalse((self.root / installer.provider_assets.artifacts("docker", ("node",))[0]).exists())

def test_installed_payload_and_config_are_not_overwritten(self):
self.install()
target = self.root / installer.COMMON[0]
target = self.root / installer.provider_assets.artifacts("docker", ("node",))[0]
target.write_bytes(b"existing-different-payload")
with self.assertRaisesRegex(installer.distribution.DistributionError, "Cached artifact differs"):
self.install()
Expand Down
4 changes: 2 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ Core reads only its environment. The installer renders `generated/core.env` from
| `OAC_EXECUTION_CONCURRENCY`, `OAC_DEFAULT_HARNESS`, `OAC_HARNESSES`, `OAC_WRITE_AUDIT_RETENTION`, `OAC_OAUTH_TRUSTED_ORIGINS` | The matching `core.*` settings |
| `OAC_HISTORY_SETTINGS_FILE` | `generated/runtime-history.json`: the [`core.runtime_history`](#settings) object, when it is set |
| `OAC_LOG_LEVEL`, `OAC_LOG_FORMAT`, `OAC_LOG_ADD_SOURCE` | `log.*`; Web reads the same three |
| `OAC_E2B_STATE_DIR` | `state/e2b/`: an absolute directory owned by Core's user, with no group or other access. Back it up with the database and `credential.key`; don't mount it into Web or a Runtime |
| `OAC_E2B_PROVIDER_BIN` | The E2B helper: `/opt/oac/e2b/oac-e2b-provider` in the Core image, `native/e2b/oac-e2b-provider` for native Core. The E2B key and template live in the database |
| `OAC_PROVIDER_ROOT` | Absolute adapter artifact root: `native/` for native Core, `/opt/oac` in the Core image. Each adapter owns its helper paths beneath this root |
| `OAC_PROVIDER_STATE_ROOT` | Absolute private state root: `state/` for native Core, `/state` in the Core image. Each adapter owns its subdirectory; E2B uses `e2b/`, owned by Core's user with no group or other access. Back it up with the database and `credential.key`; don't mount it into Web or a Runtime |
| `OAC_NATIVE_INSTALLER_DIR` | `native-installers/`, served to self-hosted machines; the Core image has a copy at `/opt/oac/native-installers`, used when this is unset. Core checks the catalog against its own release before serving it |

Core logs the file paths it loads, never environment values or file contents.
Expand Down
Loading
Loading