Skip to content

fix(server/execute): unwrap Paperclip {type,value} env entries before spawn#102

Closed
tamermina wants to merge 1 commit into
NousResearch:mainfrom
tamermina:fix/unwrap-paperclip-env-values
Closed

fix(server/execute): unwrap Paperclip {type,value} env entries before spawn#102
tamermina wants to merge 1 commit into
NousResearch:mainfrom
tamermina:fix/unwrap-paperclip-env-values

Conversation

@tamermina

Copy link
Copy Markdown

Summary

  • config.env values from Paperclip arrive as {type: "plain"|"secret", value: string} objects, not plain strings
  • Object.assign(env, userEnv) at src/server/execute.ts:425 (HEAD 937ea71) spreads these objects directly into the spawned process env
  • Node coerces each object to the literal string "[object Object]", breaking every downstream consumer: PAPERCLIP_API_KEY auth fails, PAPERCLIP_RUN_ID is corrupted, all custom user env vars are useless

Reproduce

Set an env var in a Paperclip agent's adapterConfig.env (any key/value). Spawn the adapter and console.log(process.env.YOUR_KEY) inside the Hermes process — you'll see "[object Object]" instead of the string value. The same applies to every var Paperclip stores in the wrapped shape.

Fix

Replace Object.assign(env, userEnv) with a for-loop that unwraps {type, value} entries before assigning:

const userEnv = config.env as
  | Record<string, string | { type: "plain" | "secret"; value: string }>
  | undefined;
if (userEnv && typeof userEnv === "object") {
  for (const [key, raw] of Object.entries(userEnv)) {
    if (raw && typeof raw === "object" && "value" in raw) {
      // Paperclip wraps env values as {type: "plain"|"secret", value: string}
      env[key] = String((raw as { value: unknown }).value);
    } else if (typeof raw === "string") {
      env[key] = raw;
    }
  }
}

The typeof raw === "string" branch preserves the legacy path for any caller already passing plain strings.

The type annotation is widened from Record<string, string> to Record<string, string | {type: "plain"|"secret"; value: string}> — this accurately reflects the actual runtime shape Paperclip passes and is a strictly safer cast than the existing one (which silently lies to the compiler).

Verification

  • npx tsc --noEmit — clean, zero errors
  • npm run build (i.e. tsc) — clean, zero errors
  • ESLint skipped: eslint is not in devDependencies and there is no eslint.config.* file in the repo — the lint script is broken upstream independent of this PR
  • Equivalent fix has been running in production on our self-hosted deployment for ~3 weeks as an in-container patch (sentinel PAPERCLIP-UNWRAP-ENV-PATCH in our deploy script). 21 agents across 3 companies (UNOA, CHAA, REVĪV) depend on the fixed behavior for every heartbeat run. Zero regressions observed in that period.

Risk

Low. The unwrap path only activates when raw is an object with a value key — which is exactly the shape Paperclip produces. Plain-string values fall through to the existing assignment. The fix cannot affect callers that already pass strings correctly.

… spawn

Paperclip's adapter contract stores env entries as
{type: "plain"|"secret", value: string} objects. The current
Object.assign(env, userEnv) spreads these directly into the spawned
process env, where Node coerces each object to the literal string
"[object Object]". Every consumer of these env vars (PAPERCLIP_API_KEY,
PAPERCLIP_RUN_ID, custom user env) sees the broken value and either
fails auth or makes the wrong API call.

Fix: unwrap the {value} before assigning, falling back to the raw
string if the value isn't wrapped (preserves the legacy-shape path).

Verified by replacing the equivalent in-container patch we've run on
production for ~3 weeks (sentinel: PAPERCLIP-UNWRAP-ENV-PATCH).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
pinsleepe pushed a commit to Pixel-Nomad-Global/hermes-paperclip-adapter that referenced this pull request May 27, 2026
… block

The NousResearch#118 PAPERCLIP_API_URL prompt fix and the NousResearch#102 env-unwrap fix both
declared userEnv in the same function scope. Renamed the prompt-resolution
variable to configEnv to resolve the TS2451 redeclaration error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pinsleepe pushed a commit to Pixel-Nomad-Global/hermes-paperclip-adapter that referenced this pull request May 27, 2026
Rebuilds dist/ after merging 10 upstream PRs:
  NousResearch#138 gate session persistence on success + drop legacy regex
  NousResearch#116 session-codec format validation
  NousResearch#102 unwrap Paperclip {type,value} env entries
  NousResearch#99  wire onSpawn for PID tracking
  NousResearch#96  add missing providers + prepare script (skip -Q disable + env dup)
  NousResearch#139 default model fallback to auto
  NousResearch#141 read wake metadata from ctx.context not ctx.config
  NousResearch#135 swap python3 pipes for jq in DEFAULT_PROMPT_TEMPLATE
  NousResearch#118 use PAPERCLIP_API_URL from adapter env in prompt
  NousResearch#122 make task closeout conditional on genuine completion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@tamermina tamermina closed this Jun 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant