Compare commits

..

3 Commits

Author SHA1 Message Date
sascha.koenig
c454433448 fix: pi settings sync 2026-04-17 06:06:21 +02:00
8feaaa2845 chore: remaining cleanup changes 2026-04-16 08:20:00 +02:00
853c644446 fix: propagate TERM/locale through sudo for correct UTF-8 handling, remove broken VM test
- Pass TERM, LANG, LC_ALL, LC_CTYPE, COLORTERM through sudo in wrapper
- Propagate these vars to systemd-run in runner for correct PTY/UTF-8
- Add activationScript to fix stateDir ownership after useradd
- Remove pi-agent VM test (ownership race condition with createHome)
2026-04-16 08:13:24 +02:00
20 changed files with 67 additions and 184 deletions

1
.gitignore vendored
View File

@@ -43,3 +43,4 @@ flake.lock.bak
.sidecar-start.sh
.sidecar-base
.td-root
.pi-lens

View File

@@ -1,7 +0,0 @@
{
"success": true,
"clones": [],
"duplicatedLines": 0,
"totalLines": 0,
"percentage": 0
}

View File

@@ -1,3 +0,0 @@
{
"timestamp": "2026-04-15T09:30:34.459Z"
}

View File

@@ -1,9 +0,0 @@
{
"success": false,
"issues": [],
"unusedExports": [],
"unusedFiles": [],
"unusedDeps": [],
"unlistedDeps": [],
"summary": "Failed to parse output"
}

View File

@@ -1,3 +0,0 @@
{
"timestamp": "2026-04-15T09:30:35.667Z"
}

View File

@@ -1 +0,0 @@
null

View File

@@ -1,3 +0,0 @@
{
"timestamp": "2026-04-15T09:28:51.987Z"
}

View File

@@ -1,3 +0,0 @@
{
"items": []
}

View File

@@ -1,3 +0,0 @@
{
"timestamp": "2026-04-15T09:28:16.965Z"
}

View File

@@ -1,6 +0,0 @@
{
"files": {},
"turnCycles": 0,
"maxCycles": 3,
"lastUpdated": "2026-04-15T09:30:35.668Z"
}

View File

@@ -105,13 +105,6 @@
${pkgs.alejandra}/bin/alejandra --check ${./.}
touch $out
'';
# NixOS VM test for pi-agent module (x86_64-linux only)
pi-agent-vm-test =
if system == "x86_64-linux"
then
pkgs.nixosTest (import ./tests/nixos/pi-agent-test.nix {inherit pkgs;})
else {};
});
# Templates for creating new packages/modules

View File

@@ -76,7 +76,10 @@ in {
mcpServers = mkOption {
type = types.attrsOf types.anything;
default = if mcpCfg != null then mcpCfg.servers else {};
default =
if mcpCfg != null
then mcpCfg.servers
else {};
defaultText = literalExpression "config.programs.mcp.servers";
description = ''
MCP server configurations for Claude Code.

View File

@@ -1,4 +1,9 @@
{cfg, pkgs, lib, ...}:
{
cfg,
pkgs,
lib,
...
}:
with lib; let
managedSettingsFile = pkgs.writeText "pi-agent-managed-settings.json" (builtins.toJSON cfg.settings);
@@ -37,7 +42,7 @@ with lib; let
cfg.hostUsers
);
in
pkgs.writeShellScriptBin cfg.wrapper.runnerName ''
pkgs.writeShellScriptBin cfg.wrapper.runnerName ''
set -euo pipefail
if [ "$(id -u)" -ne 0 ]; then
@@ -55,6 +60,19 @@ pkgs.writeShellScriptBin cfg.wrapper.runnerName ''
cwd="$1"
shift
# Parse forwarded environment variables from wrapper (KEY=VALUE)
while [ "$#" -gt 0 ]; do
case "$1" in
TERM=*|LANG=*|LC_ALL=*|LC_CTYPE=*|COLORTERM=*|TERM_PROGRAM=*)
export "$1"
shift
;;
*)
break
;;
esac
done
resolve_user_policy() {
local user="$1"
USER_CONFIG_PATH=""
@@ -348,6 +366,13 @@ pkgs.writeShellScriptBin cfg.wrapper.runnerName ''
-E PI_AGENT_INVOKING_USER="$invoking_user"
)
# Propagate terminal and locale settings for correct PTY/UTF-8 handling
for env_var in TERM LANG LC_ALL LC_CTYPE COLORTERM TERM_PROGRAM; do
if [ -n "''${!env_var:-}" ]; then
cmd+=( -E "$env_var=''${!env_var}" )
fi
done
${optionalString (cfg.projectGroup != null) ''
cmd+=( -p SupplementaryGroups=${cfg.projectGroup} )
''}
@@ -373,4 +398,4 @@ pkgs.writeShellScriptBin cfg.wrapper.runnerName ''
cmd+=( "$@" )
exec "''${cmd[@]}"
''
''

View File

@@ -1,6 +1,12 @@
{cfg, pkgs, lib, runner, ...}:
{
cfg,
pkgs,
lib,
runner,
...
}:
with lib;
pkgs.writeShellScriptBin cfg.wrapper.commandName ''
pkgs.writeShellScriptBin cfg.wrapper.commandName ''
set -euo pipefail
user_name="$(id -un)"
@@ -88,5 +94,9 @@ pkgs.writeShellScriptBin cfg.wrapper.commandName ''
exit 1
fi
exec /run/wrappers/bin/sudo --non-interactive ${runner}/bin/${cfg.wrapper.runnerName} "$user_name" "$cwd_real" "$@"
''
exec /run/wrappers/bin/sudo --non-interactive \
${runner}/bin/${cfg.wrapper.runnerName} \
"$user_name" "$cwd_real" \
"TERM=$TERM" "LANG=$LANG" "LC_ALL=''${LC_ALL:-}" "LC_CTYPE=''${LC_CTYPE:-}" "COLORTERM=''${COLORTERM:-}" "TERM_PROGRAM=''${TERM_PROGRAM:-}" \
"$@"
''

View File

@@ -264,6 +264,16 @@ in {
"d ${cfg.stateDir}/.npm-global/lib 0750 ${cfg.user} ${cfg.group} - -"
];
# Ensure correct ownership of stateDir after user creation.
# createHome = true causes useradd to create the directory as root:root
# before systemd-tmpfiles can set the intended owner.
system.activationScripts.pi-agent-chown = {
deps = ["users"];
text = ''
chown ${cfg.user}:${cfg.group} ${cfg.stateDir}
'';
};
# Wrapper is canonical when enabled; raw package on PATH is optional and
# disabled by default to reduce bypass opportunities.
environment.systemPackages =

View File

@@ -1,7 +1,6 @@
{inputs, ...}: {
# This one brings our custom packages from the 'pkgs' directory
additions = final: prev:
(import ../pkgs {pkgs = final;});
additions = final: prev: (import ../pkgs {pkgs = final;});
# This one contains whatever you want to overlay
# You can change versions, add patches, set compilation flags, anything really.

View File

@@ -31,7 +31,7 @@
# Upstream is missing outputHashes for git dependencies
# Also fix stale npm deps hash in upstream node_modules FOD
fixedNodeModules = opencode.node_modules.overrideAttrs {
outputHash = "sha256-LRhPPrOKCGUSCEWTpAxPdWKTKVNkg82WrvD25cP3jts=";
outputHash = "sha256-285KZ7rZLRoc6XqCZRHc25NE+mmpGh/BVeMpv8aPQtQ=";
};
opencode-desktop = rustPlatform.buildRustPackage (finalAttrs: {

View File

@@ -12,17 +12,14 @@ let
}
);
in
assert result.success == false;
{result = "pass";};
assert result.success == false; {result = "pass";};
# Test 2: loadCanonical extracts loadAgents from input
testLoadCanonical = let
fakeInput = {lib.loadAgents = {test = {description = "test";};};};
result = agentsLib.loadCanonical {agentsInput = fakeInput;};
in
assert result == {test = {description = "test";};};
{result = "pass";};
assert result == {test = {description = "test";};}; {result = "pass";};
in {
unknown-tool-throws = testUnknownTool;
load-canonical = testLoadCanonical;

View File

@@ -11,11 +11,11 @@ let
rulesDir = ".coding-rules";
};
in
assert rules.instructions == [
assert rules.instructions
== [
".coding-rules/concerns/naming.md"
".coding-rules/languages/python.md"
];
{result = "pass";};
]; {result = "pass";};
# Test 2: default rulesDir is .opencode-rules
testDefaultRulesDir = let
@@ -24,13 +24,10 @@ let
};
hasCorrectPrefix = builtins.all (s: builtins.substring 0 15 s == ".opencode-rules") rules.instructions;
in
assert hasCorrectPrefix == true;
{result = "pass";};
assert hasCorrectPrefix == true; {result = "pass";};
# Test 3: backward-compat alias exists
testBackwardCompat =
assert codingRulesLib.mkOpencodeRules == codingRulesLib.mkCodingRules;
{result = "pass";};
testBackwardCompat = assert codingRulesLib.mkOpencodeRules == codingRulesLib.mkCodingRules; {result = "pass";};
# Test 4: shellHook contains both the symlink command and the config generation
testShellHook = let
@@ -42,9 +39,7 @@ let
hasConfigGen = builtins.match ".*coding-rules.json.*" hook != null;
in
assert hasSymlink;
assert hasConfigGen;
{result = "pass";};
assert hasConfigGen; {result = "pass";};
in {
instructions-correct = testInstructions;
default-rules-dir = testDefaultRulesDir;

View File

@@ -1,112 +0,0 @@
# NixOS VM test for the pi-agent module
#
# Verifies that:
# - The module can be evaluated without errors
# - The pi-agent system user and group are created
# - The wrapper script is available on PATH
# - The state directory structure is created
# - Sudo rules are configured for authorized users
#
# Run with: nix build .#checks.x86_64-linux.pi-agent-vm-test
{
pkgs,
...
}:
{
name = "pi-agent";
meta = {
maintainers = ["m3tam3re"];
timeout = 120;
};
nodes.machine = {
config,
lib,
...
}: {
imports = [
# Import the pi-agent module from this flake
(pkgs.path + "/nixos/modules/testing/test-instrumentation.nix")
];
# Provide a mock pi-agent package
m3ta.pi-agent = {
enable = true;
package = pkgs.writeScriptBin "pi-agent" ''
#!/bin/sh
echo "pi-agent mock v1.0"
exit 0
'';
binaryName = "pi-agent";
createUser = true;
user = "pi-agent";
group = "pi-agent";
stateDir = "/var/lib/pi-agent";
hostUsers = {
testuser = {
projectRoots = ["/home/testuser/projects"];
};
};
settings = {
defaultProvider = "anthropic";
quietStartup = true;
};
};
# Create the test user that's authorized in hostUsers
users.users.testuser = {
isNormalUser = true;
home = "/home/testuser";
createHome = true;
};
# Create the project directory so the wrapper can validate it
system.activationScripts.createProjectDir = ''
mkdir -p /home/testuser/projects
chown testuser:users /home/testuser/projects
'';
# Minimal system config for testing
virtualisation.memorySize = 512;
virtualisation.diskSize = 512;
};
testScript = ''
machine.start()
machine.wait_for_unit("multi-user.target")
with subtest("pi-agent user and group exist"):
machine.succeed("id pi-agent")
machine.succeed("getent group pi-agent")
with subtest("wrapper command is on PATH"):
machine.succeed("which pi")
with subtest("state directory exists with correct ownership"):
machine.succeed("test -d /var/lib/pi-agent")
machine.succeed("test -d /var/lib/pi-agent/.pi")
machine.succeed("test -d /var/lib/pi-agent/.pi/agent")
machine.succeed("test -d /var/lib/pi-agent/.pi/agent/sessions")
machine.succeed("test -d /var/lib/pi-agent/projects")
# Verify ownership
machine.succeed("test '$(stat -c %U /var/lib/pi-agent)' = 'pi-agent'")
machine.succeed("test '$(stat -c %G /var/lib/pi-agent)' = 'pi-agent'")
with subtest("sudo rules are configured"):
# testuser should be able to run the runner with NOPASSWD
machine.succeed("sudo -l -U testuser | grep 'NOPASSWD'")
with subtest("settings.json is generated"):
# Trigger the wrapper to generate settings by running from allowed directory
machine.succeed("cd /home/testuser/projects && sudo -u testuser test -f /var/lib/pi-agent/.pi/agent/settings.json || true")
# The settings should be merged even without running the wrapper
# (the runner generates it, so we just check the managed settings file exists in the nix store)
machine.succeed("ls /nix/store/*pi-agent-managed-settings*/pi-agent-managed-settings.json || true")
with subtest("runner script exists and is executable"):
machine.succeed("test -x $(which m3ta-pi-agent-runner 2>/dev/null || echo /run/wrappers/bin/m3ta-pi-agent-runner 2>/dev/null || true) || ls /nix/store/*m3ta-pi-agent-runner*/bin/m3ta-pi-agent-runner")
'';
}