Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 60 additions & 3 deletions deployments/agicash-team-forge/configuration.nix
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,17 @@
# 1. Follow docs/secrets-bootstrap.md to generate an age key,
# register it in .sops.yaml, and create the encrypted
# secrets.yaml file containing `team-bot-token`.
# 2. Uncomment the block below: it declares the secret, the
# Discord bot that consumes it, and the first agent that uses
# the bot.
# 2. Uncomment the blocks below: they declare the secret, the
# Discord bot that consumes it, the shared library (skills,
# MCP servers, plugins) the agent draws from, and finally the
# first agent itself with its per-agent whitelist.
# 3. Redeploy. systemd starts `forge-agent-coordinator` on boot;
# the agent joins Discord and responds to @mentions.
#
# The `source` paths below are placeholders pending the migration of
# the v1 skills + dev MCP servers into this repository. See
# docs/per-agent-environment-design.md for the design contract.
#
# sops.secrets."team-bot-token" = {
# owner = "gudnuf";
# };
Expand All @@ -83,6 +88,42 @@
# tokenFile = config.sops.secrets."team-bot-token".path;
# };
#
# # --- Shared library ----------------------------------------------------
# # Skills, MCP servers, and plugins declared once at the top level; each
# # agent references them by name. Names must be unique across each family.
#
# services.forge.skills.discord-tools = {
# source = ../../skills/discord-tools;
# description = "Discord channel/thread/pin ops via REST";
# };
#
# services.forge.mcpServers.mercury = {
# command = "bun";
# args = [ "run" "/srv/forge/plugins/mercury/server.ts" ];
# env = { MERCURY_DB = "/var/lib/mercury/mercury.db"; };
# };
#
# services.forge.mcpServers.playwright = {
# command = "npx";
# args = [
# "@playwright/mcp@latest"
# "--headless"
# "--executable-path" "/run/current-system/sw/bin/chromium"
# "--viewport-size" "390x844"
# "--output-dir" "/srv/forge/browser-output"
# "--save-session"
# ];
# };
#
# services.forge.plugins.discord = {
# # Real claude-code plugin: directory containing
# # `.claude-plugin/plugin.json` plus the plugin's own commands,
# # MCP servers, etc. Loaded into the agent via --plugin-dir.
# source = ../../plugins/discord;
# };
#
# # --- First agent -------------------------------------------------------
#
# services.forge.agents.coordinator = {
# role = ''
# agicash team coordinator — the on-call agent in the team
Expand All @@ -91,6 +132,22 @@
# '';
# runAs = "gudnuf";
# discordBot = "team";
#
# skills = [ "discord-tools" ];
# mcpServers = [ "mercury" "playwright" ];
# plugins = [ "discord" ];
#
# allowedTools = [
# "Bash(git *)"
# "Edit"
# "Read"
# "mcp__mercury__send"
# "mcp__plugin_discord_discord__reply"
# ];
#
# # permissions schema is in place for the future scoped-policy work;
# # today the harness still passes --dangerously-skip-permissions, so
# # the allow/deny lists are advisory.
# };

# --- Nix GC ---
Expand Down
175 changes: 163 additions & 12 deletions modules/agents.nix
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,115 @@ in
'';
example = "gudnuf";
};

# --- Per-agent isolation whitelist (see docs/per-agent-environment-design.md) ---
# Each list references shared library entries by name. The harness
# assembles the per-agent CWD from these whitelists on every start.

skills = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Names of skills (from services.forge.skills) this agent has
access to. Each declared skill is symlinked into the agent's
per-CWD `.claude/skills/<name>` and listed in the agent's
skill-catalog (appended to its system prompt).

Skills not in this list are not invocable and not visible to
the agent's planner.
'';
example = lib.literalExpression ''[ "discord-tools" "verify" ]'';
};

mcpServers = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Names of MCP servers (from services.forge.mcpServers) this
agent has access to. Each declared server is written into the
agent's per-CWD `.mcp.json`; servers not in this list are not
launched and their tools never appear in the model's surface.
'';
example = lib.literalExpression ''[ "playwright" "mercury" ]'';
};

plugins = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Names of claude-code plugins (from services.forge.plugins)
this agent has access to. The harness adds a `--plugin-dir`
flag pointing at /etc/forge/plugin-library/<name>/ for each
entry; the plugin's bundled MCP servers register with the
`mcp__plugin_<name>_<server>__` tool prefix.
'';
example = lib.literalExpression ''[ "discord" ]'';
};

allowedTools = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Tool surface this agent is told it has, in claude-code's
--allowedTools syntax (e.g. "Bash(git *)", "Edit", "Read",
"mcp__plugin_discord_discord__reply"). Controls which tools
the model knows about and emits in tool-call grammar — distinct
from permission gating, which today is bypassed via
--dangerously-skip-permissions.

Empty list means no --allowedTools flag is passed; the model
sees the harness defaults plus any plugin/MCP-contributed tools.
'';
example = lib.literalExpression ''
[ "Bash(git *)" "Edit" "Read" "mcp__mercury__send" ]
'';
};

permissions = lib.mkOption {
type = lib.types.submodule {
options = {
allow = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Tool-call patterns the agent is allowed to invoke without
prompting. Written to the per-agent .claude/settings.json.
Today the harness passes --dangerously-skip-permissions,
so this list is advisory; the schema seam exists for the
scoped-permissions follow-up.
'';
};

deny = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Tool-call patterns the agent must never invoke. Same
caveats as `allow` — schema seam pending the scoped-
permissions follow-up.
'';
};

skipPrompts = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Reserved for the scoped-permissions follow-up. Today the
harness always passes --dangerously-skip-permissions; this
flag has no effect yet but is declared so callers can
start setting it and the harness can flip behavior when
the policy work lands.
'';
};
};
};
default = { };
description = ''
Per-agent permission policy. Schema only at this stage — see
docs/per-agent-environment-design.md "Non-goals" for the
scoped-permissions deferral.
'';
};
};
});
default = { };
Expand All @@ -86,18 +195,60 @@ in

config = lib.mkIf cfg.enable {
# Cross-cutting validation: every agent's runAs must reference a
# declared forge user. Catches typos at evaluation time instead of
# at session-start time.
assertions = lib.mapAttrsToList
(name: agent: {
assertion = builtins.hasAttr agent.runAs cfg.users;
message = ''
services.forge.agents.${name}.runAs = "${agent.runAs}" but
"${agent.runAs}" is not declared in services.forge.users.
Declare the user first, or correct the runAs.
'';
})
cfg.agents;
# declared forge user, and every library reference (skills, mcpServers,
# plugins) must point to a declared library entry. Catches typos at
# evaluation time instead of at session-start time.
assertions =
let
runAsChecks = lib.mapAttrsToList
(name: agent: {
assertion = builtins.hasAttr agent.runAs cfg.users;
message = ''
services.forge.agents.${name}.runAs = "${agent.runAs}" but
"${agent.runAs}" is not declared in services.forge.users.
Declare the user first, or correct the runAs.
'';
})
cfg.agents;

libraryRefChecks = lib.concatLists (lib.mapAttrsToList
(name: agent:
(map
(skill: {
assertion = builtins.hasAttr skill cfg.skills;
message = ''
services.forge.agents.${name}.skills references "${skill}"
but "${skill}" is not declared in services.forge.skills.
Declare the skill in the library first, or correct the
reference.
'';
})
agent.skills)
++ (map
(server: {
assertion = builtins.hasAttr server cfg.mcpServers;
message = ''
services.forge.agents.${name}.mcpServers references
"${server}" but "${server}" is not declared in
services.forge.mcpServers. Declare the server in the
library first, or correct the reference.
'';
})
agent.mcpServers)
++ (map
(plugin: {
assertion = builtins.hasAttr plugin cfg.plugins;
message = ''
services.forge.agents.${name}.plugins references
"${plugin}" but "${plugin}" is not declared in
services.forge.plugins. Declare the plugin in the
library first, or correct the reference.
'';
})
agent.plugins))
cfg.agents);
in
runAsChecks ++ libraryRefChecks;

# Implementation (wrapper scripts, systemd services, harness wiring)
# lives in sibling modules — kept here to declarations only so the
Expand Down
3 changes: 3 additions & 0 deletions modules/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
./discord.nix
./agents.nix
./secrets.nix
./skills.nix
./mcp-servers.nix
./plugins.nix
./harnesses/claude-code.nix
];

Expand Down
Loading