720 lines
34 KiB
C++
720 lines
34 KiB
C++
#include "glove/host/runtime_policy.hpp"
|
|
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#include <algorithm>
|
|
#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-runtime-policy-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 write_owner_file(const std::filesystem::path& path, std::string_view contents, mode_t mode)
|
|
-> bool {
|
|
std::ofstream output{path};
|
|
output << contents;
|
|
output.close();
|
|
return output.good() && ::chmod(path.c_str(), mode) == 0;
|
|
}
|
|
|
|
auto run() -> int {
|
|
using namespace glove::host;
|
|
|
|
temporary_directory temporary;
|
|
REQUIRE(!temporary.root().empty());
|
|
const auto harness_bin = temporary.root() / "harness-bin";
|
|
const auto dependencies = temporary.root() / "runtime-dependencies";
|
|
const auto workspace = temporary.root() / "workspace";
|
|
REQUIRE(std::filesystem::create_directory(harness_bin));
|
|
REQUIRE(std::filesystem::create_directory(dependencies));
|
|
REQUIRE(std::filesystem::create_directory(workspace));
|
|
REQUIRE(::chmod(harness_bin.c_str(), 0700) == 0);
|
|
|
|
const auto codex = harness_bin / "codex";
|
|
REQUIRE(write_owner_file(codex, "#!/bin/sh\nexit 0\n", 0700));
|
|
|
|
const auto detected = detect_runtime_harnesses({harness_bin});
|
|
REQUIRE(detected.size() == 5U);
|
|
const auto codex_detection = std::ranges::find_if(detected, [](const auto& candidate) {
|
|
return candidate.runtime_id == "codex";
|
|
});
|
|
REQUIRE(codex_detection != detected.end());
|
|
REQUIRE(codex_detection->available);
|
|
REQUIRE(codex_detection->resolved_executable == codex);
|
|
const auto claude_detection = std::ranges::find_if(detected, [](const auto& candidate) {
|
|
return candidate.runtime_id == "claude-code";
|
|
});
|
|
REQUIRE(claude_detection != detected.end());
|
|
REQUIRE(!claude_detection->available);
|
|
REQUIRE(claude_detection->diagnostic.find("expected executable") != std::string::npos);
|
|
|
|
const auto staged_directory = temporary.root() / "protected" / "codex";
|
|
runtime_harness_stage_options stage_options{
|
|
.runtime_id = "codex",
|
|
.source_executable = codex,
|
|
.protected_directory = staged_directory,
|
|
.dry_run = true,
|
|
};
|
|
auto dry_stage = stage_runtime_harness(stage_options);
|
|
REQUIRE(dry_stage.has_value());
|
|
REQUIRE(!dry_stage->changed);
|
|
REQUIRE(!std::filesystem::exists(staged_directory));
|
|
stage_options.dry_run = false;
|
|
auto staged = stage_runtime_harness(stage_options);
|
|
if (!staged) {
|
|
std::fprintf(stderr, "staging failed: %s\n", staged.error().c_str());
|
|
}
|
|
REQUIRE(staged.has_value());
|
|
REQUIRE(staged->changed);
|
|
REQUIRE(std::filesystem::is_symlink(staged->protected_entry_point));
|
|
auto repeated_stage = stage_runtime_harness(stage_options);
|
|
REQUIRE(repeated_stage.has_value());
|
|
REQUIRE(!repeated_stage->changed);
|
|
const auto staged_detection = detect_runtime_harnesses({staged_directory});
|
|
REQUIRE(staged_detection.front().runtime_id == "codex");
|
|
REQUIRE(staged_detection.front().available);
|
|
REQUIRE(staged_detection.front().resolved_executable == staged_directory / "codex");
|
|
|
|
const auto managed_runtime = temporary.root() / "managed-node";
|
|
const auto managed_bin = managed_runtime / "bin";
|
|
const auto managed_package = managed_runtime / "lib" / "node_modules" / "vendor" / "bin";
|
|
REQUIRE(std::filesystem::create_directories(managed_bin));
|
|
REQUIRE(std::filesystem::create_directories(managed_package));
|
|
// Launch-trust tests must not inherit the operator's umask. These
|
|
// directories model an owner-only managed runtime closure.
|
|
for (const auto& directory : {
|
|
managed_runtime,
|
|
managed_bin,
|
|
managed_runtime / "lib",
|
|
managed_runtime / "lib" / "node_modules",
|
|
managed_runtime / "lib" / "node_modules" / "vendor",
|
|
managed_package,
|
|
}) {
|
|
REQUIRE(::chmod(directory.c_str(), 0700) == 0);
|
|
}
|
|
const auto managed_node = managed_bin / "node";
|
|
const auto managed_script = managed_package / "codex.js";
|
|
REQUIRE(write_owner_file(managed_node, "#!/bin/sh\nexit 0\n", 0700));
|
|
REQUIRE(write_owner_file(managed_script, "#!/usr/bin/env node\n", 0700));
|
|
REQUIRE(write_owner_file(managed_runtime / "empty-package-marker", "", 0600));
|
|
const auto managed_entry = managed_bin / "codex";
|
|
REQUIRE(::symlink("../lib/node_modules/vendor/bin/codex.js", managed_entry.c_str()) == 0);
|
|
auto managed_stage_options = stage_options;
|
|
managed_stage_options.source_executable = managed_entry;
|
|
managed_stage_options.protected_directory = temporary.root() / "protected" / "managed-codex";
|
|
managed_stage_options.dry_run = true;
|
|
auto managed_stage = stage_runtime_harness(managed_stage_options);
|
|
REQUIRE(managed_stage.has_value());
|
|
if (managed_stage->launch_executable != std::filesystem::canonical(managed_node)) {
|
|
std::fprintf(
|
|
stderr,
|
|
"managed interpreter mismatch: got=%s expected=%s\n",
|
|
managed_stage->launch_executable.c_str(),
|
|
std::filesystem::canonical(managed_node).c_str()
|
|
);
|
|
}
|
|
REQUIRE(managed_stage->launch_executable == std::filesystem::canonical(managed_node));
|
|
REQUIRE(
|
|
managed_stage->launch_arguments ==
|
|
std::vector<std::string>{std::filesystem::canonical(managed_script).string()}
|
|
);
|
|
REQUIRE(
|
|
managed_stage->read_only_paths ==
|
|
std::vector<std::filesystem::path>{std::filesystem::canonical(managed_runtime)}
|
|
);
|
|
|
|
REQUIRE(::chmod(managed_runtime.c_str(), 0770) == 0);
|
|
auto snapshot_stage_options = managed_stage_options;
|
|
snapshot_stage_options.protected_directory =
|
|
temporary.root() / "protected" / "snapshotted-codex";
|
|
auto snapshot_dry_run = stage_runtime_harness(snapshot_stage_options);
|
|
REQUIRE(snapshot_dry_run.has_value());
|
|
REQUIRE(snapshot_dry_run->snapshot_digest.size() == 64U);
|
|
REQUIRE(snapshot_dry_run->snapshot_logical_bytes > 0U);
|
|
REQUIRE(snapshot_dry_run->snapshot_entries > 3U);
|
|
REQUIRE(
|
|
snapshot_dry_run->launch_executable.string().find(
|
|
"snapshots/" + snapshot_dry_run->snapshot_digest + "/payload"
|
|
) != std::string::npos
|
|
);
|
|
REQUIRE(!std::filesystem::exists(snapshot_stage_options.protected_directory));
|
|
snapshot_stage_options.dry_run = false;
|
|
auto snapshot_stage = stage_runtime_harness(snapshot_stage_options);
|
|
if (!snapshot_stage) {
|
|
std::fprintf(stderr, "snapshot staging failed: %s\n", snapshot_stage.error().c_str());
|
|
}
|
|
REQUIRE(snapshot_stage.has_value());
|
|
REQUIRE(snapshot_stage->changed);
|
|
REQUIRE(snapshot_stage->snapshot_digest == snapshot_dry_run->snapshot_digest);
|
|
REQUIRE(snapshot_stage->launch_executable != std::filesystem::canonical(managed_node));
|
|
REQUIRE(std::filesystem::is_regular_file(snapshot_stage->launch_executable));
|
|
REQUIRE(
|
|
std::filesystem::canonical(snapshot_stage->protected_entry_point) !=
|
|
std::filesystem::canonical(managed_script)
|
|
);
|
|
struct stat snapshot_metadata{};
|
|
REQUIRE(::stat(snapshot_stage->read_only_paths.front().c_str(), &snapshot_metadata) == 0);
|
|
REQUIRE((snapshot_metadata.st_mode & (S_IWUSR | S_IWGRP | S_IWOTH)) == 0);
|
|
struct stat snapshot_root_metadata{};
|
|
REQUIRE(
|
|
::stat(
|
|
snapshot_stage->read_only_paths.front().parent_path().c_str(), &snapshot_root_metadata
|
|
) == 0
|
|
);
|
|
REQUIRE((snapshot_root_metadata.st_mode & 0777U) == 0500U);
|
|
auto repeated_snapshot = stage_runtime_harness(snapshot_stage_options);
|
|
REQUIRE(repeated_snapshot.has_value());
|
|
REQUIRE(!repeated_snapshot->changed);
|
|
const auto snapshot_root = snapshot_stage->read_only_paths.front().parent_path();
|
|
REQUIRE(::chmod(snapshot_root.c_str(), 0700) == 0);
|
|
auto unprotected_snapshot = stage_runtime_harness(snapshot_stage_options);
|
|
REQUIRE(!unprotected_snapshot.has_value());
|
|
REQUIRE(unprotected_snapshot.error().find("not owner-protected") != std::string::npos);
|
|
REQUIRE(::chmod(snapshot_root.c_str(), 0500) == 0);
|
|
const auto old_snapshot_target =
|
|
std::filesystem::canonical(snapshot_stage->protected_entry_point);
|
|
{
|
|
std::ofstream changed_marker{managed_runtime / "empty-package-marker"};
|
|
changed_marker << "v2\n";
|
|
REQUIRE(changed_marker.good());
|
|
}
|
|
auto upgraded_snapshot = stage_runtime_harness(snapshot_stage_options);
|
|
REQUIRE(upgraded_snapshot.has_value());
|
|
REQUIRE(upgraded_snapshot->changed);
|
|
REQUIRE(upgraded_snapshot->snapshot_digest != snapshot_stage->snapshot_digest);
|
|
REQUIRE(
|
|
std::filesystem::canonical(upgraded_snapshot->protected_entry_point) != old_snapshot_target
|
|
);
|
|
auto repeated_upgrade = stage_runtime_harness(snapshot_stage_options);
|
|
REQUIRE(repeated_upgrade.has_value());
|
|
REQUIRE(!repeated_upgrade->changed);
|
|
|
|
const auto split_runtime = temporary.root() / "split-runtime";
|
|
const auto split_package = temporary.root() / "split-package";
|
|
REQUIRE(std::filesystem::create_directory(split_runtime));
|
|
REQUIRE(std::filesystem::create_directories(split_package / "bin"));
|
|
const auto split_interpreter = split_runtime / "node";
|
|
const auto split_entry = split_package / "bin" / "codex";
|
|
REQUIRE(write_owner_file(split_interpreter, "#!/bin/sh\nexit 0\n", 0700));
|
|
REQUIRE(write_owner_file(split_package / "package.json", "{}\n", 0600));
|
|
REQUIRE(write_owner_file(split_entry, "#!" + split_interpreter.string() + "\n", 0700));
|
|
REQUIRE(::chmod(split_runtime.c_str(), 0770) == 0);
|
|
auto split_stage_options = stage_options;
|
|
split_stage_options.source_executable = split_entry;
|
|
split_stage_options.protected_directory =
|
|
temporary.root() / "protected" / "split-runtime-codex";
|
|
split_stage_options.dry_run = false;
|
|
auto split_stage = stage_runtime_harness(split_stage_options);
|
|
if (!split_stage) {
|
|
std::fprintf(stderr, "split-root snapshot failed: %s\n", split_stage.error().c_str());
|
|
}
|
|
REQUIRE(split_stage.has_value());
|
|
REQUIRE(split_stage->snapshot_digest.size() == 64U);
|
|
REQUIRE(split_stage->launch_executable != std::filesystem::canonical(split_interpreter));
|
|
REQUIRE(std::filesystem::is_regular_file(split_stage->launch_executable));
|
|
REQUIRE(split_stage->launch_arguments.size() == 1U);
|
|
REQUIRE(std::filesystem::is_regular_file(split_stage->launch_arguments.front()));
|
|
REQUIRE(split_stage->read_only_paths.size() == 1U);
|
|
REQUIRE(split_stage->launch_executable.string().starts_with(
|
|
split_stage->read_only_paths.front().string()
|
|
));
|
|
REQUIRE(split_stage->launch_arguments.front().starts_with(
|
|
split_stage->read_only_paths.front().string()
|
|
));
|
|
|
|
// Pi adoption must treat the explicitly selected package-manager closure
|
|
// as discovery input only: even an otherwise owner-trusted source is
|
|
// snapshotted, and a nearby host Pi home never becomes launch authority.
|
|
const auto pi_runtime = temporary.root() / "pi-runtime";
|
|
const auto pi_package = pi_runtime / "lib" / "node_modules" / "@pi" / "agent";
|
|
const auto host_pi_home = temporary.root() / "host-pi-home";
|
|
REQUIRE(std::filesystem::create_directories(pi_package / "bin"));
|
|
REQUIRE(std::filesystem::create_directories(host_pi_home / "agent" / "sessions"));
|
|
REQUIRE(write_owner_file(pi_package / "bin" / "node", "#!/bin/sh\nexit 0\n", 0700));
|
|
REQUIRE(write_owner_file(pi_package / "package.json", "{}\n", 0600));
|
|
REQUIRE(write_owner_file(pi_package / "bin" / "pi", "#!/usr/bin/env node\n", 0700));
|
|
REQUIRE(
|
|
write_owner_file(host_pi_home / "agent" / "auth.json", R"({\"token\":\"host-only\"})", 0600)
|
|
);
|
|
REQUIRE(write_owner_file(
|
|
host_pi_home / "agent" / "settings.json", R"({\"packages\":[\"host-extension\"]})", 0600
|
|
));
|
|
REQUIRE(write_owner_file(host_pi_home / "agent" / "sessions" / "active.json", "{}\n", 0600));
|
|
const auto pi_entry = pi_package / "bin" / "pi";
|
|
auto pi_stage_options = stage_options;
|
|
pi_stage_options.runtime_id = "pi";
|
|
pi_stage_options.source_executable = pi_entry;
|
|
pi_stage_options.protected_directory = temporary.root() / "protected" / "pi";
|
|
pi_stage_options.dry_run = true;
|
|
auto pi_dry_stage = stage_runtime_harness(pi_stage_options);
|
|
if (!pi_dry_stage) {
|
|
std::fprintf(stderr, "Pi dry staging failed: %s\n", pi_dry_stage.error().c_str());
|
|
}
|
|
REQUIRE(pi_dry_stage.has_value());
|
|
REQUIRE(pi_dry_stage->snapshot_digest.size() == 64U);
|
|
REQUIRE(pi_dry_stage->adoption_manifest_digest.size() == 64U);
|
|
REQUIRE(pi_dry_stage->launch_executable.string().find("snapshots/") != std::string::npos);
|
|
REQUIRE(pi_dry_stage->read_only_paths.size() == 1U);
|
|
REQUIRE(
|
|
pi_dry_stage->read_only_paths.front().string().find(host_pi_home.string()) ==
|
|
std::string::npos
|
|
);
|
|
REQUIRE(!std::filesystem::exists(pi_stage_options.protected_directory));
|
|
pi_stage_options.dry_run = false;
|
|
auto pi_stage = stage_runtime_harness(pi_stage_options);
|
|
REQUIRE(pi_stage.has_value());
|
|
REQUIRE(pi_stage->snapshot_digest == pi_dry_stage->snapshot_digest);
|
|
REQUIRE(pi_stage->adoption_manifest_digest == pi_dry_stage->adoption_manifest_digest);
|
|
REQUIRE(
|
|
std::filesystem::canonical(pi_stage->protected_entry_point) !=
|
|
std::filesystem::canonical(pi_package / "bin" / "pi")
|
|
);
|
|
REQUIRE(
|
|
!std::filesystem::exists(pi_stage->read_only_paths.front() / "host-pi-home/agent/auth.json")
|
|
);
|
|
|
|
// Explicit Pi configuration is discovery input only. The generated
|
|
// sandbox settings retain only selected package closures at private-home
|
|
// relative paths; host auth/settings/sessions never enter the manifest.
|
|
const auto pi_settings = host_pi_home / "agent" / "adoption-settings.json";
|
|
const auto pi_store = temporary.root() / "pi-package-store";
|
|
const auto pi_extension = pi_store / "example-extension";
|
|
const auto pi_dependency = pi_store / "dependency-extension";
|
|
REQUIRE(std::filesystem::create_directories(pi_extension));
|
|
REQUIRE(std::filesystem::create_directories(pi_dependency));
|
|
REQUIRE(write_owner_file(pi_settings, R"({"packages":["npm:example-extension"]})", 0600));
|
|
REQUIRE(write_owner_file(
|
|
pi_extension / "package.json",
|
|
R"({"name":"example-extension","dependencies":{"dependency-extension":"1.0.0"}})",
|
|
0600
|
|
));
|
|
REQUIRE(write_owner_file(pi_extension / "index.js", "export default {};\n", 0600));
|
|
REQUIRE(
|
|
write_owner_file(pi_dependency / "package.json", R"({"name":"dependency-extension"})", 0600)
|
|
);
|
|
REQUIRE(write_owner_file(pi_dependency / "index.js", "export default {};\n", 0600));
|
|
pi_adoption_manifest_options pi_manifest_options{
|
|
.settings_path = pi_settings,
|
|
.package_store_root = pi_store,
|
|
.protected_directory = temporary.root() / "protected" / "pi-manifest",
|
|
.dry_run = true,
|
|
};
|
|
auto pi_manifest = generate_pi_adoption_manifest(pi_manifest_options);
|
|
if (!pi_manifest) {
|
|
std::fprintf(stderr, "Pi manifest generation failed: %s\n", pi_manifest.error().c_str());
|
|
}
|
|
REQUIRE(pi_manifest.has_value());
|
|
REQUIRE(pi_manifest->manifest_digest.size() == 64U);
|
|
REQUIRE(pi_manifest->snapshot_digest.size() == 64U);
|
|
const std::vector<std::string> expected_pi_packages{
|
|
"dependency-extension", "example-extension"
|
|
};
|
|
REQUIRE(pi_manifest->package_ids == expected_pi_packages);
|
|
REQUIRE(pi_manifest->generated_settings_json.find("./extensions/0") != std::string::npos);
|
|
REQUIRE(pi_manifest->generated_settings_json.find("host-only") == std::string::npos);
|
|
REQUIRE(!std::filesystem::exists(pi_manifest_options.protected_directory));
|
|
pi_manifest_options.dry_run = false;
|
|
auto applied_pi_manifest = generate_pi_adoption_manifest(pi_manifest_options);
|
|
if (!applied_pi_manifest) {
|
|
std::fprintf(stderr, "Apply Pi manifest failed: %s\n", applied_pi_manifest.error().c_str());
|
|
}
|
|
REQUIRE(applied_pi_manifest.has_value());
|
|
REQUIRE(applied_pi_manifest->changed);
|
|
REQUIRE(applied_pi_manifest->manifest_digest == pi_manifest->manifest_digest);
|
|
REQUIRE(std::filesystem::is_directory(applied_pi_manifest->snapshot_root / "payload"));
|
|
REQUIRE(std::filesystem::is_regular_file(applied_pi_manifest->manifest_path));
|
|
struct stat pi_manifest_metadata{};
|
|
REQUIRE(::lstat(applied_pi_manifest->manifest_path.c_str(), &pi_manifest_metadata) == 0);
|
|
REQUIRE((static_cast<unsigned int>(pi_manifest_metadata.st_mode) & 0777U) == 0600U);
|
|
struct stat pi_snapshot_metadata{};
|
|
REQUIRE(
|
|
::stat((applied_pi_manifest->snapshot_root / "payload").c_str(), &pi_snapshot_metadata) == 0
|
|
);
|
|
REQUIRE((pi_snapshot_metadata.st_mode & (S_IWUSR | S_IWGRP | S_IWOTH)) == 0);
|
|
auto repeated_pi_manifest = generate_pi_adoption_manifest(pi_manifest_options);
|
|
REQUIRE(repeated_pi_manifest.has_value());
|
|
REQUIRE(!repeated_pi_manifest->changed);
|
|
|
|
const auto missing_interpreter = managed_package / "missing.js";
|
|
REQUIRE(write_owner_file(missing_interpreter, "#!/usr/bin/env missing-runtime\n", 0700));
|
|
managed_stage_options.source_executable = missing_interpreter;
|
|
REQUIRE(!stage_runtime_harness(managed_stage_options).has_value());
|
|
|
|
runtime_policy_generation_options options{
|
|
.runtime_id = "codex",
|
|
.runtime_template_id = "codex-safe",
|
|
.backend = glove::supervisor::sandbox_backend::linux_production,
|
|
.executable_path = {},
|
|
.executable_search_paths = {harness_bin},
|
|
.arguments = {"--version"},
|
|
.environment = {"TERM=xterm-256color", "PATH=/usr/bin:/bin"},
|
|
.read_only_paths = {dependencies},
|
|
.allowed_path_aliases = {"workspace"},
|
|
.allowed_projection_destinations = {"libraries"},
|
|
};
|
|
auto generated = generate_runtime_policy(options);
|
|
if (!generated) {
|
|
std::fprintf(stderr, "generation failed: %s\n", generated.error().c_str());
|
|
}
|
|
REQUIRE(generated.has_value());
|
|
REQUIRE(generated->resolved_executable == std::filesystem::canonical(codex));
|
|
REQUIRE(generated->adapter_command_digest.size() == 64U);
|
|
REQUIRE(
|
|
generated->policy_template_json.find("\"runtime_discovery\":\"codex\"") != std::string::npos
|
|
);
|
|
REQUIRE(
|
|
generated->policy_template_json.find(
|
|
"\"environment\":[\"PATH=/usr/bin:/bin\",\"TERM=xterm-256color\"]"
|
|
) != std::string::npos
|
|
);
|
|
|
|
auto pinned_options = options;
|
|
pinned_options.executable_path = codex;
|
|
pinned_options.executable_search_paths.clear();
|
|
auto pinned = generate_runtime_policy(pinned_options);
|
|
REQUIRE(pinned.has_value());
|
|
REQUIRE(pinned->policy_template_json.find("\"runtime_discovery\":\"\"") != std::string::npos);
|
|
REQUIRE(
|
|
pinned->policy_template_json.find(
|
|
"\"executable_path\":\"" + std::filesystem::canonical(codex).string() + "\""
|
|
) != std::string::npos
|
|
);
|
|
auto conflicting_options = pinned_options;
|
|
conflicting_options.executable_search_paths = {harness_bin};
|
|
auto conflicting = generate_runtime_policy(conflicting_options);
|
|
REQUIRE(!conflicting.has_value());
|
|
REQUIRE(conflicting.error().find("mutually exclusive") != std::string::npos);
|
|
|
|
auto runtime_fragment = generated->policy_template_json;
|
|
REQUIRE(!runtime_fragment.empty() && runtime_fragment.back() == '\n');
|
|
runtime_fragment.pop_back();
|
|
const std::string policy =
|
|
R"({"schema_version":1,"revision":1,"max_plan_ttl_ms":120000,"runtime_templates":[)" +
|
|
runtime_fragment + R"(],"path_aliases":[{"alias":"workspace","host_path":")" +
|
|
std::filesystem::canonical(workspace).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":[]})";
|
|
const auto policy_path = temporary.root() / "session-policy.json";
|
|
REQUIRE(write_owner_file(policy_path, policy, 0600));
|
|
auto valid = validate_session_policy_file(std::filesystem::canonical(policy_path));
|
|
if (!valid) {
|
|
std::fprintf(stderr, "validation failed: %s\n", valid.error().c_str());
|
|
}
|
|
REQUIRE(valid.has_value());
|
|
|
|
auto impossible_ttl = policy;
|
|
const auto ttl_offset = impossible_ttl.find("\"max_plan_ttl_ms\":120000");
|
|
REQUIRE(ttl_offset != std::string::npos);
|
|
impossible_ttl.replace(
|
|
ttl_offset,
|
|
std::string_view{"\"max_plan_ttl_ms\":120000"}.size(),
|
|
"\"max_plan_ttl_ms\":2000"
|
|
);
|
|
const auto impossible_ttl_path = temporary.root() / "impossible-ttl-policy.json";
|
|
REQUIRE(write_owner_file(impossible_ttl_path, impossible_ttl, 0600));
|
|
auto impossible_ttl_result =
|
|
validate_session_policy_file(std::filesystem::canonical(impossible_ttl_path));
|
|
REQUIRE(!impossible_ttl_result.has_value());
|
|
REQUIRE(impossible_ttl_result.error().find("TTL must outlive") != std::string::npos);
|
|
|
|
auto mismatched = policy;
|
|
const auto digest_offset = mismatched.find(generated->adapter_command_digest);
|
|
REQUIRE(digest_offset != std::string::npos);
|
|
mismatched.replace(digest_offset, generated->adapter_command_digest.size(), 64U, 'a');
|
|
const auto invalid_path = temporary.root() / "invalid-policy.json";
|
|
REQUIRE(write_owner_file(invalid_path, mismatched, 0600));
|
|
auto invalid = validate_session_policy_file(std::filesystem::canonical(invalid_path));
|
|
REQUIRE(!invalid.has_value());
|
|
REQUIRE(invalid.error().find("runtime_templates[0]") != std::string::npos);
|
|
REQUIRE(invalid.error().find("adapter_command_digest mismatch") != std::string::npos);
|
|
|
|
auto unsupported = options;
|
|
unsupported.runtime_id = "unknown";
|
|
auto unsupported_result = generate_runtime_policy(unsupported);
|
|
REQUIRE(!unsupported_result.has_value());
|
|
REQUIRE(unsupported_result.error().find("unsupported runtime adapter") != std::string::npos);
|
|
|
|
const auto prepared_harness_root = temporary.root() / "prepared-harnesses";
|
|
const auto prepared_policy_path = temporary.root() / "policy" / "session-policy.json";
|
|
session_policy_prepare_options prepare_options{
|
|
.executable_search_paths = {harness_bin},
|
|
.protected_harness_root = prepared_harness_root,
|
|
.workspace_root = workspace,
|
|
.policy_path = prepared_policy_path,
|
|
.backend = glove::supervisor::sandbox_backend::linux_production,
|
|
.egress_policies = {},
|
|
.secret_mounts = {},
|
|
.selected_runtime_ids = {},
|
|
.pi_adoption = std::nullopt,
|
|
.hostile_content_analysis = false,
|
|
.dry_run = true,
|
|
};
|
|
auto prepared_dry_run = prepare_session_policy(prepare_options);
|
|
if (!prepared_dry_run) {
|
|
std::fprintf(
|
|
stderr, "policy preparation dry run failed: %s\n", prepared_dry_run.error().c_str()
|
|
);
|
|
}
|
|
REQUIRE(prepared_dry_run.has_value());
|
|
REQUIRE(prepared_dry_run->dry_run);
|
|
REQUIRE(prepared_dry_run->changed);
|
|
REQUIRE(prepared_dry_run->runtimes.size() == 1U);
|
|
REQUIRE(prepared_dry_run->runtimes.front().runtime_id == "codex");
|
|
REQUIRE(
|
|
prepared_dry_run->policy_json.find("\"egress_policy_ids\":[\"no-network\"]") !=
|
|
std::string::npos
|
|
);
|
|
REQUIRE(prepared_dry_run->policy_json.find("\"max_plan_ttl_ms\":600000") != std::string::npos);
|
|
REQUIRE(!std::filesystem::exists(prepared_harness_root));
|
|
REQUIRE(!std::filesystem::exists(prepared_policy_path));
|
|
|
|
auto hostile_options = prepare_options;
|
|
hostile_options.hostile_content_analysis = true;
|
|
auto prepared_hostile = prepare_session_policy(hostile_options);
|
|
REQUIRE(prepared_hostile.has_value());
|
|
REQUIRE(
|
|
prepared_hostile->policy_json.find("\"runtime_template_id\":\"codex-hostile-analysis\"") !=
|
|
std::string::npos
|
|
);
|
|
REQUIRE(
|
|
prepared_hostile->policy_json.find("\"egress_policy_ids\":[\"no-network\"]") !=
|
|
std::string::npos
|
|
);
|
|
REQUIRE(prepared_hostile->policy_json.find("\"access\":\"read\"") == std::string::npos);
|
|
REQUIRE(
|
|
prepared_hostile->policy_json.find("\"access\":\"retained_write\"") == std::string::npos
|
|
);
|
|
REQUIRE(
|
|
prepared_hostile->policy_json.find(
|
|
R"("profile_id":"hostile-analysis","cpu_time_ms":30000,"memory_bytes":536870912,"pids":64,"wall_time_ms":60000,"disk_bytes":134217728,"terminal_output_bytes":1048576)"
|
|
) != std::string::npos
|
|
);
|
|
auto hostile_apple = hostile_options;
|
|
hostile_apple.backend = glove::supervisor::sandbox_backend::apple_container;
|
|
REQUIRE(!prepare_session_policy(hostile_apple).has_value());
|
|
|
|
const auto pi_harness_bin = temporary.root() / "pi-harness-bin";
|
|
REQUIRE(std::filesystem::create_directory(pi_harness_bin));
|
|
REQUIRE(::chmod(pi_harness_bin.c_str(), 0700) == 0);
|
|
std::error_code pi_harness_error;
|
|
std::filesystem::create_symlink(pi_entry, pi_harness_bin / "pi", pi_harness_error);
|
|
REQUIRE(!pi_harness_error);
|
|
std::filesystem::create_symlink(
|
|
pi_package / "bin" / "node", pi_harness_bin / "node", pi_harness_error
|
|
);
|
|
REQUIRE(!pi_harness_error);
|
|
auto pi_prepare_options = prepare_options;
|
|
pi_prepare_options.executable_search_paths = {pi_harness_bin};
|
|
pi_prepare_options.protected_harness_root = temporary.root() / "prepared-pi-harnesses";
|
|
pi_prepare_options.policy_path = temporary.root() / "policy" / "pi-session-policy.json";
|
|
pi_prepare_options.selected_runtime_ids = {"pi"};
|
|
pi_prepare_options.pi_adoption = pi_manifest_options;
|
|
pi_prepare_options.dry_run = true;
|
|
auto prepared_pi = prepare_session_policy(pi_prepare_options);
|
|
if (!prepared_pi) {
|
|
std::fprintf(stderr, "Pi policy preparation failed: %s\n", prepared_pi.error().c_str());
|
|
}
|
|
REQUIRE(prepared_pi.has_value());
|
|
REQUIRE(
|
|
prepared_pi->policy_json.find(
|
|
"\"manifest_digest\":\"" + pi_manifest->manifest_digest + "\""
|
|
) != std::string::npos
|
|
);
|
|
REQUIRE(
|
|
prepared_pi->policy_json.find(
|
|
"\"snapshot_digest\":\"" + pi_manifest->snapshot_digest + "\""
|
|
) != std::string::npos
|
|
);
|
|
REQUIRE(prepared_pi->policy_json.find(host_pi_home.string()) == std::string::npos);
|
|
REQUIRE(prepared_pi->policy_json.find("host-only") == std::string::npos);
|
|
pi_prepare_options.dry_run = false;
|
|
auto applied_pi_policy = prepare_session_policy(pi_prepare_options);
|
|
REQUIRE(applied_pi_policy.has_value());
|
|
REQUIRE(std::filesystem::is_regular_file(pi_prepare_options.policy_path));
|
|
REQUIRE(
|
|
validate_session_policy_file(std::filesystem::canonical(pi_prepare_options.policy_path))
|
|
);
|
|
auto missing_pi_adoption = pi_prepare_options;
|
|
missing_pi_adoption.pi_adoption.reset();
|
|
REQUIRE(!prepare_session_policy(missing_pi_adoption).has_value());
|
|
auto unused_pi_adoption = prepare_options;
|
|
unused_pi_adoption.pi_adoption = pi_manifest_options;
|
|
REQUIRE(!prepare_session_policy(unused_pi_adoption).has_value());
|
|
|
|
const auto codex_auth = temporary.root() / "codex-auth.json";
|
|
REQUIRE(write_owner_file(codex_auth, R"({"token":"test-only"})", 0600));
|
|
auto online_options = prepare_options;
|
|
online_options.egress_policies = {{
|
|
.policy_id = "openai-online",
|
|
.targets = {
|
|
{.host = "api.openai.com", .port = 443, .allow_private = false},
|
|
{.host = "chatgpt.com", .port = 443, .allow_private = false},
|
|
{.host = "auth.openai.com", .port = 443, .allow_private = false},
|
|
{.host = "ab.chatgpt.com", .port = 443, .allow_private = false},
|
|
},
|
|
}};
|
|
online_options.secret_mounts = {{
|
|
.handle = "codex-auth",
|
|
.runtime_id = "codex",
|
|
.source_path = codex_auth.string(),
|
|
.target_path = "/home/agent/.codex/auth.json",
|
|
}};
|
|
auto prepared_online = prepare_session_policy(online_options);
|
|
REQUIRE(prepared_online.has_value());
|
|
auto hostile_online = online_options;
|
|
hostile_online.hostile_content_analysis = true;
|
|
REQUIRE(!prepare_session_policy(hostile_online).has_value());
|
|
REQUIRE(
|
|
prepared_online->policy_json.find(
|
|
"\"egress_policy_ids\":[\"no-network\",\"openai-online\"]"
|
|
) != std::string::npos
|
|
);
|
|
REQUIRE(prepared_online->policy_json.find("\"pids\":256") != std::string::npos);
|
|
REQUIRE(
|
|
prepared_online->policy_json.find(
|
|
R"("profile_id":"interactive","cpu_time_ms":120000,"memory_bytes":1073741824,"pids":256,"wall_time_ms":300000)"
|
|
) != std::string::npos
|
|
);
|
|
REQUIRE(
|
|
prepared_online->policy_json.find(
|
|
R"("secret_handles":["codex-auth"],"egress_policies":[{"policy_id":"openai-online")"
|
|
) != std::string::npos
|
|
);
|
|
REQUIRE(
|
|
prepared_online->policy_json.find(R"("target_path":"/home/agent/.codex/auth.json")") !=
|
|
std::string::npos
|
|
);
|
|
REQUIRE(prepared_online->policy_json.find(R"("token":"test-only")") == std::string::npos);
|
|
|
|
auto insecure_online_options = online_options;
|
|
REQUIRE(::chmod(codex_auth.c_str(), 0644) == 0);
|
|
REQUIRE(!prepare_session_policy(insecure_online_options).has_value());
|
|
REQUIRE(::chmod(codex_auth.c_str(), 0600) == 0);
|
|
|
|
prepare_options.dry_run = false;
|
|
auto prepared = prepare_session_policy(prepare_options);
|
|
if (!prepared) {
|
|
std::fprintf(stderr, "policy preparation failed: %s\n", prepared.error().c_str());
|
|
}
|
|
REQUIRE(prepared.has_value());
|
|
REQUIRE(prepared->changed);
|
|
REQUIRE(std::filesystem::is_symlink(prepared_harness_root / "codex" / "codex"));
|
|
REQUIRE(validate_session_policy_file(prepared_policy_path).has_value());
|
|
struct stat prepared_policy_metadata{};
|
|
REQUIRE(::lstat(prepared_policy_path.c_str(), &prepared_policy_metadata) == 0);
|
|
REQUIRE((static_cast<unsigned int>(prepared_policy_metadata.st_mode) & 0777U) == 0600U);
|
|
auto repeated_preparation = prepare_session_policy(prepare_options);
|
|
REQUIRE(repeated_preparation.has_value());
|
|
REQUIRE(!repeated_preparation->changed);
|
|
|
|
auto revised_options = prepare_options;
|
|
revised_options.egress_policies = {{
|
|
.policy_id = "revision-test-online",
|
|
.targets = {{.host = "example.com", .port = 443, .allow_private = false}},
|
|
}};
|
|
revised_options.dry_run = true;
|
|
auto revised_dry_run = prepare_session_policy(revised_options);
|
|
REQUIRE(revised_dry_run.has_value());
|
|
REQUIRE(revised_dry_run->changed);
|
|
REQUIRE(revised_dry_run->policy_json.find(R"("revision":2)") != std::string::npos);
|
|
{
|
|
std::ifstream unchanged_policy{prepared_policy_path};
|
|
const std::string contents{
|
|
std::istreambuf_iterator<char>{unchanged_policy},
|
|
std::istreambuf_iterator<char>{},
|
|
};
|
|
REQUIRE(contents.find(R"("revision":1)") != std::string::npos);
|
|
}
|
|
revised_options.dry_run = false;
|
|
auto revised = prepare_session_policy(revised_options);
|
|
REQUIRE(revised.has_value());
|
|
REQUIRE(revised->changed);
|
|
REQUIRE(revised->policy_json.find(R"("revision":2)") != std::string::npos);
|
|
auto repeated_revision = prepare_session_policy(revised_options);
|
|
REQUIRE(repeated_revision.has_value());
|
|
REQUIRE(!repeated_revision->changed);
|
|
REQUIRE(repeated_revision->policy_json.find(R"("revision":2)") != std::string::npos);
|
|
|
|
auto snapshotted_prepare_options = prepare_options;
|
|
snapshotted_prepare_options.executable_search_paths = {managed_bin};
|
|
snapshotted_prepare_options.selected_runtime_ids = {"codex"};
|
|
snapshotted_prepare_options.protected_harness_root =
|
|
temporary.root() / "prepared-snapshotted-harnesses";
|
|
snapshotted_prepare_options.policy_path =
|
|
temporary.root() / "snapshotted-policy" / "session-policy.json";
|
|
snapshotted_prepare_options.dry_run = true;
|
|
auto snapshotted_prepare_dry_run = prepare_session_policy(snapshotted_prepare_options);
|
|
if (!snapshotted_prepare_dry_run) {
|
|
std::fprintf(
|
|
stderr,
|
|
"snapshotted policy dry run failed: %s\n",
|
|
snapshotted_prepare_dry_run.error().c_str()
|
|
);
|
|
}
|
|
REQUIRE(snapshotted_prepare_dry_run.has_value());
|
|
REQUIRE(snapshotted_prepare_dry_run->runtimes.size() == 1U);
|
|
REQUIRE(snapshotted_prepare_dry_run->runtimes.front().snapshot_digest.size() == 64U);
|
|
REQUIRE(
|
|
snapshotted_prepare_dry_run->policy_json.find(
|
|
snapshotted_prepare_dry_run->runtimes.front().snapshot_digest
|
|
) != std::string::npos
|
|
);
|
|
snapshotted_prepare_options.dry_run = false;
|
|
auto snapshotted_prepare = prepare_session_policy(snapshotted_prepare_options);
|
|
if (!snapshotted_prepare) {
|
|
std::fprintf(
|
|
stderr,
|
|
"snapshotted policy preparation failed: %s\n",
|
|
snapshotted_prepare.error().c_str()
|
|
);
|
|
}
|
|
REQUIRE(snapshotted_prepare.has_value());
|
|
REQUIRE(snapshotted_prepare->changed);
|
|
REQUIRE(validate_session_policy_file(snapshotted_prepare_options.policy_path).has_value());
|
|
REQUIRE(
|
|
std::filesystem::canonical(
|
|
snapshotted_prepare_options.protected_harness_root / "codex" / "codex"
|
|
) != std::filesystem::canonical(managed_script)
|
|
);
|
|
|
|
return 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
auto main() -> int {
|
|
return run();
|
|
}
|