glove/tests/host/config_test.cpp
0xkite 126d99d366
feat(container): inherit local service streams
Add an adapter-selected AF_UNIX stream transport while preserving the pathname compatibility default and owner/mode identity checks.
2026-09-01 09:58:40 -06:00

491 lines
23 KiB
C++

#include "glove/host/config.hpp"
#include "glove/host/doctor.hpp"
#include "glove/host/setup.hpp"
#include "glove/supervisor/path_exposure.hpp"
#include <sys/stat.h>
#include <unistd.h>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <string>
#include <system_error>
namespace {
#define REQUIRE(condition) \
do { \
if (!(condition)) { \
std::fprintf(stderr, "REQUIRE failed: %s @ %s:%d\n", #condition, __FILE__, __LINE__); \
return 1; \
} \
} while (false)
class temporary_directory {
public:
temporary_directory() {
std::string pattern = "/tmp/glove-host-config-test-XXXXXX";
if (char* created = ::mkdtemp(pattern.data()); created != nullptr) {
root_ = created;
}
}
temporary_directory(const temporary_directory&) = delete;
auto operator=(const temporary_directory&) -> temporary_directory& = delete;
~temporary_directory() {
std::error_code ignored;
std::filesystem::remove_all(root_, ignored);
}
[[nodiscard]] auto root() const -> const std::filesystem::path& { return root_; }
private:
std::filesystem::path root_;
};
auto run() -> int {
using namespace glove::host;
temporary_directory temporary;
REQUIRE(!temporary.root().empty());
environment values{};
values.home = temporary.root().string();
values.xdg_runtime_dir = (temporary.root() / "run/user/501").string();
auto directories = resolve_directories(values);
REQUIRE(directories.has_value());
REQUIRE(directories->config == temporary.root() / ".config/glove");
REQUIRE(directories->state == temporary.root() / ".local/state/glove");
REQUIRE(directories->runtime == temporary.root() / ".local/state/glove/runtime");
const auto session_policy = temporary.root() / "session-policy.json";
setup_options managed_options{};
managed_options.session_policy = session_policy;
managed_options.dry_run = true;
auto managed_plan = plan_setup(managed_options, values);
REQUIRE(managed_plan.has_value());
REQUIRE(managed_plan->service.session_policy == session_policy);
REQUIRE(managed_plan->service.session_store == directories->state / "sessions.journal");
REQUIRE(managed_plan->service.materialization_root == directories->state / "materializations");
REQUIRE(managed_plan->service.library_bundle_root == directories->data / "library-bundles");
const auto project_root = temporary.root() / "projects\"quoted";
const auto project = project_root / "sage-protocol";
REQUIRE(std::filesystem::create_directories(project));
setup_options options{};
options.protected_root = project_root;
options.dry_run = true;
auto dry_run = plan_setup(options, values);
REQUIRE(dry_run.has_value());
REQUIRE(execute_setup(*dry_run).has_value());
REQUIRE(!std::filesystem::exists(dry_run->config_path));
options.dry_run = false;
auto plan = plan_setup(options, values);
REQUIRE(plan.has_value());
REQUIRE(execute_setup(*plan).has_value());
auto loaded = load_config(plan->config_path);
REQUIRE(loaded.has_value());
REQUIRE(loaded->runtime_directory == directories->runtime);
REQUIRE(loaded->path_exposure_policy.has_value());
auto registry = glove::supervisor::path_exposure_registry::load(
*loaded->path_exposure_policy, *loaded->path_exposure_journal, 1024U * 1024U
);
REQUIRE(registry.has_value());
REQUIRE(execute_setup(*plan).has_value());
// An explicit config path must not assume the separate XDG config child
// used for path-exposure policy already exists.
const auto isolated_config_home = temporary.root() / "isolated-config";
const auto isolated_state_home = temporary.root() / "isolated-state";
const auto isolated_data_home = temporary.root() / "isolated-data";
const auto isolated_runtime_home = temporary.root() / "isolated-runtime";
const auto explicit_config_parent = temporary.root() / "explicit-config";
for (const auto& directory : {
isolated_config_home,
isolated_state_home,
isolated_data_home,
isolated_runtime_home,
explicit_config_parent,
}) {
REQUIRE(std::filesystem::create_directory(directory));
REQUIRE(::chmod(directory.c_str(), 0700) == 0);
}
auto isolated_values = values;
isolated_values.xdg_config_home = isolated_config_home.string();
isolated_values.xdg_state_home = isolated_state_home.string();
isolated_values.xdg_data_home = isolated_data_home.string();
isolated_values.xdg_runtime_dir = isolated_runtime_home.string();
setup_options explicit_config_options{};
explicit_config_options.protected_root = project_root;
explicit_config_options.config_path = explicit_config_parent / "glove.json";
auto explicit_config_plan = plan_setup(explicit_config_options, isolated_values);
REQUIRE(explicit_config_plan.has_value());
REQUIRE(execute_setup(*explicit_config_plan).has_value());
REQUIRE(
std::filesystem::is_regular_file(
isolated_config_home / "glove" / "path-exposure-policy.json"
)
);
const auto legacy_runtime = temporary.root() / "run/user/501/glove";
REQUIRE(std::filesystem::create_directories(legacy_runtime));
REQUIRE(::chmod(legacy_runtime.c_str(), 0700) == 0);
REQUIRE(std::filesystem::remove(plan->config_path));
auto legacy_config = *loaded;
legacy_config.runtime_directory = legacy_runtime;
REQUIRE(write_config_exclusive(plan->config_path, legacy_config).has_value());
auto migration = plan_setup(options, values);
REQUIRE(migration.has_value());
REQUIRE(migration->migrate_runtime_from == legacy_runtime);
REQUIRE(execute_setup(*migration).has_value());
auto migrated = load_config(plan->config_path);
REQUIRE(migrated.has_value());
REQUIRE(migrated->runtime_directory == directories->runtime);
REQUIRE(std::filesystem::exists(legacy_runtime));
#if defined(__linux__)
REQUIRE(std::filesystem::remove(plan->config_path));
auto ssh_legacy_config = *loaded;
ssh_legacy_config.runtime_directory =
std::filesystem::path{"/run/user"} / std::to_string(::geteuid()) / "glove";
REQUIRE(write_config_exclusive(plan->config_path, ssh_legacy_config).has_value());
auto ssh_values = values;
ssh_values.xdg_runtime_dir.reset();
auto ssh_migration = plan_setup(options, ssh_values);
REQUIRE(ssh_migration.has_value());
REQUIRE(ssh_migration->migrate_runtime_from == ssh_legacy_config.runtime_directory);
#endif
REQUIRE(std::filesystem::remove(plan->config_path));
REQUIRE(execute_setup(*plan).has_value());
REQUIRE(load_config(plan->config_path).has_value());
const auto ledger_path = setup_ledger_path(plan->service);
auto ledger = load_setup_ledger(ledger_path);
REQUIRE(ledger.has_value());
REQUIRE(ledger->config_path == plan->config_path);
REQUIRE(!ledger->resources.empty());
REQUIRE(execute_setup(*plan).has_value());
REQUIRE(load_setup_ledger(ledger_path) == ledger);
const auto report = diagnose(plan->config_path);
REQUIRE(report.healthy());
REQUIRE(report.checks.size() >= 5U);
const auto derived_path = temporary.root() / ".config/glove/derived.json";
auto derived = *loaded;
derived.session_policy = session_policy;
REQUIRE(write_config_exclusive(derived_path, derived).has_value());
auto loaded_derived = load_config(derived_path);
REQUIRE(loaded_derived.has_value());
REQUIRE(*loaded_derived == derived);
REQUIRE(!write_config_exclusive(derived_path, derived).has_value());
const auto proxy_path = temporary.root() / ".config/glove/local-services.json";
auto proxy_config = managed_plan->service;
proxy_config.apple_container.reset();
proxy_config.local_service_proxy = local_service_proxy_config{
.io_timeout_ms = 2'500,
.max_concurrency = 3,
.guest_channel_adapter =
guest_channel_adapter_config{
.adapter_id = "sage-observation",
.channel_schema_id = "sage.glove-observation.v1",
.transport_id = "inherited-stream-v1",
},
.endpoints = {
local_service_proxy_endpoint{
.alias = "sage-observe",
.socket_path = temporary.root() / "services/sage.sock",
.runtime_ids = {"codex", "pi"},
},
local_service_proxy_endpoint{
.alias = "tooling.v1",
.socket_path = temporary.root() / "services/tooling.sock",
.runtime_ids = {"pi"},
},
},
};
REQUIRE(validate(proxy_config).has_value());
REQUIRE(write_config_exclusive(proxy_path, proxy_config).has_value());
REQUIRE(load_config(proxy_path) == proxy_config);
auto legacy_proxy_config = proxy_config;
legacy_proxy_config.local_service_proxy->guest_channel_adapter->transport_id.reset();
REQUIRE(validate(legacy_proxy_config).has_value());
const auto legacy_proxy_path = temporary.root() / "legacy-proxy-config.json";
REQUIRE(write_config_exclusive(legacy_proxy_path, legacy_proxy_config).has_value());
REQUIRE(load_config(legacy_proxy_path) == legacy_proxy_config);
auto generic_proxy_config = proxy_config;
generic_proxy_config.local_service_proxy->guest_channel_adapter.reset();
REQUIRE(validate(generic_proxy_config).has_value());
const auto local_service_audit =
proxy_config.receipt_journal.parent_path() / local_service_audit_filename;
const auto rejects_local_service_audit_collision = [&](config candidate) {
auto result = validate(candidate);
return !result.has_value() && result.error() == "configuration paths must be distinct" &&
result.error().find(temporary.root().string()) == std::string::npos;
};
auto audit_collision = proxy_config;
audit_collision.receipt_journal = local_service_audit;
REQUIRE(rejects_local_service_audit_collision(std::move(audit_collision)));
audit_collision = proxy_config;
audit_collision.audit_key = local_service_audit;
REQUIRE(rejects_local_service_audit_collision(std::move(audit_collision)));
audit_collision = proxy_config;
audit_collision.session_store = local_service_audit;
REQUIRE(rejects_local_service_audit_collision(std::move(audit_collision)));
// The derived control delivery journal (control-audit.jsonl next to the
// receipt journal) is always registered in the configured-path set: no
// configured path may alias it.
const auto control_audit_path =
proxy_config.receipt_journal.parent_path() / control_audit_filename;
auto control_collision = proxy_config;
control_collision.receipt_journal = control_audit_path;
REQUIRE(rejects_local_service_audit_collision(std::move(control_collision)));
control_collision = proxy_config;
control_collision.audit_key = control_audit_path;
REQUIRE(rejects_local_service_audit_collision(std::move(control_collision)));
control_collision = proxy_config;
control_collision.session_store = control_audit_path;
REQUIRE(rejects_local_service_audit_collision(std::move(control_collision)));
audit_collision = proxy_config;
audit_collision.local_service_proxy->endpoints.front().socket_path = local_service_audit;
auto endpoint_audit_collision = validate(audit_collision);
REQUIRE(!endpoint_audit_collision.has_value());
REQUIRE(endpoint_audit_collision.error().find(temporary.root().string()) == std::string::npos);
auto proxy_json = encode_config(proxy_config);
REQUIRE(proxy_json.has_value());
const auto endpoints_marker = proxy_json->find("\"endpoints\":[");
REQUIRE(endpoints_marker != std::string::npos);
const auto write_rejected_shape = [&](std::string name, std::string field) {
auto rejected = *proxy_json;
rejected.insert(endpoints_marker + std::string_view{"\"endpoints\":["}.size() + 1U, field);
const auto path = temporary.root() / ".config/glove" / name;
std::ofstream output{path, std::ios::binary};
output << rejected;
output.close();
if (!output.good() || ::chmod(path.c_str(), 0600) != 0) {
return false;
}
return !load_config(path).has_value();
};
REQUIRE(write_rejected_shape("proxy-tcp.json", "\"tcp_port\":443,"));
REQUIRE(
write_rejected_shape("proxy-guest-target.json", "\"guest_target\":\"/run/host.sock\",")
);
REQUIRE(write_rejected_shape("proxy-token.json", "\"token\":\"secret\","));
auto invalid_proxy = proxy_config;
invalid_proxy.local_service_proxy->io_timeout_ms = 0;
REQUIRE(!validate(invalid_proxy).has_value());
invalid_proxy = proxy_config;
invalid_proxy.local_service_proxy->max_concurrency = 0;
REQUIRE(!validate(invalid_proxy).has_value());
invalid_proxy = proxy_config;
invalid_proxy.local_service_proxy->max_concurrency = 17;
REQUIRE(!validate(invalid_proxy).has_value());
invalid_proxy = proxy_config;
invalid_proxy.local_service_proxy->endpoints.clear();
REQUIRE(!validate(invalid_proxy).has_value());
invalid_proxy = proxy_config;
const auto repeated_endpoint = invalid_proxy.local_service_proxy->endpoints.front();
invalid_proxy.local_service_proxy->endpoints.resize(17U, repeated_endpoint);
REQUIRE(!validate(invalid_proxy).has_value());
invalid_proxy = proxy_config;
invalid_proxy.local_service_proxy->endpoints.front().alias = "../escape";
REQUIRE(!validate(invalid_proxy).has_value());
invalid_proxy = proxy_config;
invalid_proxy.local_service_proxy->endpoints.back().alias = "sage-observe";
REQUIRE(!validate(invalid_proxy).has_value());
invalid_proxy = proxy_config;
invalid_proxy.local_service_proxy->endpoints.back().socket_path =
invalid_proxy.local_service_proxy->endpoints.front().socket_path;
REQUIRE(!validate(invalid_proxy).has_value());
invalid_proxy = proxy_config;
invalid_proxy.local_service_proxy->endpoints.front().socket_path = "relative.sock";
REQUIRE(!validate(invalid_proxy).has_value());
invalid_proxy = proxy_config;
invalid_proxy.local_service_proxy->endpoints.front().runtime_ids.clear();
REQUIRE(!validate(invalid_proxy).has_value());
invalid_proxy = proxy_config;
invalid_proxy.local_service_proxy->endpoints.front().runtime_ids = {"pi", "codex"};
REQUIRE(!validate(invalid_proxy).has_value());
invalid_proxy = proxy_config;
invalid_proxy.local_service_proxy->endpoints.front().runtime_ids = {"pi", "pi"};
REQUIRE(!validate(invalid_proxy).has_value());
invalid_proxy = proxy_config;
std::ranges::swap(
invalid_proxy.local_service_proxy->endpoints.front(),
invalid_proxy.local_service_proxy->endpoints.back()
);
REQUIRE(!validate(invalid_proxy).has_value());
invalid_proxy = proxy_config;
invalid_proxy.local_service_proxy->endpoints.front().alias = "non-ascii-\xC3\xA9";
REQUIRE(!validate(invalid_proxy).has_value());
invalid_proxy = proxy_config;
invalid_proxy.local_service_proxy->endpoints.front().alias = std::string{"nul\0alias", 9U};
REQUIRE(!validate(invalid_proxy).has_value());
invalid_proxy = proxy_config;
auto nul_path = invalid_proxy.local_service_proxy->endpoints.front().socket_path.string();
nul_path.append("\0hidden", 7U);
invalid_proxy.local_service_proxy->endpoints.front().socket_path = nul_path;
REQUIRE(!validate(invalid_proxy).has_value());
invalid_proxy = proxy_config;
invalid_proxy.local_service_proxy->guest_channel_adapter->adapter_id =
std::string{"sage\0hidden", 11U};
REQUIRE(!validate(invalid_proxy).has_value());
invalid_proxy = proxy_config;
invalid_proxy.local_service_proxy->guest_channel_adapter->transport_id = "unknown-v1";
REQUIRE(!validate(invalid_proxy).has_value());
invalid_proxy = proxy_config;
invalid_proxy.local_service_proxy->endpoints.front().socket_path =
*invalid_proxy.materialization_root / "service.sock";
REQUIRE(!validate(invalid_proxy).has_value());
invalid_proxy = proxy_config;
invalid_proxy.local_service_proxy->endpoints.front().socket_path =
*invalid_proxy.materialization_root;
REQUIRE(!validate(invalid_proxy).has_value());
invalid_proxy = proxy_config;
invalid_proxy.remote_backend = remote_backend_config{};
REQUIRE(!validate(invalid_proxy).has_value());
const auto malformed_path = temporary.root() / ".config/glove/malformed-secret.json";
const std::string secret_path = "/operator/private/endpoint.sock";
{
std::ofstream malformed{malformed_path, std::ios::binary};
malformed << "{\"local_service_proxy\":{\"endpoint\":\"" << secret_path;
}
REQUIRE(::chmod(malformed_path.c_str(), 0600) == 0);
auto malformed = load_config(malformed_path);
REQUIRE(!malformed.has_value());
REQUIRE(malformed.error() == "configuration JSON is invalid");
REQUIRE(malformed.error().find(secret_path) == std::string::npos);
#if defined(__linux__)
temporary_directory preservation_temporary;
REQUIRE(!preservation_temporary.root().empty());
environment preservation_values{};
preservation_values.home = preservation_temporary.root().string();
const auto preservation_policy = preservation_temporary.root() / "session-policy.json";
setup_options preservation_options{};
preservation_options.session_policy = preservation_policy;
auto preservation_plan = plan_setup(preservation_options, preservation_values);
REQUIRE(preservation_plan.has_value());
preservation_plan->service.local_service_proxy = proxy_config.local_service_proxy;
preservation_plan->service.local_service_proxy->endpoints.front().socket_path =
preservation_temporary.root() / "services/sage.sock";
preservation_plan->service.local_service_proxy->endpoints.back().socket_path =
preservation_temporary.root() / "services/tooling.sock";
REQUIRE(std::filesystem::create_directories(preservation_plan->config_path.parent_path()));
REQUIRE(::chmod(preservation_plan->config_path.parent_path().c_str(), 0700) == 0);
REQUIRE(write_config_exclusive(preservation_plan->config_path, preservation_plan->service)
.has_value());
auto replanned = plan_setup(preservation_options, preservation_values);
REQUIRE(replanned.has_value());
REQUIRE(
replanned->service.local_service_proxy == preservation_plan->service.local_service_proxy
);
#endif
const auto apple_path = temporary.root() / ".config/glove/apple.json";
auto apple = managed_plan->service;
apple.apple_container = apple_container_config{
.cli = "/usr/local/bin/container",
.image = "ghcr.io/sage-protocol/glove-agent-runtime@sha256:" + std::string(64U, 'a'),
.image_digest = "sha256:" + std::string(64U, 'a'),
.harness_closure_digest = "sha256:" + std::string(64U, 'b'),
.sage_guest = sage_guest_config{
.binary_digest = "sha256:" + std::string(64U, 'c'),
.source_revision = std::string(40U, 'd'),
.policy_schema_version = 1,
.library_projection_schema = "sage_bundle_v1",
},
};
REQUIRE(write_config_exclusive(apple_path, apple).has_value());
REQUIRE(load_config(apple_path) == apple);
auto invalid_apple = apple;
invalid_apple.apple_container->image_digest = "sha256:latest";
REQUIRE(!validate(invalid_apple).has_value());
invalid_apple = apple;
invalid_apple.apple_container->image = "ghcr.io/sage-protocol/glove-agent-runtime:latest";
REQUIRE(!validate(invalid_apple).has_value());
invalid_apple = apple;
invalid_apple.apple_container->harness_closure_digest = "sha256:latest";
REQUIRE(!validate(invalid_apple).has_value());
invalid_apple = apple;
invalid_apple.apple_container->sage_guest->library_projection_schema = "latest";
REQUIRE(!validate(invalid_apple).has_value());
invalid_apple = apple;
invalid_apple.apple_container->harness_closure_digest.reset();
REQUIRE(!validate(invalid_apple).has_value());
invalid_apple = apple;
invalid_apple.materialization_root.reset();
REQUIRE(!validate(invalid_apple).has_value());
REQUIRE(::chmod(plan->config_path.c_str(), 0644) == 0);
REQUIRE(!load_config(plan->config_path).has_value());
temporary_directory cleanup_temporary;
REQUIRE(!cleanup_temporary.root().empty());
environment cleanup_values{};
cleanup_values.home = cleanup_temporary.root().string();
auto cleanup_setup = plan_setup(setup_options{}, cleanup_values);
REQUIRE(cleanup_setup.has_value());
REQUIRE(execute_setup(*cleanup_setup).has_value());
auto cleanup_preview = plan_setup_cleanup(cleanup_setup->config_path);
REQUIRE(cleanup_preview.has_value());
REQUIRE(!cleanup_preview->blocked());
auto cleanup_digest = setup_ledger_sha256(cleanup_preview->ledger);
REQUIRE(cleanup_digest.has_value());
REQUIRE(!execute_setup_cleanup(*cleanup_preview, std::string(64U, '0')).has_value());
REQUIRE(std::filesystem::exists(cleanup_setup->config_path));
REQUIRE(execute_setup_cleanup(*cleanup_preview, *cleanup_digest).has_value());
REQUIRE(!std::filesystem::exists(cleanup_setup->config_path));
REQUIRE(!std::filesystem::exists(cleanup_preview->ledger.ledger_path));
temporary_directory adoption_temporary;
REQUIRE(!adoption_temporary.root().empty());
environment adoption_values{};
adoption_values.home = adoption_temporary.root().string();
auto adoption_setup = plan_setup(setup_options{}, adoption_values);
REQUIRE(adoption_setup.has_value());
REQUIRE(execute_setup(*adoption_setup).has_value());
REQUIRE(std::filesystem::remove(setup_ledger_path(adoption_setup->service)));
auto adoption = plan_setup_adoption(adoption_setup->config_path);
REQUIRE(adoption.has_value());
REQUIRE(std::ranges::none_of(adoption->resources, [](const auto& resource) {
return resource.owned;
}));
REQUIRE(execute_setup_adoption(*adoption).has_value());
auto adopted = load_setup_ledger(adoption->ledger_path);
REQUIRE(adopted == adoption);
auto adopted_cleanup = plan_setup_cleanup(adoption_setup->config_path);
REQUIRE(adopted_cleanup.has_value());
REQUIRE(!adopted_cleanup->blocked());
REQUIRE(std::ranges::all_of(adopted_cleanup->items, [](const auto& item) {
return !item.resource.owned && !item.removable;
}));
temporary_directory changed_temporary;
REQUIRE(!changed_temporary.root().empty());
environment changed_values{};
changed_values.home = changed_temporary.root().string();
auto changed_setup = plan_setup(setup_options{}, changed_values);
REQUIRE(changed_setup.has_value());
REQUIRE(execute_setup(*changed_setup).has_value());
{
std::ofstream changed{changed_setup->config_path, std::ios::app};
changed << ' ';
}
auto changed_preview = plan_setup_cleanup(changed_setup->config_path);
REQUIRE(changed_preview.has_value());
REQUIRE(changed_preview->blocked());
return 0;
}
} // namespace
auto main() -> int {
return run();
}