Repository navigation
feat: add paperclip service #1342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
3499876
0f3c2d7
bd9a978
f09d68e
4979bba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,7 @@ in | |
| ./llm | ||
| ./openclaw | ||
| ./omp | ||
| ./paperclip | ||
| ./opencode | ||
| ./pi | ||
| ./serena | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,42 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| config, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pkgs, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| inputs, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ... | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| inherit (inputs) host; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| homeDir = config.home.homeDirectory; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| instanceDir = "${homeDir}/.paperclip/instances/default"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| configJson = builtins.toJSON { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| database = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if host.isKyber then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mode = "postgres"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| connectionString = "postgres://postgres:postgres@localhost:5432/paperclip"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mode = "embedded-postgres"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| server = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| host = if host.isKyber then "0.0.0.0" else "127.0.0.1"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| host = if host.isKyber then "0.0.0.0" else "127.0.0.1"; | |
| host = "127.0.0.1"; |
Copilot
AI
Apr 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
connectionString (including username/password) is being written via pkgs.writeText, which places the full JSON (credentials included) in the Nix store where it is generally world-readable. If these credentials are meant to be secret (or even just to avoid baking defaults into the store), move them to an out-of-store secret (e.g., ${instanceDir}/.env/age/sops) and have the config reference env vars or omit the password for local trust/auth.
| configJson = builtins.toJSON { | |
| database = | |
| if host.isKyber then | |
| { | |
| mode = "postgres"; | |
| connectionString = "postgres://postgres:postgres@localhost:5432/paperclip"; | |
| } | |
| else | |
| { | |
| mode = "embedded-postgres"; | |
| }; | |
| server = { | |
| host = if host.isKyber then "0.0.0.0" else "127.0.0.1"; | |
| port = 3100; | |
| }; | |
| }; | |
| configFile = pkgs.writeText "paperclip-config.json" configJson; | |
| setupScript = pkgs.replaceVars ./setup.sh { | |
| instance_dir = instanceDir; | |
| config_file = "${configFile}"; | |
| cp = "${pkgs.coreutils}/bin/cp"; | |
| is_kyber = if host.isKyber then "true" else "false"; | |
| }; | |
| in | |
| { | |
| home.activation.paperclipConfig = config.lib.dag.entryAfter [ "writeBoundary" ] '' | |
| configFile = "${instanceDir}/config.json"; | |
| setupScript = pkgs.replaceVars ./setup.sh { | |
| instance_dir = instanceDir; | |
| config_file = configFile; | |
| cp = "${pkgs.coreutils}/bin/cp"; | |
| is_kyber = if host.isKyber then "true" else "false"; | |
| }; | |
| in | |
| { | |
| home.activation.paperclipConfig = config.lib.dag.entryAfter [ "writeBoundary" ] '' | |
| ${pkgs.coreutils}/bin/mkdir -p "${instanceDir}" | |
| ${ | |
| if host.isKyber then | |
| '' | |
| if [ -z "''${PAPERCLIP_DATABASE_URL:-}" ]; then | |
| echo "paperclip: PAPERCLIP_DATABASE_URL must be set to create ${configFile}" >&2 | |
| exit 1 | |
| fi | |
| cat > "${configFile}" <<EOF | |
| { | |
| "database": { | |
| "mode": "postgres", | |
| "connectionString": "''${PAPERCLIP_DATABASE_URL}" | |
| }, | |
| "server": { | |
| "host": "0.0.0.0", | |
| "port": 3100 | |
| } | |
| } | |
| EOF | |
| '' | |
| else | |
| '' | |
| cat > "${configFile}" <<EOF | |
| { | |
| "database": { | |
| "mode": "embedded-postgres" | |
| }, | |
| "server": { | |
| "host": "127.0.0.1", | |
| "port": 3100 | |
| } | |
| } | |
| EOF | |
| '' | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Silent failure suppression may hide critical setup errors.
The || true suffix causes all errors from the setup script to be silently ignored. This could mask failures like:
- Database creation errors
- Permission issues
- Missing config file
Consider removing || true or adding explicit error handling with logging.
🔧 Suggested fix with error logging
home.activation.paperclipConfig = config.lib.dag.entryAfter [ "writeBoundary" ] ''
- ${pkgs.bash}/bin/bash ${setupScript} || true
+ ${pkgs.bash}/bin/bash ${setupScript} || echo "Warning: Paperclip setup encountered an error" >&2
'';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| home.activation.paperclipConfig = config.lib.dag.entryAfter [ "writeBoundary" ] '' | |
| ${pkgs.bash}/bin/bash ${setupScript} || true | |
| ''; | |
| home.activation.paperclipConfig = config.lib.dag.entryAfter [ "writeBoundary" ] '' | |
| ${pkgs.bash}/bin/bash ${setupScript} || echo "Warning: Paperclip setup encountered an error" >&2 | |
| ''; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@config/paperclip/default.nix` around lines 39 - 41, The activation script
currently silences all failures by appending "|| true" to the setup invocation
in home.activation.paperclipConfig; remove that suffix so the setupScript errors
propagate (or replace it with explicit error handling that logs the failure and
exits non‑zero), ensuring the ${pkgs.bash}/bin/bash ${setupScript} invocation
returns a failing exit code on error and writes diagnostic output
(stderr/stdout) to logs before exiting so issues like DB creation or permission
errors are visible.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| INSTANCE_DIR="@instance_dir@" | ||
| CONFIG="${INSTANCE_DIR}/config.json" | ||
|
|
||
| mkdir -p "${INSTANCE_DIR}" | ||
|
|
||
| @cp@ "@config_file@" "$CONFIG" | ||
| chmod 600 "$CONFIG" | ||
|
|
||
| # Create paperclip database on docker-postgres if needed | ||
| # shellcheck disable=SC2050 -- @is_kyber@ is substituted by Nix replaceVars | ||
| if [ "@is_kyber@" = "true" ]; then | ||
| if command -v docker >/dev/null 2>&1 && docker container inspect postgres >/dev/null 2>&1; then | ||
| if ! docker exec postgres psql -U postgres -lqt | cut -d \| -f 1 | grep -qw paperclip; then | ||
| docker exec postgres createdb -U postgres paperclip | ||
| echo "Created paperclip database" >&2 | ||
| fi | ||
| fi | ||
| fi |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| ./bin-shells | ||
| ./cargo-globals | ||
| ./openclaw | ||
| ./paperclip | ||
| ./local-binaries | ||
| ./local-scripts | ||
| ./npm-globals | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,50 @@ | ||||||
| { | ||||||
| config, | ||||||
| lib, | ||||||
| inputs, | ||||||
| ... | ||||||
| }: | ||||||
| let | ||||||
| inherit (inputs) host; | ||||||
| homeDir = config.home.homeDirectory; | ||||||
| instanceDir = "${homeDir}/.paperclip/instances/default"; | ||||||
| in | ||||||
| # Only enable on kyber (server host) | ||||||
| lib.mkIf host.isKyber { | ||||||
| # Ensure Paperclip directories exist with correct permissions | ||||||
| home.activation.paperclipSetup = config.lib.dag.entryAfter [ "writeBoundary" ] '' | ||||||
| mkdir -p /tmp/paperclip | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Creating a log directory in |
||||||
| mkdir -p ${homeDir}/.paperclip | ||||||
| chmod 700 ${homeDir}/.paperclip | ||||||
|
Comment on lines
+15
to
+18
|
||||||
| ''; | ||||||
|
|
||||||
| # Systemd service for Paperclip | ||||||
| systemd.user.services.paperclip = { | ||||||
| Unit = { | ||||||
| Description = "Paperclip AI agent orchestration platform"; | ||||||
| After = [ | ||||||
| "network-online.target" | ||||||
| "docker-postgres.service" | ||||||
| ]; | ||||||
| Wants = [ "network-online.target" ]; | ||||||
| Requires = [ "docker-postgres.service" ]; | ||||||
| }; | ||||||
| Service = { | ||||||
| Type = "simple"; | ||||||
| ExecStart = "${homeDir}/.bun/bin/paperclipai run"; | ||||||
| Restart = "always"; | ||||||
| RestartSec = "5s"; | ||||||
| Environment = [ | ||||||
| "HOME=${homeDir}" | ||||||
| "PATH=${homeDir}/.local/bin:${homeDir}/.bun/bin:${homeDir}/.nix-profile/bin:${homeDir}/.local/share/pnpm:${homeDir}/.local/share/fnm/current/bin:${homeDir}/.npm-global/bin:/usr/local/bin:/usr/bin:/bin" | ||||||
| ]; | ||||||
| EnvironmentFile = "${instanceDir}/.env"; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
| EnvironmentFile = "${instanceDir}/.env"; | |
| EnvironmentFile = "-${instanceDir}/.env"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: This service requires ${instanceDir}/.env, but this PR only provisions config.json. On a fresh setup, paperclip.service will fail before exec unless that env file already exists.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At home-manager/modules/paperclip/default.nix, line 41:
<comment>This service requires `${instanceDir}/.env`, but this PR only provisions `config.json`. On a fresh setup, `paperclip.service` will fail before exec unless that env file already exists.</comment>
<file context>
@@ -0,0 +1,50 @@
+ "HOME=${homeDir}"
+ "PATH=${homeDir}/.local/bin:${homeDir}/.bun/bin:${homeDir}/.nix-profile/bin:${homeDir}/.local/share/pnpm:${homeDir}/.local/share/fnm/current/bin:${homeDir}/.npm-global/bin:/usr/local/bin:/usr/bin:/bin"
+ ];
+ EnvironmentFile = "${instanceDir}/.env";
+ WorkingDirectory = "${homeDir}/.paperclip";
+ StandardOutput = "append:/tmp/paperclip/paperclip.log";
</file context>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: Logging to /tmp/paperclip/paperclip.log is unreliable — /tmp is cleared on reboot and the activation script that creates the directory doesn't run on every boot. The service will fail to start after reboot. Either use the systemd journal (remove these lines) or use LogsDirectory=paperclip to let systemd manage the directory lifecycle.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At home-manager/modules/paperclip/default.nix, line 43:
<comment>Logging to `/tmp/paperclip/paperclip.log` is unreliable — `/tmp` is cleared on reboot and the activation script that creates the directory doesn't run on every boot. The service will fail to start after reboot. Either use the systemd journal (remove these lines) or use `LogsDirectory=paperclip` to let systemd manage the directory lifecycle.</comment>
<file context>
@@ -0,0 +1,50 @@
+ ];
+ EnvironmentFile = "${instanceDir}/.env";
+ WorkingDirectory = "${homeDir}/.paperclip";
+ StandardOutput = "append:/tmp/paperclip/paperclip.log";
+ StandardError = "append:/tmp/paperclip/paperclip.log";
+ };
</file context>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logging to /tmp is volatile and can cause the service to fail if the directory is missing after a reboot. It is recommended to use the systemd journal (the default) or a persistent path. Removing these lines will default logging to the journal, which is generally preferred for systemd services.
WorkingDirectory = "${homeDir}/.paperclip";
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,6 +56,7 @@ | |
| "mcporter": "^0.8.1", | ||
| "open-composer": "^0.8.23", | ||
| "openclaw": "^2026.3.28", | ||
| "paperclipai": "^2026.325.0", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify the paperclipai package exists and check its latest version
curl -s "https://registry.npmjs.org/paperclipai" | jq -r '.["dist-tags"].latest // "Package not found"'Repository: shunkakinoki/dotfiles Length of output: 75 🏁 Script executed: cat package.json | sed -n '50,120p'Repository: shunkakinoki/dotfiles Length of output: 1763 Dependencies and trustedDependencies are not in alphabetical order.
🔧 Suggested fix "openclaw": "^2026.3.28",
- "paperclipai": "^2026.325.0",
"oxfmt": "^0.42.0",
"oxlint": "^1.57.0",
+ "paperclipai": "^2026.325.0",
"portless": "^0.7.2",Also reorder in "openclaw",
- "paperclipai",
"oxfmt",
"oxlint",
+ "paperclipai",
"portless",🤖 Prompt for AI Agents |
||
| "oxfmt": "^0.42.0", | ||
| "oxlint": "^1.57.0", | ||
| "portless": "^0.7.2", | ||
|
|
@@ -112,6 +113,7 @@ | |
| "mcporter", | ||
| "open-composer", | ||
| "openclaw", | ||
| "paperclipai", | ||
| "oxfmt", | ||
| "oxlint", | ||
| "portless", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| #!/usr/bin/env bash | ||
| # shellcheck disable=SC2329 | ||
|
|
||
| Describe 'config/paperclip/setup.sh' | ||
| SCRIPT="$PWD/config/paperclip/setup.sh" | ||
|
|
||
| Describe 'script properties' | ||
| It 'uses bash shebang' | ||
| When run bash -c "head -1 '$SCRIPT'" | ||
| The output should include '#!/usr/bin/env bash' | ||
| End | ||
|
|
||
| It 'uses strict mode' | ||
| When run bash -c "head -5 '$SCRIPT'" | ||
| The output should include 'set -euo pipefail' | ||
| End | ||
| End | ||
|
|
||
| Describe 'config management' | ||
| It 'creates instance directory' | ||
| When run bash -c "grep 'mkdir -p' '$SCRIPT'" | ||
| The output should include 'INSTANCE_DIR' | ||
| End | ||
|
|
||
| It 'copies config file to instance directory' | ||
| When run bash -c "grep '@cp@' '$SCRIPT'" | ||
| The output should include 'config_file' | ||
| End | ||
|
|
||
| It 'sets restrictive permissions on config' | ||
| When run bash -c "grep 'chmod 600' '$SCRIPT'" | ||
| The output should include 'CONFIG' | ||
| End | ||
| End | ||
|
|
||
| Describe 'database provisioning' | ||
| It 'creates paperclip database on docker-postgres' | ||
| When run bash -c "grep 'createdb' '$SCRIPT'" | ||
| The output should include 'paperclip' | ||
| End | ||
|
|
||
| It 'checks if database already exists before creating' | ||
| When run bash -c "grep 'grep -qw paperclip' '$SCRIPT'" | ||
| The output should include 'paperclip' | ||
| End | ||
|
|
||
| It 'only runs on kyber' | ||
| When run bash -c "grep 'is_kyber' '$SCRIPT'" | ||
| The output should include 'is_kyber' | ||
| End | ||
| End | ||
|
|
||
| End |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Success message is printed even when the service restart is skipped.
The
echo "✅ paperclip restarted"on line 885 executes unconditionally, even when the host is notkyberand the restart was skipped. This could be misleading.🔧 Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents