Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9bc2e513b | |||
| c79eea7ee0 | |||
| fa6c115be4 | |||
| 480f7cf3d1 | |||
| 5ed041fd84 | |||
| fe6bc0c2d6 | |||
| 2d057f085d | |||
| 570fdae9c4 |
@@ -0,0 +1,361 @@
|
||||
You are implementing two coordinated changes to a Paperclip adapter plugin.
|
||||
The repo is at /Users/Repositories/paperclip-adapter-opencode-k8s on branch
|
||||
master. Work on a new branch off master — do NOT commit directly to master.
|
||||
|
||||
Before you start, read these files fully:
|
||||
- src/server/execute.ts (~1218 lines; this is the main file you'll edit)
|
||||
- src/server/job-manifest.ts
|
||||
- src/server/log-dedup.ts (you will delete this)
|
||||
- src/server/parse.ts
|
||||
- src/server/index.ts
|
||||
- src/index.ts
|
||||
|
||||
Run `npm install` and then `npm test`. Confirm green. Note the test count
|
||||
for later comparison. Do NOT run `npm run build` — CI handles that.
|
||||
|
||||
=============================================================================
|
||||
WHY WE ARE DOING THIS
|
||||
=============================================================================
|
||||
|
||||
Two tightly coupled bugs:
|
||||
|
||||
(A) The adapter doesn't declare `hasOutOfProcessLiveness: true` on its
|
||||
ServerAdapterModule. The revitalize reaper therefore treats it as an
|
||||
in-process adapter, expects a local child PID, finds none, and marks
|
||||
every run `process_lost` after 5 minutes of staleness.
|
||||
|
||||
(B) The adapter reads pod logs via the Kubernetes log API (follow mode).
|
||||
At production scale the stream drops every few seconds, exhausting
|
||||
the 50-reconnect cap within 2.5 minutes. Long runs lose live UI
|
||||
output, and combined with (A) they fail entirely.
|
||||
|
||||
Fix both in one PR:
|
||||
|
||||
1. Declare `hasOutOfProcessLiveness: true` in createServerAdapter().
|
||||
2. Have the pod tee opencode's stdout to a file on the shared PVC, and
|
||||
have the adapter tail that file from the Paperclip server process.
|
||||
|
||||
We are NOT going to:
|
||||
- wrap the opencode binary
|
||||
- use hooks
|
||||
- add a sidecar
|
||||
- change revitalize
|
||||
- keep the k8s log API as a fallback
|
||||
|
||||
We ARE going to:
|
||||
- replace k8s log streaming with filesystem tailing entirely
|
||||
- delete all reconnect logic and the log-dedup filter
|
||||
- keep `kubectl logs -f` working (tee preserves stdout)
|
||||
- add the liveness flag so the reaper uses staleness-based liveness
|
||||
|
||||
=============================================================================
|
||||
SCOPE OF CHANGES
|
||||
=============================================================================
|
||||
|
||||
--- hasOutOfProcessLiveness flag (src/server/index.ts) ---
|
||||
|
||||
The file today returns a plain object from createServerAdapter(). Add
|
||||
`hasOutOfProcessLiveness: true` to the returned object, matching the
|
||||
pattern from paperclip-adapter-claude-k8s. The adapter-utils type predates
|
||||
this field, so the return needs a cast.
|
||||
|
||||
Before (approximately):
|
||||
export function createServerAdapter(): ServerAdapterModule {
|
||||
return {
|
||||
type,
|
||||
execute,
|
||||
// ... other fields ...
|
||||
};
|
||||
}
|
||||
|
||||
After:
|
||||
export function createServerAdapter(): ServerAdapterModule {
|
||||
return {
|
||||
type,
|
||||
execute,
|
||||
// ... other fields ...
|
||||
// Tells the reaper to skip local PID checks and use the staleness-based
|
||||
// liveness window instead (adapter spawns K8s Jobs in separate pods).
|
||||
// Cast required: adapter-utils ServerAdapterModule type predates this field.
|
||||
hasOutOfProcessLiveness: true,
|
||||
} as ServerAdapterModule;
|
||||
}
|
||||
|
||||
--- Job manifest (src/server/job-manifest.ts) ---
|
||||
|
||||
1. MODIFY the main container command to tee stdout. Current code at
|
||||
approximately line 409:
|
||||
const mainCommand = `${configSetup}cat /tmp/prompt/prompt.txt | opencode ${opencodeArgsEscaped}`;
|
||||
Change to:
|
||||
const podLogPath =
|
||||
`/paperclip/instances/default/run-logs/${companyId}/${agentId}/${runId}.pod.ndjson`;
|
||||
const mainCommand = `${configSetup}cat /tmp/prompt/prompt.txt | opencode ${opencodeArgsEscaped} | tee ${podLogPath}`;
|
||||
|
||||
`companyId`, `agentId`, `runId` are already in scope in buildJobManifest
|
||||
via the destructuring at line 219 (`agent`, `runId`) — use `agent.id`
|
||||
and `agent.companyId`. If you prefer cleaner code, add a local:
|
||||
const companyId = agent.companyId;
|
||||
const agentId = agent.id;
|
||||
|
||||
2. MODIFY the init container command to create the parent directory before
|
||||
the main container starts. The existing init container today writes the
|
||||
prompt file with `printf`. Amend its command to also `mkdir -p` the log
|
||||
directory. The init container is at approximately line 444 (prompt
|
||||
secret path) and line 451 (direct printf path) — there are TWO init
|
||||
container variants. Amend BOTH to prepend the mkdir:
|
||||
|
||||
Variant 1 (large-prompt path, approx line 444):
|
||||
Before: `cp /tmp/prompt-secret/prompt /tmp/prompt/prompt.txt`
|
||||
After: `mkdir -p /paperclip/instances/default/run-logs/${companyId}/${agentId} && cp /tmp/prompt-secret/prompt /tmp/prompt/prompt.txt`
|
||||
|
||||
Variant 2 (direct path, approx line 451):
|
||||
Before: `printf '%s' "$PROMPT_CONTENT" > /tmp/prompt/prompt.txt`
|
||||
After: `mkdir -p /paperclip/instances/default/run-logs/${companyId}/${agentId} && printf '%s' "$PROMPT_CONTENT" > /tmp/prompt/prompt.txt`
|
||||
|
||||
Use template substitution for companyId/agentId/runId — these are all
|
||||
in scope in the builder.
|
||||
|
||||
3. EXPORT the log path builder so execute.ts can compute the same path:
|
||||
export function buildPodLogPath(companyId: string, agentId: string, runId: string): string {
|
||||
return `/paperclip/instances/default/run-logs/${companyId}/${agentId}/${runId}.pod.ndjson`;
|
||||
}
|
||||
Return this path from buildJobManifest alongside other fields in
|
||||
JobBuildResult (add `podLogPath: string` to the interface at approx
|
||||
line 46). Update the final `return { job, jobName, namespace, prompt,
|
||||
opencodeArgs, promptMetrics }` (approx line 482) to include podLogPath.
|
||||
|
||||
4. ID SANITIZATION: before using companyId/agentId/runId in the path,
|
||||
validate they match `^[a-zA-Z0-9-]+$`. Add a helper at the top of
|
||||
job-manifest.ts:
|
||||
function assertSafePathComponent(field: string, value: string): void {
|
||||
if (!/^[a-zA-Z0-9-]+$/.test(value)) {
|
||||
throw new Error(`Invalid ${field} for log path: ${value}`);
|
||||
}
|
||||
}
|
||||
Call it for companyId, agentId, and runId before computing podLogPath
|
||||
and before interpolating into the init container commands.
|
||||
|
||||
--- Adapter (src/server/execute.ts) ---
|
||||
|
||||
1. DELETE the `LogLineDedupFilter` import (approx line 13).
|
||||
2. DELETE constants (approx lines 19-26):
|
||||
LOG_STREAM_RECONNECT_DELAY_MS
|
||||
LOG_STREAM_RECONNECT_MAX_DELAY_MS
|
||||
MAX_LOG_RECONNECT_ATTEMPTS
|
||||
LOG_STREAM_BAIL_TIMEOUT_MS
|
||||
3. DELETE functions:
|
||||
streamPodLogsOnce (approx line 168)
|
||||
streamPodLogs (approx line 252)
|
||||
readPodLogs (approx line 330)
|
||||
waitForPodTermination (approx line 355) — only used by the fallback
|
||||
4. DELETE the bail timer machinery inside any function being removed
|
||||
(bailTimer, bailResolve, bailPromise, stopPoller).
|
||||
5. DELETE the fallback path in `execute` around lines 675-693:
|
||||
if (!stdout.trim()) {
|
||||
// ... waitForPodTermination + readPodLogs fallback
|
||||
} else if (!parseOpenCodeJsonl(stdout).sessionId) {
|
||||
// ... partial-stdout fallback
|
||||
}
|
||||
|
||||
6. ADD a new function `tailPodLogFile` in execute.ts. Inline is fine; do
|
||||
not create a new module. Signature:
|
||||
|
||||
interface TailOptions {
|
||||
onLog: AdapterExecutionContext["onLog"];
|
||||
stopSignal: { stopped: boolean };
|
||||
}
|
||||
|
||||
async function tailPodLogFile(
|
||||
filePath: string,
|
||||
opts: TailOptions,
|
||||
): Promise<string> { ... }
|
||||
|
||||
Behavior:
|
||||
- Wait up to 30 seconds for the file to exist. Poll with
|
||||
fs.promises.stat every 250ms. If the file doesn't appear in 30s,
|
||||
throw an Error: `Pod log file never appeared at ${filePath}`.
|
||||
- Once it exists, open with fs.promises.open(filePath, 'r').
|
||||
- Track a byte offset starting at 0.
|
||||
- Poll loop: 250ms active cadence, backs off to 1000ms if the file
|
||||
hasn't grown for 5 consecutive polls (reset to 250ms on any
|
||||
growth). For each poll:
|
||||
a. stat the file, compare size to offset
|
||||
b. if size > offset, read bytes from [offset, size) into a Buffer
|
||||
c. update offset = size
|
||||
d. concatenate any pending partial line with the new buffer,
|
||||
split on '\n'
|
||||
e. last element is the new pending partial line (if no trailing
|
||||
newline) or empty
|
||||
f. for every complete line, call onLog("stdout", line + "\n")
|
||||
and append to an in-memory accumulator (string)
|
||||
- Exit when opts.stopSignal.stopped === true. Before returning, do
|
||||
ONE final read-to-EOF to drain tail bytes. Close the handle.
|
||||
Return the accumulator.
|
||||
|
||||
Use fs.promises.open / FileHandle.read / FileHandle.close. Do NOT use
|
||||
fs.watch or chokidar.
|
||||
|
||||
7. REPLACE the existing log-streaming section of `execute`. Find where
|
||||
streamPodLogs is invoked inside a `Promise.allSettled` with
|
||||
waitForJobCompletion (approx line 660). Replace that call with
|
||||
tailPodLogFile. Pattern:
|
||||
|
||||
const { /* ..., */ podLogPath } = built;
|
||||
// ... create secret, create job, wait for pod ...
|
||||
const stopSignal = { stopped: false };
|
||||
const [tailResult, completionResult] = await Promise.allSettled([
|
||||
tailPodLogFile(podLogPath, { onLog, stopSignal }),
|
||||
waitForJobCompletion(namespace, jobName, ...).then(r => { stopSignal.stopped = true; return r; }),
|
||||
]);
|
||||
const stdout = tailResult.status === "fulfilled" ? tailResult.value : "";
|
||||
|
||||
Keep waitForJobCompletion unchanged. Keep the existing `keepaliveTimer`
|
||||
and `cancelSignal` / cancel-polling machinery unchanged — those are
|
||||
independent of log streaming.
|
||||
|
||||
8. ADD log file cleanup. Find `cleanupJob` (the function that deletes the
|
||||
K8s Job). After successful deletion, best-effort delete the log file:
|
||||
try { await fs.promises.unlink(podLogPath); } catch { /* non-fatal */ }
|
||||
Skip the unlink if `retainJobs === true`.
|
||||
cleanupJob will need podLogPath passed in; thread it from the caller.
|
||||
|
||||
--- Delete entire files ---
|
||||
|
||||
- src/server/log-dedup.ts
|
||||
- src/server/log-dedup.test.ts
|
||||
|
||||
--- Tests ---
|
||||
|
||||
- Delete any execute.test.ts tests covering streamPodLogsOnce,
|
||||
streamPodLogs, readPodLogs, waitForPodTermination, the bail timer, or
|
||||
LogLineDedupFilter. Search for those identifiers; remove matching
|
||||
describe/it blocks. Non-log-streaming tests in the same file stay.
|
||||
- Add test cases for tailPodLogFile to execute.test.ts. Cover:
|
||||
1. File appears within 30s; content is tailed line-by-line
|
||||
2. File never appears; function throws with expected message
|
||||
3. Partial trailing line buffered and emitted on next poll
|
||||
4. Stop signal exits the loop; final drain reads remaining bytes
|
||||
5. Adaptive backoff: idle polls slow; active polls speed up
|
||||
Use vitest fake timers (vi.useFakeTimers) and a tmpdir via
|
||||
`fs.mkdtempSync(path.join(os.tmpdir(), 'opencode-tailer-'))`.
|
||||
|
||||
=============================================================================
|
||||
TESTING
|
||||
=============================================================================
|
||||
|
||||
After all changes:
|
||||
1. `npm run typecheck` — must pass (the `as ServerAdapterModule` cast
|
||||
may be needed; mirror claude-k8s's pattern)
|
||||
2. `npm test` — must pass. Test count will drop vs baseline because you
|
||||
deleted tests. Record the new passing count.
|
||||
|
||||
Do NOT run the adapter end-to-end. Do NOT require a k8s cluster.
|
||||
|
||||
=============================================================================
|
||||
BRANCH, COMMIT, PUSH, PR
|
||||
=============================================================================
|
||||
|
||||
1. Create a new branch off master:
|
||||
git checkout master && git pull && git checkout -b feat/filesystem-log-tail-and-liveness-flag
|
||||
|
||||
2. Make all changes above. Commit as ONE commit:
|
||||
|
||||
feat: declare hasOutOfProcessLiveness and tail pod log from filesystem
|
||||
|
||||
Two coordinated fixes for long-running agent failures:
|
||||
|
||||
(1) Declare hasOutOfProcessLiveness: true on the ServerAdapterModule.
|
||||
Without it the reaper treated this adapter as in-process, expected
|
||||
a local child PID, and marked every run process_lost after 5min
|
||||
staleness. Flag tells the reaper to use the staleness-based
|
||||
liveness window for out-of-process adapters.
|
||||
|
||||
(2) Replace k8s log API streaming with filesystem tailing. The k8s
|
||||
follow stream drops every ~3 seconds at production scale,
|
||||
exhausting the 50-attempt reconnect cap within 2.5 minutes. Pod
|
||||
now tees opencode's stdout to
|
||||
/paperclip/instances/default/run-logs/<companyId>/<agentId>/<runId>.pod.ndjson
|
||||
on the shared PVC; adapter tails the file directly. kubectl logs -f
|
||||
still works (tee preserves stdout).
|
||||
|
||||
Deletes:
|
||||
- LogLineDedupFilter and all reconnect logic
|
||||
- streamPodLogsOnce, streamPodLogs, readPodLogs, waitForPodTermination
|
||||
- Both fallback paths (empty-stream and missing-sessionId)
|
||||
|
||||
Adds:
|
||||
- tailPodLogFile: adaptive 250ms/1s poll loop with partial-line
|
||||
buffering and tail-drain on stopSignal
|
||||
- Log file cleanup tied to retainJobs
|
||||
- Path-component sanitization (companyId/agentId/runId must match
|
||||
[a-zA-Z0-9-]+)
|
||||
|
||||
Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
|
||||
|
||||
3. Push:
|
||||
git push -u origin feat/filesystem-log-tail-and-liveness-flag
|
||||
|
||||
4. Open a PR against master with `gh pr create`:
|
||||
Title: `feat: declare hasOutOfProcessLiveness and tail pod log from filesystem`
|
||||
Body (use a heredoc):
|
||||
|
||||
## Summary
|
||||
- Declares `hasOutOfProcessLiveness: true` so the reaper uses
|
||||
staleness-based liveness instead of expecting a local PID
|
||||
- Pod tees opencode stdout to PVC; adapter tails the file directly
|
||||
- Eliminates k8s log API dependency for streaming
|
||||
- Deletes LogLineDedupFilter, reconnect logic, both fallback paths
|
||||
|
||||
## Why
|
||||
At production scale (144 concurrent runs), two bugs combined:
|
||||
(a) no liveness flag → reaper marked runs process_lost at 5min
|
||||
(b) k8s log follow stream drops every ~3s, exhausting the 50-reconnect
|
||||
cap. Runs over ~2.5min lost live output; over 5min failed outright.
|
||||
|
||||
Both must be fixed together — the flag alone doesn't help if the log
|
||||
stream still drops, and the log tail alone doesn't help if the reaper
|
||||
kills the run for missing PID.
|
||||
|
||||
## Path
|
||||
`/paperclip/instances/default/run-logs/<companyId>/<agentId>/<runId>.pod.ndjson`
|
||||
— the `.pod.ndjson` suffix distinguishes the pod-written file from
|
||||
revitalize's server-side `<runId>.ndjson` log store.
|
||||
|
||||
## Breaking
|
||||
Old Job manifests (pre-tee) are incompatible — the tailer's 30s
|
||||
"file missing" window will surface an error on in-flight runs at
|
||||
deploy time. Operator retry required. Consistent with the companion
|
||||
change in paperclip-adapter-claude-k8s.
|
||||
|
||||
## Test plan
|
||||
- [ ] npm test passes
|
||||
- [ ] Manual: deploy to cluster, run a >5min agent, confirm live UI
|
||||
output and no reaper fire
|
||||
- [ ] Manual: verify kubectl logs -f still works on the Job pod
|
||||
- [ ] Manual: confirm log file is cleaned up when Job cleanup runs
|
||||
(retainJobs=false) and preserved when retainJobs=true
|
||||
|
||||
=============================================================================
|
||||
WRAPPING UP
|
||||
=============================================================================
|
||||
|
||||
Report back with:
|
||||
1. Branch name and commit hash
|
||||
2. PR URL
|
||||
3. Final test count (numbers will drop vs baseline because you deleted
|
||||
tests — record baseline and final)
|
||||
4. Line count of execute.ts before and after (should drop significantly)
|
||||
5. Any deviation from these instructions, with reason
|
||||
|
||||
If ANY of the following happens, STOP and report instead of improvising:
|
||||
- A file path doesn't match what's described (e.g. the mainCommand
|
||||
pattern has changed)
|
||||
- A function you're supposed to delete has other callers you didn't
|
||||
expect (streamPodLogsOnce in particular may have test-only imports
|
||||
that need untangling)
|
||||
- A test you're supposed to keep depends on something you deleted
|
||||
- Typecheck fails and the fix is non-obvious
|
||||
- The `as ServerAdapterModule` cast doesn't satisfy TypeScript
|
||||
|
||||
Do NOT push to master. Do NOT tag a version. Do NOT bump package.json
|
||||
version — leave it as-is.
|
||||
Generated
+474
-7
@@ -1,26 +1,27 @@
|
||||
{
|
||||
"name": "paperclip-adapter-opencode-k8s",
|
||||
"version": "0.1.30",
|
||||
"version": "0.1.39",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "paperclip-adapter-opencode-k8s",
|
||||
"version": "0.1.30",
|
||||
"version": "0.1.39",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@kubernetes/client-node": "^1.0.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@paperclipai/adapter-utils": "2026.415.0-canary.7",
|
||||
"@paperclipai/adapter-utils": "^2026.428.0",
|
||||
"@types/node": "^24.6.0",
|
||||
"@vitest/coverage-v8": "^4.1.5",
|
||||
"esbuild": "^0.24.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^4.1.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@paperclipai/adapter-utils": ">=2026.415.0-canary.7"
|
||||
"@paperclipai/adapter-utils": ">=2026.428.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
@@ -117,6 +118,431 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
|
||||
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
|
||||
"integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
|
||||
"integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
|
||||
"integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
|
||||
"integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
|
||||
"integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
|
||||
"integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
|
||||
"integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
|
||||
"integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
|
||||
"integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
@@ -223,9 +649,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@paperclipai/adapter-utils": {
|
||||
"version": "2026.415.0-canary.7",
|
||||
"resolved": "https://registry.npmjs.org/@paperclipai/adapter-utils/-/adapter-utils-2026.415.0-canary.7.tgz",
|
||||
"integrity": "sha512-VNzIZmu1lrK6QM8Ad9WkOihZItfkj21NHKQf+artDcbwFT2hHbDAD9hdW2W9NMVxYdFvvnws3w76FI/BUbCMbQ==",
|
||||
"version": "2026.428.0",
|
||||
"resolved": "https://registry.npmjs.org/@paperclipai/adapter-utils/-/adapter-utils-2026.428.0.tgz",
|
||||
"integrity": "sha512-kGHpE7rhePPCbnG3OwXbNuHZZuI+XyuFgNSiDnrEeiSbkI2c5XHM2WnWDCZ/NGHULfJW3lWhSxGMFoYqiy38vQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -1033,6 +1459,47 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
|
||||
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.24.2",
|
||||
"@esbuild/android-arm": "0.24.2",
|
||||
"@esbuild/android-arm64": "0.24.2",
|
||||
"@esbuild/android-x64": "0.24.2",
|
||||
"@esbuild/darwin-arm64": "0.24.2",
|
||||
"@esbuild/darwin-x64": "0.24.2",
|
||||
"@esbuild/freebsd-arm64": "0.24.2",
|
||||
"@esbuild/freebsd-x64": "0.24.2",
|
||||
"@esbuild/linux-arm": "0.24.2",
|
||||
"@esbuild/linux-arm64": "0.24.2",
|
||||
"@esbuild/linux-ia32": "0.24.2",
|
||||
"@esbuild/linux-loong64": "0.24.2",
|
||||
"@esbuild/linux-mips64el": "0.24.2",
|
||||
"@esbuild/linux-ppc64": "0.24.2",
|
||||
"@esbuild/linux-riscv64": "0.24.2",
|
||||
"@esbuild/linux-s390x": "0.24.2",
|
||||
"@esbuild/linux-x64": "0.24.2",
|
||||
"@esbuild/netbsd-arm64": "0.24.2",
|
||||
"@esbuild/netbsd-x64": "0.24.2",
|
||||
"@esbuild/openbsd-arm64": "0.24.2",
|
||||
"@esbuild/openbsd-x64": "0.24.2",
|
||||
"@esbuild/sunos-x64": "0.24.2",
|
||||
"@esbuild/win32-arm64": "0.24.2",
|
||||
"@esbuild/win32-ia32": "0.24.2",
|
||||
"@esbuild/win32-x64": "0.24.2"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
|
||||
+6
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "paperclip-adapter-opencode-k8s",
|
||||
"version": "0.1.35",
|
||||
"version": "0.1.40",
|
||||
"description": "Paperclip adapter plugin that runs OpenCode agents as Kubernetes Jobs",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -17,7 +17,8 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build": "tsc && npm run build:ui-parser",
|
||||
"build:ui-parser": "esbuild src/ui-parser.ts --bundle --format=cjs --target=es2020 --outfile=dist/ui-parser.js --log-level=warning",
|
||||
"clean": "rm -rf dist",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
@@ -28,12 +29,13 @@
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@paperclipai/adapter-utils": ">=2026.415.0-canary.7"
|
||||
"@paperclipai/adapter-utils": ">=2026.428.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@paperclipai/adapter-utils": "2026.415.0-canary.7",
|
||||
"@paperclipai/adapter-utils": "^2026.428.0",
|
||||
"@types/node": "^24.6.0",
|
||||
"@vitest/coverage-v8": "^4.1.5",
|
||||
"esbuild": "^0.24.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^4.1.4"
|
||||
}
|
||||
|
||||
@@ -64,4 +64,3 @@ Notes:
|
||||
`;
|
||||
|
||||
export { createServerAdapter } from "./server/index.js";
|
||||
export { parseStdoutLine } from "./ui-parser.js";
|
||||
|
||||
@@ -11,13 +11,6 @@ export function getConfigSchema(): AdapterConfigSchema {
|
||||
hint: "Provider-specific reasoning/profile variant passed as --variant",
|
||||
group: "Core",
|
||||
},
|
||||
{
|
||||
key: "instructionsFilePath",
|
||||
label: "Instructions File Path",
|
||||
type: "text",
|
||||
hint: "Absolute path to a markdown file (e.g. AGENTS.md) prepended as system instructions before the task prompt",
|
||||
group: "Core",
|
||||
},
|
||||
{
|
||||
key: "dangerouslySkipPermissions",
|
||||
label: "Skip Permission Checks",
|
||||
|
||||
@@ -884,7 +884,6 @@ describe("execute — external cancel polling", () => {
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
delete process.env.PAPERCLIP_API_URL;
|
||||
delete process.env.PAPERCLIP_DEV_API_KEY;
|
||||
});
|
||||
|
||||
it("returns errorCode=cancelled and deletes job when issue status is cancelled", async () => {
|
||||
@@ -970,42 +969,6 @@ describe("execute — external cancel polling", () => {
|
||||
expect(result.errorCode).toBeUndefined();
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
it("uses PAPERCLIP_DEV_API_KEY over ctx.authToken when set", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
process.env.PAPERCLIP_API_URL = "http://test-api";
|
||||
process.env.PAPERCLIP_DEV_API_KEY = "dev-override-key";
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ status: "cancelled" }),
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
let jobDeleted = false;
|
||||
const batchApi = makeBatchApi();
|
||||
batchApi.deleteNamespacedJob.mockImplementation(() => { jobDeleted = true; return Promise.resolve({}); });
|
||||
batchApi.readNamespacedJob.mockImplementation(() => {
|
||||
if (jobDeleted) return Promise.reject(Object.assign(new Error("not found"), { statusCode: 404 }));
|
||||
return Promise.resolve({ status: { conditions: [] } });
|
||||
});
|
||||
vi.mocked(getBatchApi).mockReturnValue(batchApi as unknown as ReturnType<typeof getBatchApi>);
|
||||
|
||||
const ctx = makeCtx({}, { issueId: "issue-test-456" }, "ctx-auth-token");
|
||||
const executePromise = execute(ctx);
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
}
|
||||
|
||||
await executePromise;
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"http://test-api/api/issues/issue-test-456",
|
||||
expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer dev-override-key" }) }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("execute — large-prompt Secret path", () => {
|
||||
|
||||
@@ -569,9 +569,7 @@ async function streamAndAwaitJob(
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, KEEPALIVE_INTERVAL_MS));
|
||||
if (logStopSignal.stopped || cancelSignal.cancelled) break;
|
||||
try {
|
||||
// Prefer PAPERCLIP_DEV_API_KEY if set (dev override), otherwise use
|
||||
// the per-run authToken issued by Paperclip for this execution.
|
||||
const apiKey = process.env.PAPERCLIP_DEV_API_KEY ?? ctx.authToken ?? "";
|
||||
const apiKey = ctx.authToken ?? "";
|
||||
const resp = await fetch(`${apiUrl}/api/issues/${issueId}`, {
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
});
|
||||
|
||||
+2
-1
@@ -1,7 +1,7 @@
|
||||
import type { ServerAdapterModule } from "@paperclipai/adapter-utils";
|
||||
import { getAdapterSessionManagement } from "@paperclipai/adapter-utils";
|
||||
import { type, agentConfigurationDoc } from "../index.js";
|
||||
import { listK8sModels } from "./models.js";
|
||||
import { listK8sModels, STATIC_MODELS } from "./models.js";
|
||||
import { execute } from "./execute.js";
|
||||
import { testEnvironment } from "./test.js";
|
||||
import { sessionCodec } from "./session.js";
|
||||
@@ -14,6 +14,7 @@ export function createServerAdapter(): ServerAdapterModule {
|
||||
execute,
|
||||
testEnvironment,
|
||||
sessionCodec,
|
||||
models: STATIC_MODELS,
|
||||
listModels: listK8sModels,
|
||||
listSkills: listOpenCodeSkills,
|
||||
syncSkills: syncOpenCodeSkills,
|
||||
|
||||
@@ -484,15 +484,14 @@ describe("buildJobManifest — env wiring branches", () => {
|
||||
expect(env.find((e) => e.name === "PAPERCLIP_API_KEY")?.value).toBe("tok_abc");
|
||||
});
|
||||
|
||||
it("inherits PAPERCLIP_API_URL and PAPERCLIP_DEV_API_KEY from selfPod inheritedEnv", () => {
|
||||
it("inherits PAPERCLIP_API_URL from selfPod inheritedEnv", () => {
|
||||
const selfPod = {
|
||||
...mockSelfPod,
|
||||
inheritedEnv: { PAPERCLIP_API_URL: "http://api", PAPERCLIP_DEV_API_KEY: "dev_key" },
|
||||
inheritedEnv: { PAPERCLIP_API_URL: "http://api" },
|
||||
};
|
||||
const result = buildJobManifest({ ctx: mockCtx, selfPod });
|
||||
const env = result.job.spec?.template.spec?.containers[0]?.env ?? [];
|
||||
expect(env.find((e) => e.name === "PAPERCLIP_API_URL")?.value).toBe("http://api");
|
||||
expect(env.find((e) => e.name === "PAPERCLIP_DEV_API_KEY")?.value).toBe("dev_key");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -173,13 +173,6 @@ function buildEnvVars(
|
||||
if (selfPod.inheritedEnv.PAPERCLIP_API_URL) {
|
||||
paperclipEnv.PAPERCLIP_API_URL = selfPod.inheritedEnv.PAPERCLIP_API_URL;
|
||||
}
|
||||
// Inherit PAPERCLIP_DEV_API_KEY if set (dev-instance key, distinct from the
|
||||
// main-instance run JWT in PAPERCLIP_API_KEY). Used by the external cancel
|
||||
// polling in execute.ts to authenticate against the dev Paperclip instance.
|
||||
if (selfPod.inheritedEnv.PAPERCLIP_DEV_API_KEY) {
|
||||
paperclipEnv.PAPERCLIP_DEV_API_KEY = selfPod.inheritedEnv.PAPERCLIP_DEV_API_KEY;
|
||||
}
|
||||
|
||||
// Layer 3: Inherited from Deployment (Bedrock, API keys, etc.)
|
||||
const merged: Record<string, string> = {
|
||||
...selfPod.inheritedEnv,
|
||||
|
||||
@@ -3,6 +3,15 @@ import os from "node:os";
|
||||
import type { AdapterModel } from "@paperclipai/adapter-utils";
|
||||
import { asString, ensurePathInEnv, runChildProcess } from "@paperclipai/adapter-utils/server-utils";
|
||||
|
||||
export const STATIC_MODELS: AdapterModel[] = [
|
||||
{ id: "anthropic/claude-opus-4-7", label: "anthropic/claude-opus-4-7" },
|
||||
{ id: "anthropic/claude-sonnet-4-6", label: "anthropic/claude-sonnet-4-6" },
|
||||
{ id: "anthropic/claude-haiku-4-5", label: "anthropic/claude-haiku-4-5" },
|
||||
{ id: "openai/gpt-4o", label: "openai/gpt-4o" },
|
||||
{ id: "google/gemini-2.5-pro", label: "google/gemini-2.5-pro" },
|
||||
{ id: "google/gemini-2.5-flash", label: "google/gemini-2.5-flash" },
|
||||
];
|
||||
|
||||
const MODELS_CACHE_TTL_MS = 60_000;
|
||||
const MODELS_DISCOVERY_TIMEOUT_MS = 20_000;
|
||||
|
||||
|
||||
@@ -7,6 +7,16 @@ describe("createServerAdapter", () => {
|
||||
expect(adapter.type).toBe("opencode_k8s");
|
||||
});
|
||||
|
||||
it("exposes a non-empty static models list so the UI renders before listModels resolves", () => {
|
||||
const adapter = createServerAdapter();
|
||||
expect(Array.isArray(adapter.models)).toBe(true);
|
||||
expect(adapter.models!.length).toBeGreaterThan(0);
|
||||
for (const m of adapter.models!) {
|
||||
expect(m.id).toMatch(/^[^/]+\/.+/);
|
||||
expect(m.label).toBe(m.id);
|
||||
}
|
||||
});
|
||||
|
||||
it("exposes listModels for dynamic model discovery", () => {
|
||||
const adapter = createServerAdapter();
|
||||
expect(typeof adapter.listModels).toBe("function");
|
||||
|
||||
Reference in New Issue
Block a user