Compare commits
3 Commits
9a8107ea90
...
c454433448
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c454433448 | ||
| 8feaaa2845 | |||
| 853c644446 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -43,3 +43,4 @@ flake.lock.bak
|
|||||||
.sidecar-start.sh
|
.sidecar-start.sh
|
||||||
.sidecar-base
|
.sidecar-base
|
||||||
.td-root
|
.td-root
|
||||||
|
.pi-lens
|
||||||
|
|||||||
7
.pi-lens/cache/jscpd.json
vendored
7
.pi-lens/cache/jscpd.json
vendored
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"success": true,
|
|
||||||
"clones": [],
|
|
||||||
"duplicatedLines": 0,
|
|
||||||
"totalLines": 0,
|
|
||||||
"percentage": 0
|
|
||||||
}
|
|
||||||
3
.pi-lens/cache/jscpd.meta.json
vendored
3
.pi-lens/cache/jscpd.meta.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"timestamp": "2026-04-15T09:30:34.459Z"
|
|
||||||
}
|
|
||||||
9
.pi-lens/cache/knip.json
vendored
9
.pi-lens/cache/knip.json
vendored
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"success": false,
|
|
||||||
"issues": [],
|
|
||||||
"unusedExports": [],
|
|
||||||
"unusedFiles": [],
|
|
||||||
"unusedDeps": [],
|
|
||||||
"unlistedDeps": [],
|
|
||||||
"summary": "Failed to parse output"
|
|
||||||
}
|
|
||||||
3
.pi-lens/cache/knip.meta.json
vendored
3
.pi-lens/cache/knip.meta.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"timestamp": "2026-04-15T09:30:35.667Z"
|
|
||||||
}
|
|
||||||
1
.pi-lens/cache/session-start-guidance.json
vendored
1
.pi-lens/cache/session-start-guidance.json
vendored
@@ -1 +0,0 @@
|
|||||||
null
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"timestamp": "2026-04-15T09:28:51.987Z"
|
|
||||||
}
|
|
||||||
3
.pi-lens/cache/todo-baseline.json
vendored
3
.pi-lens/cache/todo-baseline.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"items": []
|
|
||||||
}
|
|
||||||
3
.pi-lens/cache/todo-baseline.meta.json
vendored
3
.pi-lens/cache/todo-baseline.meta.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"timestamp": "2026-04-15T09:28:16.965Z"
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"files": {},
|
|
||||||
"turnCycles": 0,
|
|
||||||
"maxCycles": 3,
|
|
||||||
"lastUpdated": "2026-04-15T09:30:35.668Z"
|
|
||||||
}
|
|
||||||
@@ -105,13 +105,6 @@
|
|||||||
${pkgs.alejandra}/bin/alejandra --check ${./.}
|
${pkgs.alejandra}/bin/alejandra --check ${./.}
|
||||||
touch $out
|
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
|
# Templates for creating new packages/modules
|
||||||
|
|||||||
@@ -76,7 +76,10 @@ in {
|
|||||||
|
|
||||||
mcpServers = mkOption {
|
mcpServers = mkOption {
|
||||||
type = types.attrsOf types.anything;
|
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";
|
defaultText = literalExpression "config.programs.mcp.servers";
|
||||||
description = ''
|
description = ''
|
||||||
MCP server configurations for Claude Code.
|
MCP server configurations for Claude Code.
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
{cfg, pkgs, lib, ...}:
|
{
|
||||||
|
cfg,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
with lib; let
|
with lib; let
|
||||||
managedSettingsFile = pkgs.writeText "pi-agent-managed-settings.json" (builtins.toJSON cfg.settings);
|
managedSettingsFile = pkgs.writeText "pi-agent-managed-settings.json" (builtins.toJSON cfg.settings);
|
||||||
|
|
||||||
@@ -55,6 +60,19 @@ pkgs.writeShellScriptBin cfg.wrapper.runnerName ''
|
|||||||
cwd="$1"
|
cwd="$1"
|
||||||
shift
|
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() {
|
resolve_user_policy() {
|
||||||
local user="$1"
|
local user="$1"
|
||||||
USER_CONFIG_PATH=""
|
USER_CONFIG_PATH=""
|
||||||
@@ -348,6 +366,13 @@ pkgs.writeShellScriptBin cfg.wrapper.runnerName ''
|
|||||||
-E PI_AGENT_INVOKING_USER="$invoking_user"
|
-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) ''
|
${optionalString (cfg.projectGroup != null) ''
|
||||||
cmd+=( -p SupplementaryGroups=${cfg.projectGroup} )
|
cmd+=( -p SupplementaryGroups=${cfg.projectGroup} )
|
||||||
''}
|
''}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
{cfg, pkgs, lib, runner, ...}:
|
{
|
||||||
|
cfg,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
runner,
|
||||||
|
...
|
||||||
|
}:
|
||||||
with lib;
|
with lib;
|
||||||
pkgs.writeShellScriptBin cfg.wrapper.commandName ''
|
pkgs.writeShellScriptBin cfg.wrapper.commandName ''
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -88,5 +94,9 @@ pkgs.writeShellScriptBin cfg.wrapper.commandName ''
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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:-}" \
|
||||||
|
"$@"
|
||||||
''
|
''
|
||||||
|
|||||||
@@ -264,6 +264,16 @@ in {
|
|||||||
"d ${cfg.stateDir}/.npm-global/lib 0750 ${cfg.user} ${cfg.group} - -"
|
"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
|
# Wrapper is canonical when enabled; raw package on PATH is optional and
|
||||||
# disabled by default to reduce bypass opportunities.
|
# disabled by default to reduce bypass opportunities.
|
||||||
environment.systemPackages =
|
environment.systemPackages =
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{inputs, ...}: {
|
{inputs, ...}: {
|
||||||
# This one brings our custom packages from the 'pkgs' directory
|
# This one brings our custom packages from the 'pkgs' directory
|
||||||
additions = final: prev:
|
additions = final: prev: (import ../pkgs {pkgs = final;});
|
||||||
(import ../pkgs {pkgs = final;});
|
|
||||||
|
|
||||||
# This one contains whatever you want to overlay
|
# This one contains whatever you want to overlay
|
||||||
# You can change versions, add patches, set compilation flags, anything really.
|
# You can change versions, add patches, set compilation flags, anything really.
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
# Upstream is missing outputHashes for git dependencies
|
# Upstream is missing outputHashes for git dependencies
|
||||||
# Also fix stale npm deps hash in upstream node_modules FOD
|
# Also fix stale npm deps hash in upstream node_modules FOD
|
||||||
fixedNodeModules = opencode.node_modules.overrideAttrs {
|
fixedNodeModules = opencode.node_modules.overrideAttrs {
|
||||||
outputHash = "sha256-LRhPPrOKCGUSCEWTpAxPdWKTKVNkg82WrvD25cP3jts=";
|
outputHash = "sha256-285KZ7rZLRoc6XqCZRHc25NE+mmpGh/BVeMpv8aPQtQ=";
|
||||||
};
|
};
|
||||||
|
|
||||||
opencode-desktop = rustPlatform.buildRustPackage (finalAttrs: {
|
opencode-desktop = rustPlatform.buildRustPackage (finalAttrs: {
|
||||||
|
|||||||
@@ -12,17 +12,14 @@ let
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
in
|
in
|
||||||
assert result.success == false;
|
assert result.success == false; {result = "pass";};
|
||||||
{result = "pass";};
|
|
||||||
|
|
||||||
# Test 2: loadCanonical extracts loadAgents from input
|
# Test 2: loadCanonical extracts loadAgents from input
|
||||||
testLoadCanonical = let
|
testLoadCanonical = let
|
||||||
fakeInput = {lib.loadAgents = {test = {description = "test";};};};
|
fakeInput = {lib.loadAgents = {test = {description = "test";};};};
|
||||||
result = agentsLib.loadCanonical {agentsInput = fakeInput;};
|
result = agentsLib.loadCanonical {agentsInput = fakeInput;};
|
||||||
in
|
in
|
||||||
assert result == {test = {description = "test";};};
|
assert result == {test = {description = "test";};}; {result = "pass";};
|
||||||
{result = "pass";};
|
|
||||||
|
|
||||||
in {
|
in {
|
||||||
unknown-tool-throws = testUnknownTool;
|
unknown-tool-throws = testUnknownTool;
|
||||||
load-canonical = testLoadCanonical;
|
load-canonical = testLoadCanonical;
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ let
|
|||||||
rulesDir = ".coding-rules";
|
rulesDir = ".coding-rules";
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
assert rules.instructions == [
|
assert rules.instructions
|
||||||
|
== [
|
||||||
".coding-rules/concerns/naming.md"
|
".coding-rules/concerns/naming.md"
|
||||||
".coding-rules/languages/python.md"
|
".coding-rules/languages/python.md"
|
||||||
];
|
]; {result = "pass";};
|
||||||
{result = "pass";};
|
|
||||||
|
|
||||||
# Test 2: default rulesDir is .opencode-rules
|
# Test 2: default rulesDir is .opencode-rules
|
||||||
testDefaultRulesDir = let
|
testDefaultRulesDir = let
|
||||||
@@ -24,13 +24,10 @@ let
|
|||||||
};
|
};
|
||||||
hasCorrectPrefix = builtins.all (s: builtins.substring 0 15 s == ".opencode-rules") rules.instructions;
|
hasCorrectPrefix = builtins.all (s: builtins.substring 0 15 s == ".opencode-rules") rules.instructions;
|
||||||
in
|
in
|
||||||
assert hasCorrectPrefix == true;
|
assert hasCorrectPrefix == true; {result = "pass";};
|
||||||
{result = "pass";};
|
|
||||||
|
|
||||||
# Test 3: backward-compat alias exists
|
# Test 3: backward-compat alias exists
|
||||||
testBackwardCompat =
|
testBackwardCompat = assert codingRulesLib.mkOpencodeRules == codingRulesLib.mkCodingRules; {result = "pass";};
|
||||||
assert codingRulesLib.mkOpencodeRules == codingRulesLib.mkCodingRules;
|
|
||||||
{result = "pass";};
|
|
||||||
|
|
||||||
# Test 4: shellHook contains both the symlink command and the config generation
|
# Test 4: shellHook contains both the symlink command and the config generation
|
||||||
testShellHook = let
|
testShellHook = let
|
||||||
@@ -42,9 +39,7 @@ let
|
|||||||
hasConfigGen = builtins.match ".*coding-rules.json.*" hook != null;
|
hasConfigGen = builtins.match ".*coding-rules.json.*" hook != null;
|
||||||
in
|
in
|
||||||
assert hasSymlink;
|
assert hasSymlink;
|
||||||
assert hasConfigGen;
|
assert hasConfigGen; {result = "pass";};
|
||||||
{result = "pass";};
|
|
||||||
|
|
||||||
in {
|
in {
|
||||||
instructions-correct = testInstructions;
|
instructions-correct = testInstructions;
|
||||||
default-rules-dir = testDefaultRulesDir;
|
default-rules-dir = testDefaultRulesDir;
|
||||||
|
|||||||
@@ -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")
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user