- .forgejo/workflows/ci.yaml is Crafter.Build's workflow for this app: the base installed from the Catcrafts repo, the version stamped into the crafter-review module, the suite inside the build, then a smoke test of the app as it ships - the version and home, every library resolved and no RUNPATH outside /app, the bundled tools, a freeze and a dry run on a scratch repository. Publishing holds /srv/flatpak/.publish.lock and writes only this app's flatpakref. Unverified until a crafter-flatpak runner serves this repository. - `run flatpak --deps=local` builds against the Crafter.Network and Crafter.Thread checkouts next to this one, as a native --local build does, while their commits are not on forgejo: a gitignored manifest copy with three dir sources side by side and --local. - LensTools: every program the lens allowlist names, the shell and the MCP server's node and npx resolve on PATH; strict in the Flatpak build, where the suite runs in the sandbox that ships, a skip natively when this machine lacks one. Built on the stable base (Crafter.Build 3.0.0) with --deps=local: the suite passes inside flatpak-builder (25 passed, 2 skipped), and the workflow's smoke test passes against that build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
42 lines
2.1 KiB
C++
42 lines
2.1 KiB
C++
// SPDX-License-Identifier: AGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
// The programs a lens is allowed to run are there: every Bash(<program>
|
|
// ...:*) entry of the base allowlist (the git verbs included), the shell
|
|
// claude's Bash tool runs them with, and node and npx for the Playwright MCP
|
|
// server. The suite runs inside the Flatpak build, the sandbox that ships, so
|
|
// there every one must resolve on PATH; natively the test names what this
|
|
// machine lacks and skips.
|
|
|
|
#include <unistd.h>
|
|
import std;
|
|
import Crafter.Review;
|
|
namespace fs = std::filesystem;
|
|
using namespace Crafter::Review;
|
|
|
|
int main() {
|
|
std::println("lens tools: every program a lens's allowlist names is on PATH");
|
|
std::set<std::string> programs = {"bash", "node", "npx"};
|
|
for (const std::string& tool : Tools::BaseAllow()) {
|
|
if (!tool.starts_with("Bash(") || !tool.ends_with(":*)")) continue;
|
|
const std::string prefix = tool.substr(5, tool.size() - 8);
|
|
const std::string program = prefix.substr(0, prefix.find(' '));
|
|
// A shell builtin, never a file of its own.
|
|
if (program != "cd") programs.insert(program);
|
|
}
|
|
const std::vector<std::string> path = Dyn::Split(Platform::GetEnv("PATH").value_or(""), ":");
|
|
std::vector<std::string> missing;
|
|
for (const std::string& program : programs) {
|
|
const bool found = std::ranges::any_of(path, [&](const std::string& dir) { return !dir.empty() && access((fs::path(dir) / program).c_str(), X_OK) == 0; });
|
|
if (!found) missing.push_back(program);
|
|
}
|
|
const bool shipped = fs::exists("/.flatpak-info");
|
|
if (!missing.empty() && !shipped) {
|
|
std::println("skipped: not on this machine's PATH: {}", Dyn::Join(missing, ", "));
|
|
return 77;
|
|
}
|
|
const bool ok = missing.empty() && programs.size() >= 20;
|
|
std::println("{}every program a lens's allowlist names, its shell and the MCP server's node and npx resolve on PATH ({} programs){}", ok ? " ok " : " FAIL ", programs.size(),
|
|
missing.empty() ? std::string() : std::format(" (missing: {})", Dyn::Join(missing, ", ")));
|
|
return ok ? 0 : 1;
|
|
}
|