125 lines
3.7 KiB
C++
125 lines
3.7 KiB
C++
// Direct-agent defaults must not silently expose the caller's current
|
|
// directory, place glove-managed state in the selected workspace, or let an
|
|
// agent tamper with its own audit trail.
|
|
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#ifndef GLOVE_BIN
|
|
# error "GLOVE_BIN must point at the glove executable"
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
#define REQUIRE(cond) \
|
|
do { \
|
|
if (!(cond)) { \
|
|
std::fprintf(stderr, "REQUIRE failed: %s @ %s:%d\n", #cond, __FILE__, __LINE__); \
|
|
return 1; \
|
|
} \
|
|
} while (0)
|
|
|
|
auto run_glove(std::vector<std::string> argv_owned) -> int {
|
|
std::vector<char*> argv;
|
|
argv.reserve(argv_owned.size() + 1);
|
|
for (auto& value : argv_owned) {
|
|
argv.push_back(value.data());
|
|
}
|
|
argv.push_back(nullptr);
|
|
|
|
const ::pid_t child = ::fork();
|
|
if (child < 0) {
|
|
return -1;
|
|
}
|
|
if (child == 0) {
|
|
::execv(GLOVE_BIN, argv.data());
|
|
std::_Exit(127);
|
|
}
|
|
int status = 0;
|
|
if (::waitpid(child, &status, 0) != child) {
|
|
return -1;
|
|
}
|
|
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
|
|
}
|
|
|
|
auto run() -> int {
|
|
std::error_code ec;
|
|
const auto base =
|
|
std::filesystem::temp_directory_path() / ("glove_perimeter_" + std::to_string(::getpid()));
|
|
const auto workspace = base / "workspace";
|
|
const auto marker =
|
|
std::filesystem::current_path() / ("glove-caller-secret-" + std::to_string(::getpid()));
|
|
std::filesystem::create_directories(workspace, ec);
|
|
REQUIRE(!ec);
|
|
{
|
|
std::ofstream out{marker};
|
|
out << "not for the agent\n";
|
|
REQUIRE(out.good());
|
|
}
|
|
|
|
// With no --workspace or --read, even an absolute path passed in argv is
|
|
// not readable. The command exits 42 if the boundary is too broad.
|
|
REQUIRE(
|
|
run_glove(
|
|
{GLOVE_BIN,
|
|
"exec",
|
|
"--",
|
|
"/bin/sh",
|
|
"-c",
|
|
"test ! -r \"$1\" || exit 42",
|
|
"glove-test",
|
|
marker.string()}
|
|
) == 0
|
|
);
|
|
|
|
// HOME/TMPDIR are private runtime state, not hidden mutations inside the
|
|
// user's project tree.
|
|
REQUIRE(
|
|
run_glove(
|
|
{GLOVE_BIN,
|
|
"exec",
|
|
"--workspace",
|
|
workspace.string(),
|
|
"--",
|
|
"/bin/sh",
|
|
"-c",
|
|
"case \"$HOME\" in \"$1\"/*) exit 42;; esac",
|
|
"glove-test",
|
|
workspace.string()}
|
|
) == 0
|
|
);
|
|
REQUIRE(!std::filesystem::exists(workspace / ".glove-home"));
|
|
|
|
// An append-only audit destination cannot sit below any path granted to
|
|
// the agent, even if the operator names it explicitly.
|
|
const auto exposed_audit = workspace / "audit.jsonl";
|
|
REQUIRE(
|
|
run_glove(
|
|
{GLOVE_BIN,
|
|
"exec",
|
|
"--workspace",
|
|
workspace.string(),
|
|
"--audit-log",
|
|
exposed_audit.string(),
|
|
"--",
|
|
"/usr/bin/true"}
|
|
) == 1
|
|
);
|
|
REQUIRE(!std::filesystem::exists(exposed_audit));
|
|
|
|
std::filesystem::remove(marker, ec);
|
|
std::filesystem::remove_all(base, ec);
|
|
return 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
auto main() -> int {
|
|
return run();
|
|
}
|