File size: 6,177 Bytes
be8c7bb
 
 
 
 
 
 
47b3c14
 
be8c7bb
3182e31
 
be8c7bb
6ad52e8
 
df96d5e
47b3c14
3182e31
be8c7bb
6ad52e8
 
 
 
 
 
 
 
 
 
47b3c14
9e72cf8
47b3c14
9e72cf8
be8c7bb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3182e31
be8c7bb
 
3182e31
 
be8c7bb
9161677
 
5c47206
67a5cb1
5ee080c
 
 
 
67a5cb1
 
 
 
 
 
5ee080c
67a5cb1
 
 
 
 
 
 
 
 
 
 
 
e6dbd45
4e5f895
d63dc26
4e5f895
 
 
d63dc26
4e5f895
 
 
d63dc26
4e5f895
 
 
d63dc26
4e5f895
5fa646d
 
 
 
 
 
 
 
 
 
 
 
67a5cb1
2ba784e
be8c7bb
3182e31
be8c7bb
3182e31
 
 
 
47b3c14
be8c7bb
 
 
 
 
 
 
4b5ca71
be8c7bb
4b5ca71
be8c7bb
9946cfb
 
c7255ef
 
5015cf0
9946cfb
 
c7255ef
be8c7bb
 
 
3182e31
be8c7bb
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# Stage 1: Build Paperclip from source
FROM node:lts-trixie-slim AS paperclip-builder

WORKDIR /build

RUN apt-get update && apt-get install -y \
    git \
    && rm -rf /var/lib/apt/lists/* \
    && corepack enable

# Clone Paperclip (depth=1 for speed, uses repo's default branch)
RUN git clone --depth=1 https://github.com/paperclipai/paperclip.git .

# Copy lock files early for cache efficiency: lock changes don't re-clone
RUN ls -la pnpm-lock.yaml package.json 2>/dev/null || true

# Install dependencies (corepack picks correct pnpm version from packageManager field)
RUN pnpm install

# Apply both patches in a single layer (reduces layer count, cleaner git history)
# Patch 1: React Router basename for /app path handling
# Patch 2: Recovery chain depth cap at 500 to prevent stack overflow
RUN sed -i 's|<BrowserRouter>|<BrowserRouter basename="/app">|' ui/src/main.tsx && \
    grep -q 'basename="/app"' ui/src/main.tsx || (echo "PATCH 1 FAILED: React Router basename not applied" && exit 1) && \
    PATCH_FILE=server/src/services/recovery/issue-graph-liveness.ts && \
    test -f "$PATCH_FILE" || (echo "PATCH 2 FAILED: File not found: $PATCH_FILE" && exit 1) && \
    sed -i 's/seen\.has(current\.id)/(seen.size > 500 || seen.has(current.id))/' "$PATCH_FILE" && \
    grep -q "seen.size > 500" "$PATCH_FILE" || (echo "PATCH 2 FAILED: Chain depth cap not applied" && exit 1)

# Build Paperclip (match official Dockerfile order)
RUN pnpm --filter @paperclipai/ui build
RUN pnpm --filter @paperclipai/plugin-sdk build
RUN pnpm --filter @paperclipai/server build

# Stage 2: Runtime
FROM node:lts-trixie-slim

WORKDIR /app

# Install system dependencies
RUN apt-get update && apt-get install -y \
    curl \
    postgresql-client \
    postgresql \
    postgresql-contrib \
    python3 \
    python3-pip \
    git \
    && rm -rf /var/lib/apt/lists/*

# Create PostgreSQL runtime directories
RUN mkdir -p /var/run/postgresql && chown postgres:postgres /var/run/postgresql

# Install health-server Node dependencies locally in /app
RUN npm init -y && npm install express@4 cors morgan

# Install agent CLIs globally
RUN npm install -g @google/gemini-cli @anthropic-ai/claude-code @openai/codex

# Claude Code wrapper β€” auth mode selection:
#   CLAUDE_CODE_OAUTH_TOKEN set β†’ long-lived subscription OAuth token (sk-ant-oat01-...)
#                                 takes priority; unset API key to avoid conflict
#   ANTHROPIC_API_KEY set       β†’ API key mode (pay-per-use)
#   Neither set                 β†’ uses stored credentials in ~/.claude/
# Also drops cloudflare NODE_OPTIONS and caps heap size.
RUN if [ -e /usr/local/bin/claude ]; then \
    mv /usr/local/bin/claude /usr/local/bin/claude-real && \
    { \
      echo '#!/bin/sh'; \
      echo 'unset NODE_OPTIONS'; \
      echo '[ -n "$CLAUDE_CODE_OAUTH_TOKEN" ] && unset ANTHROPIC_API_KEY'; \
      echo 'export NODE_OPTIONS="--max-old-space-size=4096 --no-deprecation --no-warnings"'; \
      echo 'exec /usr/local/bin/claude-real "$@"'; \
    } > /usr/local/bin/claude && \
    chmod +x /usr/local/bin/claude; \
fi

# Codex wrapper β€” drops cloudflare NODE_OPTIONS, caps heap size
RUN if [ -e /usr/local/bin/codex ]; then \
    mv /usr/local/bin/codex /usr/local/bin/codex-real && \
    printf '#!/bin/sh\nunset NODE_OPTIONS\nexport NODE_OPTIONS="--max-old-space-size=4096 --no-deprecation --no-warnings"\nexec /usr/local/bin/codex-real "$@"\n' > /usr/local/bin/codex && \
    chmod +x /usr/local/bin/codex; \
fi

# Gemini wrapper β€” fix for "Failed to relaunch the CLI process":
#
# ROOT CAUSE: relaunch.ts::relaunchAppInChildProcess() spawns a child process
#   with stdio IPC. That spawn fails when Paperclip pipes gemini's stdio.
#   Source: packages/cli/src/utils/relaunch.ts
#
# FIX: GEMINI_CLI_NO_RELAUNCH=1 β€” documented kill-switch in relaunch.ts that
#   makes relaunchAppInChildProcess() return early (skip spawn entirely).
#   The process then runs as the main process without a child.
#
# Also bake in other headless vars so they survive even if Paperclip spawns
# gemini with a custom env object:
#   GEMINI_SANDBOX=false          β€” skip Docker-sandbox attempt
#   GEMINI_CLI_TRUST_WORKSPACE=true β€” skip interactive workspace-trust prompt
RUN mv /usr/local/bin/gemini /usr/local/bin/gemini-real && \
    { \
      echo '#!/bin/sh'; \
      echo 'unset NODE_OPTIONS'; \
      echo 'export NODE_OPTIONS="--max-old-space-size=4096 --no-deprecation --no-warnings"'; \
      echo 'export GEMINI_CLI_NO_RELAUNCH=1'; \
      echo 'export GEMINI_SANDBOX=false'; \
      echo 'export GEMINI_CLI_TRUST_WORKSPACE=true'; \
      echo '# SANDBOX=1 = "already inside sandbox" β€” bypasses entire sandbox setup block'; \
      echo '# in gemini.tsx regardless of GEMINI_SANDBOX setting or defaults'; \
      echo 'export SANDBOX=1'; \
      echo 'exec /usr/local/bin/gemini-real "$@"'; \
    } > /usr/local/bin/gemini && \
    chmod +x /usr/local/bin/gemini

# Install Python dependencies for sync
RUN pip install --no-cache-dir --break-system-packages huggingface_hub PyYAML

# Copy full Paperclip build (including node_modules for runtime)
COPY --from=paperclip-builder /build /app/paperclip

# Ensure pnpm is available in runtime stage
RUN corepack enable

# Copy orchestration files
COPY start.sh /app/
COPY health-server.js /app/
COPY paperclip-sync.py /app/
COPY cloudflare-proxy.js /app/
COPY cloudflare-proxy-setup.py /app/
COPY cloudflare-keepalive-setup.py /app/

RUN chmod +x /app/start.sh /app/cloudflare-keepalive-setup.py

# Create non-root user for running Paperclip + agent CLIs
# Claude Code refuses --dangerously-skip-permissions when running as root
# Note: /app files stay root-owned (644/755 defaults = readable by all).
# /paperclip runtime dir is chowned to paperclip in start.sh after restore.
RUN useradd -m -u 1001 -s /bin/bash paperclip && \
    mkdir -p /paperclip /var/lib/postgresql/data && \
    chown -R postgres:postgres /var/lib/postgresql/data && \
    chown paperclip:paperclip /paperclip

EXPOSE 7861

HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=3 \
    CMD curl -f http://localhost:7861/health || exit 1

CMD ["/app/start.sh"]