glove/tests/control/gloved_lifecycle_test.cpp
0xkite 56e3cbd638
fix(linux): support delegated systemd subgroup
Keep gloved in a systemd-owned supervisor leaf while creating sibling session cgroups under the empty delegated parent. Under mapped-root supervisors, attach blocked children before releasing their existing start gate instead of combining nested UID mapping with CLONE_INTO_CGROUP.
2026-09-01 15:33:03 -06:00

1044 lines
41 KiB
C++

#include "glove/container/receipt_chain.hpp"
#include "glove/control/receipt_audit_protocol.hpp"
#include "glove/host/config.hpp"
#include "glove/host/control_client.hpp"
#include "glove/host/remote_backend.hpp"
#include "receipt_audit_wire.hpp"
#include <arpa/inet.h>
#include <fcntl.h>
#include <glaze/glaze.hpp>
#include <signal.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cerrno>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <optional>
#include <string>
#include <string_view>
#include <system_error>
#include <thread>
#include <utility>
#include <vector>
namespace wire_test {
struct rpc_error {
std::string code;
std::string message;
};
struct rpc_response {
std::string jsonrpc;
std::string id;
std::optional<glz::raw_json> result;
std::optional<rpc_error> error;
};
struct page_result {
std::uint8_t schema_version = 0;
std::vector<glove::container::authenticated_resource_enforcement_receipt> envelopes;
std::vector<glove::container::authenticated_refinement_evaluation_receipt> refinement_envelopes;
bool has_more = false;
glove::container::receipt_audit_anchor local_anchor;
};
struct session_plan_validation {
std::uint8_t schema_version = 0;
std::uint64_t policy_revision = 0;
};
struct session_record_result {
std::uint8_t schema_version = 0;
std::string session_id;
std::string controller_plan_digest;
std::string plan_content_digest;
std::string state;
std::uint64_t policy_revision = 0;
std::uint64_t expires_at_ms = 0;
std::uint64_t created_at_ms = 0;
std::optional<std::string> profile_digest;
};
} // namespace wire_test
namespace {
#define REQUIRE(cond) \
do { \
if (!(cond)) { \
std::fprintf(stderr, "REQUIRE failed: %s @ %s:%d\n", #cond, __FILE__, __LINE__); \
return 1; \
} \
} while (0)
constexpr std::string_view audit_key =
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
class temporary_directory {
public:
temporary_directory() {
std::string pattern = "/tmp/gloved-lifecycle-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 error;
std::filesystem::remove_all(root_, error);
}
[[nodiscard]] auto root() const -> const std::filesystem::path& { return root_; }
private:
std::filesystem::path root_;
};
class unique_fd {
public:
explicit unique_fd(int value = -1) noexcept : value_{value} {}
unique_fd(const unique_fd&) = delete;
auto operator=(const unique_fd&) -> unique_fd& = delete;
unique_fd(unique_fd&& other) noexcept : value_{std::exchange(other.value_, -1)} {}
auto operator=(unique_fd&&) -> unique_fd& = delete;
~unique_fd() {
if (value_ >= 0) {
(void)::close(value_);
}
}
[[nodiscard]] auto get() const noexcept -> int { return value_; }
private:
int value_ = -1;
};
class child_process {
public:
explicit child_process(::pid_t process = -1) noexcept : process_{process} {}
child_process(const child_process&) = delete;
auto operator=(const child_process&) -> child_process& = delete;
child_process(child_process&& other) noexcept
: process_{std::exchange(other.process_, -1)}, status_{std::move(other.status_)} {}
auto operator=(child_process&&) -> child_process& = delete;
~child_process() { (void)stop(); }
[[nodiscard]] auto running() -> bool {
if (process_ <= 0) {
return false;
}
int status = 0;
const auto waited = ::waitpid(process_, &status, WNOHANG);
if (waited == process_) {
process_ = -1;
status_ = status;
return false;
}
return waited == 0;
}
auto wait_for_exit() -> std::optional<int> {
if (status_) {
return status_;
}
for (int attempt = 0; attempt < 300; ++attempt) {
if (!running()) {
return status_;
}
std::this_thread::sleep_for(std::chrono::milliseconds{10});
}
return std::nullopt;
}
auto stop() -> bool {
if (process_ <= 0) {
return status_ && WIFEXITED(*status_) && WEXITSTATUS(*status_) == 0;
}
if (::kill(process_, SIGTERM) != 0 && errno != ESRCH) {
return false;
}
auto status = wait_for_exit();
if (status) {
return WIFEXITED(*status) && WEXITSTATUS(*status) == 0;
}
(void)::kill(process_, SIGKILL);
int forced_status = 0;
(void)::waitpid(process_, &forced_status, 0);
process_ = -1;
status_ = forced_status;
return false;
}
auto kill_hard() -> bool {
if (process_ <= 0 || ::kill(process_, SIGKILL) != 0) {
return false;
}
int status = 0;
while (::waitpid(process_, &status, 0) < 0) {
if (errno != EINTR) {
return false;
}
}
process_ = -1;
status_ = status;
return WIFSIGNALED(status) && WTERMSIG(status) == SIGKILL;
}
private:
::pid_t process_ = -1;
std::optional<int> status_;
};
auto write_owner_only(const std::filesystem::path& path, std::string_view value) -> bool {
std::ofstream output{path, std::ios::binary | std::ios::trunc};
output << value << '\n';
output.flush();
return output.good() && ::chmod(path.c_str(), 0600) == 0;
}
auto start_gloved(
const std::filesystem::path& runtime_dir,
const std::filesystem::path& key_path,
const std::filesystem::path& journal_path,
const std::optional<std::filesystem::path>& session_policy = std::nullopt,
const std::optional<std::filesystem::path>& session_store = std::nullopt,
const std::optional<std::filesystem::path>& materialization_root = std::nullopt,
const std::optional<std::filesystem::path>& library_bundle_root = std::nullopt
) -> child_process {
const auto process = ::fork();
if (process != 0) {
return child_process{process};
}
if (session_policy && session_store && materialization_root && library_bundle_root) {
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
::execl(
GLOVED_BIN,
GLOVED_BIN,
"--runtime-dir",
runtime_dir.c_str(),
"--audit-key",
key_path.c_str(),
"--journal",
journal_path.c_str(),
"--session-policy",
session_policy->c_str(),
"--session-store",
session_store->c_str(),
"--materialization-root",
materialization_root->c_str(),
"--library-bundle-root",
library_bundle_root->c_str(),
static_cast<char*>(nullptr)
);
} else if (session_policy && session_store && library_bundle_root) {
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
::execl(
GLOVED_BIN,
GLOVED_BIN,
"--runtime-dir",
runtime_dir.c_str(),
"--audit-key",
key_path.c_str(),
"--journal",
journal_path.c_str(),
"--session-policy",
session_policy->c_str(),
"--session-store",
session_store->c_str(),
"--library-bundle-root",
library_bundle_root->c_str(),
static_cast<char*>(nullptr)
);
} else if (session_policy && session_store && materialization_root) {
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
::execl(
GLOVED_BIN,
GLOVED_BIN,
"--runtime-dir",
runtime_dir.c_str(),
"--audit-key",
key_path.c_str(),
"--journal",
journal_path.c_str(),
"--session-policy",
session_policy->c_str(),
"--session-store",
session_store->c_str(),
"--materialization-root",
materialization_root->c_str(),
static_cast<char*>(nullptr)
);
} else if (materialization_root) {
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
::execl(
GLOVED_BIN,
GLOVED_BIN,
"--runtime-dir",
runtime_dir.c_str(),
"--audit-key",
key_path.c_str(),
"--journal",
journal_path.c_str(),
"--materialization-root",
materialization_root->c_str(),
static_cast<char*>(nullptr)
);
} else if (library_bundle_root) {
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
::execl(
GLOVED_BIN,
GLOVED_BIN,
"--runtime-dir",
runtime_dir.c_str(),
"--audit-key",
key_path.c_str(),
"--journal",
journal_path.c_str(),
"--library-bundle-root",
library_bundle_root->c_str(),
static_cast<char*>(nullptr)
);
} else if (session_policy && session_store) {
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
::execl(
GLOVED_BIN,
GLOVED_BIN,
"--runtime-dir",
runtime_dir.c_str(),
"--audit-key",
key_path.c_str(),
"--journal",
journal_path.c_str(),
"--session-policy",
session_policy->c_str(),
"--session-store",
session_store->c_str(),
static_cast<char*>(nullptr)
);
} else if (session_policy) {
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
::execl(
GLOVED_BIN,
GLOVED_BIN,
"--runtime-dir",
runtime_dir.c_str(),
"--audit-key",
key_path.c_str(),
"--journal",
journal_path.c_str(),
"--session-policy",
session_policy->c_str(),
static_cast<char*>(nullptr)
);
} else {
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
::execl(
GLOVED_BIN,
GLOVED_BIN,
"--runtime-dir",
runtime_dir.c_str(),
"--audit-key",
key_path.c_str(),
"--journal",
journal_path.c_str(),
static_cast<char*>(nullptr)
);
}
_exit(127);
}
auto start_gloved_config(const std::filesystem::path& config_path) -> child_process {
const auto process = ::fork();
if (process != 0) {
return child_process{process};
}
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg)
::execl(GLOVED_BIN, GLOVED_BIN, "--config", config_path.c_str(), static_cast<char*>(nullptr));
_exit(127);
}
auto wait_until_ready(
child_process& process,
const std::filesystem::path& socket_path,
const std::filesystem::path& secret_path
) -> bool {
for (int attempt = 0; attempt < 300; ++attempt) {
if (std::filesystem::exists(socket_path) && std::filesystem::exists(secret_path)) {
return true;
}
if (!process.running()) {
return false;
}
std::this_thread::sleep_for(std::chrono::milliseconds{10});
}
return false;
}
auto read_secret(const std::filesystem::path& path) -> std::optional<std::string> {
std::ifstream input{path, std::ios::binary};
std::string value;
input >> value;
if (!input || value.size() != 64U) {
return std::nullopt;
}
return value;
}
auto wait_until_secret_changes(
child_process& process,
const std::filesystem::path& socket_path,
const std::filesystem::path& secret_path,
std::string_view previous_secret
) -> bool {
for (int attempt = 0; attempt < 300; ++attempt) {
const auto secret = read_secret(secret_path);
if (std::filesystem::exists(socket_path) && secret && *secret != previous_secret) {
return true;
}
if (!process.running()) {
return false;
}
std::this_thread::sleep_for(std::chrono::milliseconds{10});
}
return false;
}
auto wait_until_authenticated(const glove::host::config& configured) -> bool {
for (int attempt = 0; attempt < 300; ++attempt) {
if (glove::host::supervisor_health(configured)) {
return true;
}
std::this_thread::sleep_for(std::chrono::milliseconds{10});
}
return false;
}
auto write_exact(int descriptor, const void* input, std::size_t size) -> bool {
const auto* bytes = static_cast<const std::byte*>(input);
std::size_t written = 0;
while (written < size) {
const auto result = ::write(descriptor, bytes + written, size - written);
if (result < 0 && errno == EINTR) {
continue;
}
if (result <= 0) {
return false;
}
written += static_cast<std::size_t>(result);
}
return true;
}
auto read_exact(int descriptor, void* output, std::size_t size) -> bool {
auto* bytes = static_cast<std::byte*>(output);
std::size_t consumed = 0;
while (consumed < size) {
const auto result = ::read(descriptor, bytes + consumed, size - consumed);
if (result < 0 && errno == EINTR) {
continue;
}
if (result <= 0) {
return false;
}
consumed += static_cast<std::size_t>(result);
}
return true;
}
auto connect_to(const std::filesystem::path& socket_path) -> unique_fd {
unique_fd descriptor{::socket(AF_UNIX, SOCK_STREAM, 0)};
if (descriptor.get() < 0 || ::fcntl(descriptor.get(), F_SETFD, FD_CLOEXEC) != 0) {
return unique_fd{};
}
::sockaddr_un address{};
address.sun_family = AF_UNIX;
const auto value = socket_path.string();
if (value.size() >= sizeof(address.sun_path)) {
return unique_fd{};
}
std::memcpy(address.sun_path, value.c_str(), value.size() + 1U);
if (::connect(
descriptor.get(), reinterpret_cast<const ::sockaddr*>(&address), sizeof(address)
) != 0) {
return unique_fd{};
}
return descriptor;
}
auto transact(const std::filesystem::path& socket_path, std::string_view frame)
-> std::optional<std::string> {
auto descriptor = connect_to(socket_path);
if (descriptor.get() < 0) {
return std::nullopt;
}
const auto size = htonl(static_cast<std::uint32_t>(frame.size()));
if (!write_exact(descriptor.get(), &size, sizeof(size)) ||
!write_exact(descriptor.get(), frame.data(), frame.size())) {
return std::nullopt;
}
std::uint32_t response_size = 0;
if (!read_exact(descriptor.get(), &response_size, sizeof(response_size))) {
return std::nullopt;
}
const auto decoded_size = ntohl(response_size);
if (decoded_size == 0 || decoded_size > glove::control::max_control_frame_bytes) {
return std::nullopt;
}
std::string response(decoded_size, '\0');
if (!read_exact(descriptor.get(), response.data(), response.size())) {
return std::nullopt;
}
return response;
}
auto transact_with_retry(const std::filesystem::path& socket_path, std::string_view frame)
-> std::optional<std::string> {
for (int attempt = 0; attempt < 300; ++attempt) {
if (auto response = transact(socket_path, frame)) {
return response;
}
std::this_thread::sleep_for(std::chrono::milliseconds{10});
}
return std::nullopt;
}
auto make_request(
std::string_view id,
std::string_view method,
std::string_view secret,
std::string_view payload,
std::optional<std::string_view> idempotency = std::nullopt
) -> std::string {
std::string request = "{\"jsonrpc\":\"2.0\",\"id\":\"" + std::string{id} + "\",\"method\":\"" +
std::string{method} +
"\",\"params\":{\"schema_version\":1,\"bootstrap_secret\":\"" +
std::string{secret} + "\",\"deadline_ms\":4102444800000";
if (idempotency) {
request += ",\"idempotency_key\":\"" + std::string{*idempotency} + "\"";
}
request += ",\"payload\":" + std::string{payload} + "}}";
return request;
}
auto decode_response(std::string_view frame) -> std::optional<wire_test::rpc_response> {
wire_test::rpc_response response;
if (glz::read<glz::opts{.error_on_unknown_keys = true}>(response, frame)) {
return std::nullopt;
}
return response;
}
auto page_and_acknowledge(
const std::filesystem::path& socket_path,
std::string_view secret,
const glove::container::receipt_audit_anchor& genesis
) -> bool {
const auto genesis_json = glz::write_json(genesis);
if (!genesis_json) {
return false;
}
const auto page_payload = "{\"sage_anchor\":" + *genesis_json + ",\"limit\":10}";
auto page_frame = transact_with_retry(
socket_path, make_request("page", "verify_audit_chain", secret, page_payload)
);
if (!page_frame) {
return false;
}
auto page_response = decode_response(*page_frame);
if (!page_response || !page_response->result || page_response->error) {
return false;
}
wire_test::page_result page;
if (glz::read<glz::opts{.error_on_unknown_keys = true}>(page, page_response->result->str) ||
!page.envelopes.empty() || page.has_more || page.local_anchor != genesis) {
return false;
}
const auto ack_payload = "{\"anchor\":" + *genesis_json + "}";
auto ack_frame = transact_with_retry(
socket_path,
make_request("ack", "acknowledge_audit_chain", secret, ack_payload, "lifecycle-ack")
);
auto ack_response = ack_frame ? decode_response(*ack_frame) : std::nullopt;
return ack_response && ack_response->result && !ack_response->error;
}
auto policy_json(const std::filesystem::path& source) -> std::string {
return R"({"schema_version":1,"revision":7,"max_plan_ttl_ms":120000,"runtime_templates":[{"runtime_template_id":"codex-safe","runtime_id":"codex","adapter_command_digest":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","sandbox_backend":"linux_production","allowed_path_aliases":["workspace"],"allowed_projection_destinations":["libraries"]}],"path_aliases":[{"alias":"workspace","host_path":")" +
std::filesystem::canonical(source).string() +
R"(","target_path":"/workspace","max_ttl_secs":120,"access":[{"access":"ephemeral_write","materialization":"copy","create_policy":"empty_directory","cleanup_policy":"remove","max_bytes":2097152}]}],"library_projection_destinations":[{"alias":"libraries","target_path":"/opt/sage/library-bundles"}],"resource_profiles":[{"profile_id":"small","cpu_time_ms":1000,"memory_bytes":67108864,"pids":16,"wall_time_ms":2000,"disk_bytes":2097152,"terminal_output_bytes":1048576}],"egress_policy_ids":["no-network"],"tool_policy_ids":["sage-readonly"],"secret_handles":["codex-token"]})";
}
auto exposure_policy_json(const std::filesystem::path& root) -> std::string {
return R"({"schema_version":1,"roots":[{"root_id":"projects","host_root":")" +
std::filesystem::canonical(root).string() +
R"(","allowed_modes":[{"access":"read","materialization":"bind","max_bytes":0,"cleanup_policy":"retain"}],"max_ttl_secs":3600,"allowed_runtime_template_ids":["codex-safe"]}]})";
}
auto plan_json(std::uint64_t expires_at_ms) -> std::string {
return R"({"schema_version":1,"runtime_id":"codex","runtime_template_id":"codex-safe","adapter_command_digest":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","sandbox_backend":"linux_production","egress_policy_id":"no-network","tool_policy_id":"sage-readonly","path_grants":[{"alias":"workspace","access":"ephemeral_write","materialization":"copy","max_bytes":1048576,"ttl_secs":60,"cleanup_policy":"remove"}],"library_projections":[{"projection_id":"sage-core","content_digest":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","destination_alias":"libraries"}],"secret_handles":["codex-token"],"limits":{"cpu_time_ms":1000,"memory_bytes":67108864,"pids":16,"wall_time_ms":2000,"disk_bytes":2097152,"terminal_output_bytes":1048576},"policy_revision":7,"expires_at_ms":)" +
std::to_string(expires_at_ms) + "}";
}
auto validate_plan(const std::filesystem::path& socket_path, std::string_view secret) -> bool {
const auto now_ms =
static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()
)
.count());
auto frame = transact_with_retry(
socket_path,
make_request("validate-plan", "validate_plan", secret, plan_json(now_ms + 60'000U))
);
auto response = frame ? decode_response(*frame) : std::nullopt;
if (!response || !response->result || response->error) {
return false;
}
wire_test::session_plan_validation validation;
return !glz::read<glz::opts{.error_on_unknown_keys = true}>(
validation, response->result->str
) &&
validation.schema_version == 1 && validation.policy_revision == 7;
}
auto create_or_read_session(
const std::filesystem::path& socket_path, std::string_view secret, bool create
) -> bool {
constexpr std::string_view digest =
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
if (create) {
const auto now_ms =
static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()
)
.count());
const auto payload = "{\"session_id\":\"lifecycle-session\",\"controller_plan_digest\":\"" +
std::string{digest} + "\",\"plan\":" + plan_json(now_ms + 60'000U) +
"}";
auto frame = transact_with_retry(
socket_path,
make_request(
"create-session", "create_session", secret, payload, "lifecycle-create-session"
)
);
auto response = frame ? decode_response(*frame) : std::nullopt;
if (!response || !response->result || response->error) {
return false;
}
}
auto status_frame = transact_with_retry(
socket_path,
make_request(
"session-status", "session_status", secret, "{\"session_id\":\"lifecycle-session\"}"
)
);
auto status_response = status_frame ? decode_response(*status_frame) : std::nullopt;
if (!status_response || !status_response->result || status_response->error) {
return false;
}
wire_test::session_record_result record;
return !glz::read<glz::opts{.error_on_unknown_keys = true}>(
record, status_response->result->str
) &&
record.schema_version == 1 && record.session_id == "lifecycle-session" &&
record.controller_plan_digest == digest && record.plan_content_digest.size() == 64 &&
record.state == "created" && record.policy_revision == 7;
}
auto run() -> int {
temporary_directory temp;
REQUIRE(!temp.root().empty());
REQUIRE(::chmod(temp.root().c_str(), 0700) == 0);
const auto key_path = temp.root() / "audit.key";
const auto journal_path = temp.root() / "receipts.journal";
const auto socket_path = temp.root() / "gloved.sock";
const auto secret_path = temp.root() / "bootstrap-secret";
REQUIRE(write_owner_only(key_path, audit_key));
auto genesis = glove::container::receipt_audit_anchor::create(audit_key);
REQUIRE(genesis.has_value());
REQUIRE(::chmod(temp.root().c_str(), 0755) == 0);
auto unsafe_permissions = start_gloved(temp.root(), key_path, journal_path);
auto unsafe_status = unsafe_permissions.wait_for_exit();
REQUIRE(unsafe_status.has_value());
REQUIRE(WIFEXITED(*unsafe_status));
REQUIRE(WEXITSTATUS(*unsafe_status) != 0);
REQUIRE(::chmod(temp.root().c_str(), 0700) == 0);
REQUIRE(!std::filesystem::exists(secret_path));
auto relative_path = start_gloved(temp.root(), "audit.key", journal_path);
auto relative_status = relative_path.wait_for_exit();
REQUIRE(relative_status.has_value());
REQUIRE(WIFEXITED(*relative_status));
REQUIRE(WEXITSTATUS(*relative_status) != 0);
REQUIRE(!std::filesystem::exists(secret_path));
auto first = start_gloved(temp.root(), key_path, journal_path);
REQUIRE(wait_until_ready(first, socket_path, secret_path));
const auto first_secret = read_secret(secret_path);
REQUIRE(first_secret.has_value());
struct stat metadata{};
REQUIRE(::lstat(secret_path.c_str(), &metadata) == 0);
REQUIRE(S_ISREG(metadata.st_mode));
REQUIRE((metadata.st_mode & 0777U) == 0600U);
auto competing = start_gloved(temp.root(), key_path, journal_path);
auto competing_status = competing.wait_for_exit();
REQUIRE(competing_status.has_value());
REQUIRE(WIFEXITED(*competing_status));
REQUIRE(WEXITSTATUS(*competing_status) != 0);
const auto unchanged_secret = read_secret(secret_path);
REQUIRE(unchanged_secret == first_secret);
REQUIRE(first.running());
REQUIRE(page_and_acknowledge(socket_path, *first_secret, **genesis));
REQUIRE(first.kill_hard());
REQUIRE(std::filesystem::exists(socket_path));
auto second = start_gloved(temp.root(), key_path, journal_path);
REQUIRE(wait_until_secret_changes(second, socket_path, secret_path, *first_secret));
const auto second_secret = read_secret(secret_path);
REQUIRE(second_secret.has_value());
REQUIRE(*second_secret != *first_secret);
REQUIRE(page_and_acknowledge(socket_path, *second_secret, **genesis));
REQUIRE(second.stop());
REQUIRE(!std::filesystem::exists(socket_path));
auto third = start_gloved(temp.root(), key_path, journal_path);
REQUIRE(wait_until_secret_changes(third, socket_path, secret_path, *second_secret));
const auto third_secret = read_secret(secret_path);
REQUIRE(third_secret.has_value());
REQUIRE(*third_secret != *second_secret);
REQUIRE(page_and_acknowledge(socket_path, *third_secret, **genesis));
REQUIRE(third.stop());
REQUIRE(!std::filesystem::exists(socket_path));
const auto config_path = temp.root() / "gloved.json";
const auto exposure_root = temp.root() / "projects";
const auto exposure_project = exposure_root / "sage-protocol";
REQUIRE(std::filesystem::create_directories(exposure_project));
const auto exposure_policy_path = temp.root() / "path-exposure-policy.json";
const auto exposure_journal_path = temp.root() / "path-exposures.journal";
REQUIRE(write_owner_only(exposure_policy_path, exposure_policy_json(exposure_root)));
const glove::host::config host_config{
.runtime_directory = temp.root(),
.audit_key = key_path,
.receipt_journal = journal_path,
.session_policy = std::nullopt,
.session_store = std::nullopt,
.materialization_root = std::nullopt,
.library_bundle_root = std::nullopt,
.path_exposure_policy = exposure_policy_path,
.path_exposure_journal = exposure_journal_path,
.apple_container = std::nullopt,
.remote_backend = std::nullopt,
.local_service_proxy = std::nullopt,
};
const auto encoded_config = glove::host::encode_config(host_config);
REQUIRE(encoded_config.has_value());
REQUIRE(write_owner_only(config_path, *encoded_config));
auto configured = start_gloved_config(config_path);
REQUIRE(wait_until_secret_changes(configured, socket_path, secret_path, *third_secret));
REQUIRE(wait_until_authenticated(host_config));
auto exposure = glove::host::enroll_project(
host_config,
glove::host::project_enrollment{
.project = exposure_project,
.exposure_id = "sage-protocol",
.root_id = "projects",
.display_label = "Sage protocol",
.access = glove::host::project_access::read,
.max_bytes = 0,
.ttl_secs = 3'600,
.runtime_template_ids = {"codex-safe"},
.idempotency_key = "lifecycle-enrollment",
}
);
REQUIRE(exposure.has_value());
REQUIRE(exposure->generation == 1);
REQUIRE(configured.stop());
REQUIRE(!std::filesystem::exists(socket_path));
const auto plan_source = temp.root() / "plan-source";
REQUIRE(std::filesystem::create_directory(plan_source));
std::ofstream{plan_source / "tracked.txt"} << "host-owned\n";
const auto policy_path = temp.root() / "session-policy.json";
REQUIRE(write_owner_only(policy_path, policy_json(plan_source)));
const auto pre_policy_secret = read_secret(secret_path);
REQUIRE(pre_policy_secret.has_value());
REQUIRE(::chmod(policy_path.c_str(), 0644) == 0);
auto unsafe_policy = start_gloved(temp.root(), key_path, journal_path, policy_path);
auto unsafe_policy_status = unsafe_policy.wait_for_exit();
REQUIRE(unsafe_policy_status.has_value());
REQUIRE(WIFEXITED(*unsafe_policy_status));
REQUIRE(WEXITSTATUS(*unsafe_policy_status) != 0);
REQUIRE(read_secret(secret_path) == pre_policy_secret);
const auto materialization_root = temp.root() / "materializations";
REQUIRE(std::filesystem::create_directory(materialization_root));
REQUIRE(::chmod(materialization_root.c_str(), 0700) == 0);
auto unbound_materialization = start_gloved(
temp.root(), key_path, journal_path, std::nullopt, std::nullopt, materialization_root
);
auto unbound_materialization_status = unbound_materialization.wait_for_exit();
REQUIRE(unbound_materialization_status.has_value());
REQUIRE(WIFEXITED(*unbound_materialization_status));
REQUIRE(WEXITSTATUS(*unbound_materialization_status) != 0);
REQUIRE(read_secret(secret_path) == pre_policy_secret);
REQUIRE(::chmod(policy_path.c_str(), 0600) == 0);
auto colliding_store =
start_gloved(temp.root(), key_path, journal_path, policy_path, journal_path);
auto colliding_store_status = colliding_store.wait_for_exit();
REQUIRE(colliding_store_status.has_value());
REQUIRE(WIFEXITED(*colliding_store_status));
REQUIRE(WEXITSTATUS(*colliding_store_status) != 0);
REQUIRE(read_secret(secret_path) == pre_policy_secret);
const auto library_bundle_root = temp.root() / "library-bundles";
REQUIRE(std::filesystem::create_directory(library_bundle_root));
REQUIRE(::chmod(library_bundle_root.c_str(), 0700) == 0);
auto unbound_library_root = start_gloved(
temp.root(),
key_path,
journal_path,
std::nullopt,
std::nullopt,
std::nullopt,
library_bundle_root
);
auto unbound_library_root_status = unbound_library_root.wait_for_exit();
REQUIRE(unbound_library_root_status.has_value());
REQUIRE(WIFEXITED(*unbound_library_root_status));
REQUIRE(WEXITSTATUS(*unbound_library_root_status) != 0);
REQUIRE(read_secret(secret_path) == pre_policy_secret);
auto policy_enabled = start_gloved(temp.root(), key_path, journal_path, policy_path);
REQUIRE(
wait_until_secret_changes(policy_enabled, socket_path, secret_path, *pre_policy_secret)
);
const auto policy_secret = read_secret(secret_path);
REQUIRE(policy_secret.has_value());
REQUIRE(validate_plan(socket_path, *policy_secret));
REQUIRE(policy_enabled.stop());
REQUIRE(!std::filesystem::exists(socket_path));
const auto session_store = temp.root() / "sessions.journal";
auto session_enabled =
start_gloved(temp.root(), key_path, journal_path, policy_path, session_store);
REQUIRE(wait_until_secret_changes(session_enabled, socket_path, secret_path, *policy_secret));
const auto session_secret = read_secret(secret_path);
REQUIRE(session_secret.has_value());
REQUIRE(create_or_read_session(socket_path, *session_secret, true));
const auto competing_store_path = temp.root() / "competing-sessions.journal";
auto competing_store =
start_gloved(temp.root(), key_path, journal_path, policy_path, competing_store_path);
auto competing_store_status = competing_store.wait_for_exit();
REQUIRE(competing_store_status.has_value());
REQUIRE(WIFEXITED(*competing_store_status));
REQUIRE(WEXITSTATUS(*competing_store_status) != 0);
REQUIRE(!std::filesystem::exists(competing_store_path));
REQUIRE(read_secret(secret_path) == session_secret);
REQUIRE(session_enabled.stop());
REQUIRE(std::filesystem::exists(session_store));
REQUIRE(::chmod(library_bundle_root.c_str(), 0755) == 0);
auto unsafe_library_root = start_gloved(
temp.root(),
key_path,
journal_path,
policy_path,
session_store,
std::nullopt,
library_bundle_root
);
auto unsafe_library_root_status = unsafe_library_root.wait_for_exit();
REQUIRE(unsafe_library_root_status.has_value());
REQUIRE(WIFEXITED(*unsafe_library_root_status));
REQUIRE(WEXITSTATUS(*unsafe_library_root_status) != 0);
REQUIRE(::chmod(library_bundle_root.c_str(), 0700) == 0);
auto library_enabled = start_gloved(
temp.root(),
key_path,
journal_path,
policy_path,
session_store,
std::nullopt,
library_bundle_root
);
REQUIRE(wait_until_secret_changes(library_enabled, socket_path, secret_path, *session_secret));
const auto library_secret = read_secret(secret_path);
REQUIRE(library_secret.has_value());
REQUIRE(create_or_read_session(socket_path, *library_secret, false));
REQUIRE(library_enabled.stop());
const auto pre_unsafe_store_secret = read_secret(secret_path);
REQUIRE(pre_unsafe_store_secret.has_value());
REQUIRE(::chmod(session_store.c_str(), 0644) == 0);
auto unsafe_store =
start_gloved(temp.root(), key_path, journal_path, policy_path, session_store);
auto unsafe_store_status = unsafe_store.wait_for_exit();
REQUIRE(unsafe_store_status.has_value());
REQUIRE(WIFEXITED(*unsafe_store_status));
REQUIRE(WEXITSTATUS(*unsafe_store_status) != 0);
REQUIRE(read_secret(secret_path) == pre_unsafe_store_secret);
REQUIRE(::chmod(session_store.c_str(), 0600) == 0);
auto session_recovered =
start_gloved(temp.root(), key_path, journal_path, policy_path, session_store);
REQUIRE(wait_until_secret_changes(
session_recovered, socket_path, secret_path, *pre_unsafe_store_secret
));
const auto recovered_secret = read_secret(secret_path);
REQUIRE(recovered_secret.has_value());
REQUIRE(create_or_read_session(socket_path, *recovered_secret, false));
REQUIRE(session_recovered.stop());
REQUIRE(!std::filesystem::exists(socket_path));
constexpr std::string_view remote_public_key =
"ssh-ed25519 "
"AAAAC3NzaC1lZDI1NTE5AAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
auto remote_fingerprint = glove::host::openssh_host_key_fingerprint(remote_public_key);
REQUIRE(remote_fingerprint.has_value());
const auto remote_identity = temp.root() / "remote-identity";
REQUIRE(write_owner_only(remote_identity, "test-private-key"));
auto remote_host_config = host_config;
remote_host_config.session_policy = policy_path;
remote_host_config.session_store = session_store;
remote_host_config.path_exposure_policy.reset();
remote_host_config.path_exposure_journal.reset();
remote_host_config.remote_backend = glove::host::remote_backend_config{
.host = "research.example.test",
.user = "glove_remote",
.port = 2222,
.host_public_key = std::string{remote_public_key},
.host_key_fingerprint = *remote_fingerprint,
.identity_file = remote_identity,
.executor_digest = "sha256:" + std::string(64U, 'a'),
.container_image = "registry.example.test/glove/runtime@sha256:" + std::string(64U, 'b'),
.container_image_digest = "sha256:" + std::string(64U, 'b'),
.channel_timeout_ms = 5'000,
.max_clock_skew_ms = 250,
.max_sessions = 4,
.staging_root = "/var/lib/glove-remote/staging",
};
REQUIRE(std::filesystem::remove(config_path));
auto remote_config_json = glove::host::encode_config(remote_host_config);
REQUIRE(remote_config_json.has_value());
REQUIRE(write_owner_only(config_path, *remote_config_json));
auto remote_configured = start_gloved_config(config_path);
REQUIRE(
wait_until_secret_changes(remote_configured, socket_path, secret_path, *recovered_secret)
);
const auto remote_secret = read_secret(secret_path);
REQUIRE(remote_secret.has_value());
auto remote_capabilities = transact(
socket_path, make_request("remote-capabilities", "capabilities", *remote_secret, "null")
);
REQUIRE(remote_capabilities.has_value());
auto remote_capabilities_response = decode_response(*remote_capabilities);
REQUIRE(remote_capabilities_response.has_value());
REQUIRE(remote_capabilities_response->result.has_value());
REQUIRE(
remote_capabilities_response->result->str.find("\"start_session\":false") !=
std::string::npos
);
REQUIRE(
remote_capabilities_response->result->str.find(
"\"agent_runtime_adapter_schema_version\":0"
) != std::string::npos
);
REQUIRE(
remote_capabilities_response->result->str.find("\"managed_runtime_ids\":[]") !=
std::string::npos
);
auto unavailable_start = transact(
socket_path,
make_request("remote-start", "start_session", *remote_secret, "null", "remote-start-1")
);
REQUIRE(unavailable_start.has_value());
auto unavailable_start_response = decode_response(*unavailable_start);
REQUIRE(unavailable_start_response.has_value());
REQUIRE(unavailable_start_response->error.has_value());
REQUIRE(unavailable_start_response->error->code == "method_not_found");
std::ifstream ssh_config{temp.root() / "remote-ssh/config"};
const std::string ssh_contents{
std::istreambuf_iterator<char>{ssh_config}, std::istreambuf_iterator<char>{}
};
REQUIRE(ssh_contents.find(" GlobalKnownHostsFile none\n") != std::string::npos);
REQUIRE(ssh_contents.find(" HostKeyAlgorithms ssh-ed25519\n") != std::string::npos);
REQUIRE(ssh_contents.find(" UpdateHostKeys no\n") != std::string::npos);
REQUIRE(ssh_contents.find(" ControlPath none\n") != std::string::npos);
REQUIRE(ssh_contents.find(" ControlPersist no\n") != std::string::npos);
REQUIRE(remote_configured.running());
REQUIRE(remote_configured.stop());
REQUIRE(!std::filesystem::exists(socket_path));
return 0;
}
auto probe_managed_capabilities(const std::filesystem::path& runtime_directory) -> int {
std::optional<std::string> secret;
for (int attempt = 0; attempt < 300 && !secret; ++attempt) {
secret = read_secret(runtime_directory / "bootstrap-secret");
if (!secret) {
std::this_thread::sleep_for(std::chrono::milliseconds{10});
}
}
REQUIRE(secret.has_value());
auto frame = transact_with_retry(
runtime_directory / "gloved.sock",
make_request("systemd-capabilities", "capabilities", *secret, "null")
);
REQUIRE(frame.has_value());
auto response = decode_response(*frame);
REQUIRE(response.has_value());
REQUIRE(!response->error.has_value());
REQUIRE(response->result.has_value());
const auto& capabilities = response->result->str;
REQUIRE(capabilities.find("\"start_session\":true") != std::string::npos);
REQUIRE(capabilities.find("\"agent_runtime_adapter_schema_version\":1") != std::string::npos);
const auto managed_runtimes = capabilities.find("\"managed_runtime_ids\":[");
const auto managed_runtimes_end = capabilities.find(']', managed_runtimes);
const auto pi_runtime = capabilities.find("\"pi\"", managed_runtimes);
REQUIRE(managed_runtimes != std::string::npos);
REQUIRE(managed_runtimes_end != std::string::npos);
REQUIRE(pi_runtime != std::string::npos);
REQUIRE(pi_runtime < managed_runtimes_end);
REQUIRE(capabilities.find("\"backend\":\"linux_production\"") != std::string::npos);
REQUIRE(capabilities.find("\"cpu_time\":\"cgroup_v2\"") != std::string::npos);
REQUIRE(capabilities.find("\"memory\":\"cgroup_v2\"") != std::string::npos);
REQUIRE(capabilities.find("\"pids\":\"cgroup_v2\"") != std::string::npos);
REQUIRE(capabilities.find("\"receipt_schema_version\":1") != std::string::npos);
return 0;
}
} // namespace
auto main(int argc, char* argv[]) -> int {
if (argc == 1) {
return run();
}
if (argc == 3 && std::string_view{argv[1]} == "--probe-managed-capabilities") {
return probe_managed_capabilities(argv[2]);
}
std::fprintf(stderr, "usage: %s [--probe-managed-capabilities RUNTIME_DIR]\n", argv[0]);
return 2;
}